Add axum server to nym-api (#4803)

* Migrate nym-api HTTP server from rocket to axum (#4698)

Migrate endpoints to Axum

* Squashed after PR review

Initial WIP
- bootstrap axum server with same data as rocket
- start axum server alongside rocket
- add routes for circulating-supply, contract-cache, network
- write simple bash validation that migrated APIs return 200
- mark rocket parts of code as deprecated
- start more complicated routes: WIP

Init storage always

Add coconut routes

Add api-status routes

Expand tests

WIP

Migrate unstable APIs with query params

Update bash tests

Add node-status routes

Redirect / to /swagger

Update API tests

Implement graceful shutdown

rustfmt

Fix clippy

* Add ecash routes after rebase

* PR feedback
- add CORS layer
- move logger to common crate
- remove global log filters for nym-api and axum

* Serve OpenAPI for all endpoints (#4761)

* Playing around with swagger

* Generate OpenAPI for /status routes

* Phase out static_routes as strings
- also nest routers in a clearer way

* Generate OpenAPI for /network routes

* Generate OpenAPI for /api-status routes

* Generate OpenAPI for "nym nodes" routes

* Fix some network-monitor routes

* Generate OpenAPI for /ecash routes

* Add utoipa feature to /common mods

* Add OpenAPI for unstable routes

* Fix MixNodeDetails field in models

* Introduce axum feature flag (#4775)

* Add Axum bind_address to config

* Introduce axum feature flag

* Add comment to template.rs

* Add Github action to build wtih `axum` feature

* Refactor server start & shutdown (#4777)

* Clippy: don't forget axum feature

* Refactor router so it's safer

* Implement graceful shutdown

* Nicer pattern matching

* Better Result syntax
This commit is contained in:
Dinko Zdravac
2024-08-29 15:31:01 +02:00
committed by GitHub
parent afc1b90b57
commit a0fea6edb4
68 changed files with 4653 additions and 428 deletions
+9 -1
View File
@@ -76,6 +76,14 @@ jobs:
# Enable wireguard by default on linux only
args: --workspace --features wireguard
# while disabled by default, this build ensures nothing is broken within
# `axum` feature
- name: Build with `axum` feature
uses: actions-rs/cargo@v1
with:
command: build
args: --features axum
- name: Build all examples
if: matrix.os == 'custom-linux'
uses: actions-rs/cargo@v1
@@ -109,4 +117,4 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
args: --workspace --all-targets --features wireguard -- -D warnings
args: --workspace --all-targets --features wireguard,axum -- -D warnings
Generated
+211 -105
View File
@@ -83,7 +83,7 @@ dependencies = [
"cipher",
"ctr",
"ghash",
"subtle 2.5.0",
"subtle 2.6.1",
]
[[package]]
@@ -218,6 +218,15 @@ version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "argon2"
version = "0.5.3"
@@ -394,7 +403,7 @@ dependencies = [
"http 1.1.0",
"http-body 1.0.0",
"http-body-util",
"hyper 1.4.1",
"hyper 1.4.0",
"hyper-util",
"itoa",
"matchit",
@@ -548,7 +557,7 @@ dependencies = [
"rand_core 0.6.4",
"ripemd",
"sha2 0.10.8",
"subtle 2.5.0",
"subtle 2.6.1",
"zeroize",
]
@@ -586,9 +595,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bitvec"
@@ -687,7 +696,7 @@ dependencies = [
"rand_core 0.6.4",
"serde",
"serdect 0.3.0-pre.0",
"subtle 2.5.0",
"subtle 2.6.1",
"zeroize",
]
@@ -737,9 +746,9 @@ dependencies = [
[[package]]
name = "bytemuck"
version = "1.16.0"
version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5"
checksum = "b236fc92302c97ed75b38da1f4917b5cdda4984745740f153a5d3059e48d725e"
[[package]]
name = "byteorder"
@@ -882,7 +891,7 @@ dependencies = [
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -1509,7 +1518,7 @@ version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"crossterm_winapi",
"libc",
"parking_lot 0.12.3",
@@ -1539,7 +1548,7 @@ checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array 0.14.7",
"rand_core 0.6.4",
"subtle 2.5.0",
"subtle 2.6.1",
"zeroize",
]
@@ -1621,9 +1630,9 @@ dependencies = [
[[package]]
name = "curl-sys"
version = "0.4.72+curl-8.6.0"
version = "0.4.73+curl-8.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29cbdc8314c447d11e8fd156dcdd031d9e02a7a976163e396b548c03153bc9ea"
checksum = "450ab250ecf17227c39afb9a2dd9261dc0035cb80f2612472fc0c4aac2dcb84d"
dependencies = [
"cc",
"libc",
@@ -1643,7 +1652,7 @@ dependencies = [
"byteorder",
"digest 0.9.0",
"rand_core 0.5.1",
"subtle 2.5.0",
"subtle 2.6.1",
"zeroize",
]
@@ -1660,7 +1669,7 @@ dependencies = [
"fiat-crypto",
"rustc_version 0.4.0",
"serde",
"subtle 2.5.0",
"subtle 2.6.1",
"zeroize",
]
@@ -1938,6 +1947,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "derive_arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "devise"
version = "0.4.1"
@@ -1964,7 +1984,7 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35b50dba0afdca80b187392b24f2499a88c336d5a8493e4b4ccfb608708be56a"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"proc-macro2",
"proc-macro2-diagnostics",
"quote",
@@ -1998,7 +2018,7 @@ dependencies = [
"block-buffer 0.10.4",
"const-oid",
"crypto-common",
"subtle 2.5.0",
"subtle 2.6.1",
]
[[package]]
@@ -2042,6 +2062,17 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "dkg-bypass-contract"
version = "0.1.0"
@@ -2122,7 +2153,7 @@ dependencies = [
"rand_core 0.6.4",
"serde",
"sha2 0.10.8",
"subtle 2.5.0",
"subtle 2.6.1",
"zeroize",
]
@@ -2163,7 +2194,7 @@ dependencies = [
"rand_core 0.6.4",
"sec1",
"serdect 0.2.0",
"subtle 2.5.0",
"subtle 2.6.1",
"zeroize",
]
@@ -2360,7 +2391,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
dependencies = [
"rand_core 0.6.4",
"subtle 2.5.0",
"subtle 2.6.1",
]
[[package]]
@@ -2744,7 +2775,7 @@ checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core 0.6.4",
"subtle 2.5.0",
"subtle 2.6.1",
]
[[package]]
@@ -3071,9 +3102,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.4.1"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc"
dependencies = [
"bytes",
"futures-channel",
@@ -3111,7 +3142,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
dependencies = [
"futures-util",
"http 1.1.0",
"hyper 1.4.1",
"hyper 1.4.0",
"hyper-util",
"rustls 0.22.4",
"rustls-pki-types",
@@ -3143,7 +3174,7 @@ dependencies = [
"futures-util",
"http 1.1.0",
"http-body 1.0.0",
"hyper 1.4.1",
"hyper 1.4.0",
"pin-project-lite",
"socket2",
"tokio",
@@ -3519,9 +3550,9 @@ dependencies = [
[[package]]
name = "lazy_static"
version = "1.4.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "ledger-apdu"
@@ -3578,7 +3609,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"libc",
]
@@ -3746,9 +3777,9 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.7.2"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memoffset"
@@ -3767,9 +3798,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.4"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
@@ -3957,7 +3988,7 @@ version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"cfg-if",
"libc",
"memoffset",
@@ -3969,7 +4000,7 @@ version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"cfg-if",
"cfg_aliases",
"libc",
@@ -4065,6 +4096,27 @@ dependencies = [
"libc",
]
[[package]]
name = "num_enum"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845"
dependencies = [
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "num_threads"
version = "0.1.7"
@@ -4086,6 +4138,8 @@ version = "1.1.42"
dependencies = [
"anyhow",
"async-trait",
"axum 0.7.5",
"axum-extra",
"bincode",
"bip39",
"bloomfilter",
@@ -4122,6 +4176,7 @@ dependencies = [
"nym-ecash-double-spending",
"nym-ecash-time",
"nym-gateway-client",
"nym-http-api-common",
"nym-inclusion-probability",
"nym-mixnet-contract-common",
"nym-multisig-contract-common",
@@ -4153,8 +4208,15 @@ dependencies = [
"time",
"tokio",
"tokio-stream",
"tokio-util",
"tower-http",
"tracing",
"tracing-subscriber",
"ts-rs",
"url",
"utoipa",
"utoipa-swagger-ui",
"utoipauto",
"zeroize",
]
@@ -4183,6 +4245,7 @@ dependencies = [
"thiserror",
"time",
"ts-rs",
"utoipa",
]
[[package]]
@@ -4432,7 +4495,7 @@ dependencies = [
"gloo-timers",
"http-body-util",
"humantime-serde",
"hyper 1.4.1",
"hyper 1.4.0",
"hyper-util",
"log",
"nym-bandwidth-controller",
@@ -5034,10 +5097,12 @@ version = "0.1.0"
dependencies = [
"axum 0.7.5",
"bytes",
"colored",
"mime",
"serde",
"serde_json",
"serde_yaml",
"tracing",
"utoipa",
]
@@ -5187,6 +5252,7 @@ dependencies = [
"thiserror",
"time",
"ts-rs",
"utoipa",
]
[[package]]
@@ -5288,6 +5354,7 @@ dependencies = [
"schemars",
"serde",
"url",
"utoipa",
]
[[package]]
@@ -5427,7 +5494,7 @@ dependencies = [
"fastrand 2.1.1",
"headers",
"hmac",
"hyper 1.4.1",
"hyper 1.4.0",
"ipnetwork 0.20.0",
"nym-crypto",
"nym-http-api-common",
@@ -6258,9 +6325,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "oorandom"
version = "11.1.3"
version = "11.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9"
[[package]]
name = "opaque-debug"
@@ -6467,9 +6534,9 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.5.1",
"redox_syscall 0.5.2",
"smallvec",
"windows-targets 0.52.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -6480,7 +6547,7 @@ checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle 2.5.0",
"subtle 2.6.1",
]
[[package]]
@@ -6558,9 +6625,9 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
[[package]]
name = "pest"
version = "2.7.10"
version = "2.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "560131c633294438da9f7c4b08189194b20946c8274c6b9e38881a7874dc8ee8"
checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95"
dependencies = [
"memchr",
"thiserror",
@@ -6569,9 +6636,9 @@ dependencies = [
[[package]]
name = "pest_derive"
version = "2.7.10"
version = "2.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26293c9193fbca7b1a3bf9b79dc1e388e927e6cacaa78b4a3ab705a1d3d41459"
checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a"
dependencies = [
"pest",
"pest_generator",
@@ -6579,9 +6646,9 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.7.10"
version = "2.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ec22af7d3fb470a85dd2ca96b7c577a1eb4ef6f1683a9fe9a8c16e136c04687"
checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183"
dependencies = [
"pest",
"pest_meta",
@@ -6592,9 +6659,9 @@ dependencies = [
[[package]]
name = "pest_meta"
version = "2.7.10"
version = "2.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a240022f37c361ec1878d646fc5b7d7c4d28d5946e1a80ad5a7a4f4ca0bdcd"
checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f"
dependencies = [
"once_cell",
"pest",
@@ -7175,11 +7242,11 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.5.1"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e"
checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
]
[[package]]
@@ -7306,12 +7373,13 @@ checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http 1.1.0",
"http-body 1.0.0",
"http-body-util",
"hyper 1.4.1",
"hyper 1.4.0",
"hyper-rustls 0.26.0",
"hyper-util",
"ipnet",
@@ -7349,7 +7417,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle 2.5.0",
"subtle 2.6.1",
]
[[package]]
@@ -7582,7 +7650,7 @@ version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"errno",
"libc",
"linux-raw-sys",
@@ -7623,7 +7691,7 @@ dependencies = [
"ring 0.17.8",
"rustls-pki-types",
"rustls-webpki 0.102.4",
"subtle 2.5.0",
"subtle 2.6.1",
"zeroize",
]
@@ -7817,7 +7885,7 @@ dependencies = [
"generic-array 0.14.7",
"pkcs8",
"serdect 0.2.0",
"subtle 2.5.0",
"subtle 2.6.1",
"zeroize",
]
@@ -7827,7 +7895,7 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -8273,7 +8341,7 @@ dependencies = [
"rand 0.8.5",
"rand_distr",
"sha2 0.10.8",
"subtle 2.5.0",
"subtle 2.6.1",
]
[[package]]
@@ -8574,9 +8642,9 @@ checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee"
[[package]]
name = "subtle"
version = "2.5.0"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "subtle-encoding"
@@ -8716,7 +8784,7 @@ dependencies = [
"serde_repr",
"sha2 0.10.8",
"signature",
"subtle 2.5.0",
"subtle 2.6.1",
"subtle-encoding",
"tendermint-proto 0.34.1",
"time",
@@ -8747,7 +8815,7 @@ dependencies = [
"serde_repr",
"sha2 0.10.8",
"signature",
"subtle 2.5.0",
"subtle 2.6.1",
"subtle-encoding",
"tendermint-proto 0.37.0",
"time",
@@ -8822,7 +8890,7 @@ dependencies = [
"serde",
"serde_bytes",
"serde_json",
"subtle 2.5.0",
"subtle 2.6.1",
"subtle-encoding",
"tendermint 0.37.0",
"tendermint-config",
@@ -8988,9 +9056,9 @@ dependencies = [
[[package]]
name = "tinyvec"
version = "1.6.0"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
checksum = "ce6b6a2fb3a985e99cebfaefa9faa3024743da73304ca1c683a36429613d3d22"
dependencies = [
"tinyvec_macros",
]
@@ -9286,7 +9354,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"bytes",
"futures-util",
"http 1.1.0",
@@ -9684,7 +9752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle 2.5.0",
"subtle 2.6.1",
]
[[package]]
@@ -9768,20 +9836,54 @@ dependencies = [
[[package]]
name = "utoipa-swagger-ui"
version = "6.0.0"
version = "7.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b39868d43c011961e04b41623e050aedf2cc93652562ff7935ce0f819aaf2da"
checksum = "943e0ff606c6d57d410fd5663a4d7c074ab2c5f14ab903b9514565e59fa1189e"
dependencies = [
"axum 0.7.5",
"mime_guess",
"regex",
"reqwest 0.12.4",
"rust-embed",
"serde",
"serde_json",
"url",
"utoipa",
"zip",
]
[[package]]
name = "utoipauto"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608b8f2279483be386261655b562e40877ea434eb92093c894a644fda2021860"
dependencies = [
"utoipauto-macro",
]
[[package]]
name = "utoipauto-core"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17e82ab96c5a55263b5bed151b8426410d93aa909a453acdbd4b6792b5af7d64"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "utoipauto-macro"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8338dc3c9526011ffaa2aa6bd60ddfda9d49d2123108690755c6e34844212"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
"utoipauto-core",
]
[[package]]
name = "uuid"
version = "1.8.0"
@@ -10140,7 +10242,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
dependencies = [
"windows-core",
"windows-targets 0.52.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -10149,7 +10251,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -10176,7 +10278,7 @@ version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.5",
"windows-targets 0.52.6",
]
[[package]]
@@ -10211,18 +10313,18 @@ dependencies = [
[[package]]
name = "windows-targets"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm 0.52.5",
"windows_aarch64_msvc 0.52.5",
"windows_i686_gnu 0.52.5",
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.5",
"windows_x86_64_gnu 0.52.5",
"windows_x86_64_gnullvm 0.52.5",
"windows_x86_64_msvc 0.52.5",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
@@ -10239,9 +10341,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
@@ -10257,9 +10359,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
@@ -10275,15 +10377,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
@@ -10299,9 +10401,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
@@ -10317,9 +10419,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
@@ -10335,9 +10437,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
@@ -10353,9 +10455,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
@@ -10458,18 +10560,18 @@ dependencies = [
[[package]]
name = "zerocopy"
version = "0.7.34"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.34"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
@@ -10498,14 +10600,18 @@ dependencies = [
[[package]]
name = "zip"
version = "0.6.6"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
checksum = "9cc23c04387f4da0374be4533ad1208cbb091d5c11d070dfef13676ad6497164"
dependencies = [
"byteorder",
"arbitrary",
"crc32fast",
"crossbeam-utils",
"displaydoc",
"flate2",
"indexmap 2.2.6",
"num_enum",
"thiserror",
]
[[package]]
+4 -2
View File
@@ -311,8 +311,10 @@ tracing-tree = "0.2.2"
ts-rs = "7.0.0"
tungstenite = { version = "0.20.1", default-features = false }
url = "2.5"
utoipa = "4.2.0"
utoipa-swagger-ui = "6.0.0"
utoipa = "4.2"
utoipa-rapidoc = "4.0"
utoipa-swagger-ui = "7.1"
utoipauto = "0.1"
uuid = "*"
vergen = { version = "=8.3.1", default-features = false }
walkdir = "2"
+2 -1
View File
@@ -15,7 +15,8 @@ serde = { workspace = true, features = ["derive"] }
toml = "0.7.4"
url = { workspace = true }
nym-network-defaults = { path = "../network-defaults" }
nym-network-defaults = { path = "../network-defaults", features = ["utoipa"] }
[features]
default = ["dirs"]
utoipa = [ "nym-network-defaults/utoipa" ]
@@ -22,6 +22,7 @@ thiserror = { workspace = true }
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.5.0" }
serde-json-wasm = { workspace = true }
humantime-serde = { workspace = true }
utoipa = { workspace = true, optional = true }
# TO CHECK WHETHER STILL NEEDED:
log = { workspace = true }
@@ -35,5 +36,6 @@ time = { workspace = true, features = ["serde", "macros"] }
[features]
default = []
contract-testing = []
utoipa = [ "dep:utoipa" ]
schema = ["cw2"]
generate-ts = ['ts-rs']
@@ -10,6 +10,7 @@ use std::fmt::Display;
/// Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.
#[cw_serde]
#[derive(PartialOrd)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct Gateway {
/// Network address of this gateway, for example 1.1.1.1 or foo.gateway.com
pub host: String,
@@ -25,9 +26,11 @@ pub struct Gateway {
pub location: String,
/// Base58-encoded x25519 public key used for sphinx key derivation.
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub sphinx_key: SphinxKey,
/// Base58 encoded ed25519 EdDSA public key of the gateway used to derive shared keys with clients
#[cfg_attr(feature = "utoipa", schema(value_type = String))]
pub identity_key: IdentityKey,
/// The self-reported semver version of this gateway.
@@ -36,6 +39,7 @@ pub struct Gateway {
/// Basic gateway information provided by the node operator.
#[cw_serde]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct GatewayBond {
/// Original amount pledged by the operator of this node.
pub pledge_amount: Coin,
@@ -209,8 +209,10 @@ impl Display for EpochState {
ts(export_to = "ts-packages/types/src/types/rust/Interval.ts")
)]
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct Interval {
/// Monotonously increasing id of this interval.
#[cfg_attr(feature = "utoipa", schema(value_type = u32))]
id: IntervalId,
/// Number of epochs in this interval.
@@ -226,6 +228,7 @@ pub struct Interval {
current_epoch_start: OffsetDateTime,
/// Monotonously increasing id of the current epoch in this interval.
#[cfg_attr(feature = "utoipa", schema(value_type = u32))]
current_epoch_id: EpochId,
/// The duration of all epochs in this interval.
@@ -233,6 +236,7 @@ pub struct Interval {
epoch_length: Duration,
/// The total amount of elapsed epochs since the first epoch of the first interval.
#[cfg_attr(feature = "utoipa", schema(value_type = u32))]
total_elapsed_epochs: EpochId,
}
+6 -1
View File
@@ -13,8 +13,13 @@ license.workspace = true
[dependencies]
axum.workspace = true
bytes = { workspace = true }
colored.workspace = true
mime = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
serde_yaml = { workspace = true }
utoipa = { workspace = true }
tracing.workspace = true
utoipa = { workspace = true, optional = true }
[features]
utoipa = ["dep:utoipa"]
+8 -4
View File
@@ -6,9 +6,11 @@ use axum::response::{IntoResponse, Response};
use axum::Json;
use bytes::{BufMut, BytesMut};
use serde::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
#[derive(Debug, Clone, ToSchema)]
pub mod logging;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub enum FormattedResponse<T> {
Json(Json<T>),
Yaml(Yaml<T>),
@@ -26,7 +28,8 @@ where
}
}
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone, ToSchema)]
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
#[serde(rename_all = "lowercase")]
pub enum Output {
#[default]
@@ -34,7 +37,8 @@ pub enum Output {
Yaml,
}
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema)]
#[derive(Default, Debug, Serialize, Deserialize, Copy, Clone)]
#[cfg_attr(feature = "utoipa", derive(utoipa::IntoParams, utoipa::ToSchema))]
#[serde(default)]
pub struct OutputParams {
pub output: Option<Output>,
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use axum::extract::{ConnectInfo, Request};
use axum::http::header::{HOST, USER_AGENT};
use axum::http::HeaderValue;
use axum::middleware::Next;
use axum::response::IntoResponse;
use colored::Colorize;
use std::net::SocketAddr;
use std::time::Instant;
use tracing::info;
pub async fn logger(
ConnectInfo(addr): ConnectInfo<SocketAddr>,
request: Request,
next: Next,
) -> impl IntoResponse {
// TODO dz use `OriginalUri` extractor to get full URI even for nested
// routers if routes aren't logged correctly in handlers
fn header_map(header: Option<&HeaderValue>, msg: String) -> String {
header
.map(|x| x.to_str().unwrap_or(&msg).to_string())
.unwrap_or(msg)
}
let method = request.method().to_string().green();
let uri = request.uri().to_string().blue();
let agent = header_map(
request.headers().get(USER_AGENT),
"Unknown User Agent".to_string(),
);
let host = header_map(request.headers().get(HOST), "Unknown Host".to_string());
let start = Instant::now();
// run request through all middleware, incl. extractors
let res = next.run(request).await;
let time_taken = start.elapsed();
let status = res.status();
let print_status = if status.is_client_error() || status.is_server_error() {
status.to_string().red()
} else if status.is_success() {
status.to_string().green()
} else {
status.to_string().yellow()
};
let taken = "time taken".bold();
let time_taken = match time_taken.as_millis() {
ms if ms > 500 => format!("{taken}: {}", format!("{ms}ms").red()),
ms if ms > 200 => format!("{taken}: {}", format!("{ms}ms").yellow()),
ms if ms > 50 => format!("{taken}: {}", format!("{ms}ms").bright_yellow()),
ms => format!("{taken}: {ms}ms"),
};
let agent_str = "agent".bold();
info!("[{addr} -> {host}] {method} '{uri}': {print_status} {time_taken} {agent_str}: {agent}");
res
}
+3 -1
View File
@@ -13,6 +13,7 @@ log = { workspace = true, optional = true }
schemars = { workspace = true, features = ["preserve_order"], optional = true }
serde = { workspace = true, features = ["derive"], optional = true }
url = { workspace = true, optional = true }
utoipa = { workspace = true, optional = true }
# please be extremely careful when adding new dependencies because this crate is imported by the ecash contract,
# so if anything new is added, consider feature-locking it and then just adding it to default feature
@@ -20,4 +21,5 @@ url = { workspace = true, optional = true }
[features]
default = ["env", "network"]
env = ["dotenvy", "log"]
network = ["schemars", "serde", "url"]
network = ["schemars", "serde", "url"]
utoipa = [ "dep:utoipa" ]
+5
View File
@@ -8,6 +8,7 @@ use std::ops::Not;
use url::Url;
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ChainDetails {
pub bech32_account_prefix: String,
pub mix_denom: DenomDetailsOwned,
@@ -15,6 +16,7 @@ pub struct ChainDetails {
}
#[derive(Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct NymContracts {
pub mixnet_contract_address: Option<String>,
pub vesting_contract_address: Option<String>,
@@ -27,6 +29,7 @@ pub struct NymContracts {
// I wanted to use the simpler `NetworkDetails` name, but there's a clash
// with `NetworkDetails` defined in all.rs...
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct NymNetworkDetails {
pub network_name: String,
pub chain_details: ChainDetails,
@@ -293,6 +296,7 @@ impl DenomDetails {
}
#[derive(Debug, Serialize, Deserialize, Hash, Clone, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct DenomDetailsOwned {
pub base: String,
pub display: String,
@@ -321,6 +325,7 @@ impl DenomDetailsOwned {
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ValidatorDetails {
// it is assumed those values are always valid since they're being provided in our defaults file
pub nyxd_url: String,
+25 -2
View File
@@ -50,6 +50,7 @@ tokio = { workspace = true, features = [
"time",
] }
tokio-stream = { workspace = true }
tokio-util = { workspace = true }
url = { workspace = true }
ts-rs = { workspace = true, optional = true }
@@ -70,6 +71,16 @@ rocket_okapi = { workspace = true, features = ["swagger"] }
schemars = { workspace = true, features = ["preserve_order"] }
zeroize = { workspace = true }
# for axum server
axum = { workspace = true, features = ["tokio"], optional = true }
axum-extra = { workspace = true, features = ["typed-header"], optional = true }
tower-http = { workspace = true, features = ["cors", "trace"], optional = true }
utoipa = { workspace = true, features = ["axum_extras", "time"], optional = true }
utoipa-swagger-ui = { workspace = true, features = ["axum"], optional = true}
utoipauto = { workspace = true, optional = true }
tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true }
tracing = { workspace = true, optional = true }
## ephemera-specific
#actix-web = "4"
#array-bytes = "6.0.0"
@@ -101,7 +112,7 @@ cw4 = { workspace = true }
nym-dkg = { path = "../common/dkg", features = ["cw-types"] }
nym-gateway-client = { path = "../common/client-libs/gateway-client" }
nym-inclusion-probability = { path = "../common/inclusion-probability" }
nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" }
nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"]}
nym-vesting-contract-common = { path = "../common/cosmwasm-smart-contracts/vesting-contract" }
nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" }
nym-multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" }
@@ -112,14 +123,26 @@ nym-task = { path = "../common/task" }
nym-topology = { path = "../common/topology" }
nym-api-requests = { path = "nym-api-requests", features = ["rocket-traits"] }
nym-validator-client = { path = "../common/client-libs/validator-client" }
nym-bin-common = { path = "../common/bin-common", features = ["output_format"] }
nym-bin-common = { path = "../common/bin-common", features = ["output_format", "openapi"] }
nym-node-tester-utils = { path = "../common/node-tester-utils" }
nym-node-requests = { path = "../nym-node/nym-node-requests" }
nym-types = { path = "../common/types" }
nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"] }
[features]
no-reward = []
generate-ts = ["ts-rs"]
axum = ["dep:axum",
"axum-extra",
"tower-http",
"utoipa",
"utoipauto",
"tracing-subscriber",
"tracing",
"utoipa-swagger-ui",
"nym-http-api-common/utoipa",
"nym-mixnet-contract-common/utoipa"
]
[build-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
+2 -1
View File
@@ -18,6 +18,7 @@ tendermint = { workspace = true }
time = { workspace = true, features = ["serde", "parsing", "formatting"] }
thiserror.workspace = true
rocket = { workspace = true, optional = true }
utoipa.workspace = true
sha2 = "0.10.8"
@@ -31,7 +32,7 @@ nym-crypto = { path = "../../common/crypto", features = ["serde", "asymmetric"]
nym-ecash-time = { path = "../../common/ecash-time" }
nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" }
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" }
nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false }
nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false, features = ["openapi"] }
[dev-dependencies]
+17 -14
View File
@@ -20,6 +20,7 @@ use std::collections::BTreeMap;
use std::ops::Deref;
use thiserror::Error;
use time::Date;
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, Clone, JsonSchema)]
pub struct VerifyEcashTicketBody {
@@ -60,7 +61,7 @@ impl VerifyEcashCredentialBody {
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct EcashTicketVerificationResponse {
pub verified: Result<(), EcashTicketVerificationRejection>,
}
@@ -73,7 +74,7 @@ impl EcashTicketVerificationResponse {
}
}
#[derive(Debug, Error, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Error, Serialize, Deserialize, JsonSchema, ToSchema)]
pub enum EcashTicketVerificationRejection {
#[error("invalid ticket spent date. expected either today's ({today}) or yesterday's* ({yesterday}) date but got {received} instead\n*assuming it's before 1AM UTC")]
InvalidSpentDate {
@@ -155,7 +156,7 @@ impl BlindSignRequestBody {
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct BlindedSignatureResponse {
#[schemars(with = "PlaceholderJsonSchemaImpl")]
pub blinded_signature: BlindedSignature,
@@ -198,7 +199,7 @@ impl MasterVerificationKeyResponse {
}
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[derive(Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct VerificationKeyResponse {
#[schemars(with = "PlaceholderJsonSchemaImpl")]
pub key: VerificationKeyAuth,
@@ -222,25 +223,26 @@ impl CosmosAddressResponse {
}
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[derive(Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct PartialExpirationDateSignatureResponse {
pub epoch_id: u64,
#[schemars(with = "String")]
#[serde(with = "crate::helpers::date_serde")]
#[schema(value_type = String)]
pub expiration_date: Date,
#[schemars(with = "PlaceholderJsonSchemaImpl")]
pub signatures: Vec<AnnotatedExpirationDateSignature>,
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[derive(Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct PartialCoinIndicesSignatureResponse {
pub epoch_id: u64,
#[schemars(with = "PlaceholderJsonSchemaImpl")]
pub signatures: Vec<AnnotatedCoinIndexSignature>,
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[derive(Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct AggregatedExpirationDateSignatureResponse {
pub epoch_id: u64,
@@ -252,7 +254,7 @@ pub struct AggregatedExpirationDateSignatureResponse {
pub signatures: Vec<AnnotatedExpirationDateSignature>,
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[derive(Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct AggregatedCoinIndicesSignatureResponse {
pub epoch_id: u64,
#[schemars(with = "PlaceholderJsonSchemaImpl")]
@@ -350,16 +352,17 @@ impl BatchRedeemTicketsBody {
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct EcashBatchTicketRedemptionResponse {
pub proposal_accepted: bool,
}
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct SpentCredentialsResponse {
#[serde(with = "nym_serde_helpers::base64")]
#[schemars(with = "String")]
#[schema(value_type = String)]
pub bitmap: Vec<u8>,
}
@@ -369,7 +372,7 @@ impl SpentCredentialsResponse {
}
}
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct EpochCredentialsResponse {
pub epoch_id: u64,
@@ -377,20 +380,20 @@ pub struct EpochCredentialsResponse {
pub total_issued: u32,
}
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct IssuedCredentialsResponse {
// note: BTreeMap returns ordered results so it's fine to use it with pagination
pub credentials: BTreeMap<i64, IssuedTicketbookBody>,
}
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct IssuedCredentialResponse {
pub credential: Option<IssuedTicketbookBody>,
}
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)]
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct IssuedTicketbookBody {
pub credential: IssuedTicketbook,
+87 -37
View File
@@ -1,10 +1,10 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::helpers::unix_epoch;
use crate::nym_nodes::NodeRole;
use crate::pagination::PaginatedResponse;
use cosmwasm_std::{Addr, Coin, Decimal};
use cosmwasm_std::{Addr, Coin, Decimal, Uint128};
use nym_mixnet_contract_common::families::FamilyHead;
use nym_mixnet_contract_common::mixnode::MixNodeDetails;
use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams};
@@ -22,6 +22,7 @@ use std::net::IpAddr;
use std::ops::{Deref, DerefMut};
use std::{fmt, time::Duration};
use time::OffsetDateTime;
use utoipa::{IntoParams, ToResponse, ToSchema};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct RequestError {
@@ -38,6 +39,12 @@ impl RequestError {
pub fn message(&self) -> &str {
&self.message
}
pub fn empty() -> Self {
Self {
message: String::new(),
}
}
}
impl Display for RequestError {
@@ -77,7 +84,7 @@ impl MixnodeStatus {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
@@ -88,7 +95,7 @@ pub struct MixnodeCoreStatusResponse {
pub count: i32,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
@@ -99,7 +106,7 @@ pub struct GatewayCoreStatusResponse {
pub count: i32,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
@@ -109,19 +116,39 @@ pub struct MixnodeStatusResponse {
pub status: MixnodeStatus,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct NodePerformance {
#[schema(value_type = String)]
pub most_recent: Performance,
#[schema(value_type = String)]
pub last_hour: Performance,
#[schema(value_type = String)]
pub last_24h: Performance,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(ToSchema)]
#[schema(title = "MixNodeDetails")]
pub struct MixNodeDetailsSchema {
/// Basic bond information of this mixnode, such as owner address, original pledge, etc.
pub bond_information: String,
/// Details used for computation of rewarding related data.
pub rewarding_details: String,
/// Adjustments to the mixnode that are ought to happen during future epoch transitions.
pub pending_changes: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct MixNodeBondAnnotated {
#[schema(value_type = MixNodeDetailsSchema)]
pub mixnode_details: MixNodeDetails,
#[schema(value_type = String)]
pub stake_saturation: StakeSaturation,
#[schema(value_type = String)]
pub uncapped_stake_saturation: StakeSaturation,
// NOTE: the performance field is deprecated in favour of node_performance
#[schema(value_type = String)]
pub performance: Performance,
pub node_performance: NodePerformance,
pub estimated_operator_apy: Decimal,
@@ -152,7 +179,7 @@ impl MixNodeBondAnnotated {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct GatewayBondAnnotated {
pub gateway_bond: GatewayBond,
@@ -183,13 +210,16 @@ pub struct GatewayDescription {
// for now only expose what we need. this struct will evolve in the future (or be incorporated into nym-node properly)
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Serialize, Deserialize, JsonSchema, ToSchema, IntoParams)]
pub struct ComputeRewardEstParam {
#[schema(value_type = String)]
pub performance: Option<Performance>,
pub active_in_rewarded_set: Option<bool>,
pub pledge_amount: Option<u64>,
pub total_delegation: Option<u64>,
#[schema(value_type = CoinSchema)]
pub interval_operating_cost: Option<Coin>,
#[schema(value_type = String)]
pub profit_margin_percent: Option<Percent>,
}
@@ -198,7 +228,7 @@ pub struct ComputeRewardEstParam {
feature = "generate-ts",
ts(export_to = "ts-packages/types/src/types/rust/RewardEstimationResponse.ts")
)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct RewardEstimationResponse {
pub estimation: RewardEstimate,
pub reward_params: RewardingParams,
@@ -207,15 +237,16 @@ pub struct RewardEstimationResponse {
pub as_at: i64,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct UptimeResponse {
#[schema(value_type = u32)]
pub mix_id: MixId,
// The same as node_performance.last_24h. Legacy
pub avg_uptime: u8,
pub node_performance: NodePerformance,
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct GatewayUptimeResponse {
pub identity: String,
// The same as node_performance.last_24h. Legacy
@@ -223,7 +254,7 @@ pub struct GatewayUptimeResponse {
pub node_performance: NodePerformance,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
@@ -231,16 +262,18 @@ pub struct GatewayUptimeResponse {
)]
pub struct StakeSaturationResponse {
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
#[schema(value_type = String)]
pub saturation: StakeSaturation,
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
#[schema(value_type = String)]
pub uncapped_saturation: StakeSaturation,
pub as_at: i64,
}
pub type StakeSaturation = Decimal;
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
@@ -282,7 +315,7 @@ impl fmt::Display for SelectionChance {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
#[cfg_attr(
feature = "generate-ts",
@@ -303,7 +336,7 @@ impl fmt::Display for InclusionProbabilityResponse {
}
}
#[derive(Clone, Serialize, schemars::JsonSchema)]
#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)]
pub struct AllInclusionProbabilitiesResponse {
pub inclusion_probabilities: Vec<InclusionProbability>,
pub samples: u64,
@@ -313,8 +346,9 @@ pub struct AllInclusionProbabilitiesResponse {
pub as_at: i64,
}
#[derive(Clone, Serialize, schemars::JsonSchema)]
#[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)]
pub struct InclusionProbability {
#[schema(value_type = u32)]
pub mix_id: MixId,
pub in_active: f64,
pub in_reserve: f64,
@@ -322,7 +356,7 @@ pub struct InclusionProbability {
type Uptime = u8;
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct MixnodeStatusReportResponse {
pub mix_id: MixId,
pub identity: IdentityKey,
@@ -332,22 +366,26 @@ pub struct MixnodeStatusReportResponse {
pub last_day: Uptime,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct GatewayStatusReportResponse {
pub identity: String,
pub owner: String,
#[schema(value_type = u8)]
pub most_recent: Uptime,
#[schema(value_type = u8)]
pub last_hour: Uptime,
#[schema(value_type = u8)]
pub last_day: Uptime,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct HistoricalUptimeResponse {
pub date: String,
#[schema(value_type = u8)]
pub uptime: Uptime,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct MixnodeUptimeHistoryResponse {
pub mix_id: MixId,
pub identity: String,
@@ -355,22 +393,34 @@ pub struct MixnodeUptimeHistoryResponse {
pub history: Vec<HistoricalUptimeResponse>,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct GatewayUptimeHistoryResponse {
pub identity: String,
pub owner: String,
pub history: Vec<HistoricalUptimeResponse>,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(ToSchema)]
#[schema(title = "Coin")]
pub struct CoinSchema {
pub denom: String,
#[schema(value_type = String)]
pub amount: Uint128,
}
#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema, ToResponse)]
pub struct CirculatingSupplyResponse {
#[schema(value_type = CoinSchema)]
pub total_supply: Coin,
#[schema(value_type = CoinSchema)]
pub mixmining_reserve: Coin,
#[schema(value_type = CoinSchema)]
pub vesting_tokens: Coin,
#[schema(value_type = CoinSchema)]
pub circulating_supply: Coin,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct HostInformation {
pub ip_address: Vec<IpAddr>,
pub hostname: Option<String>,
@@ -387,7 +437,7 @@ impl From<nym_node_requests::api::v1::node::models::HostInformation> for HostInf
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct HostKeys {
pub ed25519: String,
pub x25519: String,
@@ -402,7 +452,7 @@ impl From<nym_node_requests::api::v1::node::models::HostKeys> for HostKeys {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct WebSockets {
pub ws_port: u16,
@@ -426,7 +476,7 @@ where
}
// for all intents and purposes it's just OffsetDateTime, but we need JsonSchema...
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, ToSchema)]
pub struct OffsetDateTimeJsonSchemaWrapper(
#[serde(
default = "unix_epoch",
@@ -494,7 +544,7 @@ impl JsonSchema for OffsetDateTimeJsonSchemaWrapper {
}
// this struct is getting quite bloated...
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NymNodeDescription {
#[serde(default)]
pub last_polled: OffsetDateTimeJsonSchemaWrapper,
@@ -528,7 +578,7 @@ fn default_node_role() -> NodeRole {
NodeRole::Inactive
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct DescribedGateway {
pub bond: GatewayBond,
pub self_described: Option<NymNodeDescription>,
@@ -543,7 +593,7 @@ impl From<GatewayBond> for DescribedGateway {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct DescribedMixNode {
pub bond: MixNodeBond,
pub self_described: Option<NymNodeDescription>,
@@ -558,7 +608,7 @@ impl From<MixNodeBond> for DescribedMixNode {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct NetworkRequesterDetails {
/// address of the embedded network requester
pub address: String,
@@ -567,25 +617,25 @@ pub struct NetworkRequesterDetails {
pub uses_exit_policy: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct IpPacketRouterDetails {
/// address of the embedded ip packet router
pub address: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct AuthenticatorDetails {
/// address of the embedded authenticator
pub address: String,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct ApiHealthResponse {
pub status: ApiStatus,
pub uptime: u64,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum ApiStatus {
Up,
@@ -606,7 +656,7 @@ impl ApiStatus {
}
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct SignerInformationResponse {
pub cosmos_address: String,
@@ -631,7 +681,7 @@ pub struct TestRoute {
pub layer3: TestNode,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct PartialTestResult {
pub monitor_run_id: i64,
pub timestamp: i64,
+10 -6
View File
@@ -8,6 +8,7 @@ use nym_mixnet_contract_common::reward_params::Performance;
use nym_mixnet_contract_common::MixId;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use utoipa::ToSchema;
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
pub struct CachedNodesResponse<T> {
@@ -15,7 +16,7 @@ pub struct CachedNodesResponse<T> {
pub nodes: Vec<T>,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
#[serde(rename_all = "kebab-case")]
#[cfg_attr(feature = "rocket-traits", derive(rocket::form::FromFormField))]
pub enum NodeRoleQueryParam {
@@ -28,7 +29,7 @@ pub enum NodeRoleQueryParam {
ExitGateway,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub enum NodeRole {
// a properly active mixnode
Mixnode {
@@ -47,7 +48,7 @@ pub enum NodeRole {
Inactive,
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct BasicEntryInformation {
pub hostname: Option<String>,
@@ -58,13 +59,15 @@ pub struct BasicEntryInformation {
type NodeId = MixId;
// the bare minimum information needed to construct sphinx packets
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct SkimmedNode {
// in directory v3 all nodes (mixnodes AND gateways) will have a unique id
// but to keep structure consistent, introduce this field now
#[schema(value_type = u32)]
pub node_id: NodeId,
pub ed25519_identity_pubkey: String,
#[schema(value_type = Vec<String>)]
pub ip_addresses: Vec<IpAddr>,
// TODO: to be deprecated in favour of well-known hardcoded port for everyone
@@ -74,6 +77,7 @@ pub struct SkimmedNode {
pub entry: Option<BasicEntryInformation>,
/// Average node performance in last 24h period
#[schema(value_type = String)]
pub performance: Performance,
}
@@ -143,14 +147,14 @@ impl<'a> From<&'a GatewayBondAnnotated> for SkimmedNode {
// an intermediate variant that exposes additional data such as noise keys but without
// the full fat of the self-described data
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct SemiSkimmedNode {
pub basic: SkimmedNode,
pub x25519_noise_pubkey: String,
// pub location:
}
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)]
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
pub struct FullFatNode {
pub expanded: SemiSkimmedNode,
+3 -2
View File
@@ -3,15 +3,16 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
#[derive(Serialize, Deserialize, JsonSchema)]
#[derive(Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct Pagination {
pub total: usize,
pub page: u32,
pub size: usize,
}
#[derive(Serialize, Deserialize, JsonSchema)]
#[derive(Serialize, Deserialize, JsonSchema, ToSchema)]
pub struct PaginatedResponse<T> {
pub pagination: Pagination,
pub data: Vec<T>,
+2 -1
View File
@@ -50,7 +50,7 @@ pub(crate) struct CirculatingSupplyCache {
}
impl CirculatingSupplyCache {
fn new(mix_denom: String) -> CirculatingSupplyCache {
pub(crate) fn new(mix_denom: String) -> CirculatingSupplyCache {
CirculatingSupplyCache {
initialised: Arc::new(AtomicBool::new(false)),
data: Arc::new(RwLock::new(CirculatingSupplyCacheData::new(mix_denom))),
@@ -67,6 +67,7 @@ impl CirculatingSupplyCache {
}
}
#[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")]
pub(crate) fn stage(mix_denom: String) -> AdHoc {
AdHoc::on_ignite("Circulating Supply Cache Stage", |rocket| async {
rocket.manage(Self::new(mix_denom))
@@ -0,0 +1,102 @@
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::{
node_status_api::models::{AxumErrorResponse, AxumResult},
v2::AxumAppState,
};
use axum::{extract, Router};
use nym_api_requests::models::CirculatingSupplyResponse;
use nym_validator_client::nyxd::Coin;
pub(crate) fn circulating_supply_routes() -> Router<AxumAppState> {
Router::new()
.route("/", axum::routing::get(get_full_circulating_supply))
.route(
"/total-supply-value",
axum::routing::get(get_circulating_supply),
)
.route(
"/circulating-supply-value",
axum::routing::get(get_total_supply),
)
}
#[utoipa::path(
tag = "circulating-supply",
get,
path = "/v1/circulating-supply",
responses(
(status = 200, body = CirculatingSupplyResponse)
)
)]
async fn get_full_circulating_supply(
extract::State(state): extract::State<AxumAppState>,
) -> AxumResult<axum::Json<CirculatingSupplyResponse>> {
match state
.circulating_supply_cache()
.get_circulating_supply()
.await
{
Some(value) => Ok(value.into()),
None => Err(AxumErrorResponse::internal_msg("unavailable")),
}
}
#[utoipa::path(
tag = "circulating-supply",
get,
path = "/v1/circulating-supply/total-supply-value",
responses(
(status = 200, body = [f64])
)
)]
async fn get_total_supply(
extract::State(state): extract::State<AxumAppState>,
) -> AxumResult<axum::Json<f64>> {
let full_circulating_supply = match state
.circulating_supply_cache()
.get_circulating_supply()
.await
{
Some(res) => res,
None => return Err(AxumErrorResponse::internal_msg("unavailable")),
};
Ok(unym_coin_to_float_unym(full_circulating_supply.total_supply.into()).into())
}
#[utoipa::path(
tag = "circulating-supply",
get,
path = "/v1/circulating-supply/circulating-supply-value",
responses(
(status = 200, body = [f64])
)
)]
async fn get_circulating_supply(
extract::State(state): extract::State<AxumAppState>,
) -> AxumResult<axum::Json<f64>> {
let full_circulating_supply = match state
.circulating_supply_cache()
.get_circulating_supply()
.await
{
Some(res) => res,
None => return Err(AxumErrorResponse::internal_msg("unavailable")),
};
Ok(unym_coin_to_float_unym(full_circulating_supply.circulating_supply.into()).into())
}
// TODO: this is not the best place to put it, it should be more centralised,
// but for a quick fix, that's good enough for now...
// (for proper solution we should be managing `NymNetworkDetails` via rocket and grabbing display exponent
// value from the mix denom here.
const UNYM_RATIO: f64 = 1000000.;
fn unym_coin_to_float_unym(coin: Coin) -> f64 {
// our total supply can't exceed 1B so an overflow here is impossible
// (if it happened, then we SHOULD crash)
coin.amount as f64 / UNYM_RATIO
}
@@ -11,6 +11,8 @@ use crate::support::{config, nyxd};
use self::cache::refresher::CirculatingSupplyCacheRefresher;
pub(crate) mod cache;
#[cfg(feature = "axum")]
pub(crate) mod handlers;
pub(crate) mod routes;
/// Merges the routes with http information and returns it to Rocket for serving
+7 -7
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::circulating_supply_api::cache::CirculatingSupplyCache;
use crate::node_status_api::models::ErrorResponse;
use crate::node_status_api::models::RocketErrorResponse;
use nym_api_requests::models::CirculatingSupplyResponse;
use nym_validator_client::nyxd::Coin;
use rocket::http::Status;
@@ -26,10 +26,10 @@ fn unym_coin_to_float_unym(coin: Coin) -> f64 {
#[get("/circulating-supply")]
pub(crate) async fn get_full_circulating_supply(
cache: &State<CirculatingSupplyCache>,
) -> Result<Json<CirculatingSupplyResponse>, ErrorResponse> {
) -> Result<Json<CirculatingSupplyResponse>, RocketErrorResponse> {
match cache.get_circulating_supply().await {
Some(value) => Ok(Json(value)),
None => Err(ErrorResponse::new(
None => Err(RocketErrorResponse::new(
"unavailable",
Status::InternalServerError,
)),
@@ -40,11 +40,11 @@ pub(crate) async fn get_full_circulating_supply(
#[get("/circulating-supply/total-supply-value")]
pub(crate) async fn get_total_supply(
cache: &State<CirculatingSupplyCache>,
) -> Result<Json<f64>, ErrorResponse> {
) -> Result<Json<f64>, RocketErrorResponse> {
let full_circulating_supply = match cache.get_circulating_supply().await {
Some(res) => res,
None => {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"unavailable",
Status::InternalServerError,
))
@@ -60,11 +60,11 @@ pub(crate) async fn get_total_supply(
#[get("/circulating-supply/circulating-supply-value")]
pub(crate) async fn get_circulating_supply(
cache: &State<CirculatingSupplyCache>,
) -> Result<Json<f64>, ErrorResponse> {
) -> Result<Json<f64>, RocketErrorResponse> {
let full_circulating_supply = match cache.get_circulating_supply().await {
Some(res) => res,
None => {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"unavailable",
Status::InternalServerError,
))
@@ -0,0 +1,143 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::ecash::api_routes::helpers::EpochIdParam;
use crate::ecash::error::EcashError;
use crate::ecash::state::EcashState;
use crate::node_status_api::models::AxumResult;
use crate::v2::AxumAppState;
use axum::extract::Path;
use axum::{Json, Router};
use log::trace;
use nym_api_requests::ecash::models::{
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
};
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_ecash_time::{cred_exp_date, EcashTime};
use nym_validator_client::nym_api::rfc_3339_date;
use serde::Deserialize;
use std::sync::Arc;
use time::Date;
use utoipa::IntoParams;
/// routes with globally aggregated keys, signatures, etc.
pub(crate) fn aggregation_routes(ecash_state: Arc<EcashState>) -> Router<AxumAppState> {
Router::new()
.route(
"/master-verification-key:epoch_id",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|epoch_id| master_verification_key(epoch_id, ecash_state)
}),
)
.route(
"/aggregated-expiration-date-signatures:expiration_date",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|expiration_date| expiration_date_signatures(expiration_date, ecash_state)
}),
)
.route(
"/aggregated-coin-indices-signatures:epoch_id",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|epoch_id| coin_indices_signatures(epoch_id, ecash_state)
}),
)
}
#[utoipa::path(
tag = "Ecash Global Data",
get,
params(
EpochIdParam
),
path = "/v1/ecash/master-verification-key/{epoch_id}",
responses(
(status = 200, body = VerificationKeyResponse)
)
)]
async fn master_verification_key(
Path(EpochIdParam { epoch_id }): Path<EpochIdParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<VerificationKeyResponse>> {
trace!("aggregated_verification_key request");
// see if we're not in the middle of new dkg
state.ensure_dkg_not_in_progress().await?;
let key = state.master_verification_key(epoch_id).await?;
Ok(Json(VerificationKeyResponse::new(key.clone())))
}
#[derive(Deserialize, IntoParams)]
#[into_params(parameter_in = Path)]
struct ExpirationDateParam {
expiration_date: Option<String>,
}
#[utoipa::path(
tag = "Ecash Global Data",
get,
params(
ExpirationDateParam
),
path = "/v1/ecash/aggregated-expiration-date-signatures/{epoch_id}",
responses(
(status = 200, body = AggregatedExpirationDateSignatureResponse)
)
)]
async fn expiration_date_signatures(
Path(ExpirationDateParam { expiration_date }): Path<ExpirationDateParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<AggregatedExpirationDateSignatureResponse>> {
trace!("aggregated_expiration_date_signatures request");
let expiration_date = match expiration_date {
None => cred_exp_date().ecash_date(),
Some(raw) => Date::parse(&raw, &rfc_3339_date())
.map_err(|_| EcashError::MalformedExpirationDate { raw })?,
};
// see if we're not in the middle of new dkg
state.ensure_dkg_not_in_progress().await?;
let expiration_date_signatures = state
.master_expiration_date_signatures(expiration_date)
.await?;
Ok(Json(AggregatedExpirationDateSignatureResponse {
epoch_id: expiration_date_signatures.epoch_id,
expiration_date,
signatures: expiration_date_signatures.signatures.clone(),
}))
}
#[utoipa::path(
tag = "Ecash Global Data",
get,
params(
EpochIdParam
),
path = "/v1/ecash/aggregated-coin-indices-signatures/{epoch_id}",
responses(
(status = 200, body = AggregatedCoinIndicesSignatureResponse)
)
)]
async fn coin_indices_signatures(
Path(EpochIdParam { epoch_id }): Path<EpochIdParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<AggregatedCoinIndicesSignatureResponse>> {
trace!("aggregated_coin_indices_signatures request");
// see if we're not in the middle of new dkg
state.ensure_dkg_not_in_progress().await?;
let coin_indices_signatures = state.master_coin_index_signatures(epoch_id).await?;
Ok(Json(AggregatedCoinIndicesSignatureResponse {
epoch_id: coin_indices_signatures.epoch_id,
signatures: coin_indices_signatures.signatures.clone(),
}))
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::ecash::api_routes::aggregation_axum::aggregation_routes;
use crate::ecash::api_routes::issued_axum::issued_routes;
use crate::ecash::api_routes::partial_signing_axum::partial_signing_routes;
use crate::ecash::api_routes::spending_axum::spending_routes;
use crate::ecash::state::EcashState;
use crate::v2::AxumAppState;
use axum::Router;
use std::sync::Arc;
pub(crate) fn ecash_routes(ecash_state: Arc<EcashState>) -> Router<AxumAppState> {
Router::new()
.merge(aggregation_routes(Arc::clone(&ecash_state)))
.merge(issued_routes(Arc::clone(&ecash_state)))
.merge(partial_signing_routes(Arc::clone(&ecash_state)))
.merge(spending_routes(Arc::clone(&ecash_state)))
}
+7
View File
@@ -26,3 +26,10 @@ pub(crate) fn build_credentials_response(
Ok(IssuedCredentialsResponse { credentials })
}
#[cfg(feature = "axum")]
#[derive(serde::Deserialize, utoipa::IntoParams)]
#[into_params(parameter_in = Path)]
pub(super) struct EpochIdParam {
pub(super) epoch_id: Option<u64>,
}
+147
View File
@@ -0,0 +1,147 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::ecash::api_routes::helpers::build_credentials_response;
use crate::ecash::error::EcashError;
use crate::ecash::state::EcashState;
use crate::ecash::storage::EcashStorageExt;
use crate::node_status_api::models::AxumResult;
use crate::v2::AxumAppState;
use axum::extract::Path;
use axum::{Json, Router};
use nym_api_requests::ecash::models::{
EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse,
};
use nym_api_requests::ecash::CredentialsRequestBody;
use serde::Deserialize;
use std::sync::Arc;
use utoipa::IntoParams;
pub(crate) fn issued_routes(ecash_state: Arc<EcashState>) -> Router<AxumAppState> {
Router::new()
.route(
"/epoch-credentials/:epoch",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|epoch| epoch_credentials(epoch, ecash_state)
}),
)
.route(
"/issued-credential/:id",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|id| issued_credential(id, ecash_state)
}),
)
.route(
"/issued-credentials",
axum::routing::post({
let ecash_state = Arc::clone(&ecash_state);
|body| issued_credentials(body, ecash_state)
}),
)
}
#[derive(Deserialize, IntoParams)]
#[into_params(parameter_in = Path)]
struct EpochParam {
epoch: u64,
}
#[utoipa::path(
tag = "Ecash",
get,
params(
EpochParam
),
path = "/v1/ecash/epoch-credentials/{epoch}",
responses(
(status = 200, body = EpochCredentialsResponse)
)
)]
async fn epoch_credentials(
Path(EpochParam { epoch }): Path<EpochParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<EpochCredentialsResponse>> {
let issued = state.aux.storage.get_epoch_credentials(epoch).await?;
let response = if let Some(issued) = issued {
issued.into()
} else {
EpochCredentialsResponse {
epoch_id: epoch,
first_epoch_credential_id: None,
total_issued: 0,
}
};
Ok(Json(response))
}
#[derive(Deserialize, IntoParams)]
#[into_params(parameter_in = Path)]
struct IdParam {
id: i64,
}
#[utoipa::path(
tag = "Ecash",
get,
params(
IdParam
),
path = "/v1/ecash/issued-credential/{id}",
responses(
(status = 200, body = IssuedCredentialResponse)
)
)]
async fn issued_credential(
Path(IdParam { id }): Path<IdParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<IssuedCredentialResponse>> {
let issued = state.aux.storage.get_issued_credential(id).await?;
let credential = if let Some(issued) = issued {
Some(issued.try_into()?)
} else {
None
};
Ok(Json(IssuedCredentialResponse { credential }))
}
#[utoipa::path(
tag = "Ecash",
post,
request_body = CredentialsRequestBody,
path = "/v1/ecash/issued-credentials",
responses(
(status = 200, body = IssuedCredentialsResponse)
)
)]
async fn issued_credentials(
Json(params): Json<CredentialsRequestBody>,
state: Arc<EcashState>,
) -> AxumResult<Json<IssuedCredentialsResponse>> {
if params.pagination.is_some() && !params.credential_ids.is_empty() {
return Err(EcashError::InvalidQueryArguments.into());
}
let credentials = if let Some(pagination) = params.pagination {
state
.aux
.storage
.get_issued_credentials_paged(pagination)
.await?
} else {
state
.aux
.storage
.get_issued_credentials(params.credential_ids)
.await?
};
build_credentials_response(credentials)
.map(Json)
.map_err(From::from)
}
+10
View File
@@ -6,3 +6,13 @@ mod helpers;
pub(crate) mod issued;
pub(crate) mod partial_signing;
pub(crate) mod spending;
cfg_if::cfg_if! {
if #[cfg(feature = "axum")] {
pub(crate) mod aggregation_axum;
pub(crate) mod handlers;
pub(crate) mod issued_axum;
pub(crate) mod partial_signing_axum;
pub(crate) mod spending_axum;
}
}
@@ -0,0 +1,178 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::ecash::api_routes::helpers::EpochIdParam;
use crate::ecash::error::EcashError;
use crate::ecash::helpers::blind_sign;
use crate::ecash::state::EcashState;
use crate::node_status_api::models::AxumResult;
use crate::v2::AxumAppState;
use axum::extract::Path;
use axum::{Json, Router};
use nym_api_requests::ecash::{
BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse,
PartialExpirationDateSignatureResponse,
};
use nym_ecash_time::{cred_exp_date, EcashTime};
use nym_validator_client::nym_api::rfc_3339_date;
use serde::Deserialize;
use std::ops::Deref;
use std::sync::Arc;
use time::Date;
use utoipa::IntoParams;
pub(crate) fn partial_signing_routes(ecash_state: Arc<EcashState>) -> Router<AxumAppState> {
Router::new()
.route(
"/blind-sign",
axum::routing::post({
let ecash_state = Arc::clone(&ecash_state);
|body| post_blind_sign(body, ecash_state)
}),
)
.route(
"/partial-expiration-date-signatures:expiration_date",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|expiration_date| partial_expiration_date_signatures(expiration_date, ecash_state)
}),
)
.route(
"/partial-coin-indices-signatures:epoch_id",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|epoch_id| partial_coin_indices_signatures(epoch_id, ecash_state)
}),
)
}
#[utoipa::path(
tag = "Ecash",
post,
request_body = BlindSignRequestBody,
path = "/v1/ecash/blind-sign",
responses(
(status = 200, body = BlindedSignatureResponse)
)
)]
async fn post_blind_sign(
Json(blind_sign_request_body): Json<BlindSignRequestBody>,
state: Arc<EcashState>,
) -> AxumResult<Json<BlindedSignatureResponse>> {
debug!("Received blind sign request");
trace!("body: {:?}", blind_sign_request_body);
// check if we have the signing key available
debug!("checking if we actually have ecash keys derived...");
let signing_key = state.ecash_signing_key().await?;
// basic check of expiration date validity
if blind_sign_request_body.expiration_date > cred_exp_date().ecash_date() {
return Err(EcashError::ExpirationDateTooLate.into());
}
// see if we're not in the middle of new dkg
state.ensure_dkg_not_in_progress().await?;
// check if we already issued a credential for this deposit
let deposit_id = blind_sign_request_body.deposit_id;
debug!(
"checking if we have already issued credential for this deposit (deposit_id: {deposit_id})",
);
if let Some(blinded_signature) = state.already_issued(deposit_id).await? {
return Ok(Json(BlindedSignatureResponse { blinded_signature }));
}
//check if account was blacklisted
let pub_key_bs58 = blind_sign_request_body.ecash_pubkey.to_base58_string();
state.aux.ensure_not_blacklisted(&pub_key_bs58).await?;
// get the deposit details of the claimed id
debug!("getting deposit details from the chain");
let deposit = state.get_deposit(deposit_id).await?;
// check validity of the request
debug!("fully validating received request");
state
.validate_request(&blind_sign_request_body, deposit)
.await?;
// produce the partial signature
debug!("producing the partial credential");
let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.deref())?;
// store the information locally
debug!("storing the issued credential in the database");
state
.store_issued_credential(blind_sign_request_body, &blinded_signature)
.await?;
// finally return the credential to the client
Ok(Json(BlindedSignatureResponse { blinded_signature }))
}
#[derive(Deserialize, IntoParams)]
struct ExpirationDateParam {
expiration_date: Option<String>,
}
#[utoipa::path(
tag = "Ecash",
get,
params(
ExpirationDateParam
),
path = "/v1/ecash/partial-expiration-date-signatures/{expiration_date}",
responses(
(status = 200, body = PartialExpirationDateSignatureResponse)
)
)]
async fn partial_expiration_date_signatures(
Path(ExpirationDateParam { expiration_date }): Path<ExpirationDateParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<PartialExpirationDateSignatureResponse>> {
let expiration_date = match expiration_date {
None => cred_exp_date().ecash_date(),
Some(raw) => Date::parse(&raw, &rfc_3339_date())
.map_err(|_| EcashError::MalformedExpirationDate { raw })?,
};
// see if we're not in the middle of new dkg
state.ensure_dkg_not_in_progress().await?;
let expiration_date_signatures = state
.partial_expiration_date_signatures(expiration_date)
.await?;
Ok(Json(PartialExpirationDateSignatureResponse {
epoch_id: expiration_date_signatures.epoch_id,
expiration_date,
signatures: expiration_date_signatures.signatures.clone(),
}))
}
#[utoipa::path(
tag = "Ecash",
get,
params(
EpochIdParam
),
path = "/v1/ecash/partial-coin-indices-signatures/{epoch_id}",
responses(
(status = 200, body = PartialExpirationDateSignatureResponse)
)
)]
async fn partial_coin_indices_signatures(
Path(EpochIdParam { epoch_id }): Path<EpochIdParam>,
state: Arc<EcashState>,
) -> AxumResult<Json<PartialCoinIndicesSignatureResponse>> {
// see if we're not in the middle of new dkg
state.ensure_dkg_not_in_progress().await?;
let coin_indices_signatures = state.partial_coin_index_signatures(epoch_id).await?;
Ok(Json(PartialCoinIndicesSignatureResponse {
epoch_id: coin_indices_signatures.epoch_id,
signatures: coin_indices_signatures.signatures.clone(),
}))
}
@@ -0,0 +1,245 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::ecash::error::EcashError;
use crate::ecash::state::EcashState;
use crate::node_status_api::models::AxumResult;
use crate::v2::AxumAppState;
use axum::{Json, Router};
use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY;
use nym_api_requests::ecash::models::{
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationRejection,
EcashTicketVerificationResponse, SpentCredentialsResponse, VerifyEcashTicketBody,
};
use nym_compact_ecash::identify::IdentifyResult;
use nym_ecash_time::EcashTime;
use std::collections::HashSet;
use std::ops::Deref;
use std::sync::Arc;
use time::macros::time;
use time::{OffsetDateTime, Time};
pub(crate) fn spending_routes(ecash_state: Arc<EcashState>) -> Router<AxumAppState> {
Router::new()
.route(
"/verify-ecash-ticket",
axum::routing::post({
let ecash_state = Arc::clone(&ecash_state);
|body| verify_ticket(body, ecash_state)
}),
)
.route(
"/batch-redeem-ecash-tickets",
axum::routing::post({
let ecash_state = Arc::clone(&ecash_state);
|body| batch_redeem_tickets(body, ecash_state)
}),
)
.route(
"/double-spending-filter-v1",
axum::routing::get({
let ecash_state = Arc::clone(&ecash_state);
|| double_spending_filter_v1(ecash_state)
}),
)
}
const ONE_AM: Time = time!(1:00);
fn reject_ticket(
reason: EcashTicketVerificationRejection,
) -> AxumResult<Json<EcashTicketVerificationResponse>> {
Ok(Json(EcashTicketVerificationResponse::reject(reason)))
}
// TODO: optimise it; for now it's just dummy split of the original `verify_offline_credential`
// introduce bloomfilter checks without touching storage first, etc.
#[utoipa::path(
tag = "Ecash",
post,
request_body = VerifyEcashTicketBody,
path = "/v1/ecash/verify-ecash-ticket",
responses(
(status = 200, body = EcashTicketVerificationResponse)
)
)]
async fn verify_ticket(
// TODO in the future: make it send binary data rather than json
Json(verify_ticket_body): Json<VerifyEcashTicketBody>,
state: Arc<EcashState>,
) -> AxumResult<Json<EcashTicketVerificationResponse>> {
let credential_data = &verify_ticket_body.credential;
let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr;
// easy check: is there only a single payment attached?
if credential_data.payment.spend_value != 1 {
return reject_ticket(EcashTicketVerificationRejection::MultipleTickets);
}
let sn = &credential_data.encoded_serial_number();
let spend_date = credential_data.spend_date;
let epoch_id = credential_data.epoch_id;
let now = OffsetDateTime::now_utc();
let today_ecash = now.ecash_date();
#[allow(clippy::unwrap_used)]
let yesterday_ecash = today_ecash.previous_day().unwrap();
// only accept yesterday date if we're near the day transition, i.e. before 1AM UTC
if spend_date != today_ecash && now.time() > ONE_AM && spend_date != yesterday_ecash {
return reject_ticket(EcashTicketVerificationRejection::InvalidSpentDate {
today: today_ecash,
yesterday: yesterday_ecash,
received: spend_date,
});
}
// check the bloomfilter for obvious double-spending so that we wouldn't need to waste time on crypto verification
// TODO: when blacklisting is implemented, this should get removed
if state.check_bloomfilter(sn).await {
return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket);
}
// actual double spend detection with storage
if let Some(previous_payment) = state.get_ticket_data_by_serial_number(sn).await? {
match nym_compact_ecash::identify::identify(
&credential_data.payment,
&previous_payment.payment,
credential_data.pay_info,
previous_payment.pay_info,
) {
IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message?
IdentifyResult::DuplicatePayInfo(_) => {
log::warn!("Identical payInfo");
return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket);
}
IdentifyResult::DoubleSpendingPublicKeys(pub_key) => {
//Actual double spending
log::warn!(
"Double spending attempt for key {}",
pub_key.to_base58_string()
);
log::error!("UNIMPLEMENTED: blacklisting the double spend key");
return reject_ticket(EcashTicketVerificationRejection::DoubleSpend);
}
}
}
let verification_key = state.master_verification_key(Some(epoch_id)).await?;
// perform actual crypto verification
if credential_data.verify(&verification_key).is_err() {
return reject_ticket(EcashTicketVerificationRejection::InvalidTicket);
}
// finally get EXCLUSIVE lock on the bloomfilter, check if for the final time and insert the SN
let was_present = state
.update_bloomfilter(sn, spend_date, today_ecash)
.await?;
if was_present {
return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket);
}
//store credential
state
.store_verified_ticket(credential_data, gateway_cosmos_addr)
.await?;
Ok(Json(EcashTicketVerificationResponse { verified: Ok(()) }))
}
// // for particular SN returns what gateway has submitted it and whether it has been verified correctly
// async fn credential_status() -> ! {
// todo!()
// }
#[utoipa::path(
tag = "Ecash",
post,
request_body = BatchRedeemTicketsBody,
path = "/v1/ecash/batch-redeem-ecash-tickets",
responses(
(status = 200, body = EcashBatchTicketRedemptionResponse)
)
)]
async fn batch_redeem_tickets(
// TODO in the future: make it send binary data rather than json
Json(batch_redeem_credentials_body): Json<BatchRedeemTicketsBody>,
state: Arc<EcashState>,
) -> AxumResult<Json<EcashBatchTicketRedemptionResponse>> {
// 1. see if that gateway has even submitted any tickets
let Some(provider_info) = state
.get_ticket_provider(batch_redeem_credentials_body.gateway_cosmos_addr.as_ref())
.await?
else {
return Err(EcashError::NotTicketsProvided.into());
};
// 2. check if the gateway is not trying to spam the redemption requests
// (we have to protect our poor chain)
if let Some(last_redemption) = provider_info.last_batch_verification {
let now = OffsetDateTime::now_utc();
let next_allowed = last_redemption + MIN_BATCH_REDEMPTION_DELAY;
if next_allowed > now {
return Err(EcashError::TooFrequentRedemption {
last_redemption,
next_allowed,
}
.into());
}
}
// 3. verify the request digest
if !batch_redeem_credentials_body.verify_digest() {
return Err(EcashError::MismatchedRequestDigest.into());
}
// 4. verify the associated on-chain proposal (whether it's made by correct sender, has valid messages, etc.)
state
.validate_redemption_proposal(&batch_redeem_credentials_body)
.await?;
let proposal_id = batch_redeem_credentials_body.proposal_id;
let received = batch_redeem_credentials_body.included_serial_numbers;
// 5. check if **every** serial number included in the request has been verified by us
// if we have more than requested, tough luck, they're going to lose them
let verified = state.get_redeemable_tickets(provider_info).await?;
let verified_tickets: HashSet<_> = verified.iter().map(|sn| sn.deref()).collect();
for sn in &received {
if !verified_tickets.contains(sn.deref()) {
return Err(EcashError::TicketNotVerified {
serial_number_bs58: bs58::encode(sn).into_string(),
}
.into());
}
}
// TODO: offload it to separate task with work queue and batching (of tx messages) to vote for multiple proposals in the same tx
state.accept_proposal(proposal_id).await?;
Ok(Json(EcashBatchTicketRedemptionResponse {
proposal_accepted: true,
}))
}
// explicitly mark it as v1 in the URL because the response type WILL change;
// it will probably be compressed bincode or something
#[utoipa::path(
tag = "Ecash",
get,
path = "/v1/ecash/double-spending-filter-v1",
responses(
(status = 200, body = SpentCredentialsResponse)
)
)]
async fn double_spending_filter_v1(
state: Arc<EcashState>,
) -> AxumResult<Json<SpentCredentialsResponse>> {
let spent_credentials_export = state.get_bloomfilter_bytes().await;
Ok(Json(SpentCredentialsResponse::new(
spent_credentials_export,
)))
}
+3 -6
View File
@@ -268,14 +268,11 @@ impl RewardedSetUpdater {
pub(crate) fn start(
nyxd_client: Client,
nym_contract_cache: &NymContractCache,
storage: &NymApiStorage,
storage: NymApiStorage,
shutdown: &TaskManager,
) {
let mut rewarded_set_updater = RewardedSetUpdater::new(
nyxd_client,
nym_contract_cache.to_owned(),
storage.to_owned(),
);
let mut rewarded_set_updater =
RewardedSetUpdater::new(nyxd_client, nym_contract_cache.to_owned(), storage);
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { rewarded_set_updater.run(shutdown_listener).await });
}
+16 -8
View File
@@ -1,6 +1,9 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
// TODO rocket remove
#![allow(deprecated)]
#[macro_use]
extern crate rocket;
@@ -41,11 +44,15 @@ pub(crate) mod nym_nodes;
mod status;
pub(crate) mod support;
#[cfg(feature = "axum")]
mod v2;
struct ShutdownHandles {
task_manager_handle: TaskManager,
rocket_handle: rocket::Shutdown,
}
// TODO rocket: remove all such Todos once rocket is phased out completely
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
cfg_if::cfg_if! {if #[cfg(feature = "console-subscriber")] {
@@ -54,6 +61,7 @@ async fn main() -> Result<(), anyhow::Error> {
}}
setup_logging();
// TODO rocket: replace with tracing logger once rocket is eliminated from code
info!("Starting nym api...");
@@ -107,7 +115,11 @@ async fn start_nym_api_tasks(config: Config) -> anyhow::Result<ShutdownHandles>
let nym_contract_cache_state = rocket.state::<NymContractCache>().unwrap();
let node_status_cache_state = rocket.state::<NodeStatusCache>().unwrap();
let circulating_supply_cache_state = rocket.state::<CirculatingSupplyCache>().unwrap();
let maybe_storage = rocket.state::<NymApiStorage>();
let storage = if let Some(storage) = rocket.state::<NymApiStorage>() {
storage.to_owned()
} else {
storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?
};
let described_nodes_state = rocket.state::<SharedCache<DescribedNodes>>().unwrap();
// start note describe cache refresher
@@ -134,7 +146,7 @@ async fn start_nym_api_tasks(config: Config) -> anyhow::Result<ShutdownHandles>
&config.node_status_api,
nym_contract_cache_state,
node_status_cache_state,
maybe_storage,
storage.to_owned(),
nym_contract_cache_listener,
&shutdown,
);
@@ -163,19 +175,16 @@ async fn start_nym_api_tasks(config: Config) -> anyhow::Result<ShutdownHandles>
// and then only start the uptime updater (and the monitor itself, duh)
// if the monitoring if it's enabled
if config.network_monitor.enabled {
// if network monitor is enabled, the storage MUST BE available
let storage = maybe_storage.unwrap();
network_monitor::start::<SphinxMessageReceiver>(
&config.network_monitor,
nym_contract_cache_state,
storage,
&storage,
nyxd_client.clone(),
&shutdown,
)
.await;
HistoricalUptimeUpdater::start(storage, &shutdown);
HistoricalUptimeUpdater::start(storage.to_owned(), &shutdown);
// start 'rewarding' if its enabled
if config.rewarding.enabled {
@@ -183,7 +192,6 @@ async fn start_nym_api_tasks(config: Config) -> anyhow::Result<ShutdownHandles>
RewardedSetUpdater::start(nyxd_client, nym_contract_cache_state, storage, &shutdown);
}
}
// Launch the rocket, serve http endpoints and finish the startup
tokio::spawn(rocket.launch());
+100
View File
@@ -0,0 +1,100 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::network::models::{ContractInformation, NetworkDetails};
use crate::v2::AxumAppState;
use axum::{extract, Router};
use nym_contracts_common::ContractBuildInformation;
use std::collections::HashMap;
use utoipa::ToSchema;
pub(crate) fn nym_network_routes() -> Router<AxumAppState> {
Router::new()
.route("/details", axum::routing::get(network_details))
.route("/nym-contracts", axum::routing::get(nym_contracts))
.route(
"/nym-contracts-detailed",
axum::routing::get(nym_contracts_detailed),
)
}
#[utoipa::path(
tag = "network",
get,
path = "/v1/network/details",
responses(
(status = 200, body = NetworkDetails)
)
)]
async fn network_details(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<NetworkDetails> {
state.network_details().to_owned().into()
}
// it's used for schema generation so dead_code is fine
#[allow(dead_code)]
#[derive(ToSchema)]
#[schema(title = "ContractVersion")]
pub(crate) struct ContractVersionSchemaResponse {
/// contract is the crate name of the implementing contract, eg. `crate:cw20-base`
/// we will use other prefixes for other languages, and their standard global namespacing
pub contract: String,
/// version is any string that this implementation knows. It may be simple counter "1", "2".
/// or semantic version on release tags "v0.7.0", or some custom feature flag list.
/// the only code that needs to understand the version parsing is code that knows how to
/// migrate from the given contract (and is tied to it's implementation somehow)
pub version: String,
}
#[utoipa::path(
tag = "network",
get,
path = "/v1/network/nym-contracts",
responses(
(status = 200, body = HashMap<String, ContractInformation<ContractVersionSchemaResponse>>)
)
)]
async fn nym_contracts(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<HashMap<String, ContractInformation<cw2::ContractVersion>>> {
let info = state.nym_contract_cache().contract_details().await;
info.iter()
.map(|(contract, info)| {
(
contract.to_owned(),
ContractInformation {
address: info.address.as_ref().map(|a| a.to_string()),
details: info.base.clone(),
},
)
})
.collect::<HashMap<_, _>>()
.into()
}
#[utoipa::path(
tag = "network",
get,
path = "/v1/network/nym-contracts-detailed",
responses(
(status = 200, body = HashMap<String, ContractInformation<ContractBuildInformation>>)
)
)]
async fn nym_contracts_detailed(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<HashMap<String, ContractInformation<ContractBuildInformation>>> {
let info = state.nym_contract_cache().contract_details().await;
info.iter()
.map(|(contract, info)| {
(
contract.to_owned(),
ContractInformation {
address: info.address.as_ref().map(|a| a.to_string()),
details: info.detailed.clone(),
},
)
})
.collect::<HashMap<_, _>>()
.into()
}
+2
View File
@@ -6,6 +6,8 @@ use rocket::Route;
use rocket_okapi::openapi_get_routes_spec;
use rocket_okapi::settings::OpenApiSettings;
#[cfg(feature = "axum")]
pub(crate) mod handlers;
pub(crate) mod models;
mod routes;
+2
View File
@@ -6,6 +6,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
#[cfg_attr(feature = "axum", derive(utoipa::ToSchema))]
pub struct NetworkDetails {
pub(crate) connected_nyxd: String,
pub(crate) network: NymNetworkDetails,
@@ -20,6 +21,7 @@ impl NetworkDetails {
}
}
#[cfg_attr(feature = "axum", derive(utoipa::ToSchema))]
#[derive(Serialize, Deserialize, Clone, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct ContractInformation<T> {
+2 -1
View File
@@ -41,12 +41,13 @@ pub struct NodeStatusCache {
impl NodeStatusCache {
/// Creates a new cache with no data.
fn new() -> NodeStatusCache {
pub(crate) fn new() -> NodeStatusCache {
NodeStatusCache {
inner: Arc::new(RwLock::new(NodeStatusCacheData::new())),
}
}
#[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")]
pub fn stage() -> AdHoc {
AdHoc::on_ignite("Node Status Cache", |rocket| async {
rocket.manage(Self::new())
+16 -26
View File
@@ -52,12 +52,11 @@ pub(super) fn split_into_active_and_rewarded_set(
}
pub(super) async fn get_mixnode_performance_from_storage(
storage: &Option<NymApiStorage>,
storage: &NymApiStorage,
mix_id: MixId,
epoch: Interval,
) -> Option<Performance> {
storage
.as_ref()?
.get_average_mixnode_uptime_in_the_last_24hrs(
mix_id,
epoch.current_epoch_end_unix_timestamp(),
@@ -68,12 +67,11 @@ pub(super) async fn get_mixnode_performance_from_storage(
}
pub(super) async fn get_gateway_performance_from_storage(
storage: &Option<NymApiStorage>,
storage: &NymApiStorage,
gateway_id: &str,
epoch: Interval,
) -> Option<Performance> {
storage
.as_ref()?
.get_average_gateway_uptime_in_the_last_24hrs(
gateway_id,
epoch.current_epoch_end_unix_timestamp(),
@@ -84,7 +82,7 @@ pub(super) async fn get_gateway_performance_from_storage(
}
pub(super) async fn annotate_nodes_with_details(
storage: &Option<NymApiStorage>,
storage: &NymApiStorage,
mixnodes: Vec<MixNodeDetails>,
interval_reward_params: RewardingParams,
current_interval: Interval,
@@ -123,16 +121,12 @@ pub(super) async fn annotate_nodes_with_details(
current_interval,
);
let node_performance = if let Some(storage) = storage {
storage
.construct_mixnode_report(mixnode.mix_id())
.await
.map(NodePerformance::from)
.ok()
} else {
None
}
.unwrap_or_default();
let node_performance = storage
.construct_mixnode_report(mixnode.mix_id())
.await
.map(NodePerformance::from)
.ok()
.unwrap_or_default();
// safety: this conversion is infallible
let ip_addresses =
@@ -177,7 +171,7 @@ pub(super) async fn annotate_nodes_with_details(
}
pub(crate) async fn annotate_gateways_with_details(
storage: &Option<NymApiStorage>,
storage: &NymApiStorage,
gateway_bonds: Vec<GatewayBond>,
current_interval: Interval,
blacklist: &HashSet<IdentityKey>,
@@ -192,16 +186,12 @@ pub(crate) async fn annotate_gateways_with_details(
.await
.unwrap_or_default();
let node_performance = if let Some(storage) = storage {
storage
.construct_gateway_report(gateway_bond.identity())
.await
.map(NodePerformance::from)
.ok()
} else {
None
}
.unwrap_or_default();
let node_performance = storage
.construct_gateway_report(gateway_bond.identity())
.await
.map(NodePerformance::from)
.ok()
.unwrap_or_default();
// safety: this conversion is infallible
let ip_addresses = match NetworkAddress::from_str(&gateway_bond.gateway.host).unwrap() {
+2 -2
View File
@@ -29,7 +29,7 @@ pub struct NodeStatusCacheRefresher {
// Sources for when refreshing data
contract_cache: NymContractCache,
contract_cache_listener: watch::Receiver<CacheNotification>,
storage: Option<NymApiStorage>,
storage: NymApiStorage,
}
impl NodeStatusCacheRefresher {
@@ -38,7 +38,7 @@ impl NodeStatusCacheRefresher {
fallback_caching_interval: Duration,
contract_cache: NymContractCache,
contract_cache_listener: watch::Receiver<CacheNotification>,
storage: Option<NymApiStorage>,
storage: NymApiStorage,
) -> Self {
Self {
cache,
@@ -0,0 +1,32 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::v2::AxumAppState;
use axum::Router;
use nym_mixnet_contract_common::MixId;
use serde::Deserialize;
use utoipa::IntoParams;
pub(crate) mod network_monitor;
pub(crate) mod unstable;
pub(crate) mod without_monitor;
pub(crate) fn node_status_routes(network_monitor: bool) -> Router<AxumAppState> {
// in the minimal variant we would not have access to endpoints relying on existence
// of the network monitor and the associated storage
let without_network_monitor = without_monitor::mandatory_routes();
if network_monitor {
let with_network_monitor = network_monitor::network_monitor_routes();
with_network_monitor.merge(without_network_monitor)
} else {
without_network_monitor
}
}
#[derive(Deserialize, IntoParams)]
#[into_params(parameter_in = Path)]
struct MixIdParam {
mix_id: MixId,
}
@@ -0,0 +1,339 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::handlers::MixIdParam;
use crate::node_status_api::helpers::{
_compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report,
_gateway_uptime_history, _get_gateway_avg_uptime, _get_gateways_detailed,
_get_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, _get_mixnode_reward_estimation,
_get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, _mixnode_report,
_mixnode_uptime_history,
};
use crate::node_status_api::models::AxumResult;
use crate::v2::AxumAppState;
use axum::extract::{Path, Query, State};
use axum::Json;
use axum::Router;
use nym_api_requests::models::{
ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse,
GatewayStatusReportResponse, GatewayUptimeHistoryResponse, GatewayUptimeResponse,
MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse,
MixnodeUptimeHistoryResponse, RewardEstimationResponse, UptimeResponse,
};
use serde::Deserialize;
use utoipa::IntoParams;
use super::unstable;
pub(super) fn network_monitor_routes() -> Router<AxumAppState> {
Router::new()
.nest(
"/gateway/:identity",
Router::new()
.route("/report", axum::routing::get(gateway_report))
.route("/history", axum::routing::get(gateway_uptime_history))
.route(
"/core-status-count",
axum::routing::get(gateway_core_status_count),
)
.route("/avg_uptime", axum::routing::get(get_gateway_avg_uptime)),
)
.nest(
"/mixnode/:mix_id",
Router::new()
.route("/report", axum::routing::get(mixnode_report))
.route("/history", axum::routing::get(mixnode_uptime_history))
.route(
"/core-status-count",
axum::routing::get(mixnode_core_status_count),
)
.route(
"/reward-estimation",
axum::routing::get(get_mixnode_reward_estimation),
)
.route(
"/compute-reward-estimation",
axum::routing::post(compute_mixnode_reward_estimation),
)
.route("/avg_uptime", axum::routing::get(get_mixnode_avg_uptime)),
)
.nest(
"/mixnodes",
Router::new()
.route(
"/detailed-unfiltered",
axum::routing::get(get_mixnodes_detailed_unfiltered),
)
.route(
"/unstable/:mix_id/test-results",
axum::routing::get(unstable::mixnode_test_results),
),
)
.nest(
"/gateways",
Router::new()
.route("/detailed", axum::routing::get(get_gateways_detailed))
.route(
"/detailed-unfiltered",
axum::routing::get(get_gateways_detailed_unfiltered),
)
.route(
"/unstable/:gateway_identity/test-results",
axum::routing::get(unstable::gateway_test_results),
),
)
}
#[utoipa::path(
tag = "network-monitor-status",
get,
path = "/v1/status/gateway/{identity}/report",
responses(
(status = 200, body = GatewayStatusReportResponse)
)
)]
async fn gateway_report(
Path(identity): Path<String>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<GatewayStatusReportResponse>> {
Ok(Json(
_gateway_report(state.node_status_cache(), &identity).await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
path = "/v1/status/gateway/{identity}/history",
responses(
(status = 200, body = GatewayUptimeHistoryResponse)
)
)]
async fn gateway_uptime_history(
Path(identity): Path<String>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<GatewayUptimeHistoryResponse>> {
Ok(Json(
_gateway_uptime_history(state.storage(), &identity).await?,
))
}
#[derive(Deserialize, IntoParams)]
#[into_params(parameter_in = Query)]
struct SinceQueryParams {
since: Option<i64>,
}
#[utoipa::path(
tag = "network-monitor-status",
get,
params(
SinceQueryParams
),
path = "/v1/status/gateway/{identity}/core-status-count",
responses(
(status = 200, body = GatewayCoreStatusResponse)
)
)]
async fn gateway_core_status_count(
Path(identity): Path<String>,
Query(SinceQueryParams { since }): Query<SinceQueryParams>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<GatewayCoreStatusResponse>> {
Ok(Json(
_gateway_core_status_count(state.storage(), &identity, since).await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
path = "/v1/status/gateway/{identity}/avg_uptime",
responses(
(status = 200, body = GatewayUptimeResponse)
)
)]
async fn get_gateway_avg_uptime(
Path(identity): Path<String>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<GatewayUptimeResponse>> {
Ok(Json(
_get_gateway_avg_uptime(state.node_status_cache(), &identity).await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
params(
MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/report",
responses(
(status = 200, body = MixnodeStatusReportResponse)
)
)]
async fn mixnode_report(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<MixnodeStatusReportResponse>> {
Ok(Json(
_mixnode_report(state.node_status_cache(), mix_id).await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
params(
MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/history",
responses(
(status = 200, body = MixnodeUptimeHistoryResponse)
)
)]
async fn mixnode_uptime_history(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<MixnodeUptimeHistoryResponse>> {
Ok(Json(
_mixnode_uptime_history(state.storage(), mix_id).await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
params(
MixIdParam, SinceQueryParams
),
path = "/v1/status/mixnode/{mix_id}/core-status-count",
responses(
(status = 200, body = MixnodeCoreStatusResponse)
)
)]
async fn mixnode_core_status_count(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
Query(SinceQueryParams { since }): Query<SinceQueryParams>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<MixnodeCoreStatusResponse>> {
Ok(Json(
_mixnode_core_status_count(state.storage(), mix_id, since).await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
params(
MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/reward-estimation",
responses(
(status = 200, body = RewardEstimationResponse)
)
)]
async fn get_mixnode_reward_estimation(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<RewardEstimationResponse>> {
Ok(Json(
_get_mixnode_reward_estimation(
state.node_status_cache(),
state.nym_contract_cache(),
mix_id,
)
.await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
post,
params(
ComputeRewardEstParam, MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/compute-reward-estimation",
request_body = ComputeRewardEstParam,
responses(
(status = 200, body = RewardEstimationResponse)
)
)]
async fn compute_mixnode_reward_estimation(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
State(state): State<AxumAppState>,
Json(user_reward_param): Json<ComputeRewardEstParam>,
) -> AxumResult<Json<RewardEstimationResponse>> {
Ok(Json(
_compute_mixnode_reward_estimation(
&user_reward_param,
state.node_status_cache(),
state.nym_contract_cache(),
mix_id,
)
.await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
params(
MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/avg_uptime",
responses(
(status = 200, body = UptimeResponse)
)
)]
async fn get_mixnode_avg_uptime(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<UptimeResponse>> {
Ok(Json(
_get_mixnode_avg_uptime(state.node_status_cache(), mix_id).await?,
))
}
#[utoipa::path(
tag = "network-monitor-status",
get,
path = "/v1/status/mixnodes/detailed-unfiltered",
responses(
(status = 200, body = MixNodeBondAnnotated)
)
)]
pub async fn get_mixnodes_detailed_unfiltered(
State(state): State<AxumAppState>,
) -> Json<Vec<MixNodeBondAnnotated>> {
Json(_get_mixnodes_detailed_unfiltered(state.node_status_cache()).await)
}
#[utoipa::path(
tag = "network-monitor-status",
get,
path = "/v1/status/gateways/detailed",
responses(
(status = 200, body = GatewayBondAnnotated)
)
)]
pub async fn get_gateways_detailed(
State(state): State<AxumAppState>,
) -> Json<Vec<GatewayBondAnnotated>> {
Json(_get_gateways_detailed(state.node_status_cache()).await)
}
#[utoipa::path(
tag = "network-monitor-status",
get,
path = "/v1/status/gateways/detailed-unfiltered",
responses(
(status = 200, body = GatewayBondAnnotated)
)
)]
pub async fn get_gateways_detailed_unfiltered(
State(state): State<AxumAppState>,
) -> Json<Vec<GatewayBondAnnotated>> {
Json(_get_gateways_detailed_unfiltered(state.node_status_cache()).await)
}
@@ -0,0 +1,279 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::support::http::helpers::PaginationRequest;
use crate::support::storage::NymApiStorage;
use crate::v2::AxumAppState;
use axum::extract::{Path, Query, State};
use axum::Json;
use nym_api_requests::models::{
GatewayTestResultResponse, MixnodeTestResultResponse, PartialTestResult, TestNode, TestRoute,
};
use nym_api_requests::pagination::Pagination;
use nym_mixnet_contract_common::MixId;
use std::cmp::min;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
pub type DbId = i64;
// a simply in-memory cache of node details
#[derive(Debug, Clone, Default)]
pub struct NodeInfoCache {
inner: Arc<RwLock<NodeInfoCacheInner>>,
}
impl NodeInfoCache {
async fn get_mix_node_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode {
{
let read_guard = self.inner.read().await;
if let Some(cached) = read_guard.mixnodes.get(&db_id) {
trace!("cache hit for mixnode {db_id}");
return cached.clone();
}
}
trace!("cache miss for mixnode {db_id}");
let mut write_guard = self.inner.write().await;
// double-check the cache in case somebody already updated it while we were waiting for the lock
if let Some(cached) = write_guard.mixnodes.get(&db_id) {
return cached.clone();
}
let details = match storage.get_mixnode_details_by_db_id(db_id).await {
Ok(Some(details)) => details.into(),
Ok(None) => {
error!("somebody has been messing with the database! details for mixnode with database id {db_id} have been removed!");
TestNode::default()
}
Err(err) => {
// don't insert into the cache in case another request is successful
error!("failed to retrieve details for mixnode {db_id}: {err}");
return TestNode::default();
}
};
write_guard.mixnodes.insert(db_id, details.clone());
details
}
async fn get_gateway_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode {
{
let read_guard = self.inner.read().await;
if let Some(cached) = read_guard.gateways.get(&db_id) {
trace!("cache hit for gateway {db_id}");
return cached.clone();
}
}
trace!("cache miss for gateway {db_id}");
let mut write_guard = self.inner.write().await;
// double-check the cache in case somebody already updated it while we were waiting for the lock
if let Some(cached) = write_guard.gateways.get(&db_id) {
return cached.clone();
}
let details = match storage.get_gateway_details_by_db_id(db_id).await {
Ok(Some(details)) => details.into(),
Ok(None) => {
error!("somebody has been messing with the database! details for gateway with database id {db_id} have been removed!");
TestNode::default()
}
Err(err) => {
// don't insert into the cache in case another request is successful
error!("failed to retrieve details for gateway {db_id}: {err}");
return TestNode::default();
}
};
write_guard.gateways.insert(db_id, details.clone());
details
}
}
#[derive(Debug, Clone, Default)]
struct NodeInfoCacheInner {
mixnodes: HashMap<DbId, TestNode>,
gateways: HashMap<DbId, TestNode>,
}
const MAX_TEST_RESULTS_PAGE_SIZE: u32 = 100;
const DEFAULT_TEST_RESULTS_PAGE_SIZE: u32 = 50;
async fn _mixnode_test_results(
mix_id: MixId,
page: u32,
per_page: u32,
info_cache: &NodeInfoCache,
storage: &NymApiStorage,
) -> anyhow::Result<MixnodeTestResultResponse> {
// convert to db offset
// we're paging from page 0 like civilised people,
// so we have to skip (page * per_page) results
let offset = page * per_page;
let limit = per_page;
let raw_results = storage
.get_mixnode_detailed_statuses(mix_id, limit, offset)
.await?;
let total = match raw_results.first() {
None => 0,
Some(r) => storage.get_mixnode_detailed_statuses_count(r.db_id).await?,
};
let mut partial_results = Vec::new();
for result in raw_results {
let gateway = info_cache
.get_gateway_details(result.gateway_id, storage)
.await;
let layer1 = info_cache
.get_mix_node_details(result.layer1_mix_id, storage)
.await;
let layer2 = info_cache
.get_mix_node_details(result.layer2_mix_id, storage)
.await;
let layer3 = info_cache
.get_mix_node_details(result.layer3_mix_id, storage)
.await;
partial_results.push(PartialTestResult {
monitor_run_id: result.monitor_run_id,
timestamp: result.timestamp,
overall_reliability_for_all_routes_in_monitor_run: result.reliability,
test_routes: TestRoute {
gateway,
layer1,
layer2,
layer3,
},
})
}
Ok(MixnodeTestResultResponse {
pagination: Pagination {
total,
page,
size: partial_results.len(),
},
data: partial_results,
})
}
pub async fn mixnode_test_results(
Path(mix_id): Path<MixId>,
Query(pagination): Query<PaginationRequest>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<MixnodeTestResultResponse>> {
let page = pagination.page.unwrap_or_default();
let per_page = min(
pagination
.per_page
.unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE),
MAX_TEST_RESULTS_PAGE_SIZE,
);
match _mixnode_test_results(
mix_id,
page,
per_page,
state.node_info_cache(),
state.storage(),
)
.await
{
Ok(res) => Ok(Json(res)),
Err(err) => Err(AxumErrorResponse::internal_msg(format!(
"failed to retrieve mixnode test results for node {mix_id}: {err}"
))),
}
}
async fn _gateway_test_results(
gateway_identity: &str,
page: u32,
per_page: u32,
info_cache: &NodeInfoCache,
storage: &NymApiStorage,
) -> anyhow::Result<GatewayTestResultResponse> {
// convert to db offset
// we're paging from page 0 like civilised people,
// so we have to skip (page * per_page) results
let offset = page * per_page;
let limit = per_page;
let raw_results = storage
.get_gateway_detailed_statuses(gateway_identity, limit, offset)
.await?;
let total = match raw_results.first() {
None => 0,
Some(r) => storage.get_gateway_detailed_statuses_count(r.db_id).await?,
};
let mut partial_results = Vec::new();
for result in raw_results {
let gateway = info_cache
.get_gateway_details(result.gateway_id, storage)
.await;
let layer1 = info_cache
.get_mix_node_details(result.layer1_mix_id, storage)
.await;
let layer2 = info_cache
.get_mix_node_details(result.layer2_mix_id, storage)
.await;
let layer3 = info_cache
.get_mix_node_details(result.layer3_mix_id, storage)
.await;
partial_results.push(PartialTestResult {
monitor_run_id: result.monitor_run_id,
timestamp: result.timestamp,
overall_reliability_for_all_routes_in_monitor_run: result.reliability,
test_routes: TestRoute {
gateway,
layer1,
layer2,
layer3,
},
})
}
Ok(GatewayTestResultResponse {
pagination: Pagination {
total,
page,
size: partial_results.len(),
},
data: partial_results,
})
}
pub async fn gateway_test_results(
Path(gateway_identity): Path<String>,
Query(pagination): Query<PaginationRequest>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<GatewayTestResultResponse>> {
let page = pagination.page.unwrap_or_default();
let per_page = min(
pagination
.per_page
.unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE),
MAX_TEST_RESULTS_PAGE_SIZE,
);
match _gateway_test_results(
&gateway_identity,
page,
per_page,
state.node_info_cache(),
state.storage(),
)
.await
{
Ok(res) => Ok(Json(res)),
Err(err) => Err(AxumErrorResponse::internal_msg(format!(
"failed to retrieve mixnode test results for gateway {gateway_identity}: {err}"
))),
}
}
@@ -0,0 +1,176 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::handlers::MixIdParam;
use crate::node_status_api::helpers::{
_get_active_set_detailed, _get_mixnode_inclusion_probabilities,
_get_mixnode_inclusion_probability, _get_mixnode_stake_saturation, _get_mixnode_status,
_get_mixnodes_detailed, _get_rewarded_set_detailed,
};
use crate::node_status_api::models::AxumResult;
use crate::v2::AxumAppState;
use axum::extract::{Path, State};
use axum::Json;
use axum::Router;
use nym_api_requests::models::{
AllInclusionProbabilitiesResponse, InclusionProbabilityResponse, MixNodeBondAnnotated,
MixnodeStatusResponse, StakeSaturationResponse,
};
use nym_mixnet_contract_common::MixId;
pub(super) fn mandatory_routes() -> Router<AxumAppState> {
Router::new()
.nest(
"/mixnode/:mix_id",
Router::new()
.route("/status", axum::routing::get(get_mixnode_status))
.route(
"/stake-saturation",
axum::routing::get(get_mixnode_stake_saturation),
)
.route(
"/inclusion-probability",
axum::routing::get(get_mixnode_inclusion_probability),
),
)
.merge(
Router::new().nest(
"/mixnodes",
Router::new()
.route(
"/inclusion-probability",
axum::routing::get(get_mixnode_inclusion_probabilities),
)
.route("/detailed", axum::routing::get(get_mixnodes_detailed))
.route(
"/rewarded/detailed",
axum::routing::get(get_rewarded_set_detailed),
)
.route(
"/active/detailed",
axum::routing::get(get_active_set_detailed),
),
),
)
}
#[utoipa::path(
tag = "status",
get,
params(
MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/status",
responses(
(status = 200, body = MixnodeStatusResponse)
)
)]
async fn get_mixnode_status(
Path(MixIdParam { mix_id }): Path<MixIdParam>,
State(state): State<AxumAppState>,
) -> Json<MixnodeStatusResponse> {
Json(_get_mixnode_status(state.nym_contract_cache(), mix_id).await)
}
#[utoipa::path(
tag = "status",
get,
params(
MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/stake-saturation",
responses(
(status = 200, body = StakeSaturationResponse)
)
)]
async fn get_mixnode_stake_saturation(
Path(mix_id): Path<MixId>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<StakeSaturationResponse>> {
Ok(Json(
_get_mixnode_stake_saturation(
state.node_status_cache(),
state.nym_contract_cache(),
mix_id,
)
.await?,
))
}
#[utoipa::path(
tag = "status",
get,
params(
MixIdParam
),
path = "/v1/status/mixnode/{mix_id}/inclusion-probability",
responses(
(status = 200, body = InclusionProbabilityResponse)
)
)]
async fn get_mixnode_inclusion_probability(
Path(mix_id): Path<MixId>,
State(state): State<AxumAppState>,
) -> AxumResult<Json<InclusionProbabilityResponse>> {
Ok(Json(
_get_mixnode_inclusion_probability(state.node_status_cache(), mix_id).await?,
))
}
#[utoipa::path(
tag = "status",
get,
path = "/v1/status/mixnodes/inclusion-probability",
responses(
(status = 200, body = AllInclusionProbabilitiesResponse)
)
)]
async fn get_mixnode_inclusion_probabilities(
State(state): State<AxumAppState>,
) -> AxumResult<Json<AllInclusionProbabilitiesResponse>> {
Ok(Json(
_get_mixnode_inclusion_probabilities(state.node_status_cache()).await?,
))
}
#[utoipa::path(
tag = "status",
get,
path = "/v1/status/mixnodes/detailed",
responses(
(status = 200, body = MixNodeBondAnnotated)
)
)]
pub async fn get_mixnodes_detailed(
State(state): State<AxumAppState>,
) -> Json<Vec<MixNodeBondAnnotated>> {
Json(_get_mixnodes_detailed(state.node_status_cache()).await)
}
#[utoipa::path(
tag = "status",
get,
path = "/v1/status/mixnodes/rewarded/detailed",
responses(
(status = 200, body = MixNodeBondAnnotated)
)
)]
pub async fn get_rewarded_set_detailed(
State(state): State<AxumAppState>,
) -> Json<Vec<MixNodeBondAnnotated>> {
Json(_get_rewarded_set_detailed(state.node_status_cache()).await)
}
#[utoipa::path(
tag = "status",
get,
path = "/v1/status/mixnodes/active/detailed",
responses(
(status = 200, body = MixNodeBondAnnotated)
)
)]
pub async fn get_active_set_detailed(
State(state): State<AxumAppState>,
) -> Json<Vec<MixNodeBondAnnotated>> {
Json(_get_active_set_detailed(state.node_status_cache()).await)
}
+41 -63
View File
@@ -1,7 +1,8 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::ErrorResponse;
use super::reward_estimate::compute_reward_estimate;
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::storage::NymApiStorage;
use crate::support::caching::Cache;
use crate::{NodeStatusCache, NymContractCache};
@@ -15,41 +16,31 @@ use nym_api_requests::models::{
UptimeResponse,
};
use nym_mixnet_contract_common::{MixId, RewardedSetNodeStatus};
use rocket::http::Status;
use rocket::State;
use super::reward_estimate::compute_reward_estimate;
async fn get_gateway_bond_annotated(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayBondAnnotated, ErrorResponse> {
) -> AxumResult<GatewayBondAnnotated> {
cache
.gateway_annotated(identity)
.await
.ok_or(ErrorResponse::new(
"gateway bond not found",
Status::NotFound,
))
.ok_or(AxumErrorResponse::not_found("gateway bond not found"))
}
async fn get_mixnode_bond_annotated(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<MixNodeBondAnnotated, ErrorResponse> {
) -> AxumResult<MixNodeBondAnnotated> {
cache
.mixnode_annotated(mix_id)
.await
.ok_or(ErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
.ok_or(AxumErrorResponse::not_found("mixnode bond not found"))
}
pub(crate) async fn _gateway_report(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayStatusReportResponse, ErrorResponse> {
) -> AxumResult<GatewayStatusReportResponse> {
let gateway = get_gateway_bond_annotated(cache, identity).await?;
Ok(GatewayStatusReportResponse {
@@ -64,23 +55,23 @@ pub(crate) async fn _gateway_report(
pub(crate) async fn _gateway_uptime_history(
storage: &NymApiStorage,
identity: &str,
) -> Result<GatewayUptimeHistoryResponse, ErrorResponse> {
) -> AxumResult<GatewayUptimeHistoryResponse> {
storage
.get_gateway_uptime_history(identity)
.await
.map(GatewayUptimeHistoryResponse::from)
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))
.map_err(AxumErrorResponse::not_found)
}
pub(crate) async fn _gateway_core_status_count(
storage: &State<NymApiStorage>,
storage: &NymApiStorage,
identity: &str,
since: Option<i64>,
) -> Result<GatewayCoreStatusResponse, ErrorResponse> {
) -> AxumResult<GatewayCoreStatusResponse> {
let count = storage
.get_core_gateway_status_count(identity, since)
.await
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))?;
.map_err(AxumErrorResponse::not_found)?;
Ok(GatewayCoreStatusResponse {
identity: identity.to_string(),
@@ -91,7 +82,7 @@ pub(crate) async fn _gateway_core_status_count(
pub(crate) async fn _mixnode_report(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<MixnodeStatusReportResponse, ErrorResponse> {
) -> AxumResult<MixnodeStatusReportResponse> {
let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?;
Ok(MixnodeStatusReportResponse {
@@ -107,23 +98,23 @@ pub(crate) async fn _mixnode_report(
pub(crate) async fn _mixnode_uptime_history(
storage: &NymApiStorage,
mix_id: MixId,
) -> Result<MixnodeUptimeHistoryResponse, ErrorResponse> {
) -> AxumResult<MixnodeUptimeHistoryResponse> {
storage
.get_mixnode_uptime_history(mix_id)
.await
.map(MixnodeUptimeHistoryResponse::from)
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))
.map_err(AxumErrorResponse::not_found)
}
pub(crate) async fn _mixnode_core_status_count(
storage: &State<NymApiStorage>,
storage: &NymApiStorage,
mix_id: MixId,
since: Option<i64>,
) -> Result<MixnodeCoreStatusResponse, ErrorResponse> {
) -> AxumResult<MixnodeCoreStatusResponse> {
let count = storage
.get_core_mixnode_status_count(mix_id, since)
.await
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))?;
.map_err(AxumErrorResponse::not_found)?;
Ok(MixnodeCoreStatusResponse { mix_id, count })
}
@@ -138,22 +129,22 @@ pub(crate) async fn _get_mixnode_status(
}
pub(crate) async fn _get_mixnode_reward_estimation(
cache: &State<NodeStatusCache>,
validator_cache: &State<NymContractCache>,
cache: &NodeStatusCache,
validator_cache: &NymContractCache,
mix_id: MixId,
) -> Result<RewardEstimationResponse, ErrorResponse> {
) -> AxumResult<RewardEstimationResponse> {
let (mixnode, status) = cache.mixnode_details(mix_id).await;
if let Some(mixnode) = mixnode {
let reward_params = validator_cache.interval_reward_params().await;
let as_at = reward_params.timestamp();
let reward_params = reward_params
.into_inner()
.ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?;
.ok_or_else(AxumErrorResponse::internal)?;
let current_interval = validator_cache
.current_interval()
.await
.into_inner()
.ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?;
.ok_or_else(AxumErrorResponse::internal)?;
let reward_estimation = compute_reward_estimate(
&mixnode.mixnode_details,
@@ -170,31 +161,28 @@ pub(crate) async fn _get_mixnode_reward_estimation(
as_at: as_at.unix_timestamp(),
})
} else {
Err(ErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
Err(AxumErrorResponse::not_found("mixnode bond not found"))
}
}
pub(crate) async fn _compute_mixnode_reward_estimation(
user_reward_param: ComputeRewardEstParam,
user_reward_param: &ComputeRewardEstParam,
cache: &NodeStatusCache,
validator_cache: &NymContractCache,
mix_id: MixId,
) -> Result<RewardEstimationResponse, ErrorResponse> {
) -> AxumResult<RewardEstimationResponse> {
let (mixnode, actual_status) = cache.mixnode_details(mix_id).await;
if let Some(mut mixnode) = mixnode {
let reward_params = validator_cache.interval_reward_params().await;
let as_at = reward_params.timestamp();
let reward_params = reward_params
.into_inner()
.ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?;
.ok_or_else(AxumErrorResponse::internal)?;
let current_interval = validator_cache
.current_interval()
.await
.into_inner()
.ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?;
.ok_or_else(AxumErrorResponse::internal)?;
// For these parameters we either use the provided ones, or fall back to the system ones
let performance = user_reward_param.performance.unwrap_or(mixnode.performance);
@@ -222,21 +210,20 @@ pub(crate) async fn _compute_mixnode_reward_estimation(
.profit_margin_percent = profit_margin_percent;
}
if let Some(interval_operating_cost) = user_reward_param.interval_operating_cost {
if let Some(interval_operating_cost) = &user_reward_param.interval_operating_cost {
mixnode
.mixnode_details
.rewarding_details
.cost_params
.interval_operating_cost = interval_operating_cost;
.interval_operating_cost = interval_operating_cost.clone();
}
if mixnode.mixnode_details.rewarding_details.operator
+ mixnode.mixnode_details.rewarding_details.delegates
> reward_params.interval.staking_supply
{
return Err(ErrorResponse::new(
return Err(AxumErrorResponse::unprocessable_entity(
"Pledge plus delegation too large",
Status::UnprocessableEntity,
));
}
@@ -255,10 +242,7 @@ pub(crate) async fn _compute_mixnode_reward_estimation(
as_at: as_at.unix_timestamp(),
})
} else {
Err(ErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
Err(AxumErrorResponse::not_found("mixnode bond not found"))
}
}
@@ -266,7 +250,7 @@ pub(crate) async fn _get_mixnode_stake_saturation(
cache: &NodeStatusCache,
validator_cache: &NymContractCache,
mix_id: MixId,
) -> Result<StakeSaturationResponse, ErrorResponse> {
) -> AxumResult<StakeSaturationResponse> {
let (mixnode, _) = cache.mixnode_details(mix_id).await;
if let Some(mixnode) = mixnode {
// Recompute the stake saturation just so that we can confidently state that the `as_at`
@@ -275,7 +259,7 @@ pub(crate) async fn _get_mixnode_stake_saturation(
let as_at = reward_params.timestamp();
let rewarding_params = reward_params
.into_inner()
.ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?;
.ok_or_else(AxumErrorResponse::internal)?;
Ok(StakeSaturationResponse {
saturation: mixnode
@@ -289,17 +273,14 @@ pub(crate) async fn _get_mixnode_stake_saturation(
as_at: as_at.unix_timestamp(),
})
} else {
Err(ErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
Err(AxumErrorResponse::not_found("mixnode bond not found"))
}
}
pub(crate) async fn _get_mixnode_inclusion_probability(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<InclusionProbabilityResponse, ErrorResponse> {
) -> AxumResult<InclusionProbabilityResponse> {
cache
.inclusion_probabilities()
.await
@@ -309,13 +290,13 @@ pub(crate) async fn _get_mixnode_inclusion_probability(
in_active: p.in_active.into(),
in_reserve: p.in_reserve.into(),
})
.ok_or_else(|| ErrorResponse::new("mixnode bond not found", Status::NotFound))
.ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))
}
pub(crate) async fn _get_mixnode_avg_uptime(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<UptimeResponse, ErrorResponse> {
) -> AxumResult<UptimeResponse> {
let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?;
Ok(UptimeResponse {
@@ -328,7 +309,7 @@ pub(crate) async fn _get_mixnode_avg_uptime(
pub(crate) async fn _get_gateway_avg_uptime(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayUptimeResponse, ErrorResponse> {
) -> AxumResult<GatewayUptimeResponse> {
let gateway = get_gateway_bond_annotated(cache, identity).await?;
Ok(GatewayUptimeResponse {
@@ -340,7 +321,7 @@ pub(crate) async fn _get_gateway_avg_uptime(
pub(crate) async fn _get_mixnode_inclusion_probabilities(
cache: &NodeStatusCache,
) -> Result<AllInclusionProbabilitiesResponse, ErrorResponse> {
) -> AxumResult<AllInclusionProbabilitiesResponse> {
if let Some(prob) = cache.inclusion_probabilities().await {
let as_at = prob.timestamp();
let prob = prob.into_inner();
@@ -353,10 +334,7 @@ pub(crate) async fn _get_mixnode_inclusion_probabilities(
as_at: as_at.unix_timestamp(),
})
} else {
Err(ErrorResponse::new(
"No data available",
Status::ServiceUnavailable,
))
Err(AxumErrorResponse::service_unavailable())
}
}
@@ -0,0 +1,405 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::RocketErrorResponse;
use crate::storage::NymApiStorage;
use crate::support::caching::Cache;
use crate::{NodeStatusCache, NymContractCache};
use cosmwasm_std::Decimal;
use nym_api_requests::models::{
AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated,
GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse,
GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated,
MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse,
MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse,
UptimeResponse,
};
use nym_mixnet_contract_common::{MixId, RewardedSetNodeStatus};
use rocket::http::Status;
use rocket::State;
use super::reward_estimate::compute_reward_estimate;
async fn get_gateway_bond_annotated(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayBondAnnotated, RocketErrorResponse> {
cache
.gateway_annotated(identity)
.await
.ok_or(RocketErrorResponse::new(
"gateway bond not found",
Status::NotFound,
))
}
async fn get_mixnode_bond_annotated(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<MixNodeBondAnnotated, RocketErrorResponse> {
cache
.mixnode_annotated(mix_id)
.await
.ok_or(RocketErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
}
pub(crate) async fn _gateway_report(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayStatusReportResponse, RocketErrorResponse> {
let gateway = get_gateway_bond_annotated(cache, identity).await?;
Ok(GatewayStatusReportResponse {
identity: gateway.identity().to_owned(),
owner: gateway.owner().to_string(),
most_recent: gateway.node_performance.most_recent.round_to_integer(),
last_hour: gateway.node_performance.last_hour.round_to_integer(),
last_day: gateway.node_performance.last_24h.round_to_integer(),
})
}
pub(crate) async fn _gateway_uptime_history(
storage: &NymApiStorage,
identity: &str,
) -> Result<GatewayUptimeHistoryResponse, RocketErrorResponse> {
storage
.get_gateway_uptime_history(identity)
.await
.map(GatewayUptimeHistoryResponse::from)
.map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))
}
pub(crate) async fn _gateway_core_status_count(
storage: &State<NymApiStorage>,
identity: &str,
since: Option<i64>,
) -> Result<GatewayCoreStatusResponse, RocketErrorResponse> {
let count = storage
.get_core_gateway_status_count(identity, since)
.await
.map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))?;
Ok(GatewayCoreStatusResponse {
identity: identity.to_string(),
count,
})
}
pub(crate) async fn _mixnode_report(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<MixnodeStatusReportResponse, RocketErrorResponse> {
let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?;
Ok(MixnodeStatusReportResponse {
mix_id,
identity: mixnode.identity_key().to_owned(),
owner: mixnode.owner().to_string(),
most_recent: mixnode.node_performance.most_recent.round_to_integer(),
last_hour: mixnode.node_performance.last_hour.round_to_integer(),
last_day: mixnode.node_performance.last_24h.round_to_integer(),
})
}
pub(crate) async fn _mixnode_uptime_history(
storage: &NymApiStorage,
mix_id: MixId,
) -> Result<MixnodeUptimeHistoryResponse, RocketErrorResponse> {
storage
.get_mixnode_uptime_history(mix_id)
.await
.map(MixnodeUptimeHistoryResponse::from)
.map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))
}
pub(crate) async fn _mixnode_core_status_count(
storage: &State<NymApiStorage>,
mix_id: MixId,
since: Option<i64>,
) -> Result<MixnodeCoreStatusResponse, RocketErrorResponse> {
let count = storage
.get_core_mixnode_status_count(mix_id, since)
.await
.map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))?;
Ok(MixnodeCoreStatusResponse { mix_id, count })
}
pub(crate) async fn _get_mixnode_status(
cache: &NymContractCache,
mix_id: MixId,
) -> MixnodeStatusResponse {
MixnodeStatusResponse {
status: cache.mixnode_status(mix_id).await,
}
}
pub(crate) async fn _get_mixnode_reward_estimation(
cache: &State<NodeStatusCache>,
validator_cache: &State<NymContractCache>,
mix_id: MixId,
) -> Result<RewardEstimationResponse, RocketErrorResponse> {
let (mixnode, status) = cache.mixnode_details(mix_id).await;
if let Some(mixnode) = mixnode {
let reward_params = validator_cache.interval_reward_params().await;
let as_at = reward_params.timestamp();
let reward_params = reward_params
.into_inner()
.ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?;
let current_interval = validator_cache
.current_interval()
.await
.into_inner()
.ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?;
let reward_estimation = compute_reward_estimate(
&mixnode.mixnode_details,
mixnode.performance,
status.into(),
reward_params,
current_interval,
);
Ok(RewardEstimationResponse {
estimation: reward_estimation,
reward_params,
epoch: current_interval,
as_at: as_at.unix_timestamp(),
})
} else {
Err(RocketErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
}
}
pub(crate) async fn _compute_mixnode_reward_estimation(
user_reward_param: ComputeRewardEstParam,
cache: &NodeStatusCache,
validator_cache: &NymContractCache,
mix_id: MixId,
) -> Result<RewardEstimationResponse, RocketErrorResponse> {
let (mixnode, actual_status) = cache.mixnode_details(mix_id).await;
if let Some(mut mixnode) = mixnode {
let reward_params = validator_cache.interval_reward_params().await;
let as_at = reward_params.timestamp();
let reward_params = reward_params
.into_inner()
.ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?;
let current_interval = validator_cache
.current_interval()
.await
.into_inner()
.ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?;
// For these parameters we either use the provided ones, or fall back to the system ones
let performance = user_reward_param.performance.unwrap_or(mixnode.performance);
let status = match user_reward_param.active_in_rewarded_set {
Some(true) => Some(RewardedSetNodeStatus::Active),
Some(false) => Some(RewardedSetNodeStatus::Standby),
None => actual_status.into(),
};
if let Some(pledge_amount) = user_reward_param.pledge_amount {
mixnode.mixnode_details.rewarding_details.operator =
Decimal::from_ratio(pledge_amount, 1u64);
}
if let Some(total_delegation) = user_reward_param.total_delegation {
mixnode.mixnode_details.rewarding_details.delegates =
Decimal::from_ratio(total_delegation, 1u64);
}
if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent {
mixnode
.mixnode_details
.rewarding_details
.cost_params
.profit_margin_percent = profit_margin_percent;
}
if let Some(interval_operating_cost) = user_reward_param.interval_operating_cost {
mixnode
.mixnode_details
.rewarding_details
.cost_params
.interval_operating_cost = interval_operating_cost;
}
if mixnode.mixnode_details.rewarding_details.operator
+ mixnode.mixnode_details.rewarding_details.delegates
> reward_params.interval.staking_supply
{
return Err(RocketErrorResponse::new(
"Pledge plus delegation too large",
Status::UnprocessableEntity,
));
}
let reward_estimation = compute_reward_estimate(
&mixnode.mixnode_details,
performance,
status,
reward_params,
current_interval,
);
Ok(RewardEstimationResponse {
estimation: reward_estimation,
reward_params,
epoch: current_interval,
as_at: as_at.unix_timestamp(),
})
} else {
Err(RocketErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
}
}
pub(crate) async fn _get_mixnode_stake_saturation(
cache: &NodeStatusCache,
validator_cache: &NymContractCache,
mix_id: MixId,
) -> Result<StakeSaturationResponse, RocketErrorResponse> {
let (mixnode, _) = cache.mixnode_details(mix_id).await;
if let Some(mixnode) = mixnode {
// Recompute the stake saturation just so that we can confidently state that the `as_at`
// field is consistent and correct. Luckily this is very cheap.
let reward_params = validator_cache.interval_reward_params().await;
let as_at = reward_params.timestamp();
let rewarding_params = reward_params
.into_inner()
.ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?;
Ok(StakeSaturationResponse {
saturation: mixnode
.mixnode_details
.rewarding_details
.bond_saturation(&rewarding_params),
uncapped_saturation: mixnode
.mixnode_details
.rewarding_details
.uncapped_bond_saturation(&rewarding_params),
as_at: as_at.unix_timestamp(),
})
} else {
Err(RocketErrorResponse::new(
"mixnode bond not found",
Status::NotFound,
))
}
}
pub(crate) async fn _get_mixnode_inclusion_probability(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<InclusionProbabilityResponse, RocketErrorResponse> {
cache
.inclusion_probabilities()
.await
.map(Cache::into_inner)
.and_then(|p| p.node(mix_id).cloned())
.map(|p| InclusionProbabilityResponse {
in_active: p.in_active.into(),
in_reserve: p.in_reserve.into(),
})
.ok_or_else(|| RocketErrorResponse::new("mixnode bond not found", Status::NotFound))
}
pub(crate) async fn _get_mixnode_avg_uptime(
cache: &NodeStatusCache,
mix_id: MixId,
) -> Result<UptimeResponse, RocketErrorResponse> {
let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?;
Ok(UptimeResponse {
mix_id,
avg_uptime: mixnode.node_performance.last_24h.round_to_integer(),
node_performance: mixnode.node_performance,
})
}
pub(crate) async fn _get_gateway_avg_uptime(
cache: &NodeStatusCache,
identity: &str,
) -> Result<GatewayUptimeResponse, RocketErrorResponse> {
let gateway = get_gateway_bond_annotated(cache, identity).await?;
Ok(GatewayUptimeResponse {
identity: identity.to_string(),
avg_uptime: gateway.node_performance.last_24h.round_to_integer(),
node_performance: gateway.node_performance,
})
}
pub(crate) async fn _get_mixnode_inclusion_probabilities(
cache: &NodeStatusCache,
) -> Result<AllInclusionProbabilitiesResponse, RocketErrorResponse> {
if let Some(prob) = cache.inclusion_probabilities().await {
let as_at = prob.timestamp();
let prob = prob.into_inner();
Ok(AllInclusionProbabilitiesResponse {
inclusion_probabilities: prob.inclusion_probabilities,
samples: prob.samples,
elapsed: prob.elapsed,
delta_max: prob.delta_max,
delta_l2: prob.delta_l2,
as_at: as_at.unix_timestamp(),
})
} else {
Err(RocketErrorResponse::new(
"No data available",
Status::ServiceUnavailable,
))
}
}
pub(crate) async fn _get_mixnodes_detailed(cache: &NodeStatusCache) -> Vec<MixNodeBondAnnotated> {
cache
.mixnodes_annotated_filtered()
.await
.unwrap_or_default()
}
pub(crate) async fn _get_mixnodes_detailed_unfiltered(
cache: &NodeStatusCache,
) -> Vec<MixNodeBondAnnotated> {
cache.mixnodes_annotated_full().await.unwrap_or_default()
}
pub(crate) async fn _get_rewarded_set_detailed(
cache: &NodeStatusCache,
) -> Vec<MixNodeBondAnnotated> {
cache
.rewarded_set_annotated()
.await
.unwrap_or_default()
.into_inner()
}
pub(crate) async fn _get_active_set_detailed(cache: &NodeStatusCache) -> Vec<MixNodeBondAnnotated> {
cache
.active_set_annotated()
.await
.unwrap_or_default()
.into_inner()
}
pub(crate) async fn _get_gateways_detailed(cache: &NodeStatusCache) -> Vec<GatewayBondAnnotated> {
cache
.gateways_annotated_filtered()
.await
.unwrap_or_default()
}
pub(crate) async fn _get_gateways_detailed_unfiltered(
cache: &NodeStatusCache,
) -> Vec<GatewayBondAnnotated> {
cache.gateways_annotated_full().await.unwrap_or_default()
}
+38 -34
View File
@@ -15,10 +15,14 @@ use rocket_okapi::{openapi_get_routes_spec, settings::OpenApiSettings};
use std::time::Duration;
pub(crate) mod cache;
#[cfg(feature = "axum")]
pub(crate) mod handlers;
#[cfg(feature = "axum")]
pub(crate) mod helpers;
pub(crate) mod helpers_deprecated;
pub(crate) mod models;
pub(crate) mod reward_estimate;
pub(crate) mod routes;
pub(crate) mod routes_deprecated;
pub(crate) mod uptime_updater;
pub(crate) mod utils;
@@ -32,42 +36,42 @@ pub(crate) fn node_status_routes(
) -> (Vec<Route>, OpenApi) {
if enabled {
openapi_get_routes_spec![
settings: routes::gateway_report,
routes::gateway_uptime_history,
routes::gateway_core_status_count,
routes::mixnode_report,
routes::mixnode_uptime_history,
routes::mixnode_core_status_count,
routes::get_mixnode_status,
routes::get_mixnode_reward_estimation,
routes::compute_mixnode_reward_estimation,
routes::get_mixnode_stake_saturation,
routes::get_mixnode_inclusion_probability,
routes::get_mixnode_avg_uptime,
routes::get_gateway_avg_uptime,
routes::get_mixnode_inclusion_probabilities,
routes::get_mixnodes_detailed,
routes::get_mixnodes_detailed_unfiltered,
routes::get_rewarded_set_detailed,
routes::get_active_set_detailed,
routes::get_gateways_detailed,
routes::get_gateways_detailed_unfiltered,
routes::unstable::mixnode_test_results,
routes::unstable::gateway_test_results,
routes::submit_gateway_monitoring_results,
routes::submit_node_monitoring_results,
settings: routes_deprecated::gateway_report,
routes_deprecated::gateway_uptime_history,
routes_deprecated::gateway_core_status_count,
routes_deprecated::mixnode_report,
routes_deprecated::mixnode_uptime_history,
routes_deprecated::mixnode_core_status_count,
routes_deprecated::get_mixnode_status,
routes_deprecated::get_mixnode_reward_estimation,
routes_deprecated::compute_mixnode_reward_estimation,
routes_deprecated::get_mixnode_stake_saturation,
routes_deprecated::get_mixnode_inclusion_probability,
routes_deprecated::get_mixnode_avg_uptime,
routes_deprecated::get_gateway_avg_uptime,
routes_deprecated::get_mixnode_inclusion_probabilities,
routes_deprecated::get_mixnodes_detailed,
routes_deprecated::get_mixnodes_detailed_unfiltered,
routes_deprecated::get_rewarded_set_detailed,
routes_deprecated::get_active_set_detailed,
routes_deprecated::get_gateways_detailed,
routes_deprecated::get_gateways_detailed_unfiltered,
routes_deprecated::unstable::mixnode_test_results,
routes_deprecated::unstable::gateway_test_results,
routes_deprecated::submit_gateway_monitoring_results,
routes_deprecated::submit_node_monitoring_results,
]
} else {
// in the minimal variant we would not have access to endpoints relying on existence
// of the network monitor and the associated storage
openapi_get_routes_spec![
settings: routes::get_mixnode_status,
routes::get_mixnode_stake_saturation,
routes::get_mixnode_inclusion_probability,
routes::get_mixnode_inclusion_probabilities,
routes::get_mixnodes_detailed,
routes::get_rewarded_set_detailed,
routes::get_active_set_detailed,
settings: routes_deprecated::get_mixnode_status,
routes_deprecated::get_mixnode_stake_saturation,
routes_deprecated::get_mixnode_inclusion_probability,
routes_deprecated::get_mixnode_inclusion_probabilities,
routes_deprecated::get_mixnodes_detailed,
routes_deprecated::get_rewarded_set_detailed,
routes_deprecated::get_active_set_detailed,
]
}
}
@@ -80,7 +84,7 @@ pub(crate) fn start_cache_refresh(
config: &config::NodeStatusAPI,
nym_contract_cache_state: &NymContractCache,
node_status_cache_state: &NodeStatusCache,
storage: Option<&storage::NymApiStorage>,
storage: storage::NymApiStorage,
nym_contract_cache_listener: tokio::sync::watch::Receiver<support::caching::CacheNotification>,
shutdown: &TaskManager,
) {
@@ -89,7 +93,7 @@ pub(crate) fn start_cache_refresh(
config.debug.caching_interval,
nym_contract_cache_state.to_owned(),
nym_contract_cache_listener,
storage.cloned(),
storage,
);
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { nym_api_cache_refresher.run(shutdown_listener).await });
+104 -6
View File
@@ -330,21 +330,22 @@ impl From<HistoricalUptime> for HistoricalUptimeResponse {
}
}
pub(crate) struct ErrorResponse {
#[deprecated(note = "TODO rocket remove once Rocket is phased out")]
pub(crate) struct RocketErrorResponse {
error_message: RequestError,
status: Status,
}
impl ErrorResponse {
impl RocketErrorResponse {
pub(crate) fn new(error_message: impl Into<String>, status: Status) -> Self {
ErrorResponse {
RocketErrorResponse {
error_message: RequestError::new(error_message),
status,
}
}
}
impl<'r, 'o: 'r> Responder<'r, 'o> for ErrorResponse {
impl<'r, 'o: 'r> Responder<'r, 'o> for RocketErrorResponse {
fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> {
// piggyback on the existing implementation
// also prefer json over plain for ease of use in frontend
@@ -355,7 +356,7 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for ErrorResponse {
}
}
impl JsonSchema for ErrorResponse {
impl JsonSchema for RocketErrorResponse {
fn schema_name() -> String {
"ErrorResponse".to_owned()
}
@@ -384,7 +385,7 @@ impl JsonSchema for ErrorResponse {
}
}
impl OpenApiResponderInner for ErrorResponse {
impl OpenApiResponderInner for RocketErrorResponse {
fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> {
let mut responses = Responses::default();
ensure_status_code_exists(&mut responses, 404);
@@ -392,6 +393,103 @@ impl OpenApiResponderInner for ErrorResponse {
}
}
#[cfg(feature = "axum")]
pub(crate) use axum_error::{AxumErrorResponse, AxumResult};
#[cfg(feature = "axum")]
/// TODO rocket: extract types from this module when axum becomes the only server in Nym API
mod axum_error {
pub use super::*;
use crate::ecash::error::{EcashError, RedemptionError};
use std::fmt::Display;
// TODO rocket remove smurf name after eliminating `rocket`
pub(crate) type AxumResult<T> = Result<T, AxumErrorResponse>;
pub(crate) struct AxumErrorResponse {
message: RequestError,
status: axum::http::StatusCode,
}
impl AxumErrorResponse {
pub(crate) fn internal_msg(msg: impl Display) -> Self {
Self {
message: RequestError::new(msg.to_string()),
status: axum::http::StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub(crate) fn internal() -> Self {
Self {
message: RequestError::new("Internal server error"),
status: axum::http::StatusCode::INTERNAL_SERVER_ERROR,
}
}
pub(crate) fn not_implemented() -> Self {
Self {
message: RequestError::empty(),
status: axum::http::StatusCode::NOT_IMPLEMENTED,
}
}
pub(crate) fn not_found(msg: impl Display) -> Self {
Self {
message: RequestError::new(msg.to_string()),
status: axum::http::StatusCode::NOT_FOUND,
}
}
pub(crate) fn service_unavailable() -> Self {
Self {
message: RequestError::empty(),
status: axum::http::StatusCode::SERVICE_UNAVAILABLE,
}
}
pub(crate) fn unprocessable_entity(msg: impl Display) -> Self {
Self {
message: RequestError::new(msg.to_string()),
status: axum::http::StatusCode::UNPROCESSABLE_ENTITY,
}
}
}
impl axum::response::IntoResponse for AxumErrorResponse {
fn into_response(self) -> axum::response::Response {
(self.status, self.message.message().to_string()).into_response()
}
}
impl From<NymApiStorageError> for AxumErrorResponse {
fn from(value: NymApiStorageError) -> Self {
error!("{value}");
Self {
message: RequestError::empty(),
status: axum::http::StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl From<EcashError> for AxumErrorResponse {
fn from(value: EcashError) -> Self {
Self {
message: RequestError::new(value.to_string()),
status: axum::http::StatusCode::BAD_REQUEST,
}
}
}
#[cfg(feature = "axum")]
impl From<RedemptionError> for AxumErrorResponse {
fn from(value: RedemptionError) -> Self {
Self {
message: RequestError::new(value.to_string()),
status: axum::http::StatusCode::BAD_REQUEST,
}
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum NymApiStorageError {
#[error("could not find status report associated with mixnode {mix_id}")]
@@ -15,9 +15,9 @@ use rocket::serde::json::Json;
use rocket::State;
use rocket_okapi::openapi;
use super::helpers::_get_gateways_detailed;
use super::helpers_deprecated::_get_gateways_detailed;
use super::NodeStatusCache;
use crate::node_status_api::helpers::{
use crate::node_status_api::helpers_deprecated::{
_compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report,
_gateway_uptime_history, _get_active_set_detailed, _get_gateway_avg_uptime,
_get_gateways_detailed_unfiltered, _get_mixnode_avg_uptime,
@@ -26,7 +26,7 @@ use crate::node_status_api::helpers::{
_get_mixnodes_detailed, _get_mixnodes_detailed_unfiltered, _get_rewarded_set_detailed,
_mixnode_core_status_count, _mixnode_report, _mixnode_uptime_history,
};
use crate::node_status_api::models::ErrorResponse;
use crate::node_status_api::models::RocketErrorResponse;
use crate::storage::NymApiStorage;
use crate::NymContractCache;
@@ -35,23 +35,23 @@ use crate::NymContractCache;
pub(crate) async fn submit_gateway_monitoring_results(
message: Json<MonitorMessage>,
storage: &State<NymApiStorage>,
) -> Result<(), ErrorResponse> {
) -> Result<(), RocketErrorResponse> {
if !message.from_allowed() {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"Monitor not registered to submit results".to_string(),
rocket::http::Status::Forbidden,
));
}
if !message.timely() {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"Message is too old".to_string(),
rocket::http::Status::BadRequest,
));
}
if !message.verify() {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"Invalid signature".to_string(),
rocket::http::Status::BadRequest,
));
@@ -65,7 +65,7 @@ pub(crate) async fn submit_gateway_monitoring_results(
Ok(_) => Ok(()),
Err(err) => {
error!("failed to submit gateway monitoring results: {}", err);
Err(ErrorResponse::new(
Err(RocketErrorResponse::new(
"failed to submit gateway monitoring results".to_string(),
rocket::http::Status::InternalServerError,
))
@@ -78,23 +78,23 @@ pub(crate) async fn submit_gateway_monitoring_results(
pub(crate) async fn submit_node_monitoring_results(
message: Json<MonitorMessage>,
storage: &State<NymApiStorage>,
) -> Result<(), ErrorResponse> {
) -> Result<(), RocketErrorResponse> {
if !message.from_allowed() {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"Monitor not registered to submit results".to_string(),
rocket::http::Status::Forbidden,
));
}
if !message.timely() {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"Message is too old".to_string(),
rocket::http::Status::BadRequest,
));
}
if !message.verify() {
return Err(ErrorResponse::new(
return Err(RocketErrorResponse::new(
"Invalid signature".to_string(),
rocket::http::Status::BadRequest,
));
@@ -108,7 +108,7 @@ pub(crate) async fn submit_node_monitoring_results(
Ok(_) => Ok(()),
Err(err) => {
error!("failed to submit node monitoring results: {}", err);
Err(ErrorResponse::new(
Err(RocketErrorResponse::new(
"failed to submit node monitoring results".to_string(),
rocket::http::Status::InternalServerError,
))
@@ -121,7 +121,7 @@ pub(crate) async fn submit_node_monitoring_results(
pub(crate) async fn gateway_report(
cache: &State<NodeStatusCache>,
identity: &str,
) -> Result<Json<GatewayStatusReportResponse>, ErrorResponse> {
) -> Result<Json<GatewayStatusReportResponse>, RocketErrorResponse> {
Ok(Json(_gateway_report(cache, identity).await?))
}
@@ -130,7 +130,7 @@ pub(crate) async fn gateway_report(
pub(crate) async fn gateway_uptime_history(
storage: &State<NymApiStorage>,
identity: &str,
) -> Result<Json<GatewayUptimeHistoryResponse>, ErrorResponse> {
) -> Result<Json<GatewayUptimeHistoryResponse>, RocketErrorResponse> {
Ok(Json(_gateway_uptime_history(storage, identity).await?))
}
@@ -140,7 +140,7 @@ pub(crate) async fn gateway_core_status_count(
storage: &State<NymApiStorage>,
identity: &str,
since: Option<i64>,
) -> Result<Json<GatewayCoreStatusResponse>, ErrorResponse> {
) -> Result<Json<GatewayCoreStatusResponse>, RocketErrorResponse> {
Ok(Json(
_gateway_core_status_count(storage, identity, since).await?,
))
@@ -151,7 +151,7 @@ pub(crate) async fn gateway_core_status_count(
pub(crate) async fn mixnode_report(
cache: &State<NodeStatusCache>,
mix_id: MixId,
) -> Result<Json<MixnodeStatusReportResponse>, ErrorResponse> {
) -> Result<Json<MixnodeStatusReportResponse>, RocketErrorResponse> {
Ok(Json(_mixnode_report(cache, mix_id).await?))
}
@@ -160,7 +160,7 @@ pub(crate) async fn mixnode_report(
pub(crate) async fn mixnode_uptime_history(
storage: &State<NymApiStorage>,
mix_id: MixId,
) -> Result<Json<MixnodeUptimeHistoryResponse>, ErrorResponse> {
) -> Result<Json<MixnodeUptimeHistoryResponse>, RocketErrorResponse> {
Ok(Json(_mixnode_uptime_history(storage, mix_id).await?))
}
@@ -170,7 +170,7 @@ pub(crate) async fn mixnode_core_status_count(
storage: &State<NymApiStorage>,
mix_id: MixId,
since: Option<i64>,
) -> Result<Json<MixnodeCoreStatusResponse>, ErrorResponse> {
) -> Result<Json<MixnodeCoreStatusResponse>, RocketErrorResponse> {
Ok(Json(
_mixnode_core_status_count(storage, mix_id, since).await?,
))
@@ -191,7 +191,7 @@ pub(crate) async fn get_mixnode_reward_estimation(
cache: &State<NodeStatusCache>,
validator_cache: &State<NymContractCache>,
mix_id: MixId,
) -> Result<Json<RewardEstimationResponse>, ErrorResponse> {
) -> Result<Json<RewardEstimationResponse>, RocketErrorResponse> {
Ok(Json(
_get_mixnode_reward_estimation(cache, validator_cache, mix_id).await?,
))
@@ -207,7 +207,7 @@ pub(crate) async fn compute_mixnode_reward_estimation(
cache: &State<NodeStatusCache>,
validator_cache: &State<NymContractCache>,
mix_id: MixId,
) -> Result<Json<RewardEstimationResponse>, ErrorResponse> {
) -> Result<Json<RewardEstimationResponse>, RocketErrorResponse> {
Ok(Json(
_compute_mixnode_reward_estimation(
user_reward_param.into_inner(),
@@ -225,7 +225,7 @@ pub(crate) async fn get_mixnode_stake_saturation(
cache: &State<NodeStatusCache>,
validator_cache: &State<NymContractCache>,
mix_id: MixId,
) -> Result<Json<StakeSaturationResponse>, ErrorResponse> {
) -> Result<Json<StakeSaturationResponse>, RocketErrorResponse> {
Ok(Json(
_get_mixnode_stake_saturation(cache, validator_cache, mix_id).await?,
))
@@ -236,7 +236,7 @@ pub(crate) async fn get_mixnode_stake_saturation(
pub(crate) async fn get_mixnode_inclusion_probability(
cache: &State<NodeStatusCache>,
mix_id: MixId,
) -> Result<Json<InclusionProbabilityResponse>, ErrorResponse> {
) -> Result<Json<InclusionProbabilityResponse>, RocketErrorResponse> {
Ok(Json(
_get_mixnode_inclusion_probability(cache, mix_id).await?,
))
@@ -247,7 +247,7 @@ pub(crate) async fn get_mixnode_inclusion_probability(
pub(crate) async fn get_mixnode_avg_uptime(
cache: &State<NodeStatusCache>,
mix_id: MixId,
) -> Result<Json<UptimeResponse>, ErrorResponse> {
) -> Result<Json<UptimeResponse>, RocketErrorResponse> {
Ok(Json(_get_mixnode_avg_uptime(cache, mix_id).await?))
}
@@ -256,7 +256,7 @@ pub(crate) async fn get_mixnode_avg_uptime(
pub(crate) async fn get_gateway_avg_uptime(
cache: &State<NodeStatusCache>,
identity: &str,
) -> Result<Json<GatewayUptimeResponse>, ErrorResponse> {
) -> Result<Json<GatewayUptimeResponse>, RocketErrorResponse> {
Ok(Json(_get_gateway_avg_uptime(cache, identity).await?))
}
@@ -264,7 +264,7 @@ pub(crate) async fn get_gateway_avg_uptime(
#[get("/mixnodes/inclusion_probability")]
pub(crate) async fn get_mixnode_inclusion_probabilities(
cache: &State<NodeStatusCache>,
) -> Result<Json<AllInclusionProbabilitiesResponse>, ErrorResponse> {
) -> Result<Json<AllInclusionProbabilitiesResponse>, RocketErrorResponse> {
Ok(Json(_get_mixnode_inclusion_probabilities(cache).await?))
}
@@ -317,7 +317,7 @@ pub async fn get_gateways_detailed_unfiltered(
}
pub mod unstable {
use crate::node_status_api::models::ErrorResponse;
use crate::node_status_api::models::RocketErrorResponse;
use crate::support::http::helpers::PaginationRequest;
use crate::support::storage::NymApiStorage;
use nym_api_requests::models::{
@@ -486,7 +486,7 @@ pub mod unstable {
pagination: PaginationRequest,
info_cache: &State<NodeInfoCache>,
storage: &State<NymApiStorage>,
) -> Result<Json<MixnodeTestResultResponse>, ErrorResponse> {
) -> Result<Json<MixnodeTestResultResponse>, RocketErrorResponse> {
let page = pagination.page.unwrap_or_default();
let per_page = min(
pagination
@@ -497,7 +497,7 @@ pub mod unstable {
match _mixnode_test_results(mix_id, page, per_page, info_cache, storage).await {
Ok(res) => Ok(Json(res)),
Err(err) => Err(ErrorResponse::new(
Err(err) => Err(RocketErrorResponse::new(
format!("failed to retrieve mixnode test results for node {mix_id}: {err}"),
Status::InternalServerError,
)),
@@ -570,7 +570,7 @@ pub mod unstable {
pagination: PaginationRequest,
info_cache: &State<NodeInfoCache>,
storage: &State<NymApiStorage>,
) -> Result<Json<GatewayTestResultResponse>, ErrorResponse> {
) -> Result<Json<GatewayTestResultResponse>, RocketErrorResponse> {
let page = pagination.page.unwrap_or_default();
let per_page = min(
pagination
@@ -581,7 +581,7 @@ pub mod unstable {
match _gateway_test_results(gateway_identity, page, per_page, info_cache, storage).await {
Ok(res) => Ok(Json(res)),
Err(err) => Err(ErrorResponse::new(
Err(err) => Err(RocketErrorResponse::new(
format!(
"failed to retrieve mixnode test results for gateway {gateway_identity}: {err}"
),
@@ -118,8 +118,8 @@ impl HistoricalUptimeUpdater {
}
}
pub(crate) fn start(storage: &NymApiStorage, shutdown: &TaskManager) {
let uptime_updater = HistoricalUptimeUpdater::new(storage.to_owned());
pub(crate) fn start(storage: NymApiStorage, shutdown: &TaskManager) {
let uptime_updater = HistoricalUptimeUpdater::new(storage);
let shutdown_listener = shutdown.subscribe();
tokio::spawn(async move { uptime_updater.run(shutdown_listener).await });
}
+2 -1
View File
@@ -31,13 +31,14 @@ pub struct NymContractCache {
}
impl NymContractCache {
fn new() -> Self {
pub(crate) fn new() -> Self {
NymContractCache {
initialised: Arc::new(AtomicBool::new(false)),
inner: Arc::new(RwLock::new(ValidatorCacheData::new())),
}
}
#[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")]
pub fn stage() -> AdHoc {
AdHoc::on_ignite("Validator Cache Stage", |rocket| async {
rocket.manage(Self::new())
+270
View File
@@ -0,0 +1,270 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::{
node_status_api::helpers_deprecated::{
_get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed,
},
v2::AxumAppState,
};
use axum::{extract, Router};
use nym_api_requests::models::MixNodeBondAnnotated;
use nym_mixnet_contract_common::{
mixnode::MixNodeDetails, reward_params::RewardingParams, GatewayBond, Interval, MixId,
};
use std::collections::HashSet;
pub(crate) fn nym_contract_cache_routes() -> Router<AxumAppState> {
Router::new()
.route("/mixnodes", axum::routing::get(get_mixnodes))
.route(
"/mixnodes/detailed",
axum::routing::get(get_mixnodes_detailed),
)
.route("/gateways", axum::routing::get(get_gateways))
.route("/mixnodes/rewarded", axum::routing::get(get_rewarded_set))
.route(
"/mixnodes/rewarded/detailed",
axum::routing::get(get_rewarded_set_detailed),
)
.route("/mixnodes/active", axum::routing::get(get_active_set))
.route(
"/mixnodes/active/detailed",
axum::routing::get(get_active_set_detailed),
)
.route(
"/mixnodes/blacklisted",
axum::routing::get(get_blacklisted_mixnodes),
)
.route(
"/gateways/blacklisted",
axum::routing::get(get_blacklisted_gateways),
)
.route(
"/epoch/reward_params",
axum::routing::get(get_interval_reward_params),
)
.route("/epoch/current", axum::routing::get(get_current_epoch))
}
#[utoipa::path(
tag = "contract-cache",
get,
path = "/v1/mixnodes",
responses(
(status = 200, body = Vec<MixNodeDetails>)
)
)]
async fn get_mixnodes(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<Vec<MixNodeDetails>> {
state.nym_contract_cache().mixnodes_filtered().await.into()
}
// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated,
// replace this with
// ```
// pub fn get_mixnodes_detailed() -> Redirect {
// Redirect::to(uri!("/v1/status/mixnodes/detailed"))
// }
// ```
#[utoipa::path(
tag = "contract-cache",
get,
path = "/v1/mixnodes/detailed",
responses(
(status = 200, body = Vec<MixNodeBondAnnotated>)
)
)]
async fn get_mixnodes_detailed(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<Vec<MixNodeBondAnnotated>> {
_get_mixnodes_detailed(state.node_status_cache())
.await
.into()
}
#[utoipa::path(
tag = "contract-cache",
get,
path = "/v1/gateways",
responses(
(status = 200, body = Vec<GatewayBond>)
)
)]
async fn get_gateways(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<Vec<GatewayBond>> {
state.nym_contract_cache().gateways_filtered().await.into()
}
#[utoipa::path(
tag = "contract-cache",
get,
path = "/v1/mixnodes/rewarded",
responses(
(status = 200, body = Vec<MixNodeDetails>)
)
)]
async fn get_rewarded_set(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<Vec<MixNodeDetails>> {
state
.nym_contract_cache()
.rewarded_set()
.await
.to_owned()
.into()
}
// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated,
// replace this with
// ```
// pub fn get_mixnodes_set_detailed() -> Redirect {
// Redirect::to(uri!("/v1/status/mixnodes/rewarded/detailed"))
// }
// ```
#[utoipa::path(
tag = "contract-cache",
get,
path = "/v1/mixnodes/rewarded/detailed",
responses(
(status = 200, body = Vec<MixNodeBondAnnotated>)
)
)]
async fn get_rewarded_set_detailed(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<Vec<MixNodeBondAnnotated>> {
_get_rewarded_set_detailed(state.node_status_cache())
.await
.into()
}
#[utoipa::path(
tag = "contract-cache",
get,
path = "/v1/mixnodes/active",
responses(
(status = 200, body = Vec<MixNodeDetails>)
)
)]
async fn get_active_set(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<Vec<MixNodeDetails>> {
state
.nym_contract_cache()
.active_set()
.await
.to_owned()
.into()
}
// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated,
// replace this with
// ```
// pub fn get_active_set_detailed() -> Redirect {
// Redirect::to(uri!("/status/mixnodes/active/detailed"))
// }
// ```
#[utoipa::path(
tag = "contract-cache",
get,
path = "/v1/mixnodes/active/detailed",
responses(
(status = 200, body = Vec<MixNodeBondAnnotated>)
)
)]
async fn get_active_set_detailed(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<Vec<MixNodeBondAnnotated>> {
_get_active_set_detailed(state.node_status_cache())
.await
.into()
}
#[utoipa::path(
tag = "contract-cache",
get,
path = "/v1/mixnodes/blacklisted",
responses(
(status = 200, body = Option<HashSet<MixId>>)
)
)]
async fn get_blacklisted_mixnodes(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<Option<HashSet<MixId>>> {
let blacklist = state
.nym_contract_cache()
.mixnodes_blacklist()
.await
.to_owned();
if blacklist.is_empty() {
None
} else {
Some(blacklist)
}
.into()
}
#[utoipa::path(
tag = "contract-cache",
get,
path = "/v1/gateways/blacklisted",
responses(
(status = 200, body = Option<HashSet<String>>)
)
)]
async fn get_blacklisted_gateways(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<Option<HashSet<String>>> {
let blacklist = state
.nym_contract_cache()
.gateways_blacklist()
.await
.to_owned();
if blacklist.is_empty() {
None
} else {
Some(blacklist)
}
.into()
}
#[utoipa::path(
tag = "contract-cache",
get,
path = "/v1/epoch/reward_params",
responses(
(status = 200, body = Option<RewardingParams>)
)
)]
async fn get_interval_reward_params(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<Option<RewardingParams>> {
state
.nym_contract_cache()
.interval_reward_params()
.await
.to_owned()
.into()
}
#[utoipa::path(
tag = "contract-cache",
get,
path = "/v1/epoch/current",
responses(
(status = 200, body = Option<Interval>)
)
)]
async fn get_current_epoch(
extract::State(state): extract::State<AxumAppState>,
) -> axum::Json<Option<Interval>> {
state
.nym_contract_cache()
.current_interval()
.await
.to_owned()
.into()
}
+2
View File
@@ -12,6 +12,8 @@ use rocket_okapi::settings::OpenApiSettings;
use self::cache::refresher::NymContractCacheRefresher;
pub(crate) mod cache;
#[cfg(feature = "axum")]
pub(crate) mod handlers;
pub(crate) mod routes;
pub(crate) fn nym_contract_cache_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
+3 -1
View File
@@ -3,7 +3,9 @@
use crate::{
node_status_api::{
helpers::{_get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed},
helpers_deprecated::{
_get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed,
},
NodeStatusCache,
},
nym_contract_cache::cache::NymContractCache,
+95
View File
@@ -0,0 +1,95 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::v2::AxumAppState;
use axum::{extract::State, Json, Router};
use nym_api_requests::models::{DescribedGateway, DescribedMixNode};
use nym_mixnet_contract_common::MixNodeBond;
use std::ops::Deref;
// obviously this should get refactored later on because gateways will go away.
// unless maybe this will be filtering based on which nodes got assigned gateway role? TBD
pub(crate) fn nym_node_routes() -> axum::Router<AxumAppState> {
Router::new()
.route(
"/gateways/described",
axum::routing::get(get_gateways_described),
)
.route(
"/mixnodes/described",
axum::routing::get(get_mixnodes_described),
)
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/v1/gateways/described",
responses(
(status = 200, body = Vec<DescribedGateway>)
)
)]
async fn get_gateways_described(State(state): State<AxumAppState>) -> Json<Vec<DescribedGateway>> {
let gateways = state.nym_contract_cache().gateways_filtered().await;
if gateways.is_empty() {
return Json(Vec::new());
}
// if the self describe cache is unavailable, well, don't attach describe data
let Ok(self_descriptions) = state.described_nodes_state().get().await else {
return Json(gateways.into_iter().map(Into::into).collect());
};
// TODO: this is extremely inefficient, but given we don't have many gateways,
// it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo,
// but at that point (I hope) the whole caching situation should get refactored
Json(
gateways
.into_iter()
.map(|bond| DescribedGateway {
self_described: self_descriptions.deref().get(bond.identity()).cloned(),
bond,
})
.collect(),
)
}
#[utoipa::path(
tag = "Nym Nodes",
get,
path = "/v1/mixnodes/described",
responses(
(status = 200, body = Vec<DescribedMixNode>)
)
)]
async fn get_mixnodes_described(State(state): State<AxumAppState>) -> Json<Vec<DescribedMixNode>> {
let mixnodes = state
.nym_contract_cache()
.mixnodes_filtered()
.await
.into_iter()
.map(|m| m.bond_information)
.collect::<Vec<MixNodeBond>>();
if mixnodes.is_empty() {
return Json(Vec::new());
}
// if the self describe cache is unavailable, well, don't attach describe data
let Ok(self_descriptions) = state.described_nodes_state().get().await else {
return Json(mixnodes.into_iter().map(Into::into).collect());
};
// TODO: this is extremely inefficient, but given we don't have many gateways,
// it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo,
// but at that point (I hope) the whole caching situation should get refactored
Json(
mixnodes
.into_iter()
.map(|bond| DescribedMixNode {
self_described: self_descriptions.deref().get(bond.identity()).cloned(),
bond,
})
.collect(),
)
}
+377
View File
@@ -0,0 +1,377 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
//! All routes/nodes are split into three tiers:
//!
//! `/skimmed`
//! - used by clients
//! - returns the very basic information for routing purposes
//!
//! `/semi-skimmed`
//! - used by other nodes/VPN
//! - returns more additional information such noise keys
//!
//! `/full-fat`
//! - used by explorers, et al.
//! - returns almost everything there is about the nodes
//!
//! There's also additional split based on the role:
//! - `?role` => filters based on the specific role (mixnode/gateway/(in the future: entry/exit))
//! - `/mixnodes/<tier>` => only returns mixnode role data
//! - `/gateway/<tier>` => only returns (entry) gateway role data
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::v2::AxumAppState;
use axum::extract::Query;
use axum::extract::State;
use axum::{Json, Router};
use nym_api_requests::nym_nodes::{
CachedNodesResponse, FullFatNode, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode,
};
use nym_bin_common::version_checker;
use serde::Deserialize;
use std::cmp::min;
use std::ops::Deref;
pub(crate) fn nym_node_routes_unstable() -> axum::Router<AxumAppState> {
Router::new()
.route("/skimmed", axum::routing::get(nodes_basic))
.route("/semi-skimmed", axum::routing::get(nodes_expanded))
.route("/full-fat", axum::routing::get(nodes_detailed))
.nest(
"/gateways",
Router::new()
.route("/skimmed", axum::routing::get(gateways_basic))
.route("/semi-skimmed", axum::routing::get(gateways_expanded))
.route("/full-fat", axum::routing::get(gateways_detailed)),
)
.nest(
"/mixnodes",
Router::new()
.route("/skimmed", axum::routing::get(mixnodes_basic))
.route("/semi-skimmed", axum::routing::get(mixnodes_expanded))
.route("/full-fat", axum::routing::get(mixnodes_detailed)),
)
}
#[derive(Debug, Deserialize, utoipa::IntoParams)]
struct NodesParams {
#[param(inline)]
role: Option<NodeRoleQueryParam>,
semver_compatibility: Option<String>,
}
#[derive(Debug, Deserialize, utoipa::IntoParams)]
struct SemverCompatibilityQueryParam {
semver_compatibility: Option<String>,
}
impl SemverCompatibilityQueryParam {
pub fn new(semver_compatibility: Option<String>) -> Self {
Self {
semver_compatibility,
}
}
}
#[utoipa::path(
tag = "Unstable Nym Nodes",
get,
params(NodesParams),
path = "/v1/unstable/nym-nodes/skimmed",
responses(
(status = 200, body = CachedNodesResponse<SkimmedNode>)
)
)]
async fn nodes_basic(
state: State<AxumAppState>,
Query(NodesParams {
role,
semver_compatibility,
}): Query<NodesParams>,
) -> AxumResult<Json<CachedNodesResponse<SkimmedNode>>> {
if let Some(role) = role {
match role {
NodeRoleQueryParam::ActiveMixnode => {
return mixnodes_basic(
state,
Query(SemverCompatibilityQueryParam::new(semver_compatibility)),
)
.await
}
NodeRoleQueryParam::EntryGateway => {
return gateways_basic(
state,
Query(SemverCompatibilityQueryParam::new(semver_compatibility)),
)
.await;
}
_ => {}
}
}
Err(AxumErrorResponse::not_implemented())
}
#[utoipa::path(
tag = "Unstable Nym Nodes",
get,
params(NodesParams),
path = "/v1/unstable/nym-nodes/semi-skimmed",
responses(
(status = 200, body = CachedNodesResponse<SemiSkimmedNode>)
)
)]
async fn nodes_expanded(
state: State<AxumAppState>,
Query(NodesParams {
role,
semver_compatibility,
}): Query<NodesParams>,
) -> AxumResult<Json<CachedNodesResponse<SemiSkimmedNode>>> {
if let Some(role) = role {
match role {
NodeRoleQueryParam::ActiveMixnode => {
return mixnodes_expanded(
state,
Query(SemverCompatibilityQueryParam::new(semver_compatibility)),
)
.await
}
NodeRoleQueryParam::EntryGateway => {
return gateways_expanded(
state,
Query(SemverCompatibilityQueryParam::new(semver_compatibility)),
)
.await
}
_ => {}
}
}
Err(AxumErrorResponse::not_implemented())
}
#[utoipa::path(
tag = "Unstable Nym Nodes",
get,
params(NodesParams),
path = "/v1/unstable/nym-nodes/full-fat",
responses(
(status = 200, body = CachedNodesResponse<FullFatNode>)
)
)]
async fn nodes_detailed(
state: State<AxumAppState>,
Query(NodesParams {
role,
semver_compatibility,
}): Query<NodesParams>,
) -> AxumResult<Json<CachedNodesResponse<FullFatNode>>> {
if let Some(role) = role {
match role {
NodeRoleQueryParam::ActiveMixnode => {
return mixnodes_detailed(
state,
Query(SemverCompatibilityQueryParam::new(semver_compatibility)),
)
.await
}
NodeRoleQueryParam::EntryGateway => {
return gateways_detailed(
state,
Query(SemverCompatibilityQueryParam::new(semver_compatibility)),
)
.await
}
_ => {}
}
}
Err(AxumErrorResponse::not_implemented())
}
#[utoipa::path(
tag = "Unstable Nym Nodes",
get,
params(SemverCompatibilityQueryParam),
path = "/v1/unstable/nym-nodes/gateways/skimmed",
responses(
(status = 200, body = CachedNodesResponse<SkimmedNode>)
)
)]
async fn gateways_basic(
state: State<AxumAppState>,
Query(SemverCompatibilityQueryParam {
semver_compatibility,
}): Query<SemverCompatibilityQueryParam>,
) -> AxumResult<Json<CachedNodesResponse<SkimmedNode>>> {
let status_cache = state.node_status_cache();
let describe_cache = state.described_nodes_state();
let gateways_cache =
status_cache
.gateways_cache()
.await
.ok_or(AxumErrorResponse::internal_msg(
"could not obtain gateways cache",
))?;
if gateways_cache.is_empty() {
return Ok(Json(CachedNodesResponse {
refreshed_at: gateways_cache.timestamp().into(),
nodes: vec![],
}));
}
// if the self describe cache is unavailable don't try to use self-describe data
let Ok(self_descriptions) = describe_cache.get().await else {
return Ok(Json(CachedNodesResponse {
refreshed_at: gateways_cache.timestamp().into(),
nodes: gateways_cache.values().map(Into::into).collect(),
}));
};
let refreshed_at = min(gateways_cache.timestamp(), self_descriptions.timestamp());
// the same comment holds as with `get_gateways_described`.
// this is inefficient and will have to get refactored with directory v3
Ok(Json(CachedNodesResponse {
refreshed_at: refreshed_at.into(),
nodes: gateways_cache
.values()
.filter(|annotated_bond| {
if let Some(semver_compatibility) = semver_compatibility.as_ref() {
version_checker::is_minor_version_compatible(
&annotated_bond.gateway_bond.gateway.version,
semver_compatibility,
)
} else {
true
}
})
.map(|annotated_bond| {
SkimmedNode::from_described_gateway(
annotated_bond,
self_descriptions.deref().get(annotated_bond.identity()),
)
})
.collect(),
}))
}
#[utoipa::path(
tag = "Unstable Nym Nodes",
get,
params(SemverCompatibilityQueryParam),
path = "/v1/unstable/nym-nodes/gateways/semi-skimmed",
responses(
(status = 200, body = CachedNodesResponse<SemiSkimmedNode>)
)
)]
async fn gateways_expanded(
State(_state): State<AxumAppState>,
Query(SemverCompatibilityQueryParam {
semver_compatibility: _semver_compatibility,
}): Query<SemverCompatibilityQueryParam>,
) -> AxumResult<Json<CachedNodesResponse<SemiSkimmedNode>>> {
Err(AxumErrorResponse::not_implemented())
}
#[utoipa::path(
tag = "Unstable Nym Nodes",
get,
params(SemverCompatibilityQueryParam),
path = "/v1/unstable/nym-nodes/gateways/full-fat",
responses(
(status = 200, body = CachedNodesResponse<FullFatNode>)
)
)]
async fn gateways_detailed(
State(_state): State<AxumAppState>,
Query(SemverCompatibilityQueryParam {
semver_compatibility: _semver_compatibility,
}): Query<SemverCompatibilityQueryParam>,
) -> AxumResult<Json<CachedNodesResponse<FullFatNode>>> {
Err(AxumErrorResponse::not_implemented())
}
#[utoipa::path(
tag = "Unstable Nym Nodes",
get,
params(SemverCompatibilityQueryParam),
path = "/v1/unstable/nym-nodes/mixnodes/skimmed",
responses(
(status = 200, body = CachedNodesResponse<SkimmedNode>)
)
)]
async fn mixnodes_basic(
state: State<AxumAppState>,
Query(SemverCompatibilityQueryParam {
semver_compatibility,
}): Query<SemverCompatibilityQueryParam>,
) -> AxumResult<Json<CachedNodesResponse<SkimmedNode>>> {
let mixnodes_cache = state
.node_status_cache()
.active_mixnodes_cache()
.await
.ok_or(AxumErrorResponse::internal_msg(
"could not obtain mixnodes cache",
))?;
Ok(Json(CachedNodesResponse {
refreshed_at: mixnodes_cache.timestamp().into(),
nodes: mixnodes_cache
.iter()
.filter(|annotated_bond| {
if let Some(semver_compatibility) = semver_compatibility.as_ref() {
version_checker::is_minor_version_compatible(
&annotated_bond
.mixnode_details
.bond_information
.mix_node
.version,
semver_compatibility,
)
} else {
true
}
})
.map(Into::into)
.collect(),
}))
}
#[utoipa::path(
tag = "Unstable Nym Nodes",
get,
params(SemverCompatibilityQueryParam),
path = "/v1/unstable/nym-nodes/mixnodes/semi-skimmed",
responses(
(status = 200, body = CachedNodesResponse<SemiSkimmedNode>)
)
)]
async fn mixnodes_expanded(
State(_state): State<AxumAppState>,
Query(SemverCompatibilityQueryParam {
semver_compatibility: _semver_compatibility,
}): Query<SemverCompatibilityQueryParam>,
) -> AxumResult<Json<CachedNodesResponse<SemiSkimmedNode>>> {
Err(AxumErrorResponse::not_implemented())
}
#[utoipa::path(
tag = "Unstable Nym Nodes",
get,
params(SemverCompatibilityQueryParam),
path = "/v1/unstable/nym-nodes/mixnodes/full-fat",
responses(
(status = 200, body = CachedNodesResponse<FullFatNode>)
)
)]
async fn mixnodes_detailed(
State(_state): State<AxumAppState>,
Query(SemverCompatibilityQueryParam {
semver_compatibility: _semver_compatibility,
}): Query<SemverCompatibilityQueryParam>,
) -> AxumResult<Json<CachedNodesResponse<FullFatNode>>> {
Err(AxumErrorResponse::not_implemented())
}
+6 -1
View File
@@ -6,11 +6,16 @@ use rocket::Route;
use rocket_okapi::openapi_get_routes_spec;
use rocket_okapi::settings::OpenApiSettings;
#[cfg(feature = "axum")]
pub(crate) mod handlers;
#[cfg(feature = "axum")]
pub(crate) mod handlers_unstable;
pub(crate) mod routes;
mod unstable_routes;
/// Merges the routes with http information and returns it to Rocket for serving
pub(crate) fn nym_node_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
pub(crate) fn nym_node_routes_deprecated(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
openapi_get_routes_spec![
settings: routes::get_gateways_described, routes::get_mixnodes_described
]
+40 -19
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_describe_cache::DescribedNodes;
use crate::node_status_api::models::ErrorResponse;
use crate::node_status_api::models::RocketErrorResponse;
use crate::node_status_api::NodeStatusCache;
use crate::support::caching::cache::SharedCache;
use nym_api_requests::nym_nodes::{
@@ -39,7 +39,7 @@ pub async fn nodes_basic(
describe_cache: &State<SharedCache<DescribedNodes>>,
role: Option<NodeRoleQueryParam>,
semver_compatibility: Option<String>,
) -> Result<Json<CachedNodesResponse<SkimmedNode>>, ErrorResponse> {
) -> Result<Json<CachedNodesResponse<SkimmedNode>>, RocketErrorResponse> {
if let Some(role) = role {
match role {
NodeRoleQueryParam::ActiveMixnode => {
@@ -52,7 +52,10 @@ pub async fn nodes_basic(
}
}
Err(ErrorResponse::new("unimplemented", Status::NotImplemented))
Err(RocketErrorResponse::new(
"unimplemented",
Status::NotImplemented,
))
}
#[openapi(tag = "Unstable Nym Nodes")]
@@ -61,7 +64,7 @@ pub async fn nodes_expanded(
cache: &State<NodeStatusCache>,
role: Option<NodeRoleQueryParam>,
semver_compatibility: Option<String>,
) -> Result<Json<CachedNodesResponse<SemiSkimmedNode>>, ErrorResponse> {
) -> Result<Json<CachedNodesResponse<SemiSkimmedNode>>, RocketErrorResponse> {
if let Some(role) = role {
match role {
NodeRoleQueryParam::ActiveMixnode => {
@@ -74,7 +77,10 @@ pub async fn nodes_expanded(
}
}
Err(ErrorResponse::new("unimplemented", Status::NotImplemented))
Err(RocketErrorResponse::new(
"unimplemented",
Status::NotImplemented,
))
}
#[openapi(tag = "Unstable Nym Nodes")]
@@ -83,7 +89,7 @@ pub async fn nodes_detailed(
cache: &State<NodeStatusCache>,
role: Option<NodeRoleQueryParam>,
semver_compatibility: Option<String>,
) -> Result<Json<CachedNodesResponse<FullFatNode>>, ErrorResponse> {
) -> Result<Json<CachedNodesResponse<FullFatNode>>, RocketErrorResponse> {
if let Some(role) = role {
match role {
NodeRoleQueryParam::ActiveMixnode => {
@@ -96,7 +102,10 @@ pub async fn nodes_detailed(
}
}
Err(ErrorResponse::new("unimplemented", Status::NotImplemented))
Err(RocketErrorResponse::new(
"unimplemented",
Status::NotImplemented,
))
}
#[openapi(tag = "Unstable Nym Nodes")]
@@ -105,11 +114,11 @@ pub async fn gateways_basic(
status_cache: &State<NodeStatusCache>,
describe_cache: &State<SharedCache<DescribedNodes>>,
semver_compatibility: Option<String>,
) -> Result<Json<CachedNodesResponse<SkimmedNode>>, ErrorResponse> {
) -> Result<Json<CachedNodesResponse<SkimmedNode>>, RocketErrorResponse> {
let gateways_cache = status_cache
.gateways_cache()
.await
.ok_or(ErrorResponse::new(
.ok_or(RocketErrorResponse::new(
"could not obtain gateways cache",
Status::InternalServerError,
))?;
@@ -162,10 +171,13 @@ pub async fn gateways_basic(
pub async fn gateways_expanded(
cache: &State<NodeStatusCache>,
semver_compatibility: Option<String>,
) -> Result<Json<CachedNodesResponse<SemiSkimmedNode>>, ErrorResponse> {
) -> Result<Json<CachedNodesResponse<SemiSkimmedNode>>, RocketErrorResponse> {
let _ = cache;
let _ = semver_compatibility;
Err(ErrorResponse::new("unimplemented", Status::NotImplemented))
Err(RocketErrorResponse::new(
"unimplemented",
Status::NotImplemented,
))
}
#[openapi(tag = "Unstable Nym Nodes")]
@@ -173,10 +185,13 @@ pub async fn gateways_expanded(
pub async fn gateways_detailed(
cache: &State<NodeStatusCache>,
semver_compatibility: Option<String>,
) -> Result<Json<CachedNodesResponse<FullFatNode>>, ErrorResponse> {
) -> Result<Json<CachedNodesResponse<FullFatNode>>, RocketErrorResponse> {
let _ = cache;
let _ = semver_compatibility;
Err(ErrorResponse::new("unimplemented", Status::NotImplemented))
Err(RocketErrorResponse::new(
"unimplemented",
Status::NotImplemented,
))
}
#[openapi(tag = "Unstable Nym Nodes")]
@@ -184,11 +199,11 @@ pub async fn gateways_detailed(
pub async fn mixnodes_basic(
cache: &State<NodeStatusCache>,
semver_compatibility: Option<String>,
) -> Result<Json<CachedNodesResponse<SkimmedNode>>, ErrorResponse> {
) -> Result<Json<CachedNodesResponse<SkimmedNode>>, RocketErrorResponse> {
let mixnodes_cache = cache
.active_mixnodes_cache()
.await
.ok_or(ErrorResponse::new(
.ok_or(RocketErrorResponse::new(
"could not obtain mixnodes cache",
Status::InternalServerError,
))?;
@@ -220,10 +235,13 @@ pub async fn mixnodes_basic(
pub async fn mixnodes_expanded(
cache: &State<NodeStatusCache>,
semver_compatibility: Option<String>,
) -> Result<Json<CachedNodesResponse<SemiSkimmedNode>>, ErrorResponse> {
) -> Result<Json<CachedNodesResponse<SemiSkimmedNode>>, RocketErrorResponse> {
let _ = cache;
let _ = semver_compatibility;
Err(ErrorResponse::new("unimplemented", Status::NotImplemented))
Err(RocketErrorResponse::new(
"unimplemented",
Status::NotImplemented,
))
}
#[openapi(tag = "Unstable Nym Nodes")]
@@ -231,8 +249,11 @@ pub async fn mixnodes_expanded(
pub async fn mixnodes_detailed(
cache: &State<NodeStatusCache>,
semver_compatibility: Option<String>,
) -> Result<Json<CachedNodesResponse<FullFatNode>>, ErrorResponse> {
) -> Result<Json<CachedNodesResponse<FullFatNode>>, RocketErrorResponse> {
let _ = cache;
let _ = semver_compatibility;
Err(ErrorResponse::new("unimplemented", Status::NotImplemented))
Err(RocketErrorResponse::new(
"unimplemented",
Status::NotImplemented,
))
}
+92
View File
@@ -0,0 +1,92 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::status::ApiStatusState;
use crate::v2::AxumAppState;
use axum::Json;
use axum::Router;
use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse};
use nym_bin_common::build_information::BinaryBuildInformationOwned;
use nym_compact_ecash::Base58;
use std::sync::Arc;
pub(crate) fn api_status_routes() -> Router<AxumAppState> {
let api_status_state = Arc::new(ApiStatusState::new());
Router::new()
.route(
"/health",
axum::routing::get({
let state = Arc::clone(&api_status_state);
|| health(state)
}),
)
.route(
"/build-information",
axum::routing::get({
let state = Arc::clone(&api_status_state);
|| build_information(state)
}),
)
.route(
"/signer-information",
axum::routing::get({
let state = Arc::clone(&api_status_state);
|| signer_information(state)
}),
)
}
#[utoipa::path(
tag = "API Status",
get,
path = "/v1/api-status/health",
responses(
(status = 200, body = ApiHealthResponse)
)
)]
async fn health(state: Arc<ApiStatusState>) -> Json<ApiHealthResponse> {
let uptime = state.startup_time.elapsed();
let health = ApiHealthResponse::new_healthy(uptime);
Json(health)
}
#[utoipa::path(
tag = "API Status",
get,
path = "/v1/api-status/build-information",
responses(
(status = 200, body = BinaryBuildInformationOwned)
)
)]
async fn build_information(state: Arc<ApiStatusState>) -> Json<BinaryBuildInformationOwned> {
Json(state.build_information.to_owned())
}
#[utoipa::path(
tag = "API Status",
get,
path = "/v1/api-status/signer-information",
responses(
(status = 200, body = SignerInformationResponse)
)
)]
async fn signer_information(
state: Arc<ApiStatusState>,
) -> AxumResult<Json<SignerInformationResponse>> {
let signer_state = state.signer_information.as_ref().ok_or_else(|| {
AxumErrorResponse::internal_msg("this api does not expose zk-nym signing functionalities")
})?;
Ok(Json(SignerInformationResponse {
cosmos_address: signer_state.cosmos_address.clone(),
identity: signer_state.identity.clone(),
announce_address: signer_state.announce_address.clone(),
verification_key: signer_state
.coconut_keypair
.verification_key()
.await
.map(|maybe_vk| maybe_vk.to_bs58()),
}))
}
+2
View File
@@ -10,6 +10,8 @@ use rocket_okapi::openapi_get_routes_spec;
use rocket_okapi::settings::OpenApiSettings;
use tokio::time::Instant;
#[cfg(feature = "axum")]
pub(crate) mod handlers;
pub(crate) mod routes;
pub(crate) struct ApiStatusState {
+3 -3
View File
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::ErrorResponse;
use crate::node_status_api::models::RocketErrorResponse;
use crate::status::ApiStatusState;
use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse};
use nym_bin_common::build_information::BinaryBuildInformationOwned;
@@ -31,9 +31,9 @@ pub(crate) async fn build_information(
#[get("/signer-information")]
pub(crate) async fn signer_information(
state: &State<ApiStatusState>,
) -> Result<Json<SignerInformationResponse>, ErrorResponse> {
) -> Result<Json<SignerInformationResponse>, RocketErrorResponse> {
let signer_state = state.signer_information.as_ref().ok_or_else(|| {
ErrorResponse::new(
RocketErrorResponse::new(
"this api does not expose zk-nym signing functionalities",
Status::InternalServerError,
)
+33 -11
View File
@@ -3,7 +3,7 @@
use crate::start_nym_api_tasks;
use crate::support::config::helpers::try_load_current_config;
use anyhow::bail;
use cfg_if::cfg_if;
#[derive(clap::Args, Debug)]
pub(crate) struct Args {
@@ -43,7 +43,7 @@ pub(crate) struct Args {
pub(crate) enable_zk_nym: Option<bool>,
/// Announced address that is going to be put in the DKG contract where zk-nym clients will connect
/// to obtain their credentials
/// to obtain their credentials
/// default: None - config value will be used instead
#[clap(long)]
pub(crate) announce_address: Option<url::Url>,
@@ -62,18 +62,40 @@ pub(crate) async fn execute(args: Args) -> anyhow::Result<()> {
config.validate()?;
let mut shutdown_handlers = start_nym_api_tasks(config).await?;
#[cfg(feature = "axum")]
let mut axum_shutdown = crate::v2::start_nym_api_tasks_v2(&config).await?;
let mut rocket_shutdown = start_nym_api_tasks(config).await?;
// it doesn't matter which server catches the interrupt: it needs only be caught once
if let Err(err) = rocket_shutdown.task_manager_handle.catch_interrupt().await {
error!("Error stopping Rocket tasks: {err}");
}
let res = shutdown_handlers
.task_manager_handle
.catch_interrupt()
.await;
log::info!("Stopping nym API");
shutdown_handlers.rocket_handle.notify();
rocket_shutdown.rocket_handle.notify();
if let Err(err) = res {
// that's a nasty workaround, but anyhow errors are generally nicer, especially on exit
bail!("{err}")
// because Rocket caught the interrupt, it had already signalled its
// background tasks to retire. Now do that for axum
cfg_if! {
if #[cfg(feature = "axum")] {
axum_shutdown.task_manager_mut().signal_shutdown().ok();
axum_shutdown.task_manager_mut().wait_for_shutdown().await;
let running_server = axum_shutdown.shutdown_axum();
match running_server.await {
Ok(Ok(_)) => {
info!("Axum HTTP server shut down without errors");
},
Ok(Err(err)) => {
error!("Axum HTTP server terminated with: {err}");
anyhow::bail!(err)
},
Err(err) => {
error!("Server task panicked: {err}");
}
};
}
}
Ok(())
+18
View File
@@ -16,6 +16,7 @@ use nym_config::{
};
use serde::{Deserialize, Serialize};
use std::io;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::{Path, PathBuf};
use std::time::Duration;
use url::Url;
@@ -225,6 +226,17 @@ impl Config {
}
}
// TODO rocket: when axum becomes the main server, change its bind addr default here
fn default_http_socket_addr() -> SocketAddr {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081)
} else {
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8081)
}
}
}
// we only really care about the mnemonic being zeroized
#[derive(Debug, Deserialize, PartialEq, Eq, Serialize, Zeroize, ZeroizeOnDrop)]
pub struct Base {
@@ -234,6 +246,11 @@ pub struct Base {
#[zeroize(skip)]
pub local_validator: Url,
/// Socket address Axum will use for binding its HTTP API.
#[zeroize(skip)]
#[serde(default = "default_http_socket_addr")]
pub bind_address: SocketAddr,
/// Mnemonic used for rewarding and/or multisig operations
// TODO: similarly to the note in gateway, this should get moved to a separate file
#[serde(deserialize_with = "de_maybe_stringified")]
@@ -256,6 +273,7 @@ impl Base {
storage_paths: NymApiPaths::new_default(&id),
id,
local_validator: default_validator,
bind_address: default_http_socket_addr(),
mnemonic: None,
}
}
+5 -1
View File
@@ -15,6 +15,10 @@ id = '{{ base.id }}'
# Validator server to which the API will be getting information about the network.
local_validator = '{{ base.local_validator }}'
# Socket address this api will use for binding its http API.
# Note: only used if `axum` feature is enabled.
bind_address = '{{ base.bind_address }}'
# Mnemonic used for rewarding and validator interaction
mnemonic = '{{ base.mnemonic }}'
@@ -58,7 +62,7 @@ route_test_packets = {{ network_monitor.debug.route_test_packets }}
# Number of test packets sent to each node during regular monitor test run.
per_node_test_packets = {{ network_monitor.debug.per_node_test_packets }}
##### node status api config options #####
+12 -27
View File
@@ -8,19 +8,18 @@ use crate::ecash::{self, comm::QueryCommunicationChannel};
use crate::network::models::NetworkDetails;
use crate::network::network_routes;
use crate::node_describe_cache::DescribedNodes;
use crate::node_status_api::routes::unstable;
use crate::node_status_api::routes_deprecated::unstable;
use crate::node_status_api::{self, NodeStatusCache};
use crate::nym_contract_cache::cache::NymContractCache;
use crate::nym_nodes::nym_node_routes_next;
use crate::nym_nodes::{nym_node_routes_deprecated, nym_node_routes_next};
use crate::status::{api_status_routes, ApiStatusState, SignerState};
use crate::support::caching::cache::SharedCache;
use crate::support::config::Config;
use crate::support::{nyxd, storage};
use crate::{circulating_supply_api, nym_contract_cache, nym_nodes::nym_node_routes};
use crate::{circulating_supply_api, nym_contract_cache};
use anyhow::{bail, Context, Result};
use nym_crypto::asymmetric::identity;
use nym_validator_client::nyxd::Coin;
use rocket::http::Method;
use rocket::{Ignite, Rocket};
use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors};
use rocket_okapi::mount_endpoints_and_merged_docs;
@@ -52,33 +51,26 @@ pub(crate) async fn setup_rocket(
"/network" => network_routes(&openapi_settings),
"/api-status" => api_status_routes(&openapi_settings),
"/ecash" => ecash::routes_open_api(&openapi_settings, config.coconut_signer.enabled),
"" => nym_node_routes(&openapi_settings),
"" => nym_node_routes_deprecated(&openapi_settings),
// => when we move those routes, we'll need to add a redirection for backwards compatibility
"/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings)
}
let storage =
storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?;
let rocket = rocket
.manage(network_details)
.manage(SharedCache::<DescribedNodes>::new())
.mount("/swagger", make_swagger_ui(&openapi::get_docs()))
.attach(setup_cors()?)
.attach(setup_rocket_cors()?)
.attach(NymContractCache::stage())
.attach(NodeStatusCache::stage())
.attach(CirculatingSupplyCache::stage(mix_denom.clone()))
.attach(storage::NymApiStorage::stage(storage.clone()))
.manage(unstable::NodeInfoCache::default());
// This is not a very nice approach. A lazy value would be more suitable, but that's still
// a nightly feature: https://github.com/rust-lang/rust/issues/74465
let storage = if config.coconut_signer.enabled || config.network_monitor.enabled {
Some(
storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path)
.await?,
)
} else {
None
};
let mut status_state = ApiStatusState::new();
let rocket = if config.coconut_signer.enabled {
@@ -117,7 +109,7 @@ pub(crate) async fn setup_rocket(
identity_keypair,
coconut_keypair,
comm_channel,
storage.clone().unwrap(),
storage.clone(),
)
.await?;
@@ -126,23 +118,16 @@ pub(crate) async fn setup_rocket(
rocket
};
// see if we should start up network monitor
let rocket = if config.network_monitor.enabled {
rocket.attach(storage::NymApiStorage::stage(storage.unwrap()))
} else {
rocket
};
Ok(rocket.manage(status_state).ignite().await?)
}
fn setup_cors() -> Result<Cors> {
fn setup_rocket_cors() -> Result<Cors> {
let allowed_origins = AllowedOrigins::all();
// You can also deserialize this
let cors = rocket_cors::CorsOptions {
allowed_origins,
allowed_methods: vec![Method::Post, Method::Get]
allowed_methods: vec![rocket::http::Method::Post, rocket::http::Method::Get]
.into_iter()
.map(From::from)
.collect(),
+1
View File
@@ -64,6 +64,7 @@ impl NymApiStorage {
Ok(storage)
}
#[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")]
pub(crate) fn stage(storage: NymApiStorage) -> AdHoc {
AdHoc::try_on_ignite("SQLx Database", |rocket| async {
Ok(rocket.manage(storage))
+85
View File
@@ -0,0 +1,85 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::network::handlers::ContractVersionSchemaResponse;
use nym_api_requests::models;
use utoipa::OpenApi;
use utoipauto::utoipauto;
// TODO once https://github.com/ProbablyClem/utoipauto/pull/38 is released:
// include ",./nym-api/nym-api-requests/src from nym-api-requests" (and other packages mentioned below)
// for automatic model discovery based on ToSchema / IntoParams implementation.
// Then you can remove `components(schemas)` manual imports below
#[utoipauto(paths = "./nym-api/src")]
#[derive(OpenApi)]
#[openapi(
info(title = "Nym API"),
tags(),
components(schemas(
models::CirculatingSupplyResponse,
models::CoinSchema,
nym_mixnet_contract_common::Interval,
nym_api_requests::models::GatewayStatusReportResponse,
nym_api_requests::models::GatewayUptimeHistoryResponse,
nym_api_requests::models::HistoricalUptimeResponse,
nym_api_requests::models::GatewayCoreStatusResponse,
nym_api_requests::models::GatewayUptimeResponse,
nym_api_requests::models::RewardEstimationResponse,
nym_api_requests::models::UptimeResponse,
nym_api_requests::models::ComputeRewardEstParam,
nym_api_requests::models::MixNodeBondAnnotated,
nym_api_requests::models::GatewayBondAnnotated,
nym_api_requests::models::MixnodeTestResultResponse,
nym_api_requests::models::StakeSaturationResponse,
nym_api_requests::models::InclusionProbabilityResponse,
nym_api_requests::models::AllInclusionProbabilitiesResponse,
nym_api_requests::models::InclusionProbability,
nym_api_requests::models::SelectionChance,
crate::network::models::NetworkDetails,
nym_config::defaults::NymNetworkDetails,
nym_config::defaults::ChainDetails,
nym_config::defaults::DenomDetailsOwned,
nym_config::defaults::ValidatorDetails,
nym_config::defaults::NymContracts,
ContractVersionSchemaResponse,
crate::network::models::ContractInformation<ContractVersionSchemaResponse>,
nym_api_requests::models::ApiHealthResponse,
nym_api_requests::models::ApiStatus,
nym_bin_common::build_information::BinaryBuildInformationOwned,
nym_api_requests::models::SignerInformationResponse,
nym_api_requests::models::DescribedGateway,
nym_api_requests::models::MixNodeDetailsSchema,
nym_mixnet_contract_common::Gateway,
nym_mixnet_contract_common::GatewayBond,
nym_api_requests::models::NymNodeDescription,
nym_api_requests::models::HostInformation,
nym_api_requests::models::HostKeys,
nym_node_requests::api::v1::node::models::AuxiliaryDetails,
nym_api_requests::models::NetworkRequesterDetails,
nym_api_requests::models::IpPacketRouterDetails,
nym_api_requests::models::AuthenticatorDetails,
nym_api_requests::models::WebSockets,
nym_api_requests::nym_nodes::NodeRole,
nym_api_requests::models::DescribedMixNode,
nym_api_requests::ecash::VerificationKeyResponse,
nym_api_requests::ecash::models::AggregatedExpirationDateSignatureResponse,
nym_api_requests::ecash::models::AggregatedCoinIndicesSignatureResponse,
nym_api_requests::ecash::models::EpochCredentialsResponse,
nym_api_requests::ecash::models::IssuedCredentialResponse,
nym_api_requests::ecash::models::IssuedTicketbookBody,
nym_api_requests::ecash::models::BlindedSignatureResponse,
nym_api_requests::ecash::models::PartialExpirationDateSignatureResponse,
nym_api_requests::ecash::models::PartialCoinIndicesSignatureResponse,
nym_api_requests::ecash::models::EcashTicketVerificationResponse,
nym_api_requests::ecash::models::EcashTicketVerificationRejection,
nym_api_requests::ecash::models::EcashBatchTicketRedemptionResponse,
nym_api_requests::ecash::models::SpentCredentialsResponse,
nym_api_requests::ecash::models::IssuedCredentialsResponse,
nym_api_requests::nym_nodes::SkimmedNode,
nym_api_requests::nym_nodes::BasicEntryInformation,
nym_api_requests::nym_nodes::SemiSkimmedNode,
nym_api_requests::nym_nodes::NodeRoleQueryParam,
))
)]
pub(super) struct ApiDoc;
+313
View File
@@ -0,0 +1,313 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use super::support::nyxd;
use crate::circulating_supply_api::cache::CirculatingSupplyCache;
use crate::ecash::api_routes::handlers::ecash_routes;
use crate::ecash::client::Client;
use crate::ecash::comm::QueryCommunicationChannel;
use crate::ecash::dkg::controller::keys::{
can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists,
};
use crate::ecash::dkg::controller::DkgController;
use crate::ecash::state::EcashState;
use crate::epoch_operations::{self, RewardedSetUpdater};
use crate::network::models::NetworkDetails;
use crate::node_describe_cache::{self, DescribedNodes};
use crate::node_status_api::handlers::unstable;
use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater;
use crate::node_status_api::{self, NodeStatusCache};
use crate::nym_contract_cache::cache::NymContractCache;
use crate::status::{ApiStatusState, SignerState};
use crate::support::caching::cache::SharedCache;
use crate::support::config::Config;
use crate::support::storage;
use crate::{circulating_supply_api, ecash, network_monitor, nym_contract_cache};
use anyhow::{bail, Context};
use nym_config::defaults::NymNetworkDetails;
use nym_sphinx::receiver::SphinxMessageReceiver;
use nym_task::TaskManager;
use nym_validator_client::nyxd::Coin;
use router::RouterBuilder;
use std::sync::Arc;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
pub(crate) mod api_docs;
pub(crate) mod router;
const TASK_MANAGER_TIMEOUT_S: u64 = 10;
/// Shutdown goes 2 directions:
/// 1. signal background tasks to gracefully finish
/// 2. signal server itself
///
/// These are done through separate shutdown handles. Ofcourse, shut down server
/// AFTER you have shut down BG tasks (or past their grace period).
pub(crate) struct ShutdownHandles {
task_manager: TaskManager,
axum_shutdown_button: ShutdownAxum,
/// Tokio JoinHandle for axum server's task
axum_join_handle: AxumJoinHandle,
}
impl ShutdownHandles {
/// Cancellation token is given to Axum server constructor. When the token
/// receives a shutdown signal, Axum server will shut down gracefully.
pub(crate) fn new(
task_manager: TaskManager,
axum_server_handle: AxumJoinHandle,
shutdown_button: CancellationToken,
) -> Self {
Self {
task_manager,
axum_shutdown_button: ShutdownAxum(shutdown_button.clone()),
axum_join_handle: axum_server_handle,
}
}
pub(crate) fn task_manager_mut(&mut self) -> &mut TaskManager {
&mut self.task_manager
}
/// Signal server to shut down, then return join handle to its
/// `tokio` task
///
/// https://tikv.github.io/doc/tokio/task/struct.JoinHandle.html
#[must_use]
pub(crate) fn shutdown_axum(self) -> AxumJoinHandle {
self.axum_shutdown_button.0.cancel();
self.axum_join_handle
}
}
struct ShutdownAxum(CancellationToken);
type AxumJoinHandle = JoinHandle<std::io::Result<()>>;
#[derive(Clone)]
// TODO rocket remove smurf name after eliminating rocket
pub(crate) struct AxumAppState {
nym_contract_cache: NymContractCache,
node_status_cache: NodeStatusCache,
circulating_supply_cache: CirculatingSupplyCache,
storage: storage::NymApiStorage,
described_nodes_state: SharedCache<DescribedNodes>,
network_details: NetworkDetails,
node_info_cache: unstable::NodeInfoCache,
}
impl AxumAppState {
pub(crate) fn nym_contract_cache(&self) -> &NymContractCache {
&self.nym_contract_cache
}
pub(crate) fn node_status_cache(&self) -> &NodeStatusCache {
&self.node_status_cache
}
pub(crate) fn circulating_supply_cache(&self) -> &CirculatingSupplyCache {
&self.circulating_supply_cache
}
pub(crate) fn network_details(&self) -> &NetworkDetails {
&self.network_details
}
pub(crate) fn described_nodes_state(&self) -> &SharedCache<DescribedNodes> {
&self.described_nodes_state
}
pub(crate) fn storage(&self) -> &storage::NymApiStorage {
&self.storage
}
pub(crate) fn node_info_cache(&self) -> &unstable::NodeInfoCache {
&self.node_info_cache
}
}
pub(crate) async fn start_nym_api_tasks_v2(config: &Config) -> anyhow::Result<ShutdownHandles> {
let nyxd_client = nyxd::Client::new(config);
let connected_nyxd = config.get_nyxd_url();
let nym_network_details = NymNetworkDetails::new_from_env();
let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details);
let coconut_keypair = ecash::keys::KeyPair::new();
// if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen
if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.coconut_signer)? {
let issued_for = loaded_keys.issued_for_epoch;
coconut_keypair.set(loaded_keys).await;
if can_validate_coconut_keys(&nyxd_client, issued_for).await? {
coconut_keypair.validate()
}
}
let identity_keypair = config.base.storage_paths.load_identity()?;
let identity_public_key = *identity_keypair.public_key();
let router = RouterBuilder::with_default_routes(config.network_monitor.enabled);
let nym_contract_cache_state = NymContractCache::new();
let node_status_cache_state = NodeStatusCache::new();
let mix_denom = network_details.network.chain_details.mix_denom.base.clone();
let circulating_supply_cache = CirculatingSupplyCache::new(mix_denom.to_owned());
let described_nodes_state = SharedCache::<DescribedNodes>::new();
let storage =
storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?;
let node_info_cache = unstable::NodeInfoCache::default();
let mut status_state = ApiStatusState::new();
// if coconut signer is enabled, add /coconut to server
let router = if config.coconut_signer.enabled {
// make sure we have some tokens to cover multisig fees
let balance = nyxd_client.balance(&mix_denom).await?;
if balance.amount < ecash::MINIMUM_BALANCE {
let address = nyxd_client.address().await;
let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom);
bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}")
}
let cosmos_address = nyxd_client.address().await.to_string();
let announce_address = config
.coconut_signer
.announce_address
.clone()
.map(|u| u.to_string())
.unwrap_or_default();
status_state.add_zk_nym_signer(SignerState {
cosmos_address,
identity: identity_keypair.public_key().to_base58_string(),
announce_address,
coconut_keypair: coconut_keypair.clone(),
});
let ecash_contract = nyxd_client
.get_ecash_contract_address()
.await
.context("e-cash contract address is required to setup the zk-nym signer")?;
let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone());
let ecash_state = EcashState::new(
ecash_contract,
nyxd_client.clone(),
identity_keypair,
coconut_keypair.clone(),
comm_channel,
storage.clone(),
)
.await?;
router.nest("/v1/ecash", ecash_routes(Arc::new(ecash_state)))
} else {
router
};
let router = router.with_state(AxumAppState {
nym_contract_cache: nym_contract_cache_state.clone(),
node_status_cache: node_status_cache_state.clone(),
circulating_supply_cache: circulating_supply_cache.clone(),
storage: storage.clone(),
described_nodes_state: described_nodes_state.clone(),
network_details,
node_info_cache,
});
let task_manager = TaskManager::new(TASK_MANAGER_TIMEOUT_S);
// start note describe cache refresher
// we should be doing the below, but can't due to our current startup structure
// let refresher = node_describe_cache::new_refresher(&config.topology_cacher);
// let cache = refresher.get_shared_cache();
node_describe_cache::new_refresher_with_initial_value(
&config.topology_cacher,
nym_contract_cache_state.clone(),
described_nodes_state,
)
.named("node-self-described-data-refresher")
.start(task_manager.subscribe_named("node-self-described-data-refresher"));
// start all the caches first
let nym_contract_cache_listener = nym_contract_cache::start_refresher(
&config.node_status_api,
&nym_contract_cache_state,
nyxd_client.clone(),
&task_manager,
);
node_status_api::start_cache_refresh(
&config.node_status_api,
&nym_contract_cache_state,
&node_status_cache_state,
storage.clone(),
nym_contract_cache_listener,
&task_manager,
);
circulating_supply_api::start_cache_refresh(
&config.circulating_supply_cacher,
nyxd_client.clone(),
&circulating_supply_cache,
&task_manager,
);
// start dkg task
if config.coconut_signer.enabled {
let dkg_bte_keypair = load_bte_keypair(&config.coconut_signer)?;
DkgController::start(
&config.coconut_signer,
nyxd_client.clone(),
coconut_keypair,
dkg_bte_keypair,
identity_public_key,
rand::rngs::OsRng,
&task_manager,
)?;
}
// and then only start the uptime updater (and the monitor itself, duh)
// if the monitoring is enabled
if config.network_monitor.enabled {
network_monitor::start::<SphinxMessageReceiver>(
&config.network_monitor,
&nym_contract_cache_state,
&storage,
nyxd_client.clone(),
&task_manager,
)
.await;
HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager);
// start 'rewarding' if its enabled
if config.rewarding.enabled {
epoch_operations::ensure_rewarding_permission(&nyxd_client).await?;
RewardedSetUpdater::start(
nyxd_client,
&nym_contract_cache_state,
storage,
&task_manager,
);
}
}
let bind_address = config.base.bind_address.to_owned();
let server = router.build_server(&bind_address).await?;
let cancellation_token = CancellationToken::new();
let shutdown_button = cancellation_token.clone();
let axum_shutdown_receiver = cancellation_token.cancelled_owned();
let server_handle = tokio::spawn(async move {
{
info!("Started Axum HTTP V2 server on {bind_address}");
server.run(axum_shutdown_receiver).await
}
});
let shutdown = ShutdownHandles::new(task_manager, server_handle, shutdown_button);
Ok(shutdown)
}
+136
View File
@@ -0,0 +1,136 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::circulating_supply_api::handlers::circulating_supply_routes;
use crate::network::handlers::nym_network_routes;
use crate::node_status_api::handlers::node_status_routes;
use crate::nym_contract_cache::handlers::nym_contract_cache_routes;
use crate::nym_nodes::handlers::nym_node_routes;
use crate::nym_nodes::handlers_unstable::nym_node_routes_unstable;
use crate::status;
use crate::v2::AxumAppState;
use anyhow::anyhow;
use axum::Router;
use core::net::SocketAddr;
use nym_http_api_common::logging::logger;
use tokio::net::TcpListener;
use tokio_util::sync::WaitForCancellationFutureOwned;
use tower_http::cors::CorsLayer;
use utoipa::OpenApi;
use utoipa_swagger_ui::SwaggerUi;
/// Wrapper around `axum::Router` which ensures correct [order of layers][order].
/// Add new routes as if you were working directly with `axum`.
///
/// Why? Middleware like logger, CORS, TLS which need to handle request before other
/// layers should be added last. Using this builder pattern ensures that.
///
/// [order]: https://docs.rs/axum/latest/axum/middleware/index.html#ordering
pub(crate) struct RouterBuilder {
unfinished_router: Router<AxumAppState>,
}
impl RouterBuilder {
/// All routes should be, if possible, added here. Exceptions are e.g.
/// routes which are added conditionally in other places based on some `if`.
pub(crate) fn with_default_routes(network_monitor: bool) -> Self {
// https://docs.rs/tower-http/0.1.1/tower_http/trace/index.html
// TODO rocket use tracing instead of env_logger
// https://github.com/tokio-rs/axum/blob/main/examples/tracing-aka-logging/src/main.rs
// .layer(
// TraceLayer::new_for_http()
// .make_span_with(DefaultMakeSpan::new().include_headers(true))
// .on_request(DefaultOnRequest::new())
// .on_response(DefaultOnResponse::new().latency_unit(tower_http::LatencyUnit::Micros)),
// )
// .route("/", axum::routing::get(|| async {axum::response::Redirect::permanent("/swagger")}))
// .route("/swagger", axum::routing::get(hello))
let default_routes = Router::new()
.merge(
SwaggerUi::new("/swagger")
.url("/api-docs/openapi.json", super::api_docs::ApiDoc::openapi()),
)
.nest(
"/v1",
Router::new()
.nest("/circulating-supply", circulating_supply_routes())
.merge(nym_contract_cache_routes())
.nest("/status", node_status_routes(network_monitor))
.nest("/network", nym_network_routes())
.nest("/api-status", status::handlers::api_status_routes())
.merge(nym_node_routes())
.nest("/unstable/nym-nodes", nym_node_routes_unstable()), // CORS layer needs to be "outside" of routes
);
Self {
unfinished_router: default_routes,
}
}
pub(crate) fn nest(self, path: &str, router: Router<AxumAppState>) -> Self {
Self {
unfinished_router: self.unfinished_router.nest(path, router),
}
}
/// Invoke this as late as possible before constructing HTTP server
/// (after all routes were added).
pub(crate) fn with_state(self, state: AxumAppState) -> RouterWithState {
RouterWithState {
router: self.finalize_routes().with_state(state),
}
}
/// Middleware added here intercepts the request before it gets to other routes.
fn finalize_routes(self) -> Router<AxumAppState> {
self.unfinished_router
.layer(setup_cors())
.layer(axum::middleware::from_fn(logger))
}
}
fn setup_cors() -> CorsLayer {
CorsLayer::new()
.allow_origin(tower_http::cors::Any)
.allow_methods([axum::http::Method::GET, axum::http::Method::POST])
.allow_headers(tower_http::cors::Any)
.allow_credentials(false)
}
pub(crate) struct RouterWithState {
router: Router,
}
impl RouterWithState {
pub(crate) async fn build_server(
self,
bind_address: &SocketAddr,
) -> anyhow::Result<ApiHttpServer> {
let listener = tokio::net::TcpListener::bind(bind_address)
.await
.map_err(|err| anyhow!("Couldn't bind to address {} due to {}", bind_address, err))?;
Ok(ApiHttpServer {
router: self.router,
listener,
})
}
}
pub(crate) struct ApiHttpServer {
router: Router,
listener: TcpListener,
}
impl ApiHttpServer {
pub async fn run(self, receiver: WaitForCancellationFutureOwned) -> Result<(), std::io::Error> {
// into_make_service_with_connect_info allows us to see client ip address
axum::serve(
self.listener,
self.router
.into_make_service_with_connect_info::<SocketAddr>(),
)
.with_graceful_shutdown(receiver)
.await
}
}
+217
View File
@@ -0,0 +1,217 @@
#!/bin/bash
# color codes
GREEN='\033[0;32m'
RED='\033[0;31m'
BLUE='\033[0;34m'
DEFAULT='\033[0m'
axum_server_addr="http://localhost:8081"
rocket_server_addr="http://localhost:8000"
function compare_responses() {
local api="$1"
shift
local url_1="${axum_server_addr}${api}"
local url_2="${rocket_server_addr}${api}"
local response_1=$(curl -s "${url_1}")
local normalized_resp_1=$(echo "${response_1}" | jq --sort-keys '.')
local response_2=$(curl -s "${url_2}")
local normalized_resp_2=$(echo "${response_2}" | jq --sort-keys '.')
echo -e "Response: \n${response_1}"
# jq . "${response_1}"
if [[ "${response_1}" == "${response_2}" ]]; then
# if cmp -s <(jq -S . "$response_1") <(jq -S ."$response_2"); then
echo -e "${GREEN}Responses are the same ${DEFAULT}"
else
echo -e "${RED}"
echo "${response_2}"
# jq . "${response_2}"
echo -e "Responses are different"
diff <(echo "${normalized_resp_1}") <(echo "${normalized_resp_2}")
echo -e "${DEFAULT}"
fi
}
function validate_response() {
local expected_status="$1"
shift
local url=("$@")
local query=(curl -I -X 'GET' "${url[@]}" -H 'accept: application/json')
# execute given curl
echo -e "Executing\n${BLUE}\t${query[*]}${DEFAULT}"
local response=$("${query[@]}" -w "\n%{http_code}\n%{content_type}")
# parse status code & content type
local http_code=$(echo "$response" | tail -n2 | head -n1)
local content_type=$(echo "$response" | tail -n1)
# -e flag for coloured output
if [ "$http_code" -eq "$expected_status" ]; then
echo -e "${GREEN}HTTP code $http_code as expected, Content-Type is $content_type ${DEFAULT}"
return 0
else
echo -e "${RED}Unexpected HTTP code: $http_code, Content-Type is $content_type ${DEFAULT}"
return 1
fi
}
function title() {
local title="$1"
echo -e "\n${BLUE}\t╔═══ ${title} ═══╗${DEFAULT}"
}
function circulating_supply() {
title "/circulating-supply"
local api="/v1/circulating-supply"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/circulating-supply/total-supply-value"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/circulating-supply/circulating-supply-value"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
}
function contract_cache() {
title "/contract-cache"
local api="/v1/mixnodes"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/mixnodes/detailed"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/gateways"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/mixnodes/active"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/mixnodes/active/detailed"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/mixnodes/rewarded"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/mixnodes/rewarded/detailed"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/mixnodes/blacklisted"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/gateways/blacklisted"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/epoch/reward_params"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/epoch/current"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
}
function network() {
title "/network"
local api="/v1/network/details"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/network/nym-contracts"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/network/nym-contracts-detailed"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
}
function api_status() {
title "/api-status"
local api="/v1/api-status/health"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/api-status/build-information"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/api-status/signer-information"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
}
function unstable() {
title "/unstable"
validate_response 501 "${target_address}/v1/unstable/nym-nodes/skimmed"
validate_response 501 "${target_address}/v1/unstable/nym-nodes/semi-skimmed"
validate_response 501 "${target_address}/v1/unstable/nym-nodes/full-fat"
local api="/v1/unstable/nym-nodes/gateways/skimmed"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/unstable/nym-nodes/gateways/skimmed?semver_compatibility"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/unstable/nym-nodes/gateways/skimmed?semver_compatibility=2.0.0"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
validate_response 501 "${target_address}/v1/unstable/nym-nodes/gateways/semi-skimmed"
validate_response 501 "${target_address}/v1/unstable/nym-nodes/gateways/semi-skimmed?semver_compatibility"
validate_response 501 "${target_address}/v1/unstable/nym-nodes/gateways/semi-skimmed?semver_compatibility=2.0.0"
validate_response 501 "${target_address}/v1/unstable/nym-nodes/gateways/full-fat"
validate_response 501 "${target_address}/v1/unstable/nym-nodes/gateways/full-fat?semver_compatibility"
validate_response 501 "${target_address}/v1/unstable/nym-nodes/gateways/full-fat?semver_compatibility=2.0.0"
local api="/v1/unstable/nym-nodes/mixnodes/skimmed"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/unstable/nym-nodes/mixnodes/skimmed?semver_compatibility"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
local api="/v1/unstable/nym-nodes/mixnodes/skimmed?semver_compatibility=2.0.0"
validate_response 200 "${target_address}${api}"
compare_responses ${api}
validate_response 501 "${target_address}/v1/unstable/nym-nodes/mixnodes/semi-skimmed"
validate_response 501 "${target_address}/v1/unstable/nym-nodes/mixnodes/semi-skimmed?semver_compatibility"
validate_response 501 "${target_address}/v1/unstable/nym-nodes/mixnodes/semi-skimmed?semver_compatibility=2.0.0"
validate_response 501 "${target_address}/v1/unstable/nym-nodes/mixnodes/full-fat"
validate_response 501 "${target_address}/v1/unstable/nym-nodes/mixnodes/full-fat?semver_compatibility"
validate_response 501 "${target_address}/v1/unstable/nym-nodes/mixnodes/full-fat?semver_compatibility=2.0.0"
}
+24
View File
@@ -0,0 +1,24 @@
#!/bin/bash
# Very basic validation, only that endpoints are reachable
clear
source $(dirname "$0")/definitions.sh
function fire_on_all_apis() {
local target_address="$1"
circulating_supply
contract_cache
network
api_status
unstable
}
fire_on_all_apis "http://localhost:8081"
fire_on_all_apis "http://localhost:8000"