diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 77d220e633..d7a76f2673 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -57,6 +57,12 @@ jobs: command: fmt args: --all -- --check + - name: Clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: --workspace --all-targets -- -D warnings + - name: Build all binaries uses: actions-rs/cargo@v1 with: @@ -82,9 +88,3 @@ jobs: with: command: test args: --workspace -- --ignored - - - name: Clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --workspace --all-targets -- -D warnings diff --git a/.github/workflows/push-node-status-api.yaml b/.github/workflows/push-node-status-api.yaml index 1a082a1ad2..941a8619f0 100644 --- a/.github/workflows/push-node-status-api.yaml +++ b/.github/workflows/push-node-status-api.yaml @@ -1,11 +1,55 @@ name: Build and upload Node Status API container to harbor.nymte.ch - on: workflow_dispatch: +env: + WORKING_DIRECTORY: "nym-node-status-api" + CONTAINER_NAME: "node-status-api" + jobs: - my-job: - runs-on: arc-ubuntu-22.04 + build-container: + runs-on: arc-ubuntu-22.04-dind steps: - - name: my-step - run: echo "Hello World!" + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: harbor.nymte.ch + username: ${{ secrets.HARBOR_ROBOT_USERNAME }} + password: ${{ secrets.HARBOR_ROBOT_SECRET }} + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure git identity + run: | + git config --global user.email "lawrence@nymtech.net" + git config --global user.name "Lawrence Stalder" + + - name: Get version from cargo.toml + uses: mikefarah/yq@v4.44.3 + id: get_version + with: + cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml + + - name: Check if tag exists + run: | + if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then + echo "Tag ${{ steps.get_version.outputs.result }} already exists" + fi + + - name: Remove existing tag if exists + run: | + if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then + git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + fi + + - name: Create tag + run: | + git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}" + git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + + - name: BuildAndPushImageOnHarbor + run: | + docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest + docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags diff --git a/Cargo.lock b/Cargo.lock index 0a402bea90..def19342de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -318,10 +318,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", - "event-listener", + "event-listener 2.5.3", "futures-core", ] +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -455,6 +466,7 @@ checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" dependencies = [ "async-trait", "axum-core 0.4.5", + "axum-macros", "bytes", "futures-util", "http 1.1.0", @@ -542,6 +554,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "axum-test" version = "16.2.0" @@ -2142,8 +2165,8 @@ dependencies = [ "cosmwasm-std", "cosmwasm-storage", "cw-storage-plus", - "nym-coconut-dkg-common", - "nym-contracts-common", + "nym-coconut-dkg-common 0.1.0", + "nym-contracts-common 0.5.0", ] [[package]] @@ -2320,6 +2343,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -2362,6 +2394,27 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + [[package]] name = "explorer-api" version = "1.1.41" @@ -2374,13 +2427,13 @@ dependencies = [ "itertools 0.13.0", "log", "maxminddb", - "nym-bin-common", - "nym-contracts-common", + "nym-bin-common 0.6.0", + "nym-contracts-common 0.5.0", "nym-explorer-api-requests", - "nym-mixnet-contract-common", - "nym-network-defaults", + "nym-mixnet-contract-common 0.6.0", + "nym-network-defaults 0.1.0", "nym-task", - "nym-validator-client", + "nym-validator-client 0.1.0", "okapi", "pretty_env_logger", "rand", @@ -3363,11 +3416,11 @@ dependencies = [ "clap 4.5.20", "dirs", "importer-contract", - "nym-bin-common", - "nym-mixnet-contract-common", - "nym-network-defaults", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-bin-common 0.6.0", + "nym-mixnet-contract-common 0.6.0", + "nym-network-defaults 0.1.0", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "serde", "serde_json", "tokio", @@ -3572,7 +3625,7 @@ dependencies = [ "crossbeam-utils", "curl", "curl-sys", - "event-listener", + "event-listener 2.5.3", "futures-lite", "http 0.2.12", "log", @@ -4036,8 +4089,8 @@ dependencies = [ "async-trait", "futures", "js-sys", - "nym-bin-common", - "nym-http-api-client", + "nym-bin-common 0.6.0", + "nym-http-api-client 0.1.0", "nym-ordered-buffer", "nym-service-providers-common", "nym-socks5-requests", @@ -4054,6 +4107,30 @@ dependencies = [ "wasm-utils", ] +[[package]] +name = "moka" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" +dependencies = [ + "async-lock", + "async-trait", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "event-listener 5.3.1", + "futures-util", + "once_cell", + "parking_lot", + "quanta", + "rustc_version 0.4.0", + "smallvec", + "tagptr", + "thiserror", + "triomphe", + "uuid", +] + [[package]] name = "multer" version = "2.1.0" @@ -4372,37 +4449,37 @@ dependencies = [ "humantime-serde", "itertools 0.13.0", "k256", - "nym-api-requests", + "nym-api-requests 0.1.0", "nym-bandwidth-controller", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-coconut", - "nym-coconut-dkg-common", - "nym-compact-ecash", - "nym-config", - "nym-contracts-common", + "nym-coconut-dkg-common 0.1.0", + "nym-compact-ecash 0.1.0", + "nym-config 0.1.0", + "nym-contracts-common 0.5.0", "nym-credential-storage", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", "nym-dkg", - "nym-ecash-contract-common", + "nym-ecash-contract-common 0.1.0", "nym-ecash-double-spending", - "nym-ecash-time", + "nym-ecash-time 0.1.0", "nym-gateway-client", "nym-http-api-common", "nym-inclusion-probability", - "nym-mixnet-contract-common", - "nym-multisig-contract-common", - "nym-node-requests", + "nym-mixnet-contract-common 0.6.0", + "nym-multisig-contract-common 0.1.0", + "nym-node-requests 0.1.0", "nym-node-tester-utils", - "nym-pemstore", - "nym-serde-helpers", + "nym-pemstore 0.3.0", + "nym-serde-helpers 0.1.0", "nym-sphinx", "nym-task", "nym-topology", "nym-types", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "pin-project", "rand", "rand_chacha", @@ -4437,14 +4514,14 @@ dependencies = [ "cosmwasm-std", "ecdsa", "getset", - "nym-compact-ecash", - "nym-credentials-interface", - "nym-crypto", - "nym-ecash-time", - "nym-mixnet-contract-common", - "nym-network-defaults", - "nym-node-requests", - "nym-serde-helpers", + "nym-compact-ecash 0.1.0", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-ecash-time 0.1.0", + "nym-mixnet-contract-common 0.6.0", + "nym-network-defaults 0.1.0", + "nym-node-requests 0.1.0", + "nym-serde-helpers 0.1.0", "schemars", "serde", "serde_json", @@ -4456,6 +4533,32 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-api-requests" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "bs58", + "cosmrs 0.17.0-pre", + "cosmwasm-std", + "ecdsa", + "getset", + "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-credentials-interface 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-serde-helpers 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "schemars", + "serde", + "sha2 0.10.8", + "tendermint 0.37.0", + "thiserror", + "time", + "utoipa", +] + [[package]] name = "nym-async-file-watcher" version = "0.1.0" @@ -4481,16 +4584,16 @@ dependencies = [ "ipnetwork 0.20.0", "log", "nym-authenticator-requests", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", - "nym-config", + "nym-config 0.1.0", "nym-credential-verification", - "nym-credentials-interface", - "nym-crypto", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", "nym-gateway-requests", "nym-gateway-storage", "nym-id", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-sdk", "nym-service-provider-requests-common", "nym-service-providers-common", @@ -4498,7 +4601,7 @@ dependencies = [ "nym-task", "nym-types", "nym-wireguard", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "rand", "serde", "serde_json", @@ -4516,11 +4619,11 @@ dependencies = [ "base64 0.22.1", "bincode", "hmac", - "nym-credentials-interface", - "nym-crypto", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", "nym-service-provider-requests-common", "nym-sphinx", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "rand", "serde", "sha2 0.10.8", @@ -4536,12 +4639,12 @@ dependencies = [ "log", "nym-credential-storage", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", - "nym-ecash-contract-common", - "nym-ecash-time", - "nym-network-defaults", - "nym-validator-client", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-ecash-contract-common 0.1.0", + "nym-ecash-time 0.1.0", + "nym-network-defaults 0.1.0", + "nym-validator-client 0.1.0", "rand", "thiserror", "url", @@ -4571,6 +4674,21 @@ dependencies = [ "vergen", ] +[[package]] +name = "nym-bin-common" +version = "0.6.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "const-str", + "log", + "pretty_env_logger", + "schemars", + "semver 1.0.23", + "serde", + "utoipa", + "vergen", +] + [[package]] name = "nym-bity-integration" version = "0.1.0" @@ -4580,7 +4698,7 @@ dependencies = [ "eyre", "k256", "nym-cli-commands", - "nym-validator-client", + "nym-validator-client 0.1.0", "serde", "serde_json", "thiserror", @@ -4600,10 +4718,10 @@ dependencies = [ "dotenvy", "inquire", "log", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-cli-commands", - "nym-network-defaults", - "nym-validator-client", + "nym-network-defaults 0.1.0", + "nym-validator-client 0.1.0", "pretty_env_logger", "serde", "serde_json", @@ -4634,26 +4752,26 @@ dependencies = [ "k256", "log", "nym-bandwidth-controller", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", - "nym-coconut-dkg-common", - "nym-config", - "nym-contracts-common", + "nym-coconut-dkg-common 0.1.0", + "nym-config 0.1.0", + "nym-contracts-common 0.5.0", "nym-credential-storage", "nym-credential-utils", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", - "nym-ecash-contract-common", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-ecash-contract-common 0.1.0", "nym-id", - "nym-mixnet-contract-common", - "nym-multisig-contract-common", - "nym-network-defaults", - "nym-pemstore", + "nym-mixnet-contract-common 0.6.0", + "nym-multisig-contract-common 0.1.0", + "nym-network-defaults 0.1.0", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-types", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "rand", "serde", "serde_json", @@ -4677,21 +4795,21 @@ dependencies = [ "futures", "log", "nym-bandwidth-controller", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", "nym-client-websocket-requests", - "nym-config", + "nym-config 0.1.0", "nym-credential-storage", "nym-credentials", - "nym-crypto", + "nym-crypto 0.4.0", "nym-gateway-requests", "nym-id", - "nym-network-defaults", - "nym-pemstore", + "nym-network-defaults 0.1.0", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-task", "nym-topology", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "serde", "serde_json", @@ -4725,24 +4843,24 @@ dependencies = [ "nym-client-core-config-types", "nym-client-core-gateways-storage", "nym-client-core-surb-storage", - "nym-config", + "nym-config 0.1.0", "nym-country-group", "nym-credential-storage", - "nym-credentials-interface", - "nym-crypto", - "nym-ecash-time", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-ecash-time 0.1.0", "nym-explorer-client", "nym-gateway-client", "nym-gateway-requests", "nym-id", "nym-metrics", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-nonexhaustive-delayqueue", - "nym-pemstore", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-task", "nym-topology", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "rand_chacha", "serde", @@ -4770,9 +4888,9 @@ name = "nym-client-core-config-types" version = "0.1.0" dependencies = [ "humantime-serde", - "nym-config", + "nym-config 0.1.0", "nym-country-group", - "nym-pemstore", + "nym-pemstore 0.3.0", "nym-sphinx-addressing", "nym-sphinx-params", "serde", @@ -4787,7 +4905,7 @@ dependencies = [ "async-trait", "cosmrs 0.17.0-pre", "log", - "nym-crypto", + "nym-crypto 0.4.0", "nym-gateway-requests", "serde", "sqlx", @@ -4805,7 +4923,7 @@ dependencies = [ "async-trait", "dashmap", "log", - "nym-crypto", + "nym-crypto 0.4.0", "nym-sphinx", "nym-task", "sqlx", @@ -4821,7 +4939,7 @@ dependencies = [ "anyhow", "futures", "js-sys", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-node-tester-utils", "nym-node-tester-wasm", "rand", @@ -4860,7 +4978,7 @@ dependencies = [ "group", "itertools 0.13.0", "nym-dkg", - "nym-pemstore", + "nym-pemstore 0.3.0", "rand", "rand_chacha", "serde", @@ -4877,7 +4995,17 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw2", - "nym-multisig-contract-common", + "nym-multisig-contract-common 0.1.0", +] + +[[package]] +name = "nym-coconut-bandwidth-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", ] [[package]] @@ -4889,8 +5017,29 @@ dependencies = [ "cw-utils", "cw2", "cw4", - "nym-contracts-common", - "nym-multisig-contract-common", + "nym-contracts-common 0.5.0", + "nym-multisig-contract-common 0.1.0", +] + +[[package]] +name = "nym-coconut-dkg-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "cw2", + "cw4", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", +] + +[[package]] +name = "nym-common-models" +version = "0.1.0" +dependencies = [ + "serde", ] [[package]] @@ -4906,8 +5055,8 @@ dependencies = [ "ff", "group", "itertools 0.13.0", - "nym-network-defaults", - "nym-pemstore", + "nym-network-defaults 0.1.0", + "nym-pemstore 0.3.0", "rand", "rayon", "serde", @@ -4917,6 +5066,28 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-compact-ecash" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "bincode", + "bls12_381", + "bs58", + "cfg-if", + "digest 0.9.0", + "ff", + "group", + "itertools 0.12.1", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "rand", + "serde", + "sha2 0.9.9", + "thiserror", + "zeroize", +] + [[package]] name = "nym-config" version = "0.1.0" @@ -4924,7 +5095,21 @@ dependencies = [ "dirs", "handlebars", "log", - "nym-network-defaults", + "nym-network-defaults 0.1.0", + "serde", + "toml 0.8.14", + "url", +] + +[[package]] +name = "nym-config" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "dirs", + "handlebars", + "log", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "serde", "toml 0.8.14", "url", @@ -4945,6 +5130,21 @@ dependencies = [ "vergen", ] +[[package]] +name = "nym-contracts-common" +version = "0.5.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", + "thiserror", + "vergen", +] + [[package]] name = "nym-country-group" version = "0.1.0" @@ -4960,7 +5160,7 @@ dependencies = [ "anyhow", "bs58", "lazy_static", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-ffi-shared", "nym-sdk", "nym-sphinx-anonymous-replies", @@ -4974,9 +5174,9 @@ dependencies = [ "async-trait", "bincode", "log", - "nym-compact-ecash", + "nym-compact-ecash 0.1.0", "nym-credentials", - "nym-ecash-time", + "nym-ecash-time 0.1.0", "serde", "sqlx", "thiserror", @@ -4991,12 +5191,12 @@ dependencies = [ "log", "nym-bandwidth-controller", "nym-client-core", - "nym-config", + "nym-config 0.1.0", "nym-credential-storage", "nym-credentials", - "nym-credentials-interface", - "nym-ecash-time", - "nym-validator-client", + "nym-credentials-interface 0.1.0", + "nym-ecash-time 0.1.0", + "nym-validator-client 0.1.0", "thiserror", "time", "tokio", @@ -5010,15 +5210,15 @@ dependencies = [ "cosmwasm-std", "cw-utils", "futures", - "nym-api-requests", + "nym-api-requests 0.1.0", "nym-credentials", - "nym-credentials-interface", - "nym-ecash-contract-common", + "nym-credentials-interface 0.1.0", + "nym-ecash-contract-common 0.1.0", "nym-ecash-double-spending", "nym-gateway-requests", "nym-gateway-storage", "nym-task", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "si-scale", "thiserror", @@ -5035,14 +5235,14 @@ dependencies = [ "bls12_381", "cosmrs 0.17.0-pre", "log", - "nym-api-requests", - "nym-credentials-interface", - "nym-crypto", - "nym-ecash-contract-common", - "nym-ecash-time", - "nym-network-defaults", - "nym-serde-helpers", - "nym-validator-client", + "nym-api-requests 0.1.0", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-ecash-contract-common 0.1.0", + "nym-ecash-time 0.1.0", + "nym-network-defaults 0.1.0", + "nym-serde-helpers 0.1.0", + "nym-validator-client 0.1.0", "rand", "serde", "thiserror", @@ -5055,9 +5255,25 @@ name = "nym-credentials-interface" version = "0.1.0" dependencies = [ "bls12_381", - "nym-compact-ecash", - "nym-ecash-time", - "nym-network-defaults", + "nym-compact-ecash 0.1.0", + "nym-ecash-time 0.1.0", + "nym-network-defaults 0.1.0", + "rand", + "serde", + "strum 0.26.3", + "thiserror", + "time", +] + +[[package]] +name = "nym-credentials-interface" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "bls12_381", + "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-ecash-time 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "rand", "serde", "strum 0.26.3", @@ -5081,8 +5297,8 @@ dependencies = [ "generic-array 0.14.7", "hkdf", "hmac", - "nym-pemstore", - "nym-sphinx-types", + "nym-pemstore 0.3.0", + "nym-sphinx-types 0.2.0", "rand", "rand_chacha", "serde", @@ -5093,6 +5309,23 @@ dependencies = [ "zeroize", ] +[[package]] +name = "nym-crypto" +version = "0.4.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "bs58", + "ed25519-dalek", + "nym-pemstore 0.3.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-sphinx-types 0.2.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "serde", + "serde_bytes", + "subtle-encoding", + "thiserror", + "x25519-dalek", + "zeroize", +] + [[package]] name = "nym-data-observatory" version = "0.1.0" @@ -5101,9 +5334,9 @@ dependencies = [ "axum 0.7.7", "chrono", "clap 4.5.20", - "nym-bin-common", - "nym-network-defaults", - "nym-node-requests", + "nym-bin-common 0.6.0", + "nym-network-defaults 0.1.0", + "nym-node-requests 0.1.0", "nym-task", "serde", "serde_json", @@ -5129,8 +5362,8 @@ dependencies = [ "ff", "group", "lazy_static", - "nym-contracts-common", - "nym-pemstore", + "nym-contracts-common 0.5.0", + "nym-pemstore 0.3.0", "rand", "rand_chacha", "rand_core 0.6.4", @@ -5151,7 +5384,21 @@ dependencies = [ "cw-controllers", "cw-utils", "cw2", - "nym-multisig-contract-common", + "nym-multisig-contract-common 0.1.0", + "thiserror", +] + +[[package]] +name = "nym-ecash-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "thiserror", ] @@ -5161,14 +5408,22 @@ version = "0.1.0" dependencies = [ "bit-vec", "bloomfilter", - "nym-network-defaults", + "nym-network-defaults 0.1.0", ] [[package]] name = "nym-ecash-time" version = "0.1.0" dependencies = [ - "nym-compact-ecash", + "nym-compact-ecash 0.1.0", + "time", +] + +[[package]] +name = "nym-ecash-time" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ "time", ] @@ -5192,13 +5447,25 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-exit-policy" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "tracing", + "utoipa", +] + [[package]] name = "nym-explorer-api-requests" version = "0.1.0" dependencies = [ - "nym-api-requests", - "nym-contracts-common", - "nym-mixnet-contract-common", + "nym-api-requests 0.1.0", + "nym-contracts-common 0.5.0", + "nym-mixnet-contract-common 0.6.0", "schemars", "serde", "ts-rs", @@ -5208,12 +5475,12 @@ dependencies = [ name = "nym-explorer-client" version = "0.1.0" dependencies = [ - "log", "nym-explorer-api-requests", "reqwest 0.12.4", "serde", "thiserror", "tokio", + "tracing", "url", ] @@ -5224,7 +5491,7 @@ dependencies = [ "anyhow", "bs58", "lazy_static", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-sdk", "nym-sphinx-anonymous-replies", "tokio", @@ -5249,31 +5516,31 @@ dependencies = [ "futures", "humantime-serde", "ipnetwork 0.20.0", - "nym-api-requests", + "nym-api-requests 0.1.0", "nym-authenticator", - "nym-bin-common", - "nym-config", + "nym-bin-common 0.6.0", + "nym-config 0.1.0", "nym-credential-verification", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", "nym-gateway-requests", "nym-gateway-stats-storage", "nym-gateway-storage", "nym-ip-packet-router", "nym-mixnet-client", "nym-mixnode-common", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-network-requester", "nym-node-http-api", - "nym-pemstore", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-statistics-common", "nym-task", "nym-types", - "nym-validator-client", + "nym-validator-client 0.1.0", "nym-wireguard", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "once_cell", "rand", "serde", @@ -5302,14 +5569,14 @@ dependencies = [ "nym-bandwidth-controller", "nym-credential-storage", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", "nym-gateway-requests", - "nym-network-defaults", - "nym-pemstore", + "nym-network-defaults 0.1.0", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-task", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "serde", "si-scale", @@ -5335,11 +5602,11 @@ dependencies = [ "bs58", "futures", "generic-array 0.14.7", - "nym-compact-ecash", + "nym-compact-ecash 0.1.0", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", - "nym-pemstore", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-task", "rand", @@ -5358,7 +5625,7 @@ dependencies = [ name = "nym-gateway-stats-storage" version = "0.1.0" dependencies = [ - "nym-credentials-interface", + "nym-credentials-interface 0.1.0", "nym-sphinx", "sqlx", "thiserror", @@ -5375,7 +5642,7 @@ dependencies = [ "bincode", "defguard_wireguard_rs", "log", - "nym-credentials-interface", + "nym-credentials-interface 0.1.0", "nym-gateway-requests", "nym-sphinx", "sqlx", @@ -5391,7 +5658,7 @@ version = "0.2.0" dependencies = [ "anyhow", "lazy_static", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-ffi-shared", "nym-sdk", "nym-sphinx-anonymous-replies", @@ -5412,13 +5679,42 @@ dependencies = [ "serde", ] +[[package]] +name = "nym-group-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "cosmwasm-schema", + "cw-controllers", + "cw4", + "schemars", + "serde", +] + [[package]] name = "nym-http-api-client" version = "0.1.0" dependencies = [ "async-trait", "http 1.1.0", - "nym-bin-common", + "nym-bin-common 0.6.0", + "reqwest 0.12.4", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "nym-http-api-client" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "async-trait", + "http 1.1.0", + "nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", "reqwest 0.12.4", "serde", "serde_json", @@ -5462,7 +5758,7 @@ dependencies = [ "anyhow", "bs58", "clap 4.5.20", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-credential-storage", "nym-id", "tokio", @@ -5484,8 +5780,8 @@ version = "0.1.0" dependencies = [ "bincode", "bytes", - "nym-bin-common", - "nym-crypto", + "nym-bin-common 0.6.0", + "nym-crypto 0.4.0", "nym-sphinx", "rand", "serde", @@ -5507,14 +5803,14 @@ dependencies = [ "etherparse", "futures", "log", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", - "nym-config", - "nym-crypto", - "nym-exit-policy", + "nym-config 0.1.0", + "nym-crypto 0.4.0", + "nym-exit-policy 0.1.0", "nym-id", "nym-ip-packet-requests", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-network-requester", "nym-sdk", "nym-service-providers-common", @@ -5523,7 +5819,7 @@ dependencies = [ "nym-tun", "nym-types", "nym-wireguard", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "rand", "reqwest 0.12.4", "serde", @@ -5582,7 +5878,7 @@ dependencies = [ "cw2", "humantime-serde", "log", - "nym-contracts-common", + "nym-contracts-common 0.5.0", "rand_chacha", "schemars", "serde", @@ -5594,6 +5890,26 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-mixnet-contract-common" +version = "0.6.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "humantime-serde", + "log", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "schemars", + "serde", + "serde-json-wasm", + "serde_repr", + "thiserror", + "time", +] + [[package]] name = "nym-mixnode" version = "1.1.37" @@ -5609,24 +5925,24 @@ dependencies = [ "humantime-serde", "lazy_static", "log", - "nym-bin-common", - "nym-config", - "nym-contracts-common", - "nym-crypto", + "nym-bin-common 0.6.0", + "nym-config 0.1.0", + "nym-contracts-common 0.5.0", + "nym-crypto 0.4.0", "nym-http-api-common", "nym-metrics", "nym-mixnet-client", "nym-mixnode-common", "nym-node-http-api", "nym-nonexhaustive-delayqueue", - "nym-pemstore", + "nym-pemstore 0.3.0", "nym-sphinx", "nym-sphinx-params", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "nym-task", "nym-topology", "nym-types", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "serde", "serde_json", @@ -5647,19 +5963,19 @@ dependencies = [ "futures", "humantime-serde", "log", - "nym-bin-common", - "nym-crypto", + "nym-bin-common 0.6.0", + "nym-crypto 0.4.0", "nym-metrics", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-node-http-api", "nym-sphinx-acknowledgements", "nym-sphinx-addressing", "nym-sphinx-forwarding", "nym-sphinx-framing", "nym-sphinx-params", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "nym-task", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "serde", "thiserror", @@ -5684,6 +6000,22 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nym-multisig-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw3", + "cw4", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "nym-network-defaults" version = "0.1.0" @@ -5696,6 +6028,19 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-network-defaults" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "dotenvy", + "log", + "schemars", + "serde", + "url", + "utoipa", +] + [[package]] name = "nym-network-monitor" version = "0.1.0" @@ -5706,14 +6051,14 @@ dependencies = [ "dashmap", "futures", "log", - "nym-bin-common", - "nym-crypto", - "nym-network-defaults", + "nym-bin-common 0.6.0", + "nym-crypto 0.4.0", + "nym-network-defaults 0.1.0", "nym-sdk", "nym-sphinx", "nym-topology", "nym-types", - "nym-validator-client", + "nym-validator-client 0.1.0", "petgraph", "rand", "rand_chacha", @@ -5741,16 +6086,16 @@ dependencies = [ "ipnetwork 0.20.0", "log", "nym-async-file-watcher", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", "nym-client-websocket-requests", - "nym-config", + "nym-config 0.1.0", "nym-credential-storage", "nym-credentials", - "nym-crypto", - "nym-exit-policy", + "nym-crypto 0.4.0", + "nym-exit-policy 0.1.0", "nym-id", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-ordered-buffer", "nym-sdk", "nym-service-providers-common", @@ -5792,22 +6137,22 @@ dependencies = [ "humantime-serde", "ipnetwork 0.20.0", "nym-authenticator", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core-config-types", - "nym-config", - "nym-crypto", + "nym-config 0.1.0", + "nym-crypto 0.4.0", "nym-gateway", "nym-ip-packet-router", "nym-mixnode", "nym-network-requester", "nym-node-http-api", - "nym-pemstore", + "nym-pemstore 0.3.0", "nym-sphinx-acknowledgements", "nym-sphinx-addressing", "nym-task", "nym-types", "nym-wireguard", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "rand", "semver 1.0.23", "serde", @@ -5835,10 +6180,10 @@ dependencies = [ "hmac", "hyper 1.4.1", "ipnetwork 0.20.0", - "nym-crypto", + "nym-crypto 0.4.0", "nym-http-api-common", "nym-metrics", - "nym-node-requests", + "nym-node-requests 0.1.0", "nym-task", "nym-wireguard", "rand", @@ -5863,11 +6208,11 @@ dependencies = [ "celes", "humantime 2.1.0", "humantime-serde", - "nym-bin-common", - "nym-crypto", - "nym-exit-policy", - "nym-http-api-client", - "nym-wireguard-types", + "nym-bin-common 0.6.0", + "nym-crypto 0.4.0", + "nym-exit-policy 0.1.0", + "nym-http-api-client 0.1.0", + "nym-wireguard-types 0.1.0", "rand_chacha", "schemars", "serde", @@ -5878,13 +6223,91 @@ dependencies = [ "utoipa", ] +[[package]] +name = "nym-node-requests" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "async-trait", + "base64 0.22.1", + "celes", + "humantime 2.1.0", + "humantime-serde", + "nym-bin-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-crypto 0.4.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-exit-policy 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-wireguard-types 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "schemars", + "serde", + "serde_json", + "thiserror", + "time", + "utoipa", +] + +[[package]] +name = "nym-node-status-agent" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap 4.5.20", + "nym-bin-common 0.6.0", + "nym-common-models", + "reqwest 0.12.4", + "serde_json", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "nym-node-status-api" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum 0.7.7", + "chrono", + "clap 4.5.20", + "cosmwasm-std", + "envy", + "futures-util", + "moka", + "nym-bin-common 0.6.0", + "nym-common-models", + "nym-explorer-client", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-node-requests 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-task", + "nym-validator-client 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "regex", + "reqwest 0.12.4", + "serde", + "serde_json", + "serde_json_path", + "sqlx", + "strum 0.26.3", + "strum_macros 0.26.4", + "thiserror", + "tokio", + "tokio-util", + "tower-http", + "tracing", + "tracing-log 0.2.0", + "tracing-subscriber", + "utoipa", + "utoipa-swagger-ui", + "utoipauto", +] + [[package]] name = "nym-node-tester-utils" version = "0.1.0" dependencies = [ "futures", "log", - "nym-crypto", + "nym-crypto 0.4.0", "nym-sphinx", "nym-sphinx-params", "nym-task", @@ -5935,8 +6358,8 @@ dependencies = [ "anyhow", "clap 4.5.20", "log", - "nym-bin-common", - "nym-network-defaults", + "nym-bin-common 0.6.0", + "nym-network-defaults 0.1.0", "nym-sdk", "nym-service-providers-common", "nym-socks5-requests", @@ -5978,6 +6401,14 @@ dependencies = [ "pem", ] +[[package]] +name = "nym-pemstore" +version = "0.3.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "pem", +] + [[package]] name = "nym-sdk" version = "0.1.0" @@ -5997,15 +6428,15 @@ dependencies = [ "httpcodec", "log", "nym-bandwidth-controller", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", "nym-credential-storage", "nym-credential-utils", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", "nym-gateway-requests", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-ordered-buffer", "nym-service-providers-common", "nym-socks5-client-core", @@ -6013,7 +6444,7 @@ dependencies = [ "nym-sphinx", "nym-task", "nym-topology", - "nym-validator-client", + "nym-validator-client 0.1.0", "parking_lot", "pretty_env_logger", "rand", @@ -6043,6 +6474,17 @@ dependencies = [ "time", ] +[[package]] +name = "nym-serde-helpers" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "base64 0.22.1", + "bs58", + "serde", + "time", +] + [[package]] name = "nym-service-provider-requests-common" version = "0.1.0" @@ -6057,7 +6499,7 @@ dependencies = [ "anyhow", "async-trait", "log", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-sdk", "nym-socks5-requests", "nym-sphinx-anonymous-replies", @@ -6074,21 +6516,21 @@ dependencies = [ "bs58", "clap 4.5.20", "log", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", - "nym-config", + "nym-config 0.1.0", "nym-credential-storage", "nym-credentials", - "nym-crypto", + "nym-crypto 0.4.0", "nym-gateway-requests", "nym-id", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-ordered-buffer", - "nym-pemstore", + "nym-pemstore 0.3.0", "nym-socks5-client-core", "nym-sphinx", "nym-topology", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "serde", "serde_json", @@ -6110,17 +6552,17 @@ dependencies = [ "log", "nym-bandwidth-controller", "nym-client-core", - "nym-config", - "nym-contracts-common", + "nym-config 0.1.0", + "nym-contracts-common 0.5.0", "nym-credential-storage", - "nym-mixnet-contract-common", - "nym-network-defaults", + "nym-mixnet-contract-common 0.6.0", + "nym-network-defaults 0.1.0", "nym-service-providers-common", "nym-socks5-proxy-helpers", "nym-socks5-requests", "nym-sphinx", "nym-task", - "nym-validator-client", + "nym-validator-client 0.1.0", "pin-project", "rand", "reqwest 0.12.4", @@ -6142,11 +6584,11 @@ dependencies = [ "jni", "lazy_static", "log", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-client-core", - "nym-config", + "nym-config 0.1.0", "nym-credential-storage", - "nym-crypto", + "nym-crypto 0.4.0", "nym-socks5-client-core", "rand", "safer-ffi", @@ -6175,7 +6617,7 @@ version = "0.1.0" dependencies = [ "bincode", "log", - "nym-exit-policy", + "nym-exit-policy 0.1.0", "nym-service-providers-common", "nym-sphinx-addressing", "serde", @@ -6189,9 +6631,9 @@ name = "nym-sphinx" version = "0.1.0" dependencies = [ "log", - "nym-crypto", + "nym-crypto 0.4.0", "nym-metrics", - "nym-mixnet-contract-common", + "nym-mixnet-contract-common 0.6.0", "nym-sphinx-acknowledgements", "nym-sphinx-addressing", "nym-sphinx-anonymous-replies", @@ -6201,7 +6643,7 @@ dependencies = [ "nym-sphinx-framing", "nym-sphinx-params", "nym-sphinx-routing", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "nym-topology", "rand", "rand_chacha", @@ -6215,12 +6657,12 @@ name = "nym-sphinx-acknowledgements" version = "0.1.0" dependencies = [ "generic-array 0.14.7", - "nym-crypto", - "nym-pemstore", + "nym-crypto 0.4.0", + "nym-pemstore 0.3.0", "nym-sphinx-addressing", "nym-sphinx-params", "nym-sphinx-routing", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "nym-topology", "rand", "serde", @@ -6232,8 +6674,8 @@ dependencies = [ name = "nym-sphinx-addressing" version = "0.1.0" dependencies = [ - "nym-crypto", - "nym-sphinx-types", + "nym-crypto 0.4.0", + "nym-sphinx-types 0.2.0", "rand", "serde", "thiserror", @@ -6244,11 +6686,11 @@ name = "nym-sphinx-anonymous-replies" version = "0.1.0" dependencies = [ "bs58", - "nym-crypto", + "nym-crypto 0.4.0", "nym-sphinx-addressing", "nym-sphinx-params", "nym-sphinx-routing", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "nym-topology", "rand", "rand_chacha", @@ -6263,11 +6705,11 @@ version = "0.1.0" dependencies = [ "dashmap", "log", - "nym-crypto", + "nym-crypto 0.4.0", "nym-metrics", "nym-sphinx-addressing", "nym-sphinx-params", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "rand", "serde", "thiserror", @@ -6278,14 +6720,14 @@ dependencies = [ name = "nym-sphinx-cover" version = "0.1.0" dependencies = [ - "nym-crypto", + "nym-crypto 0.4.0", "nym-sphinx-acknowledgements", "nym-sphinx-addressing", "nym-sphinx-chunking", "nym-sphinx-forwarding", "nym-sphinx-params", "nym-sphinx-routing", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "nym-topology", "rand", "thiserror", @@ -6298,7 +6740,7 @@ dependencies = [ "nym-outfox", "nym-sphinx-addressing", "nym-sphinx-params", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "thiserror", ] @@ -6313,7 +6755,7 @@ dependencies = [ "nym-sphinx-addressing", "nym-sphinx-forwarding", "nym-sphinx-params", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "thiserror", "tokio", "tokio-util", @@ -6323,8 +6765,8 @@ dependencies = [ name = "nym-sphinx-params" version = "0.1.0" dependencies = [ - "nym-crypto", - "nym-sphinx-types", + "nym-crypto 0.4.0", + "nym-sphinx-types 0.2.0", "serde", "thiserror", ] @@ -6334,7 +6776,7 @@ name = "nym-sphinx-routing" version = "0.1.0" dependencies = [ "nym-sphinx-addressing", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "thiserror", ] @@ -6347,12 +6789,21 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nym-sphinx-types" +version = "0.2.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "sphinx-packet", + "thiserror", +] + [[package]] name = "nym-statistics-common" version = "0.1.0" dependencies = [ "futures", - "nym-credentials-interface", + "nym-credentials-interface 0.1.0", "nym-sphinx", "time", ] @@ -6392,14 +6843,14 @@ dependencies = [ "async-trait", "bs58", "log", - "nym-api-requests", - "nym-bin-common", - "nym-config", - "nym-crypto", - "nym-mixnet-contract-common", + "nym-api-requests 0.1.0", + "nym-bin-common 0.6.0", + "nym-config 0.1.0", + "nym-crypto 0.4.0", + "nym-mixnet-contract-common 0.6.0", "nym-sphinx-addressing", "nym-sphinx-routing", - "nym-sphinx-types", + "nym-sphinx-types 0.2.0", "rand", "reqwest 0.12.4", "semver 1.0.23", @@ -6417,7 +6868,7 @@ version = "0.1.0" dependencies = [ "etherparse", "log", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "thiserror", "tokio", "tokio-tun", @@ -6434,11 +6885,11 @@ dependencies = [ "hmac", "itertools 0.13.0", "log", - "nym-config", - "nym-crypto", - "nym-mixnet-contract-common", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-config 0.1.0", + "nym-crypto 0.4.0", + "nym-mixnet-contract-common 0.6.0", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "reqwest 0.12.4", "schemars", "serde", @@ -6472,21 +6923,20 @@ dependencies = [ "flate2", "futures", "itertools 0.13.0", - "log", - "nym-api-requests", - "nym-coconut-bandwidth-contract-common", - "nym-coconut-dkg-common", - "nym-compact-ecash", - "nym-config", - "nym-contracts-common", - "nym-ecash-contract-common", - "nym-group-contract-common", - "nym-http-api-client", - "nym-mixnet-contract-common", - "nym-multisig-contract-common", - "nym-network-defaults", - "nym-serde-helpers", - "nym-vesting-contract-common", + "nym-api-requests 0.1.0", + "nym-coconut-bandwidth-contract-common 0.1.0", + "nym-coconut-dkg-common 0.1.0", + "nym-compact-ecash 0.1.0", + "nym-config 0.1.0", + "nym-contracts-common 0.5.0", + "nym-ecash-contract-common 0.1.0", + "nym-group-contract-common 0.1.0", + "nym-http-api-client 0.1.0", + "nym-mixnet-contract-common 0.6.0", + "nym-multisig-contract-common 0.1.0", + "nym-network-defaults 0.1.0", + "nym-serde-helpers 0.1.0", + "nym-vesting-contract-common 0.7.0", "prost 0.12.6", "reqwest 0.12.4", "serde", @@ -6496,12 +6946,62 @@ dependencies = [ "thiserror", "time", "tokio", + "tracing", "ts-rs", "url", "wasmtimer", "zeroize", ] +[[package]] +name = "nym-validator-client" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bip32", + "bip39", + "colored", + "cosmrs 0.17.0-pre", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "cw2", + "cw3", + "cw4", + "eyre", + "flate2", + "futures", + "itertools 0.13.0", + "nym-api-requests 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-coconut-bandwidth-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-coconut-dkg-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-compact-ecash 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-config 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-ecash-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-group-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-http-api-client 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-multisig-contract-common 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-vesting-contract-common 0.7.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "prost 0.12.6", + "reqwest 0.12.4", + "serde", + "serde_json", + "sha2 0.9.9", + "tendermint-rpc", + "thiserror", + "time", + "tokio", + "tracing", + "url", + "wasmtimer", + "zeroize", +] + [[package]] name = "nym-validator-rewarder" version = "0.1.0" @@ -6513,18 +7013,18 @@ dependencies = [ "futures", "humantime 2.1.0", "humantime-serde", - "nym-bin-common", - "nym-coconut-bandwidth-contract-common", - "nym-coconut-dkg-common", - "nym-compact-ecash", - "nym-config", + "nym-bin-common 0.6.0", + "nym-coconut-bandwidth-contract-common 0.1.0", + "nym-coconut-dkg-common 0.1.0", + "nym-compact-ecash 0.1.0", + "nym-config 0.1.0", "nym-credentials", - "nym-credentials-interface", - "nym-crypto", - "nym-ecash-time", - "nym-network-defaults", + "nym-credentials-interface 0.1.0", + "nym-crypto 0.4.0", + "nym-ecash-time 0.1.0", + "nym-network-defaults 0.1.0", "nym-task", - "nym-validator-client", + "nym-validator-client 0.1.0", "nyxd-scraper", "rand_chacha", "serde", @@ -6546,13 +7046,26 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw2", - "nym-contracts-common", - "nym-mixnet-contract-common", + "nym-contracts-common 0.5.0", + "nym-mixnet-contract-common 0.6.0", "serde", "thiserror", "ts-rs", ] +[[package]] +name = "nym-vesting-contract-common" +version = "0.7.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "nym-contracts-common 0.5.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-mixnet-contract-common 0.6.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "serde", + "thiserror", +] + [[package]] name = "nym-wallet-types" version = "1.0.0" @@ -6560,12 +7073,12 @@ dependencies = [ "cosmrs 0.15.0", "cosmwasm-std", "hex-literal", - "nym-config", - "nym-mixnet-contract-common", - "nym-network-defaults", + "nym-config 0.1.0", + "nym-mixnet-contract-common 0.6.0", + "nym-network-defaults 0.1.0", "nym-types", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "serde", "serde_json", "strum 0.23.0", @@ -6586,11 +7099,11 @@ dependencies = [ "log", "nym-authenticator-requests", "nym-credential-verification", - "nym-crypto", + "nym-crypto 0.4.0", "nym-gateway-storage", - "nym-network-defaults", + "nym-network-defaults 0.1.0", "nym-task", - "nym-wireguard-types", + "nym-wireguard-types 0.1.0", "thiserror", "tokio", "tokio-stream", @@ -6603,15 +7116,29 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "log", - "nym-config", - "nym-crypto", - "nym-network-defaults", + "nym-config 0.1.0", + "nym-crypto 0.4.0", + "nym-network-defaults 0.1.0", "rand", "serde", "thiserror", "x25519-dalek", ] +[[package]] +name = "nym-wireguard-types" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork#8d68cf88dac1adf1f68726a9b6e0740f8cddcf32" +dependencies = [ + "base64 0.22.1", + "log", + "nym-config 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "nym-network-defaults 0.1.0 (git+https://github.com/nymtech/nym?branch=pre-dir-v2-fork)", + "serde", + "thiserror", + "x25519-dalek", +] + [[package]] name = "nymvisor" version = "0.1.8" @@ -6627,8 +7154,8 @@ dependencies = [ "humantime-serde", "nix 0.27.1", "nym-async-file-watcher", - "nym-bin-common", - "nym-config", + "nym-bin-common 0.6.0", + "nym-config 0.1.0", "nym-task", "reqwest 0.12.4", "serde", @@ -7405,6 +7932,21 @@ dependencies = [ "psl-types", ] +[[package]] +name = "quanta" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -7496,6 +8038,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "raw-cpuid" +version = "11.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "rayon" version = "1.10.0" @@ -8355,6 +8906,59 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_json_path" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bc0207b6351893eafa1e39aa9aea452abb6425ca7b02dd64faf29109e7a33ba" +dependencies = [ + "inventory", + "nom", + "once_cell", + "regex", + "serde", + "serde_json", + "serde_json_path_core", + "serde_json_path_macros", + "thiserror", +] + +[[package]] +name = "serde_json_path_core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d64fe53ce1aaa31bea2b2b46d3b6ab6a37e61854bedcbd9f174e188f3f7d79" +dependencies = [ + "inventory", + "once_cell", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "serde_json_path_macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a31e8177a443fd3e94917f12946ae7891dfb656e6d4c5e79b8c5d202fbcb723" +dependencies = [ + "inventory", + "once_cell", + "serde_json_path_core", + "serde_json_path_macros_internal", +] + +[[package]] +name = "serde_json_path_macros_internal" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75dde5a1d2ed78dfc411fc45592f72d3694436524d3353683ecb3d22009731dc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "serde_path_to_error" version = "0.1.16" @@ -8719,7 +9323,7 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener", + "event-listener 2.5.3", "futures-channel", "futures-core", "futures-intrusive", @@ -9129,6 +9733,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tap" version = "1.0.1" @@ -9323,19 +9933,19 @@ dependencies = [ "cw-utils", "dkg-bypass-contract", "indicatif", - "nym-bin-common", - "nym-coconut-dkg-common", - "nym-compact-ecash", - "nym-config", - "nym-contracts-common", - "nym-crypto", - "nym-ecash-contract-common", - "nym-group-contract-common", - "nym-mixnet-contract-common", - "nym-multisig-contract-common", - "nym-pemstore", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-bin-common 0.6.0", + "nym-coconut-dkg-common 0.1.0", + "nym-compact-ecash 0.1.0", + "nym-config 0.1.0", + "nym-contracts-common 0.5.0", + "nym-crypto 0.4.0", + "nym-ecash-contract-common 0.1.0", + "nym-group-contract-common 0.1.0", + "nym-mixnet-contract-common 0.6.0", + "nym-multisig-contract-common 0.1.0", + "nym-pemstore 0.3.0", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "rand", "serde", "serde_json", @@ -9901,6 +10511,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + [[package]] name = "try-lock" version = "0.2.5" @@ -9923,11 +10539,11 @@ name = "ts-rs-cli" version = "0.1.0" dependencies = [ "anyhow", - "nym-api-requests", - "nym-mixnet-contract-common", + "nym-api-requests 0.1.0", + "nym-mixnet-contract-common 0.6.0", "nym-types", - "nym-validator-client", - "nym-vesting-contract-common", + "nym-validator-client 0.1.0", + "nym-vesting-contract-common 0.7.0", "nym-wallet-types", "ts-rs", "walkdir", @@ -10571,15 +11187,15 @@ dependencies = [ "js-sys", "nym-bandwidth-controller", "nym-client-core", - "nym-config", + "nym-config 0.1.0", "nym-credential-storage", - "nym-crypto", + "nym-crypto 0.4.0", "nym-gateway-client", "nym-sphinx", "nym-sphinx-acknowledgements", "nym-task", "nym-topology", - "nym-validator-client", + "nym-validator-client 0.1.0", "rand", "serde", "serde-wasm-bindgen 0.6.5", @@ -11134,12 +11750,12 @@ dependencies = [ "bs58", "getrandom", "js-sys", - "nym-bin-common", + "nym-bin-common 0.6.0", "nym-coconut", - "nym-compact-ecash", + "nym-compact-ecash 0.1.0", "nym-credentials", - "nym-crypto", - "nym-http-api-client", + "nym-crypto 0.4.0", + "nym-http-api-client 0.1.0", "rand", "reqwest 0.12.4", "serde", diff --git a/Cargo.toml b/Cargo.toml index 6303eccca0..d99e3d7532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ members = [ "common/ip-packet-requests", "common/ledger", "common/mixnode-common", + "common/models", "common/network-defaults", "common/node-tester-utils", "common/nonexhaustive-delayqueue", @@ -120,6 +121,8 @@ members = [ "nym-node", "nym-node/nym-node-http-api", "nym-node/nym-node-requests", + "nym-node-status-api", + "nym-node-status-agent", "nym-outfox", "nym-validator-rewarder", "tools/echo-server", @@ -147,13 +150,16 @@ members = [ default-members = [ "clients/native", "clients/socks5", + "common/models", "explorer-api", "gateway", "mixnode", "nym-api", "nym-data-observatory", "nym-node", + "nym-node-status-api", "nym-validator-rewarder", + "nym-node-status-api", "service-providers/authenticator", "service-providers/ip-packet-router", "service-providers/network-requester", @@ -234,10 +240,12 @@ dotenvy = "0.15.6" ecdsa = "0.16" ed25519-dalek = "2.1" etherparse = "0.13.0" +envy = "0.4" eyre = "0.6.9" fastrand = "2.1.1" flate2 = "1.0.34" futures = "0.3.28" +futures-util = "0.3" generic-array = "0.14.7" getrandom = "0.2.10" getset = "0.1.3" @@ -267,6 +275,7 @@ ledger-transport-hid = "0.10.0" log = "0.4" maxminddb = "0.23.0" mime = "0.3.17" +moka = { version = "0.12", features = ["future"] } nix = "0.27.1" notify = "5.1.0" okapi = "0.7.0" @@ -300,6 +309,7 @@ serde = "1.0.211" serde_bytes = "0.11.15" serde_derive = "1.0" serde_json = "1.0.132" +serde_json_path = "0.6.7" serde_repr = "0.1" serde_with = "3.9.0" serde_yaml = "0.9.25" @@ -308,6 +318,7 @@ si-scale = "0.2.3" sphinx-packet = "0.1.1" sqlx = "0.7.4" strum = "0.26" +strum_macros = "0.26" subtle-encoding = "0.5" syn = "1" sysinfo = "0.30.13" @@ -329,6 +340,7 @@ tracing = "0.1.37" tracing-opentelemetry = "0.19.0" tracing-subscriber = "0.3.16" tracing-tree = "0.2.2" +tracing-log = "0.2" ts-rs = "10.0.0" tungstenite = { version = "0.20.1", default-features = false } url = "2.5" diff --git a/common/bin-common/Cargo.toml b/common/bin-common/Cargo.toml index b63631ddaa..11a2c76f2b 100644 --- a/common/bin-common/Cargo.toml +++ b/common/bin-common/Cargo.toml @@ -45,3 +45,4 @@ tracing = [ "opentelemetry", ] clap = [ "dep:clap", "dep:clap_complete", "dep:clap_complete_fig" ] +models = [] diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index d17b100672..5cd86e2dba 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -25,7 +25,7 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } nym-http-api-client = { path = "../../../common/http-api-client" } thiserror = { workspace = true } -log = { workspace = true } +tracing = { workspace = true } url = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["sync", "time"] } time = { workspace = true, features = ["formatting"] } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index b8375abdbd..472a6a9933 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -265,6 +265,13 @@ impl NymApiClient { NymApiClient { nym_api } } + #[cfg(not(target_arch = "wasm32"))] + pub fn new_with_timeout(api_url: Url, timeout: std::time::Duration) -> Self { + let nym_api = nym_api::Client::new(api_url, Some(timeout)); + + NymApiClient { nym_api } + } + pub fn new_with_user_agent(api_url: Url, user_agent: UserAgent) -> Self { let nym_api = nym_api::Client::builder::<_, ValidatorClientError>(api_url) .expect("invalid api url") diff --git a/common/client-libs/validator-client/src/connection_tester.rs b/common/client-libs/validator-client/src/connection_tester.rs index 15d2efe250..c9b4b3e435 100644 --- a/common/client-libs/validator-client/src/connection_tester.rs +++ b/common/client-libs/validator-client/src/connection_tester.rs @@ -121,36 +121,36 @@ async fn test_nyxd_connection( { Ok(Err(NyxdError::TendermintErrorRpc(e))) => { // If we get a tendermint-rpc error, we classify the node as not contactable - log::warn!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e); + tracing::warn!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e); false } Ok(Err(NyxdError::AbciError { code, log, .. })) => { // We accept the mixnet contract not found as ok from a connection standpoint. This happens // for example on a pre-launch network. - log::debug!( + tracing::debug!( "Checking: nyxd url: {url}: {}, but with abci error: {code}: {log}", "success".green() ); code == 18 } Ok(Err(error @ NyxdError::NoContractAddressAvailable(_))) => { - log::warn!("Checking: nyxd url: {url}: {}: {error}", "failed".red()); + tracing::warn!("Checking: nyxd url: {url}: {}: {error}", "failed".red()); false } Ok(Err(e)) => { // For any other error, we're optimistic and just try anyway. - log::warn!( + tracing::warn!( "Checking: nyxd_url: {url}: {}, but with error: {e}", "success".green() ); true } Ok(Ok(_)) => { - log::debug!("Checking: nyxd_url: {url}: {}", "success".green()); + tracing::debug!("Checking: nyxd_url: {url}: {}", "success".green()); true } Err(e) => { - log::warn!("Checking: nyxd_url: {url}: {}: {e}", "failed".red()); + tracing::warn!("Checking: nyxd_url: {url}: {}: {e}", "failed".red()); false } }; @@ -169,15 +169,15 @@ async fn test_nym_api_connection( .await { Ok(Ok(_)) => { - log::debug!("Checking: api_url: {url}: {}", "success".green()); + tracing::debug!("Checking: api_url: {url}: {}", "success".green()); true } Ok(Err(e)) => { - log::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); + tracing::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); false } Err(e) => { - log::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); + tracing::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); false } }; diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 9660e470c7..2e5ce565f9 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -41,6 +41,7 @@ use nym_mixnet_contract_common::mixnode::MixNodeDetails; use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId}; use time::format_description::BorrowedFormatItem; use time::Date; +use tracing::instrument; pub mod error; pub mod routes; @@ -52,11 +53,13 @@ pub fn rfc_3339_date() -> Vec> { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NymApiClientExt: ApiClient { + #[instrument(level = "debug", skip(self))] async fn get_mixnodes(&self) -> Result, NymAPIError> { self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS) .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -70,6 +73,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_gateways_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -83,6 +87,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_detailed_unfiltered( &self, ) -> Result, NymAPIError> { @@ -98,11 +103,13 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_gateways(&self) -> Result, NymAPIError> { self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) .await } + #[instrument(level = "debug", skip(self))] async fn get_gateways_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED], @@ -111,6 +118,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED], @@ -119,6 +127,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[tracing::instrument(level = "debug", skip_all)] async fn get_basic_mixnodes( &self, semver_compatibility: Option, @@ -142,6 +151,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_basic_gateways( &self, semver_compatibility: Option, @@ -167,6 +177,7 @@ pub trait NymApiClientExt: ApiClient { /// retrieve basic information for nodes are capable of operating as an entry gateway /// this includes legacy gateways and nym-nodes + #[instrument(level = "debug", skip(self))] async fn get_all_basic_entry_assigned_nodes( &self, semver_compatibility: Option, @@ -208,6 +219,7 @@ pub trait NymApiClientExt: ApiClient { /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch /// this includes legacy mixnodes and nym-nodes + #[instrument(level = "debug", skip(self))] async fn get_basic_active_mixing_assigned_nodes( &self, semver_compatibility: Option, @@ -247,6 +259,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_active_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE], @@ -255,6 +268,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_active_mixnodes_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -269,6 +283,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_rewarded_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::REWARDED], @@ -277,6 +292,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_report( &self, mix_id: NodeId, @@ -294,6 +310,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_gateway_report( &self, identity: IdentityKeyRef<'_>, @@ -311,6 +328,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_history( &self, mix_id: NodeId, @@ -328,6 +346,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_gateway_history( &self, identity: IdentityKeyRef<'_>, @@ -345,6 +364,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_rewarded_mixnodes_detailed( &self, ) -> Result, NymAPIError> { @@ -361,6 +381,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, @@ -392,6 +413,7 @@ pub trait NymApiClientExt: ApiClient { } } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_core_status_count( &self, mix_id: NodeId, @@ -424,6 +446,7 @@ pub trait NymApiClientExt: ApiClient { } } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_status( &self, mix_id: NodeId, @@ -441,6 +464,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_reward_estimation( &self, mix_id: NodeId, @@ -458,6 +482,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn compute_mixnode_reward_estimation( &self, mix_id: NodeId, @@ -477,6 +502,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_stake_saturation( &self, mix_id: NodeId, @@ -494,6 +520,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnode_inclusion_probability( &self, mix_id: NodeId, @@ -511,6 +538,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_current_node_performance( &self, node_id: NodeId, @@ -541,6 +569,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_blacklisted(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED], @@ -549,6 +578,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn get_gateways_blacklisted(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::GATEWAYS, routes::BLACKLISTED], @@ -557,6 +587,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self, request_body))] async fn blind_sign( &self, request_body: &BlindSignRequestBody, @@ -573,6 +604,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self, request_body))] async fn verify_ecash_ticket( &self, request_body: &VerifyEcashTicketBody, @@ -589,6 +621,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self, request_body))] async fn batch_redeem_ecash_tickets( &self, request_body: &BatchRedeemTicketsBody, @@ -605,6 +638,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn double_spending_filter_v1(&self) -> Result { self.get_json( &[ @@ -617,6 +651,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn partial_expiration_date_signatures( &self, expiration_date: Option, @@ -640,6 +675,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn partial_coin_indices_signatures( &self, epoch_id: Option, @@ -660,6 +696,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn global_expiration_date_signatures( &self, expiration_date: Option, @@ -683,6 +720,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn global_coin_indices_signatures( &self, epoch_id: Option, @@ -703,6 +741,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn master_verification_key( &self, epoch_id: Option, @@ -722,6 +761,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn epoch_credentials( &self, dkg_epoch: EpochId, @@ -738,6 +778,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn issued_credential( &self, credential_id: i64, @@ -754,6 +795,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn issued_credentials( &self, credential_ids: Vec, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs index 4d8fd3237c..8674d7cb0a 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs @@ -8,9 +8,9 @@ use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmrs::AccountId; use cosmwasm_std::Addr; -use log::trace; use nym_coconut_dkg_common::types::{ChunkIndex, NodeIndex, StateAdvanceResponse}; use serde::Deserialize; +use tracing::trace; use nym_coconut_dkg_common::dealer::RegisteredDealerDetails; pub use nym_coconut_dkg_common::{ diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs index 5cea0a1ba2..8feceebdbf 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs @@ -29,7 +29,6 @@ use cosmrs::proto::cosmwasm::wasm::v1::{ }; use cosmrs::tendermint::{block, chain, Hash}; use cosmrs::{AccountId, Coin as CosmosCoin, Tx}; -use log::trace; use prost::Message; use serde::{Deserialize, Serialize}; @@ -68,7 +67,7 @@ pub trait CosmWasmClient: TendermintRpcClient { Res: Message + Default, { if let Some(ref abci_path) = path { - trace!("performing query on abci path {abci_path}") + tracing::trace!("performing query on abci path {abci_path}") } let mut buf = Vec::with_capacity(req.encoded_len()); req.encode(&mut buf)?; @@ -297,7 +296,7 @@ pub trait CosmWasmClient: TendermintRpcClient { let start = Instant::now(); loop { - log::debug!( + tracing::debug!( "Polling for result of including {} in a block...", broadcasted.hash ); @@ -522,7 +521,7 @@ pub trait CosmWasmClient: TendermintRpcClient { .make_abci_query::<_, QuerySmartContractStateResponse>(path, req) .await?; - trace!("raw query response: {}", String::from_utf8_lossy(&res.data)); + tracing::trace!("raw query response: {}", String::from_utf8_lossy(&res.data)); Ok(serde_json::from_slice(&res.data)?) } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs index 8d0bb0fb4f..cec29e9c50 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs @@ -25,12 +25,12 @@ use cosmrs::proto::cosmos::tx::signing::v1beta1::SignMode; use cosmrs::staking::{MsgDelegate, MsgUndelegate}; use cosmrs::tx::{self, Msg}; use cosmrs::{cosmwasm, AccountId, Any, Tx}; -use log::debug; use serde::Serialize; use sha2::Digest; use sha2::Sha256; use std::time::SystemTime; use tendermint_rpc::endpoint::broadcast; +use tracing::debug; fn empty_fee() -> tx::Fee { tx::Fee { diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs index d6e42daac7..559ad434a2 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs @@ -7,9 +7,9 @@ use base64::Engine; use cosmrs::abci::TxMsgData; use cosmrs::cosmwasm::MsgExecuteContractResponse; use cosmrs::proto::cosmos::base::query::v1beta1::{PageRequest, PageResponse}; -use log::error; use prost::bytes::Bytes; use tendermint_rpc::endpoint::broadcast; +use tracing::error; pub use cosmrs::abci::MsgResponse; diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index a8ad8e64d1..f35c662e88 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Display; use std::time::Duration; use thiserror::Error; -use tracing::warn; +use tracing::{instrument, warn}; use url::Url; pub use reqwest::IntoUrl; @@ -202,6 +202,7 @@ impl Client { self.reqwest_client.get(url) } + #[instrument(level = "debug", skip_all, fields(path=?path))] async fn send_get_request( &self, path: PathSegments<'_>, @@ -212,6 +213,7 @@ impl Client { V: AsRef, E: Display, { + tracing::trace!("Sending GET request"); let url = sanitize_url(&self.base_url, path, params); #[cfg(target_arch = "wasm32")] @@ -277,6 +279,7 @@ impl Client { } } + #[instrument(level = "debug", skip_all)] pub async fn get_json( &self, path: PathSegments<'_>, @@ -512,12 +515,14 @@ pub fn sanitize_url, V: AsRef>( url } +#[tracing::instrument(level = "debug", skip_all)] pub async fn parse_response(res: Response, allow_empty: bool) -> Result> where T: DeserializeOwned, E: DeserializeOwned + Display, { let status = res.status(); + tracing::debug!("Status: {} (success: {})", &status, status.is_success()); if !allow_empty { if let Some(0) = res.content_length() { @@ -526,6 +531,18 @@ where } if res.status().is_success() { + #[cfg(debug_assertions)] + { + let text = res.text().await.inspect_err(|err| { + tracing::error!("Couldn't even get response text: {err}"); + })?; + tracing::trace!("Result:\n{:#?}", text); + + serde_json::from_str(&text) + .map_err(|err| HttpClientError::GenericRequestFailure(err.to_string())) + } + + #[cfg(not(debug_assertions))] Ok(res.json().await?) } else if res.status() == StatusCode::NOT_FOUND { Err(HttpClientError::NotFound) diff --git a/common/models/Cargo.toml b/common/models/Cargo.toml new file mode 100644 index 0000000000..acb6e35682 --- /dev/null +++ b/common/models/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nym-common-models" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +serde = { workspace = true, features = ["derive"] } diff --git a/common/models/src/lib.rs b/common/models/src/lib.rs new file mode 100644 index 0000000000..3d85e66947 --- /dev/null +++ b/common/models/src/lib.rs @@ -0,0 +1 @@ +pub mod ns_api; diff --git a/common/models/src/ns_api.rs b/common/models/src/ns_api.rs new file mode 100644 index 0000000000..9c3373802a --- /dev/null +++ b/common/models/src/ns_api.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TestrunAssignment { + /// has nothing to do with GW identity key. This is PK from `gateways` table + pub testrun_id: i64, + pub gateway_pk_id: i64, +} diff --git a/envs/canary.env b/envs/canary.env index c4aac15082..db70a19fd0 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -19,5 +19,5 @@ MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftq COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9 EXPLORER_API=https://canary-explorer.performance.nymte.ch/api -NYXD="https://canary-validator.performance.nymte.ch" -NYM_API="https://canary-api.performance.nymte.ch/api" +NYXD=https://canary-validator.performance.nymte.ch +NYM_API=https://canary-api.performance.nymte.ch/api diff --git a/envs/mainnet.env b/envs/mainnet.env index e40e54a471..419547bf97 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -21,8 +21,8 @@ COCONUT_DKG_CONTRACT_ADDRESS=n19604yflqggs9mk2z26mqygq43q2kr3n932egxx630svywd5mp REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" -NYXD="https://rpc.nymtech.net" -NYM_API="https://validator.nymtech.net/api/" +NYXD=https://rpc.nymtech.net +NYM_API=https://validator.nymtech.net/api/ NYXD_WS="wss://rpc.nymtech.net/websocket" -EXPLORER_API="https://explorer.nymtech.net/api/" +EXPLORER_API=https://explorer.nymtech.net/api/ NYM_VPN_API="https://nymvpn.com/api" diff --git a/envs/qa.env b/envs/qa.env index e88f845416..34306a62ea 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -19,5 +19,5 @@ VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8 REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39 EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api -NYXD="https://qa-validator.qa.nymte.ch" -NYM_API="https://qa-nym-api.qa.nymte.ch/api" +NYXD=https://qa-validator.qa.nymte.ch +NYM_API=https://qa-nym-api.qa.nymte.ch/api diff --git a/envs/sandbox.env b/envs/sandbox.env index 4763269a6f..6310b8fa89 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -20,6 +20,6 @@ ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jl STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" EXPLORER_API=https://sandbox-explorer.nymtech.net/api -NYXD="https://rpc.sandbox.nymtech.net" -NYXD_WS="wss://rpc.sandbox.nymtech.net/websocket" -NYM_API="https://sandbox-nym-api1.nymtech.net/api" +NYXD=https://rpc.sandbox.nymtech.net +NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket +NYM_API=https://sandbox-nym-api1.nymtech.net/api diff --git a/explorer-api/explorer-client/Cargo.toml b/explorer-api/explorer-client/Cargo.toml index 2397c83f7b..d32429e9a1 100644 --- a/explorer-api/explorer-client/Cargo.toml +++ b/explorer-api/explorer-client/Cargo.toml @@ -7,12 +7,12 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -log.workspace = true nym-explorer-api-requests = { path = "../explorer-api-requests" } reqwest = { workspace = true, features = ["json"] } serde.workspace = true thiserror.workspace = true url.workspace = true +tracing = {workspace = true, features = ["attributes"]} [dev-dependencies] tokio = { workspace = true, features = ["full"] } diff --git a/explorer-api/explorer-client/src/lib.rs b/explorer-api/explorer-client/src/lib.rs index 3ff4807b2b..50c5431c25 100644 --- a/explorer-api/explorer-client/src/lib.rs +++ b/explorer-api/explorer-client/src/lib.rs @@ -3,6 +3,7 @@ use std::time::Duration; use reqwest::StatusCode; use thiserror::Error; +use tracing::instrument; use url::Url; // Re-export request types @@ -47,6 +48,12 @@ impl ExplorerClient { Ok(Self { client, url }) } + #[cfg(not(target_arch = "wasm32"))] + pub fn new_with_timeout(url: url::Url, timeout: Duration) -> Result { + let client = reqwest::Client::builder().timeout(timeout).build()?; + Ok(Self { client, url }) + } + #[cfg(target_arch = "wasm32")] pub fn new(url: url::Url) -> Result { let client = reqwest::Client::builder().build()?; @@ -58,10 +65,11 @@ impl ExplorerClient { paths: &[&str], ) -> Result { let url = combine_url(self.url.clone(), paths)?; - log::trace!("Sending GET request {url:?}"); + tracing::debug!("Sending GET request"); Ok(self.client.get(url).send().await?) } + #[instrument(level = "trace", skip_all, fields(paths=?paths))] async fn query_explorer_api(&self, paths: &[&str]) -> Result where T: std::fmt::Debug, @@ -70,12 +78,14 @@ impl ExplorerClient { let response = self.send_get_request(paths).await?; if response.status().is_success() { let res = response.json::().await?; - log::trace!("Got response: {res:?}"); + tracing::trace!("Got response: {res:?}"); Ok(res) } else if response.status() == StatusCode::NOT_FOUND { Err(ExplorerApiError::NotFound) } else { - Err(ExplorerApiError::RequestFailure(response.text().await?)) + let status = response.status(); + let err_msg = format!("{}: {}", response.text().await?, status); + Err(ExplorerApiError::RequestFailure(err_msg)) } } diff --git a/nym-data-observatory/README_SQLX.md b/nym-data-observatory/README_SQLX.md index 3e1021c8eb..72cca4294f 100644 --- a/nym-data-observatory/README_SQLX.md +++ b/nym-data-observatory/README_SQLX.md @@ -68,6 +68,7 @@ warning: no queries found; do you have the `offline` feature enabled ### Possible solutions - does your `sqlx-cli` version match `sqlx` version from `Cargo.toml`? + + `cargo install -f sqlx-cli --version ` ``` cargo install sqlx-cli --version --force ``` diff --git a/nym-node-status-agent/.gitignore b/nym-node-status-agent/.gitignore new file mode 100644 index 0000000000..bf462f3e05 --- /dev/null +++ b/nym-node-status-agent/.gitignore @@ -0,0 +1 @@ +nym-gateway-probe diff --git a/nym-node-status-agent/Cargo.toml b/nym-node-status-agent/Cargo.toml new file mode 100644 index 0000000000..a2ce8cdf97 --- /dev/null +++ b/nym-node-status-agent/Cargo.toml @@ -0,0 +1,27 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + + +[package] +name = "nym-node-status-agent" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true +readme.workspace = true + +[dependencies] +anyhow = { workspace = true} +clap = { workspace = true, features = ["derive", "env"] } +nym-bin-common = { path = "../common/bin-common", features = ["models"]} +nym-common-models = { path = "../common/models" } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process"] } +tokio-util = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +reqwest = { workspace = true, features = ["json"] } +serde_json = { workspace = true } diff --git a/nym-node-status-agent/run.sh b/nym-node-status-agent/run.sh new file mode 100755 index 0000000000..5054a90825 --- /dev/null +++ b/nym-node-status-agent/run.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set -eu + +export RUST_LOG=${RUST_LOG:-debug} + +crate_root=$(dirname $(realpath "$0")) +gateway_probe_src=$(dirname $(dirname "$crate_root"))/nym-vpn-client/nym-vpn-core +echo "gateway_probe_src=$gateway_probe_src" +echo "crate_root=$crate_root" + +export NODE_STATUS_AGENT_PROBE_PATH="$crate_root/nym-gateway-probe" + +# build & copy over GW probe +function copy_gw_probe() { + pushd $gateway_probe_src + cargo build --release --package nym-gateway-probe + cp target/release/nym-gateway-probe "$crate_root" + $crate_root/nym-gateway-probe --version + popd +} + +function build_agent() { + cargo build --package nym-node-status-agent --release +} + +function swarm() { + local workers=$1 + echo "Running $workers in parallel" + + build_agent + + for ((i=1; i<=$workers; i++)); do + ../target/release/nym-node-status-agent run-probe & + done + + wait + + echo "All agents completed" +} + +export NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1" +export NODE_STATUS_AGENT_SERVER_PORT="8000" + +copy_gw_probe + +swarm 30 + +# cargo run -- run-probe diff --git a/nym-node-status-agent/src/cli.rs b/nym-node-status-agent/src/cli.rs new file mode 100644 index 0000000000..c4465797d0 --- /dev/null +++ b/nym-node-status-agent/src/cli.rs @@ -0,0 +1,109 @@ +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use nym_common_models::ns_api::TestrunAssignment; +use std::sync::OnceLock; +use tracing::instrument; + +use crate::probe::GwProbe; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Args { + #[command(subcommand)] + pub(crate) command: Command, + #[arg(short, long, env = "NODE_STATUS_AGENT_SERVER_ADDRESS")] + pub(crate) server_address: String, + + #[arg(short = 'p', long, env = "NODE_STATUS_AGENT_SERVER_PORT")] + pub(crate) server_port: u16, + // TODO dz accept keypair for identification / auth +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Command { + RunProbe { + /// path of binary to run + #[arg(long, env = "NODE_STATUS_AGENT_PROBE_PATH")] + probe_path: String, + #[arg(short, long, env = "NODE_STATUS_AGENT_GATEWAY_ID")] + gateway_id: Option, + }, +} + +impl Args { + pub(crate) async fn execute(&self) -> anyhow::Result<()> { + match &self.command { + Command::RunProbe { + probe_path, + gateway_id, + } => self.run_probe(probe_path, gateway_id).await?, + } + + Ok(()) + } + + async fn run_probe(&self, probe_path: &str, gateway_id: &Option) -> anyhow::Result<()> { + let server_address = format!("{}:{}", &self.server_address, self.server_port); + + let probe = GwProbe::new(probe_path.to_string()); + + let version = probe.version().await; + tracing::info!("Probe version:\n{}", version); + + let testrun = request_testrun(&server_address).await?; + + let log = probe.run_and_get_log(gateway_id); + + submit_results(&server_address, testrun.testrun_id, log).await?; + + Ok(()) + } +} + +const URL_BASE: &str = "internal/testruns"; + +#[instrument(level = "debug", skip_all)] +async fn request_testrun(server_addr: &str) -> anyhow::Result { + let target_url = format!("{}/{}", server_addr, URL_BASE); + let client = reqwest::Client::new(); + let res = client + .get(target_url) + .send() + .await + .and_then(|response| response.error_for_status())?; + res.json() + .await + .map(|testrun| { + tracing::info!("Received testrun assignment: {:?}", testrun); + testrun + }) + .map_err(|err| { + tracing::error!("err"); + err.into() + }) +} + +#[instrument(level = "debug", skip(probe_outcome))] +async fn submit_results( + server_addr: &str, + testrun_id: i64, + probe_outcome: String, +) -> anyhow::Result<()> { + let target_url = format!("{}/{}/{}", server_addr, URL_BASE, testrun_id); + let client = reqwest::Client::new(); + let res = client + .post(target_url) + .body(probe_outcome) + .send() + .await + .and_then(|response| response.error_for_status())?; + + tracing::debug!("Submitted results: {})", res.status()); + Ok(()) +} diff --git a/nym-node-status-agent/src/main.rs b/nym-node-status-agent/src/main.rs new file mode 100644 index 0000000000..133828b7fa --- /dev/null +++ b/nym-node-status-agent/src/main.rs @@ -0,0 +1,78 @@ +use crate::cli::Args; +use clap::Parser; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{filter::Directive, EnvFilter}; + +mod cli; +mod probe; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + setup_tracing(); + let args = Args::parse(); + + let server_addr = format!("{}:{}", args.server_address, args.server_port); + test_ns_api_conn(&server_addr).await?; + + args.execute().await?; + + Ok(()) +} + +async fn test_ns_api_conn(server_addr: &str) -> anyhow::Result<()> { + reqwest::get(server_addr) + .await + .map(|res| { + tracing::info!( + "Testing connection to NS API at {server_addr}: {}", + res.status() + ); + }) + .map_err(|err| anyhow::anyhow!("Couldn't connect to server on {}: {}", server_addr, err)) +} + +pub(crate) fn setup_tracing() { + fn directive_checked(directive: impl Into) -> Directive { + directive + .into() + .parse() + .expect("Failed to parse log directive") + } + + let log_builder = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .compact() + // Display source code file paths + .with_file(true) + // Display source code line numbers + .with_line_number(true) + .with_thread_ids(true) + // Don't display the event's target (module path) + .with_target(false); + + let mut filter = EnvFilter::builder() + // if RUST_LOG isn't set, set default level + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + // these crates are more granularly filtered + let filter_crates = [ + "reqwest", + "rustls", + "hyper", + "sqlx", + "h2", + "tendermint_rpc", + "tower_http", + "axum", + ]; + for crate_name in filter_crates { + filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); + } + + filter = filter.add_directive(directive_checked("nym_bin_common=debug")); + filter = filter.add_directive(directive_checked("nym_explorer_client=debug")); + filter = filter.add_directive(directive_checked("nym_network_defaults=debug")); + filter = filter.add_directive(directive_checked("nym_validator_client=debug")); + + log_builder.with_env_filter(filter).init(); +} diff --git a/nym-node-status-agent/src/probe.rs b/nym-node-status-agent/src/probe.rs new file mode 100644 index 0000000000..c75900e936 --- /dev/null +++ b/nym-node-status-agent/src/probe.rs @@ -0,0 +1,54 @@ +use tracing::error; + +pub(crate) struct GwProbe { + path: String, +} + +impl GwProbe { + pub(crate) fn new(probe_path: String) -> Self { + Self { path: probe_path } + } + + pub(crate) async fn version(&self) -> String { + let mut command = tokio::process::Command::new(&self.path); + command.stdout(std::process::Stdio::piped()); + command.arg("--version"); + + match command.spawn() { + Ok(child) => { + if let Ok(output) = child.wait_with_output().await { + return String::from_utf8(output.stdout) + .unwrap_or("Unable to get log from test run".to_string()); + } + "Unable to get probe version".to_string() + } + Err(e) => { + error!("Failed to get probe version: {}", e); + "Failed to get probe version".to_string() + } + } + } + + pub(crate) fn run_and_get_log(&self, gateway_key: &Option) -> String { + let mut command = std::process::Command::new(&self.path); + command.stdout(std::process::Stdio::piped()); + + if let Some(gateway_id) = gateway_key { + command.arg("--gateway").arg(gateway_id); + } + + match command.spawn() { + Ok(child) => { + if let Ok(output) = child.wait_with_output() { + return String::from_utf8(output.stdout) + .unwrap_or("Unable to get log from test run".to_string()); + } + "Unable to get log from test run".to_string() + } + Err(e) => { + error!("Failed to spawn test: {}", e); + "Failed to spawn test run task".to_string() + } + } + } +} diff --git a/nym-node-status-api/.gitignore b/nym-node-status-api/.gitignore new file mode 100644 index 0000000000..91459afabe --- /dev/null +++ b/nym-node-status-api/.gitignore @@ -0,0 +1,6 @@ +data/ +enter_db.sh +nym-gateway-probe +nym-node-status-api +*.sqlite +*.sqlite-journal diff --git a/nym-node-status-api/Cargo.toml b/nym-node-status-api/Cargo.toml new file mode 100644 index 0000000000..4cc9ff5d28 --- /dev/null +++ b/nym-node-status-api/Cargo.toml @@ -0,0 +1,66 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "nym-node-status-api" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = { workspace = true } +axum = { workspace = true, features = ["tokio", "macros"] } +chrono = { workspace = true } +clap = { workspace = true, features = ["cargo", "derive", "env", "string"] } +cosmwasm-std = { workspace = true } +envy = { workspace = true } +futures-util = { workspace = true } +moka = { workspace = true, features = ["future"] } +nym-bin-common = { path = "../common/bin-common", features = ["models"]} +nym-common-models = { path = "../common/models" } +nym-explorer-client = { path = "../explorer-api/explorer-client" } +# TODO dz: before Nym API client breaking changes. Update to latest develop once new Nym API is live +nym-network-defaults = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" } +nym-validator-client = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" } +# nym-network-defaults = { path = "../common/network-defaults" } +# nym-validator-client = { path = "../common/client-libs/validator-client" } +nym-task = { path = "../common/task" } +nym-node-requests = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" } +# nym-node-requests = { path = "../nym-node/nym-node-requests", features = ["openapi"] } +regex = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +serde_json_path = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread"] } +tokio-util = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing-log = { workspace = true } +tower-http = { workspace = true, features = ["cors", "trace"] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } +# TODO dz `cargo update async-trait` +# for automatic schema detection, which was merged, but not released yet +# https://github.com/ProbablyClem/utoipauto/pull/38 +# utoipauto = { git = "https://github.com/ProbablyClem/utoipauto", rev = "eb04cba" } +utoipauto = { workspace = true } + +[build-dependencies] +anyhow = { workspace = true } +tokio = { workspace = true, features = ["macros"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } diff --git a/nym-node-status-api/Dockerfile b/nym-node-status-api/Dockerfile new file mode 100644 index 0000000000..ceab7c9392 --- /dev/null +++ b/nym-node-status-api/Dockerfile @@ -0,0 +1,15 @@ +FROM rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-node-status-api + +RUN cargo build --release + +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-node-status-api ./ +ENTRYPOINT [ "/nym/nym-node-status-api" ] diff --git a/nym-node-status-api/Dockerfile.dev b/nym-node-status-api/Dockerfile.dev new file mode 100644 index 0000000000..2967a6e605 --- /dev/null +++ b/nym-node-status-api/Dockerfile.dev @@ -0,0 +1,8 @@ +FROM ubuntu:22.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY nym-node-status-api/nym-node-status-api ./ +ENTRYPOINT [ "/nym/nym-node-status-api" ] diff --git a/nym-node-status-api/build.rs b/nym-node-status-api/build.rs new file mode 100644 index 0000000000..bfe6e54e74 --- /dev/null +++ b/nym-node-status-api/build.rs @@ -0,0 +1,51 @@ +use anyhow::{anyhow, Result}; +use sqlx::{Connection, SqliteConnection}; +use std::fs::Permissions; +use std::os::unix::fs::PermissionsExt; +use tokio::{fs::File, io::AsyncWriteExt}; + +const SQLITE_DB_FILENAME: &str = "nym-node-status-api.sqlite"; + +/// If you need to re-run migrations or reset the db, just run +/// cargo clean -p nym-node-status-api +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + let out_dir = read_env_var("OUT_DIR")?; + let database_path = format!("sqlite://{}/{}?mode=rwc", out_dir, SQLITE_DB_FILENAME); + + write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME).await?; + let mut conn = SqliteConnection::connect(&database_path).await?; + sqlx::migrate!("./migrations").run(&mut conn).await?; + + #[cfg(target_family = "unix")] + println!("cargo::rustc-env=DATABASE_URL=sqlite://{}", &database_path); + + #[cfg(target_family = "windows")] + // for some strange reason we need to add a leading `/` to the windows path even though it's + // not a valid windows path... but hey, it works... + println!("cargo::rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + rerun_if_changed(); + Ok(()) +} + +fn read_env_var(var: &str) -> Result { + std::env::var(var).map_err(|_| anyhow!("You need to set {} env var", var)) +} + +fn rerun_if_changed() { + println!("cargo::rerun-if-changed=migrations"); + println!("cargo::rerun-if-changed=src/db/queries"); +} + +/// use `./enter_db.sh` to inspect DB +async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Result<()> { + let mut file = File::create("enter_db.sh").await?; + let _ = file.write(b"#!/bin/bash\n").await?; + file.write_all(format!("sqlite3 {}/{}", out_dir, db_filename).as_bytes()) + .await?; + + file.set_permissions(Permissions::from_mode(0o755)) + .await + .map_err(From::from) +} diff --git a/nym-node-status-api/launch_node_status_api.sh b/nym-node-status-api/launch_node_status_api.sh new file mode 100755 index 0000000000..f9ebd364a9 --- /dev/null +++ b/nym-node-status-api/launch_node_status_api.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +set -e + +export RUST_LOG=${RUST_LOG:-debug} + +export NYM_API_CLIENT_TIMEOUT=60 +export EXPLORER_CLIENT_TIMEOUT=60 + +export ENVIRONMENT="mainnet.env" + +function run_bare() { + # export necessary env vars + set -a + source ../envs/$ENVIRONMENT + set +a + export RUST_LOG=debug + + # --conection-url is provided in build.rs + cargo run --package nym-node-status-api +} + +function run_docker() { + cargo build --package nym-node-status-api --release + cp ../target/release/nym-node-status-api . + + cd .. + docker build -t node-status-api -f nym-node-status-api/Dockerfile.dev . + docker run --env-file envs/${ENVIRONMENT} \ + -e EXPLORER_CLIENT_TIMEOUT=$EXPLORER_CLIENT_TIMEOUT \ + -e NYM_API_CLIENT_TIMEOUT=$NYM_API_CLIENT_TIMEOUT \ + -e DATABASE_URL="sqlite://node-status-api.sqlite?mode=rwc" \ + -e RUST_LOG=${RUST_LOG} node-status-api + +} + +run_bare + +# run_docker diff --git a/nym-node-status-api/migrations/000_init.sql b/nym-node-status-api/migrations/000_init.sql new file mode 100644 index 0000000000..1e5683e2c9 --- /dev/null +++ b/nym-node-status-api/migrations/000_init.sql @@ -0,0 +1,112 @@ +CREATE TABLE gateways +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_identity_key VARCHAR NOT NULL UNIQUE, + self_described VARCHAR, + explorer_pretty_bond VARCHAR, + last_probe_result VARCHAR, + last_probe_log VARCHAR, + config_score INTEGER NOT NULL DEFAULT (0), + config_score_successes REAL NOT NULL DEFAULT (0), + config_score_samples REAL NOT NULL DEFAULT (0), + routing_score INTEGER NOT NULL DEFAULT (0), + routing_score_successes REAL NOT NULL DEFAULT (0), + routing_score_samples REAL NOT NULL DEFAULT (0), + test_run_samples REAL NOT NULL DEFAULT (0), + last_testrun_utc INTEGER, + last_updated_utc INTEGER NOT NULL, + bonded INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0, + blacklisted INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0, + performance INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX idx_gateway_description_gateway_identity_key ON gateways (gateway_identity_key); + + +CREATE TABLE mixnodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + identity_key VARCHAR NOT NULL UNIQUE, + mix_id INTEGER NOT NULL UNIQUE, + bonded INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0, + total_stake INTEGER NOT NULL, + host VARCHAR NOT NULL, + http_api_port INTEGER NOT NULL, + blacklisted INTEGER CHECK (blacklisted in (0, 1)) NOT NULL DEFAULT 0, + full_details VARCHAR, + self_described VARCHAR, + last_updated_utc INTEGER NOT NULL + , is_dp_delegatee INTEGER CHECK (is_dp_delegatee IN (0, 1)) NOT NULL DEFAULT 0); +CREATE INDEX idx_mixnodes_mix_id ON mixnodes (mix_id); +CREATE INDEX idx_mixnodes_identity_key ON mixnodes (identity_key); + +CREATE TABLE + mixnode_description ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mix_id INTEGER UNIQUE NOT NULL, + moniker VARCHAR, + website VARCHAR, + security_contact VARCHAR, + details VARCHAR, + last_updated_utc INTEGER NOT NULL, + FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id) + ); + +-- Indexes for description table +CREATE INDEX idx_mixnode_description_mix_id ON mixnode_description (mix_id); + + +CREATE TABLE summary +( + key VARCHAR PRIMARY KEY, + value_json VARCHAR, + last_updated_utc INTEGER NOT NULL +); + + +CREATE TABLE summary_history +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date VARCHAR UNIQUE NOT NULL, + timestamp_utc INTEGER NOT NULL, + value_json VARCHAR +); +CREATE INDEX idx_summary_history_timestamp_utc ON summary_history (timestamp_utc); +CREATE INDEX idx_summary_history_date ON summary_history (date); + + +CREATE TABLE gateway_description ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_identity_key VARCHAR UNIQUE NOT NULL, + moniker VARCHAR, + website VARCHAR, + security_contact VARCHAR, + details VARCHAR, + last_updated_utc INTEGER NOT NULL, + FOREIGN KEY (gateway_identity_key) REFERENCES gateways (gateway_identity_key) + ); + + +CREATE TABLE + mixnode_daily_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mix_id INTEGER NOT NULL, + total_stake BIGINT NOT NULL, + date_utc VARCHAR NOT NULL, + packets_received INTEGER DEFAULT 0, + packets_sent INTEGER DEFAULT 0, + packets_dropped INTEGER DEFAULT 0, + FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id), + UNIQUE (mix_id, date_utc) -- This constraint automatically creates an index + ); + + +CREATE TABLE testruns +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_id INTEGER, + status INTEGER NOT NULL, -- 0=pending, 1=in-progress, 2=complete + timestamp_utc INTEGER NOT NULL, + ip_address VARCHAR NOT NULL, + log VARCHAR NOT NULL, + FOREIGN KEY (gateway_id) REFERENCES gateways (id) +); diff --git a/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/src/cli/mod.rs new file mode 100644 index 0000000000..84ee86577f --- /dev/null +++ b/nym-node-status-api/src/cli/mod.rs @@ -0,0 +1,77 @@ +use clap::Parser; +use nym_bin_common::bin_info; +use reqwest::Url; +use std::{sync::OnceLock, time::Duration}; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Clone, Debug, Parser)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + /// Network name for the network to which we're connecting. + #[clap(long, env = "NETWORK_NAME")] + pub(crate) network_name: String, + + /// Explorer api url. + #[clap(short, long, env = "EXPLORER_API")] + pub(crate) explorer_api: String, + + /// Nym api url. + #[clap(short, long, env = "NYM_API")] + pub(crate) nym_api: String, + + /// TTL for the http cache. + #[clap( + long, + default_value_t = 30, + env = "NYM_NODE_STATUS_API_NYM_HTTP_CACHE_TTL" + )] + pub(crate) nym_http_cache_ttl: u64, + + /// HTTP port on which to run node status api. + #[clap(long, default_value_t = 8000, env = "NYM_NODE_STATUS_API_HTTP_PORT")] + pub(crate) http_port: u16, + + /// Nyxd address. + #[clap(long, env = "NYXD")] + pub(crate) nyxd_addr: Url, + + /// Nym api client timeout. + #[clap(long, default_value = "15", env = "NYM_API_CLIENT_TIMEOUT")] + #[arg(value_parser = parse_duration)] + pub(crate) nym_api_client_timeout: Duration, + + /// Explorer api client timeout. + #[clap(long, default_value = "15", env = "EXPLORER_CLIENT_TIMEOUT")] + #[arg(value_parser = parse_duration)] + pub(crate) explorer_client_timeout: Duration, + + /// Connection url for the database. + #[clap(long, env = "DATABASE_URL")] + pub(crate) database_url: String, + + #[clap( + long, + default_value = "600", + env = "NODE_STATUS_API_MONITOR_REFRESH_INTERVAL" + )] + #[arg(value_parser = parse_duration)] + pub(crate) monitor_refresh_interval: Duration, + + #[clap( + long, + default_value = "600", + env = "NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL" + )] + #[arg(value_parser = parse_duration)] + pub(crate) testruns_refresh_interval: Duration, +} + +fn parse_duration(arg: &str) -> Result { + let seconds = arg.parse()?; + Ok(std::time::Duration::from_secs(seconds)) +} diff --git a/nym-node-status-api/src/db/mod.rs b/nym-node-status-api/src/db/mod.rs new file mode 100644 index 0000000000..86e25e9227 --- /dev/null +++ b/nym-node-status-api/src/db/mod.rs @@ -0,0 +1,35 @@ +use anyhow::{anyhow, Result}; +use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, ConnectOptions, SqlitePool}; +use std::str::FromStr; + +pub(crate) mod models; +pub(crate) mod queries; + +static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); + +pub(crate) type DbPool = SqlitePool; + +pub(crate) struct Storage { + pool: DbPool, +} + +impl Storage { + pub async fn init(connection_url: String) -> Result { + let connect_options = SqliteConnectOptions::from_str(&connection_url)? + .create_if_missing(true) + .disable_statement_logging(); + + let pool = sqlx::SqlitePool::connect_with(connect_options) + .await + .map_err(|err| anyhow!("Failed to connect to {}: {}", &connection_url, err))?; + + MIGRATOR.run(&pool).await?; + + Ok(Storage { pool }) + } + + /// Cloning pool is cheap, it's the same underlying set of connections + pub fn pool_owned(&self) -> DbPool { + self.pool.clone() + } +} diff --git a/nym-node-status-api/src/db/models.rs b/nym-node-status-api/src/db/models.rs new file mode 100644 index 0000000000..f704c1ed8a --- /dev/null +++ b/nym-node-status-api/src/db/models.rs @@ -0,0 +1,334 @@ +use crate::{ + http::{self, models::SummaryHistory}, + monitor::NumericalCheckedCast, +}; +use nym_node_requests::api::v1::node::models::NodeDescription; +use serde::{Deserialize, Serialize}; +use strum_macros::{EnumString, FromRepr}; +use utoipa::ToSchema; + +pub(crate) struct GatewayRecord { + pub(crate) identity_key: String, + pub(crate) bonded: bool, + pub(crate) blacklisted: bool, + pub(crate) self_described: Option, + pub(crate) explorer_pretty_bond: Option, + pub(crate) last_updated_utc: i64, + pub(crate) performance: u8, +} + +#[derive(Debug, Clone)] +pub(crate) struct GatewayDto { + pub(crate) gateway_identity_key: String, + pub(crate) bonded: bool, + pub(crate) blacklisted: bool, + pub(crate) performance: i64, + pub(crate) self_described: Option, + pub(crate) explorer_pretty_bond: Option, + pub(crate) last_probe_result: Option, + pub(crate) last_probe_log: Option, + pub(crate) last_testrun_utc: Option, + pub(crate) last_updated_utc: i64, + pub(crate) moniker: String, + pub(crate) security_contact: String, + pub(crate) details: String, + pub(crate) website: String, +} + +impl TryFrom for http::models::Gateway { + type Error = anyhow::Error; + + fn try_from(value: GatewayDto) -> Result { + // Instead of using routing_score_successes / routing_score_samples, we use the + // number of successful testruns in the last 24h. + let routing_score = 0f32; + let config_score = 0u32; + let last_updated_utc = + timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339(); + let last_testrun_utc = value + .last_testrun_utc + .and_then(|i| i.cast_checked().ok()) + .map(|t| timestamp_as_utc(t).to_rfc3339()); + + let self_described = value.self_described.clone().unwrap_or("null".to_string()); + let explorer_pretty_bond = value + .explorer_pretty_bond + .clone() + .unwrap_or("null".to_string()); + let last_probe_result = value + .last_probe_result + .clone() + .unwrap_or("null".to_string()); + let last_probe_log = value.last_probe_log.clone(); + + let self_described = serde_json::from_str(&self_described).unwrap_or(None); + let explorer_pretty_bond = serde_json::from_str(&explorer_pretty_bond).unwrap_or(None); + let last_probe_result = serde_json::from_str(&last_probe_result).unwrap_or(None); + + let bonded = value.bonded; + let blacklisted = value.blacklisted; + let performance = value.performance as u8; + + let description = NodeDescription { + moniker: value.moniker.clone(), + website: value.website.clone(), + security_contact: value.security_contact.clone(), + details: value.details.clone(), + }; + + Ok(http::models::Gateway { + gateway_identity_key: value.gateway_identity_key.clone(), + bonded, + blacklisted, + performance, + self_described, + explorer_pretty_bond, + description, + last_probe_result, + last_probe_log, + routing_score, + config_score, + last_testrun_utc, + last_updated_utc, + }) + } +} + +fn timestamp_as_utc(unix_timestamp: u64) -> chrono::DateTime { + let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(unix_timestamp); + d.into() +} + +pub(crate) struct MixnodeRecord { + pub(crate) mix_id: u32, + pub(crate) identity_key: String, + pub(crate) bonded: bool, + pub(crate) total_stake: i64, + pub(crate) host: String, + pub(crate) http_port: u16, + pub(crate) blacklisted: bool, + pub(crate) full_details: String, + pub(crate) self_described: Option, + pub(crate) last_updated_utc: i64, + pub(crate) is_dp_delegatee: bool, +} + +#[derive(Debug, Clone)] +pub(crate) struct MixnodeDto { + pub(crate) mix_id: i64, + pub(crate) bonded: bool, + pub(crate) blacklisted: bool, + pub(crate) is_dp_delegatee: bool, + pub(crate) total_stake: i64, + pub(crate) full_details: String, + pub(crate) self_described: Option, + pub(crate) last_updated_utc: i64, + pub(crate) moniker: String, + pub(crate) website: String, + pub(crate) security_contact: String, + pub(crate) details: String, +} + +impl TryFrom for http::models::Mixnode { + type Error = anyhow::Error; + + fn try_from(value: MixnodeDto) -> Result { + let mix_id = value.mix_id.cast_checked()?; + let full_details = value.full_details.clone(); + let full_details = serde_json::from_str(&full_details).unwrap_or(None); + + let self_described = value + .self_described + .clone() + .map(|v| serde_json::from_str(&v).unwrap_or(serde_json::Value::Null)); + + let last_updated_utc = + timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339(); + let blacklisted = value.blacklisted; + let is_dp_delegatee = value.is_dp_delegatee; + let moniker = value.moniker.clone(); + let website = value.website.clone(); + let security_contact = value.security_contact.clone(); + let details = value.details.clone(); + + Ok(http::models::Mixnode { + mix_id, + bonded: value.bonded, + blacklisted, + is_dp_delegatee, + total_stake: value.total_stake, + full_details, + description: NodeDescription { + moniker, + website, + security_contact, + details, + }, + self_described, + last_updated_utc, + }) + } +} + +#[allow(unused)] +#[derive(Debug, Clone)] +pub(crate) struct BondedStatusDto { + pub(crate) id: i64, + pub(crate) identity_key: String, + pub(crate) bonded: bool, +} + +#[allow(unused)] +#[derive(Debug, Clone, Default)] +pub(crate) struct SummaryDto { + pub(crate) key: String, + pub(crate) value_json: String, + pub(crate) last_updated_utc: i64, +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct SummaryHistoryDto { + #[allow(dead_code)] + pub id: i64, + pub date: String, + pub value_json: String, + pub timestamp_utc: i64, +} + +impl TryFrom for SummaryHistory { + type Error = anyhow::Error; + + fn try_from(value: SummaryHistoryDto) -> Result { + let value_json = serde_json::from_str(&value.value_json).unwrap_or_default(); + Ok(SummaryHistory { + value_json, + date: value.date.clone(), + timestamp_utc: timestamp_as_utc(value.timestamp_utc.cast_checked()?).to_rfc3339(), + }) + } +} + +pub(crate) const MIXNODES_BONDED_COUNT: &str = "mixnodes.bonded.count"; +pub(crate) const MIXNODES_BONDED_ACTIVE: &str = "mixnodes.bonded.active"; +pub(crate) const MIXNODES_BONDED_INACTIVE: &str = "mixnodes.bonded.inactive"; +pub(crate) const MIXNODES_BONDED_RESERVE: &str = "mixnodes.bonded.reserve"; +pub(crate) const MIXNODES_BLACKLISTED_COUNT: &str = "mixnodes.blacklisted.count"; + +pub(crate) const GATEWAYS_BONDED_COUNT: &str = "gateways.bonded.count"; +pub(crate) const GATEWAYS_EXPLORER_COUNT: &str = "gateways.explorer.count"; +pub(crate) const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count"; + +pub(crate) const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count"; +pub(crate) const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count"; + +// `utoipa`` goes crazy if you use module-qualified prefix as field type so we +// have to import it +use gateway::GatewaySummary; +use mixnode::MixnodeSummary; + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub(crate) struct NetworkSummary { + pub(crate) mixnodes: MixnodeSummary, + pub(crate) gateways: GatewaySummary, +} + +pub(crate) mod mixnode { + use super::*; + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct MixnodeSummary { + pub(crate) bonded: MixnodeSummaryBonded, + pub(crate) blacklisted: MixnodeSummaryBlacklisted, + pub(crate) historical: MixnodeSummaryHistorical, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct MixnodeSummaryBonded { + pub(crate) count: i32, + pub(crate) active: i32, + pub(crate) inactive: i32, + pub(crate) reserve: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct MixnodeSummaryBlacklisted { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct MixnodeSummaryHistorical { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } +} + +pub(crate) mod gateway { + use super::*; + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummary { + pub(crate) bonded: GatewaySummaryBonded, + pub(crate) blacklisted: GatewaySummaryBlacklisted, + pub(crate) historical: GatewaySummaryHistorical, + pub(crate) explorer: GatewaySummaryExplorer, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummaryExplorer { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummaryBonded { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummaryHistorical { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummaryBlacklisted { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } +} + +#[derive(Debug, Clone)] +pub struct TestRunDto { + pub id: i64, + pub gateway_id: i64, + pub status: i64, + pub timestamp_utc: i64, + pub ip_address: String, + pub log: String, +} + +#[derive(Debug, Clone, strum_macros::Display, EnumString, FromRepr, PartialEq)] +#[repr(u8)] +pub(crate) enum TestRunStatus { + Complete = 2, + InProgress = 1, + Pending = 0, +} + +#[derive(Debug, Clone)] +pub struct GatewayIdentityDto { + pub gateway_identity_key: String, + pub bonded: bool, +} + +#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros +#[derive(Debug, Clone)] +pub struct GatewayInfoDto { + pub id: i64, + pub gateway_identity_key: String, + pub self_described: Option, + pub explorer_pretty_bond: Option, +} diff --git a/nym-node-status-api/src/db/queries/gateways.rs b/nym-node-status-api/src/db/queries/gateways.rs new file mode 100644 index 0000000000..02b5d05dc3 --- /dev/null +++ b/nym-node-status-api/src/db/queries/gateways.rs @@ -0,0 +1,160 @@ +use crate::{ + db::{ + models::{BondedStatusDto, GatewayDto, GatewayRecord}, + DbPool, + }, + http::models::Gateway, +}; +use futures_util::TryStreamExt; +use nym_validator_client::models::DescribedGateway; +use tracing::error; + +pub(crate) async fn insert_gateways( + pool: &DbPool, + gateways: Vec, +) -> anyhow::Result<()> { + let mut db = pool.acquire().await?; + for record in gateways { + sqlx::query!( + "INSERT INTO gateways + (gateway_identity_key, bonded, blacklisted, + self_described, explorer_pretty_bond, + last_updated_utc, performance) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(gateway_identity_key) DO UPDATE SET + bonded=excluded.bonded, + blacklisted=excluded.blacklisted, + self_described=excluded.self_described, + explorer_pretty_bond=excluded.explorer_pretty_bond, + last_updated_utc=excluded.last_updated_utc, + performance = excluded.performance;", + record.identity_key, + record.bonded, + record.blacklisted, + record.self_described, + record.explorer_pretty_bond, + record.last_updated_utc, + record.performance + ) + .execute(&mut *db) + .await?; + } + + Ok(()) +} + +pub(crate) async fn write_blacklisted_gateways_to_db<'a, I>( + pool: &DbPool, + gateways: I, +) -> anyhow::Result<()> +where + I: Iterator, +{ + let mut conn = pool.acquire().await?; + for gateway_identity_key in gateways { + sqlx::query!( + "UPDATE gateways + SET blacklisted = true + WHERE gateway_identity_key = ?;", + gateway_identity_key, + ) + .execute(&mut *conn) + .await?; + } + + Ok(()) +} + +/// Ensure all gateways that are set as bonded, are still bonded +pub(crate) async fn ensure_gateways_still_bonded( + pool: &DbPool, + gateways: &[DescribedGateway], +) -> anyhow::Result { + let bonded_gateways_rows = get_all_bonded_gateways_row_ids_by_status(pool, true).await?; + let unbonded_gateways_rows = bonded_gateways_rows.iter().filter(|v| { + !gateways + .iter() + .any(|bonded| *bonded.bond.identity() == v.identity_key) + }); + + let recently_unbonded_gateways = unbonded_gateways_rows.to_owned().count(); + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let mut transaction = pool.begin().await?; + for row in unbonded_gateways_rows { + sqlx::query!( + "UPDATE gateways + SET bonded = ?, last_updated_utc = ? + WHERE id = ?;", + false, + last_updated_utc, + row.id, + ) + .execute(&mut *transaction) + .await?; + } + transaction.commit().await?; + + Ok(recently_unbonded_gateways) +} + +async fn get_all_bonded_gateways_row_ids_by_status( + pool: &DbPool, + status: bool, +) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + BondedStatusDto, + r#"SELECT + id as "id!", + gateway_identity_key as "identity_key!", + bonded as "bonded: bool" + FROM gateways + WHERE bonded = ?"#, + status, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + Ok(items) +} + +pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + GatewayDto, + r#"SELECT + gw.gateway_identity_key as "gateway_identity_key!", + gw.bonded as "bonded: bool", + gw.blacklisted as "blacklisted: bool", + gw.performance as "performance!", + gw.self_described as "self_described?", + gw.explorer_pretty_bond as "explorer_pretty_bond?", + gw.last_probe_result as "last_probe_result?", + gw.last_probe_log as "last_probe_log?", + gw.last_testrun_utc as "last_testrun_utc?", + gw.last_updated_utc as "last_updated_utc!", + COALESCE(gd.moniker, "NA") as "moniker!", + COALESCE(gd.website, "NA") as "website!", + COALESCE(gd.security_contact, "NA") as "security_contact!", + COALESCE(gd.details, "NA") as "details!" + FROM gateways gw + LEFT JOIN gateway_description gd + ON gw.gateway_identity_key = gd.gateway_identity_key + ORDER BY gw.gateway_identity_key"#, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + let items: Vec = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; + tracing::trace!("Fetched {} gateways from DB", items.len()); + Ok(items) +} diff --git a/nym-node-status-api/src/db/queries/misc.rs b/nym-node-status-api/src/db/queries/misc.rs new file mode 100644 index 0000000000..2aa6356051 --- /dev/null +++ b/nym-node-status-api/src/db/queries/misc.rs @@ -0,0 +1,88 @@ +use crate::db::{models::NetworkSummary, DbPool}; +use chrono::{DateTime, Utc}; + +/// take `last_updated` instead of calculating it so that `summary` matches +/// `daily_summary` +pub(crate) async fn insert_summaries( + pool: &DbPool, + summaries: &[(&str, &usize)], + summary: &NetworkSummary, + last_updated: DateTime, +) -> anyhow::Result<()> { + insert_summary(pool, summaries, last_updated).await?; + + insert_summary_history(pool, summary, last_updated).await?; + + Ok(()) +} + +async fn insert_summary( + pool: &DbPool, + summaries: &[(&str, &usize)], + last_updated: DateTime, +) -> anyhow::Result<()> { + let timestamp = last_updated.timestamp(); + let mut tx = pool.begin().await?; + + for (kind, value) in summaries { + let value = value.to_string(); + sqlx::query!( + "INSERT INTO summary + (key, value_json, last_updated_utc) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + value_json=excluded.value_json, + last_updated_utc=excluded.last_updated_utc;", + kind, + value, + timestamp + ) + .execute(&mut *tx) + .await + .map_err(|err| { + tracing::error!("Failed to insert data for {kind}: {err}, aborting transaction",); + err + })?; + } + + tx.commit().await?; + + Ok(()) +} + +/// For ``, `summary_history` is updated with fresh data on every +/// iteration. +/// +/// After UTC midnight, summary is inserted for `` and last entry for +/// `` stays there forever. +/// +/// This is not aggregate data, it's a set of latest data points +async fn insert_summary_history( + pool: &DbPool, + summary: &NetworkSummary, + last_updated: DateTime, +) -> anyhow::Result<()> { + let mut conn = pool.acquire().await?; + + let value_json = serde_json::to_string(&summary)?; + let timestamp = last_updated.timestamp(); + let now_rfc3339 = last_updated.to_rfc3339(); + // YYYY-MM-DD, without time + let date = &now_rfc3339[..10]; + + sqlx::query!( + "INSERT INTO summary_history + (date, timestamp_utc, value_json) + VALUES (?, ?, ?) + ON CONFLICT(date) DO UPDATE SET + timestamp_utc=excluded.timestamp_utc, + value_json=excluded.value_json;", + date, + timestamp, + value_json + ) + .execute(&mut *conn) + .await?; + + Ok(()) +} diff --git a/nym-node-status-api/src/db/queries/mixnodes.rs b/nym-node-status-api/src/db/queries/mixnodes.rs new file mode 100644 index 0000000000..58af9bd429 --- /dev/null +++ b/nym-node-status-api/src/db/queries/mixnodes.rs @@ -0,0 +1,177 @@ +use futures_util::TryStreamExt; +use nym_validator_client::models::MixNodeBondAnnotated; +use tracing::error; + +use crate::{ + db::{ + models::{BondedStatusDto, MixnodeDto, MixnodeRecord}, + DbPool, + }, + http::models::{DailyStats, Mixnode}, +}; + +pub(crate) async fn insert_mixnodes( + pool: &DbPool, + mixnodes: Vec, +) -> anyhow::Result<()> { + let mut conn = pool.acquire().await?; + + for record in mixnodes.iter() { + // https://www.sqlite.org/lang_upsert.html + sqlx::query!( + "INSERT INTO mixnodes + (mix_id, identity_key, bonded, total_stake, + host, http_api_port, blacklisted, full_details, + self_described, last_updated_utc, is_dp_delegatee) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(mix_id) DO UPDATE SET + bonded=excluded.bonded, + total_stake=excluded.total_stake, host=excluded.host, + http_api_port=excluded.http_api_port,blacklisted=excluded.blacklisted, + full_details=excluded.full_details,self_described=excluded.self_described, + last_updated_utc=excluded.last_updated_utc, + is_dp_delegatee = excluded.is_dp_delegatee;", + record.mix_id, + record.identity_key, + record.bonded, + record.total_stake, + record.host, + record.http_port, + record.blacklisted, + record.full_details, + record.self_described, + record.last_updated_utc, + record.is_dp_delegatee + ) + .execute(&mut *conn) + .await?; + } + + Ok(()) +} + +pub(crate) async fn get_all_mixnodes(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + MixnodeDto, + r#"SELECT + mn.mix_id as "mix_id!", + mn.bonded as "bonded: bool", + mn.blacklisted as "blacklisted: bool", + mn.is_dp_delegatee as "is_dp_delegatee: bool", + mn.total_stake as "total_stake!", + mn.full_details as "full_details!", + mn.self_described as "self_described", + mn.last_updated_utc as "last_updated_utc!", + COALESCE(md.moniker, "NA") as "moniker!", + COALESCE(md.website, "NA") as "website!", + COALESCE(md.security_contact, "NA") as "security_contact!", + COALESCE(md.details, "NA") as "details!" + FROM mixnodes mn + LEFT JOIN mixnode_description md ON mn.mix_id = md.mix_id + ORDER BY mn.mix_id"# + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + let items = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; + Ok(items) +} + +/// We fetch the latest 30 days of data as a subquery and then +/// return it in ascending order, so we don't break existing UI +pub(crate) async fn get_daily_stats(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + DailyStats, + r#" + SELECT + date_utc as "date_utc!", + packets_received as "total_packets_received!: i64", + packets_sent as "total_packets_sent!: i64", + packets_dropped as "total_packets_dropped!: i64", + total_stake as "total_stake!: i64" + FROM ( + SELECT + date_utc, + SUM(packets_received) as packets_received, + SUM(packets_sent) as packets_sent, + SUM(packets_dropped) as packets_dropped, + SUM(total_stake) as total_stake + FROM mixnode_daily_stats + GROUP BY date_utc + ORDER BY date_utc DESC + LIMIT 30 + ) + GROUP BY date_utc + ORDER BY date_utc + "# + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + Ok(items) +} + +/// Ensure all mixnodes that are set as bonded, are still bonded +pub(crate) async fn ensure_mixnodes_still_bonded( + pool: &DbPool, + mixnodes: &[MixNodeBondAnnotated], +) -> anyhow::Result { + let bonded_mixnodes_rows = get_all_bonded_mixnodes_row_ids_by_status(pool, true).await?; + let unbonded_mixnodes_rows = bonded_mixnodes_rows.iter().filter(|v| { + !mixnodes + .iter() + .any(|bonded| *bonded.mixnode_details.bond_information.identity() == v.identity_key) + }); + + let recently_unbonded_mixnodes = unbonded_mixnodes_rows.to_owned().count(); + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let mut transaction = pool.begin().await?; + for row in unbonded_mixnodes_rows { + sqlx::query!( + "UPDATE mixnodes + SET bonded = ?, last_updated_utc = ? + WHERE id = ?;", + false, + last_updated_utc, + row.id, + ) + .execute(&mut *transaction) + .await?; + } + transaction.commit().await?; + + Ok(recently_unbonded_mixnodes) +} + +async fn get_all_bonded_mixnodes_row_ids_by_status( + pool: &DbPool, + status: bool, +) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + BondedStatusDto, + r#"SELECT + id as "id!", + identity_key as "identity_key!", + bonded as "bonded: bool" + FROM mixnodes + WHERE bonded = ?"#, + status, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + Ok(items) +} diff --git a/nym-node-status-api/src/db/queries/mod.rs b/nym-node-status-api/src/db/queries/mod.rs new file mode 100644 index 0000000000..349e62e7c7 --- /dev/null +++ b/nym-node-status-api/src/db/queries/mod.rs @@ -0,0 +1,15 @@ +mod gateways; +mod misc; +mod mixnodes; +mod summary; +pub(crate) mod testruns; + +pub(crate) use gateways::{ + ensure_gateways_still_bonded, get_all_gateways, insert_gateways, + write_blacklisted_gateways_to_db, +}; +pub(crate) use misc::insert_summaries; +pub(crate) use mixnodes::{ + ensure_mixnodes_still_bonded, get_all_mixnodes, get_daily_stats, insert_mixnodes, +}; +pub(crate) use summary::{get_summary, get_summary_history}; diff --git a/nym-node-status-api/src/db/queries/summary.rs b/nym-node-status-api/src/db/queries/summary.rs new file mode 100644 index 0000000000..103712a9a4 --- /dev/null +++ b/nym-node-status-api/src/db/queries/summary.rs @@ -0,0 +1,209 @@ +use chrono::{DateTime, Utc}; +use futures_util::TryStreamExt; +use std::collections::HashMap; +use tracing::error; + +use crate::{ + db::{ + models::{ + gateway::{ + GatewaySummary, GatewaySummaryBlacklisted, GatewaySummaryBonded, + GatewaySummaryExplorer, GatewaySummaryHistorical, + }, + mixnode::{ + MixnodeSummary, MixnodeSummaryBlacklisted, MixnodeSummaryBonded, + MixnodeSummaryHistorical, + }, + NetworkSummary, SummaryDto, SummaryHistoryDto, + }, + DbPool, + }, + http::{ + error::{HttpError, HttpResult}, + models::SummaryHistory, + }, +}; + +pub(crate) async fn get_summary_history(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + SummaryHistoryDto, + r#"SELECT + id as "id!", + date as "date!", + timestamp_utc as "timestamp_utc!", + value_json as "value_json!" + FROM summary_history + ORDER BY date DESC + LIMIT 30"#, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + let items = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; + Ok(items) +} + +async fn get_summary_dto(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + Ok(sqlx::query_as!( + SummaryDto, + r#"SELECT + key as "key!", + value_json as "value_json!", + last_updated_utc as "last_updated_utc!" + FROM summary"# + ) + .fetch(&mut *conn) + .try_collect::>() + .await?) +} + +pub(crate) async fn get_summary(pool: &DbPool) -> HttpResult { + let items = get_summary_dto(pool).await.map_err(|err| { + tracing::error!("Couldn't get Summary from DB: {err}"); + HttpError::internal() + })?; + from_summary_dto(items).await +} + +async fn from_summary_dto(items: Vec) -> HttpResult { + const MIXNODES_BONDED_COUNT: &str = "mixnodes.bonded.count"; + const MIXNODES_BONDED_ACTIVE: &str = "mixnodes.bonded.active"; + const MIXNODES_BONDED_INACTIVE: &str = "mixnodes.bonded.inactive"; + const MIXNODES_BONDED_RESERVE: &str = "mixnodes.bonded.reserve"; + const MIXNODES_BLACKLISTED_COUNT: &str = "mixnodes.blacklisted.count"; + const GATEWAYS_BONDED_COUNT: &str = "gateways.bonded.count"; + const GATEWAYS_EXPLORER_COUNT: &str = "gateways.explorer.count"; + const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count"; + const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count"; + const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count"; + + // convert database rows into a map by key + let mut map = HashMap::new(); + for item in items { + map.insert(item.key.clone(), item); + } + + // check we have all the keys we are expecting, and build up a map of errors for missing one + let keys = [ + GATEWAYS_BONDED_COUNT, + GATEWAYS_EXPLORER_COUNT, + GATEWAYS_HISTORICAL_COUNT, + GATEWAYS_BLACKLISTED_COUNT, + MIXNODES_BLACKLISTED_COUNT, + MIXNODES_BONDED_ACTIVE, + MIXNODES_BONDED_COUNT, + MIXNODES_BONDED_INACTIVE, + MIXNODES_BONDED_RESERVE, + MIXNODES_HISTORICAL_COUNT, + ]; + + let mut errors: Vec<&str> = vec![]; + for key in keys { + if !map.contains_key(key) { + errors.push(key); + } + } + + // return an error if anything is missing, with a nice list + if !errors.is_empty() { + tracing::error!("Summary value missing: {}", errors.join(", ")); + return Err(HttpError::internal()); + } + + // strip the options and use default values (anything missing is trapped above) + let mixnodes_bonded_count: SummaryDto = + map.get(MIXNODES_BONDED_COUNT).cloned().unwrap_or_default(); + let mixnodes_bonded_active: SummaryDto = + map.get(MIXNODES_BONDED_ACTIVE).cloned().unwrap_or_default(); + let mixnodes_bonded_inactive: SummaryDto = map + .get(MIXNODES_BONDED_INACTIVE) + .cloned() + .unwrap_or_default(); + let mixnodes_bonded_reserve: SummaryDto = map + .get(MIXNODES_BONDED_RESERVE) + .cloned() + .unwrap_or_default(); + let mixnodes_blacklisted_count: SummaryDto = map + .get(MIXNODES_BLACKLISTED_COUNT) + .cloned() + .unwrap_or_default(); + let gateways_bonded_count: SummaryDto = + map.get(GATEWAYS_BONDED_COUNT).cloned().unwrap_or_default(); + let gateways_explorer_count: SummaryDto = map + .get(GATEWAYS_EXPLORER_COUNT) + .cloned() + .unwrap_or_default(); + let mixnodes_historical_count: SummaryDto = map + .get(MIXNODES_HISTORICAL_COUNT) + .cloned() + .unwrap_or_default(); + let gateways_historical_count: SummaryDto = map + .get(GATEWAYS_HISTORICAL_COUNT) + .cloned() + .unwrap_or_default(); + let gateways_blacklisted_count: SummaryDto = map + .get(GATEWAYS_BLACKLISTED_COUNT) + .cloned() + .unwrap_or_default(); + + Ok(NetworkSummary { + mixnodes: MixnodeSummary { + bonded: MixnodeSummaryBonded { + count: to_count_i32(&mixnodes_bonded_count), + active: to_count_i32(&mixnodes_bonded_active), + reserve: to_count_i32(&mixnodes_bonded_reserve), + inactive: to_count_i32(&mixnodes_bonded_inactive), + last_updated_utc: to_timestamp(&mixnodes_bonded_count), + }, + blacklisted: MixnodeSummaryBlacklisted { + count: to_count_i32(&mixnodes_blacklisted_count), + last_updated_utc: to_timestamp(&mixnodes_blacklisted_count), + }, + historical: MixnodeSummaryHistorical { + count: to_count_i32(&mixnodes_historical_count), + last_updated_utc: to_timestamp(&mixnodes_historical_count), + }, + }, + gateways: GatewaySummary { + bonded: GatewaySummaryBonded { + count: to_count_i32(&gateways_bonded_count), + last_updated_utc: to_timestamp(&gateways_bonded_count), + }, + blacklisted: GatewaySummaryBlacklisted { + count: to_count_i32(&gateways_blacklisted_count), + last_updated_utc: to_timestamp(&gateways_blacklisted_count), + }, + historical: GatewaySummaryHistorical { + count: to_count_i32(&gateways_historical_count), + last_updated_utc: to_timestamp(&gateways_historical_count), + }, + explorer: GatewaySummaryExplorer { + count: to_count_i32(&gateways_explorer_count), + last_updated_utc: to_timestamp(&gateways_explorer_count), + }, + }, + }) +} + +fn to_count_i32(value: &SummaryDto) -> i32 { + value.value_json.parse::().unwrap_or_default() +} + +fn to_timestamp(value: &SummaryDto) -> String { + timestamp_as_utc(value.last_updated_utc as u64).to_rfc3339() +} + +fn timestamp_as_utc(unix_timestamp: u64) -> DateTime { + let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(unix_timestamp); + d.into() +} diff --git a/nym-node-status-api/src/db/queries/testruns.rs b/nym-node-status-api/src/db/queries/testruns.rs new file mode 100644 index 0000000000..cf5b8a3bbf --- /dev/null +++ b/nym-node-status-api/src/db/queries/testruns.rs @@ -0,0 +1,126 @@ +use crate::http::models::TestrunAssignment; +use crate::{ + db::models::{TestRunDto, TestRunStatus}, + testruns::now_utc, +}; +use anyhow::Context; +use sqlx::{pool::PoolConnection, Sqlite}; + +pub(crate) async fn get_testrun_by_id( + conn: &mut PoolConnection, + testrun_id: i64, +) -> anyhow::Result { + sqlx::query_as!( + TestRunDto, + r#"SELECT + id as "id!", + gateway_id as "gateway_id!", + status as "status!", + timestamp_utc as "timestamp_utc!", + ip_address as "ip_address!", + log as "log!" + FROM testruns + WHERE id = ? + ORDER BY timestamp_utc"#, + testrun_id + ) + .fetch_one(conn.as_mut()) + .await + .context(format!("Couldn't retrieve testrun {testrun_id}")) +} + +pub(crate) async fn get_oldest_testrun_and_make_it_pending( + // TODO dz accept mut reference, repeat in all similar functions + conn: PoolConnection, +) -> anyhow::Result> { + let mut conn = conn; + let assignment = sqlx::query_as!( + TestrunAssignment, + r#"UPDATE testruns + SET status = ? + WHERE rowid = + ( + SELECT rowid + FROM testruns + WHERE status = ? + ORDER BY timestamp_utc asc + LIMIT 1 + ) + RETURNING + id as "testrun_id!", + gateway_id as "gateway_pk_id!" + "#, + TestRunStatus::InProgress as i64, + TestRunStatus::Pending as i64, + ) + .fetch_optional(&mut *conn) + .await?; + + Ok(assignment) +} + +pub(crate) async fn update_testrun_status( + conn: &mut PoolConnection, + testrun_id: i64, + status: TestRunStatus, +) -> anyhow::Result<()> { + let status = status as u32; + sqlx::query!( + "UPDATE testruns SET status = ? WHERE id = ?", + status, + testrun_id + ) + .execute(conn.as_mut()) + .await?; + + Ok(()) +} + +pub(crate) async fn update_gateway_last_probe_log( + conn: &mut PoolConnection, + gateway_pk: i64, + log: &str, +) -> anyhow::Result<()> { + sqlx::query!( + "UPDATE gateways SET last_probe_log = ? WHERE id = ?", + log, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} + +pub(crate) async fn update_gateway_last_probe_result( + conn: &mut PoolConnection, + gateway_pk: i64, + result: &str, +) -> anyhow::Result<()> { + sqlx::query!( + "UPDATE gateways SET last_probe_result = ? WHERE id = ?", + result, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} + +pub(crate) async fn update_gateway_score( + conn: &mut PoolConnection, + gateway_pk: i64, +) -> anyhow::Result<()> { + let now = now_utc().timestamp(); + sqlx::query!( + "UPDATE gateways SET last_testrun_utc = ?, last_updated_utc = ? WHERE id = ?", + now, + now, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} diff --git a/nym-node-status-api/src/http/api/gateways.rs b/nym-node-status-api/src/http/api/gateways.rs new file mode 100644 index 0000000000..c1f4073767 --- /dev/null +++ b/nym-node-status-api/src/http/api/gateways.rs @@ -0,0 +1,110 @@ +use axum::{ + extract::{Path, Query, State}, + Json, Router, +}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::http::{ + error::{HttpError, HttpResult}, + models::{Gateway, GatewaySkinny}, + state::AppState, + PagedResult, Pagination, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(gateways)) + .route("/skinny", axum::routing::get(gateways_skinny)) + .route("/:identity_key", axum::routing::get(get_gateway)) +} + +#[utoipa::path( + tag = "Gateways", + get, + params( + Pagination + ), + path = "/v2/gateways", + responses( + (status = 200, body = PagedGateway) + ) +)] +async fn gateways( + Query(pagination): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let res = state.cache().get_gateway_list(db).await; + + Ok(Json(PagedResult::paginate(pagination, res))) +} + +#[utoipa::path( + tag = "Gateways", + get, + params( + Pagination + ), + path = "/v2/gateways/skinny", + responses( + (status = 200, body = PagedGatewaySkinny) + ) +)] +async fn gateways_skinny( + Query(pagination): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let res = state.cache().get_gateway_list(db).await; + let res: Vec = res + .iter() + .filter(|g| g.bonded) + .map(|g| GatewaySkinny { + gateway_identity_key: g.gateway_identity_key.clone(), + self_described: g.self_described.clone(), + performance: g.performance, + explorer_pretty_bond: g.explorer_pretty_bond.clone(), + last_probe_result: g.last_probe_result.clone(), + last_testrun_utc: g.last_testrun_utc.clone(), + last_updated_utc: g.last_updated_utc.clone(), + routing_score: g.routing_score, + config_score: g.config_score, + }) + .collect(); + + Ok(Json(PagedResult::paginate(pagination, res))) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct IdentityKeyParam { + identity_key: String, +} + +#[utoipa::path( + tag = "Gateways", + get, + params( + IdentityKeyParam + ), + path = "/v2/gateways/{identity_key}", + responses( + (status = 200, body = Gateway) + ) +)] +async fn get_gateway( + Path(IdentityKeyParam { identity_key }): Path, + State(state): State, +) -> HttpResult> { + let db = state.db_pool(); + let res = state.cache().get_gateway_list(db).await; + + match res + .iter() + .find(|item| item.gateway_identity_key == identity_key) + { + Some(res) => Ok(Json(res.clone())), + None => Err(HttpError::invalid_input(identity_key)), + } +} diff --git a/nym-node-status-api/src/http/api/mixnodes.rs b/nym-node-status-api/src/http/api/mixnodes.rs new file mode 100644 index 0000000000..f42d0bf91c --- /dev/null +++ b/nym-node-status-api/src/http/api/mixnodes.rs @@ -0,0 +1,91 @@ +use axum::{ + extract::{Path, Query, State}, + Json, Router, +}; +use serde::Deserialize; +use tracing::instrument; +use utoipa::IntoParams; + +use crate::http::{ + error::{HttpError, HttpResult}, + models::{DailyStats, Mixnode}, + state::AppState, + PagedResult, Pagination, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(mixnodes)) + .route("/:mix_id", axum::routing::get(get_mixnodes)) + .route("/stats", axum::routing::get(get_stats)) +} + +#[utoipa::path( + tag = "Mixnodes", + get, + params( + Pagination + ), + path = "/v2/mixnodes", + responses( + (status = 200, body = PagedMixnode) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all, fields(page=pagination.page, size=pagination.size))] +async fn mixnodes( + Query(pagination): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let res = state.cache().get_mixnodes_list(db).await; + + Ok(Json(PagedResult::paginate(pagination, res))) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct MixIdParam { + mix_id: String, +} + +#[utoipa::path( + tag = "Mixnodes", + get, + params( + MixIdParam + ), + path = "/v2/mixnodes/{mix_id}", + responses( + (status = 200, body = Mixnode) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all, fields(mix_id = mix_id))] +async fn get_mixnodes( + Path(MixIdParam { mix_id }): Path, + State(state): State, +) -> HttpResult> { + match mix_id.parse::() { + Ok(parsed_mix_id) => { + let res = state.cache().get_mixnodes_list(state.db_pool()).await; + + match res.iter().find(|item| item.mix_id == parsed_mix_id) { + Some(res) => Ok(Json(res.clone())), + None => Err(HttpError::invalid_input(mix_id)), + } + } + Err(_e) => Err(HttpError::invalid_input(mix_id)), + } +} + +#[utoipa::path( + tag = "Mixnodes", + get, + path = "/v2/mixnodes/stats", + responses( + (status = 200, body = Vec) + ) +)] +async fn get_stats(State(state): State) -> HttpResult>> { + let stats = state.cache().get_mixnode_stats(state.db_pool()).await; + Ok(Json(stats)) +} diff --git a/nym-node-status-api/src/http/api/mod.rs b/nym-node-status-api/src/http/api/mod.rs new file mode 100644 index 0000000000..ed24fa80f5 --- /dev/null +++ b/nym-node-status-api/src/http/api/mod.rs @@ -0,0 +1,88 @@ +use anyhow::anyhow; +use axum::{response::Redirect, Router}; +use tokio::net::ToSocketAddrs; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +use crate::http::{server::HttpServer, state::AppState}; + +pub(crate) mod gateways; +pub(crate) mod mixnodes; +pub(crate) mod services; +pub(crate) mod summary; +pub(crate) mod testruns; + +pub(crate) struct RouterBuilder { + unfinished_router: Router, +} + +impl RouterBuilder { + pub(crate) fn with_default_routes() -> Self { + let router = Router::new() + .merge( + SwaggerUi::new("/swagger") + .url("/api-docs/openapi.json", super::api_docs::ApiDoc::openapi()), + ) + .route( + "/", + axum::routing::get(|| async { Redirect::permanent("/swagger") }), + ) + .nest( + "/v2", + Router::new() + .nest("/gateways", gateways::routes()) + .nest("/mixnodes", mixnodes::routes()) + .nest("/services", services::routes()) + .nest("/summary", summary::routes()), + ) + .nest( + "/internal", + Router::new().nest("/testruns", testruns::routes()), + ); + + Self { + unfinished_router: router, + } + } + + pub(crate) fn with_state(self, state: AppState) -> RouterWithState { + RouterWithState { + router: self.finalize_routes().with_state(state), + } + } + + fn finalize_routes(self) -> Router { + // layers added later wrap earlier layers + self.unfinished_router + // CORS layer needs to wrap other API layers + .layer(setup_cors()) + // logger should be outermost layer + .layer(TraceLayer::new_for_http()) + } +} + +pub(crate) struct RouterWithState { + router: Router, +} + +impl RouterWithState { + pub(crate) async fn build_server( + self, + bind_address: A, + ) -> anyhow::Result { + tokio::net::TcpListener::bind(bind_address) + .await + .map(|listener| HttpServer::new(self.router, listener)) + .map_err(|err| anyhow!("Couldn't bind to address due to {}", err)) + } +} + +fn setup_cors() -> CorsLayer { + use axum::http::Method; + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods([Method::POST, Method::GET, Method::PATCH, Method::OPTIONS]) + .allow_headers(tower_http::cors::Any) + .allow_credentials(false) +} diff --git a/nym-node-status-api/src/http/api/services/json_path.rs b/nym-node-status-api/src/http/api/services/json_path.rs new file mode 100644 index 0000000000..caefc6489d --- /dev/null +++ b/nym-node-status-api/src/http/api/services/json_path.rs @@ -0,0 +1,58 @@ +use serde_json_path::JsonPath; + +use crate::http::models::Gateway; + +pub(super) struct ParseJsonPaths { + pub(super) path_ip_address: JsonPath, + pub(super) path_hostname: JsonPath, + pub(super) path_service_provider_client_id: JsonPath, +} + +impl ParseJsonPaths { + pub fn new() -> Result { + Ok(ParseJsonPaths { + path_ip_address: JsonPath::parse("$.host_information.ip_address[0]")?, + path_hostname: JsonPath::parse("$.host_information.hostname")?, + path_service_provider_client_id: JsonPath::parse("$.network_requester.address")?, + }) + } +} + +pub(super) struct ParsedDetails { + pub(super) ip_address: Option, + pub(super) hostname: Option, + pub(super) service_provider_client_id: Option, +} + +impl ParsedDetails { + fn get_string_from_json_path( + value: &Option, + path: &JsonPath, + ) -> Option { + match value { + Some(value) => path + .query(value) + .exactly_one() + .map(|v2| v2.as_str().map(|v3| v3.to_string())) + .ok() + .flatten(), + None => None, + } + } + pub fn new(paths: &ParseJsonPaths, g: &Gateway) -> ParsedDetails { + ParsedDetails { + hostname: ParsedDetails::get_string_from_json_path( + &g.self_described, + &paths.path_hostname, + ), + ip_address: ParsedDetails::get_string_from_json_path( + &g.self_described, + &paths.path_ip_address, + ), + service_provider_client_id: ParsedDetails::get_string_from_json_path( + &g.self_described, + &paths.path_service_provider_client_id, + ), + } + } +} diff --git a/nym-node-status-api/src/http/api/services/mod.rs b/nym-node-status-api/src/http/api/services/mod.rs new file mode 100644 index 0000000000..5650684c43 --- /dev/null +++ b/nym-node-status-api/src/http/api/services/mod.rs @@ -0,0 +1,134 @@ +use crate::http::{ + error::{HttpError, HttpResult}, + models::Service, + state::AppState, + PagedResult, Pagination, +}; +use axum::{ + extract::{Query, State}, + Json, Router, +}; +use json_path::{ParseJsonPaths, ParsedDetails}; +use tracing::instrument; + +mod json_path; + +pub(crate) fn routes() -> Router { + Router::new().route("/", axum::routing::get(mixnodes)) +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub(crate) struct ServicesQueryParams { + size: Option, + page: Option, + wss: Option, + hostname: Option, + entry: Option, +} + +#[utoipa::path( + tag = "Services", + get, + params( + ServicesQueryParams, + ), + path = "/v2/services", + responses( + (status = 200, body = PagedService) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip(state))] +async fn mixnodes( + Query(ServicesQueryParams { + size, + page, + wss, + hostname, + entry, + }): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let cache = state.cache(); + + let show_only_wss = wss.unwrap_or(false); + let show_only_with_hostname = hostname.unwrap_or(false); + let show_entry_gateways_only = entry.unwrap_or(false); + + let paths = ParseJsonPaths::new().map_err(|e| { + tracing::error!("Invalidly configured ParseJsonPaths: {e}"); + HttpError::internal() + })?; + let res = cache.get_gateway_list(db).await; + let res: Vec = res + .iter() + .map(|g| { + let details = ParsedDetails::new(&paths, g); + + let s = Service { + gateway_identity_key: g.gateway_identity_key.clone(), + ip_address: details.ip_address, + service_provider_client_id: details.service_provider_client_id, + hostname: details.hostname, + last_successful_ping_utc: g.last_testrun_utc.clone(), + last_updated_utc: g.last_updated_utc.clone(), + // routing_score: g.routing_score, + routing_score: 1f32, + mixnet_websockets: g + .self_described + .clone() + .and_then(|s| s.get("mixnet_websockets").cloned()), + }; + + let f = ServiceFilter::new(&s); + + (s, f) + }) + .filter(|(_, f)| { + let mut keep = f.has_network_requester_sp; + + if show_entry_gateways_only { + keep = true; + } + + if show_only_wss { + keep &= f.has_wss; + } + if show_only_with_hostname { + keep &= f.has_hostname; + } + + keep + }) + .map(|(s, _)| s) + .collect(); + + Ok(Json(PagedResult::paginate(Pagination { size, page }, res))) +} + +struct ServiceFilter { + has_wss: bool, + has_network_requester_sp: bool, + has_hostname: bool, +} + +impl ServiceFilter { + fn new(s: &Service) -> Self { + let has_wss = match &s.mixnet_websockets { + Some(v) => v.get("wss_port").map(|v2| !v2.is_null()).unwrap_or(false), + None => false, + }; + let has_hostname = s.hostname.is_some(); + let has_network_requester_sp = match &s.service_provider_client_id { + Some(v) => !v.is_empty(), + None => false, + }; + + ServiceFilter { + has_wss, + has_hostname, + has_network_requester_sp, + } + } +} diff --git a/nym-node-status-api/src/http/api/summary.rs b/nym-node-status-api/src/http/api/summary.rs new file mode 100644 index 0000000000..729141509c --- /dev/null +++ b/nym-node-status-api/src/http/api/summary.rs @@ -0,0 +1,43 @@ +use axum::{extract::State, Json, Router}; +use tracing::instrument; + +use crate::{ + db::models::NetworkSummary, + http::{error::HttpResult, models::SummaryHistory, state::AppState}, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(summary)) + .route("/history", axum::routing::get(summary_history)) +} + +#[utoipa::path( + tag = "Summary", + get, + path = "/v2/summary", + responses( + (status = 200, body = NetworkSummary) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all)] +async fn summary(State(state): State) -> HttpResult> { + crate::db::queries::get_summary(state.db_pool()) + .await + .map(Json) +} + +#[utoipa::path( + tag = "Summary", + get, + path = "/v2/summary/history", + responses( + (status = 200, body = Vec) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all)] +async fn summary_history(State(state): State) -> HttpResult>> { + Ok(Json( + state.cache().get_summary_history(state.db_pool()).await, + )) +} diff --git a/nym-node-status-api/src/http/api/testruns.rs b/nym-node-status-api/src/http/api/testruns.rs new file mode 100644 index 0000000000..4bbc4d6295 --- /dev/null +++ b/nym-node-status-api/src/http/api/testruns.rs @@ -0,0 +1,116 @@ +use axum::Json; +use axum::{ + extract::{Path, State}, + Router, +}; +use reqwest::StatusCode; + +use crate::db::models::TestRunStatus; +use crate::db::queries; +use crate::{ + db, + http::{ + error::{HttpError, HttpResult}, + models::TestrunAssignment, + state::AppState, + }, +}; + +// TODO dz consider adding endpoint to trigger testrun scan for a given gateway_id +// like in H< src/http/testruns.rs + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(request_testrun)) + .route("/:testrun_id", axum::routing::post(submit_testrun)) +} + +#[tracing::instrument(level = "debug", skip_all)] +async fn request_testrun(State(state): State) -> HttpResult> { + // TODO dz log agent's key + // TODO dz log agent's network probe version + tracing::debug!("Agent X requested testrun"); + + let db = state.db_pool(); + let conn = db + .acquire() + .await + .map_err(HttpError::internal_with_logging)?; + + return match db::queries::testruns::get_oldest_testrun_and_make_it_pending(conn).await { + Ok(res) => { + if let Some(testrun) = res { + // TODO dz consider adding a column to testruns table with agent's public key + tracing::debug!( + "🏃‍ Assigned testrun row_id {} to agent X", + &testrun.testrun_id + ); + Ok(Json(testrun)) + } else { + Err(HttpError::not_found("No testruns available")) + } + } + Err(err) => Err(HttpError::internal_with_logging(err)), + }; +} + +// TODO dz accept testrun_id as query parameter +#[tracing::instrument(level = "debug", skip_all)] +async fn submit_testrun( + Path(testrun_id): Path, + State(state): State, + body: String, +) -> HttpResult { + tracing::debug!( + "Agent submitted testrun {}. Total length: {}", + testrun_id, + body.len(), + ); + // TODO dz store testrun results + + let db = state.db_pool(); + let mut conn = db + .acquire() + .await + .map_err(HttpError::internal_with_logging)?; + + let testrun = queries::testruns::get_testrun_by_id(&mut conn, testrun_id) + .await + .map_err(|e| { + tracing::error!("{e}"); + HttpError::not_found(testrun_id) + })?; + // TODO dz this should be part of a single transaction: commit after everything is done + queries::testruns::update_testrun_status(&mut conn, testrun_id, TestRunStatus::Complete) + .await + .map_err(HttpError::internal_with_logging)?; + queries::testruns::update_gateway_last_probe_log(&mut conn, testrun.gateway_id, &body) + .await + .map_err(HttpError::internal_with_logging)?; + let result = get_result_from_log(&body); + queries::testruns::update_gateway_last_probe_result(&mut conn, testrun.gateway_id, &result) + .await + .map_err(HttpError::internal_with_logging)?; + queries::testruns::update_gateway_score(&mut conn, testrun.gateway_id) + .await + .map_err(HttpError::internal_with_logging)?; + // TODO dz log gw identity key + + tracing::info!( + "✅ Testrun row_id {} for gateway {} complete", + testrun.id, + testrun.gateway_id + ); + + Ok(StatusCode::CREATED) +} + +fn get_result_from_log(log: &str) -> String { + let re = regex::Regex::new(r"\n\{\s").unwrap(); + let result: Vec<_> = re.splitn(log, 2).collect(); + if result.len() == 2 { + let res = format!("{} {}", "{", result[1]).to_string(); + return res; + } + "".to_string() +} diff --git a/nym-node-status-api/src/http/api_docs.rs b/nym-node-status-api/src/http/api_docs.rs new file mode 100644 index 0000000000..9ac5238fc4 --- /dev/null +++ b/nym-node-status-api/src/http/api_docs.rs @@ -0,0 +1,15 @@ +use crate::http::{Gateway, GatewaySkinny, Mixnode, Service}; +use utoipa::OpenApi; +use utoipauto::utoipauto; + +// manually import external structs which are behind feature flags because they +// can't be automatically discovered +// https://github.com/ProbablyClem/utoipauto/issues/13#issuecomment-1974911829 +#[utoipauto(paths = "./nym-node-status-api/src")] +#[derive(OpenApi)] +#[openapi( + info(title = "Node Status API"), + tags(), + components(schemas(nym_node_requests::api::v1::node::models::NodeDescription,)) +)] +pub(super) struct ApiDoc; diff --git a/nym-node-status-api/src/http/error.rs b/nym-node-status-api/src/http/error.rs new file mode 100644 index 0000000000..81a61e2d3e --- /dev/null +++ b/nym-node-status-api/src/http/error.rs @@ -0,0 +1,42 @@ +use std::fmt::Display; + +pub(crate) type HttpResult = Result; + +pub(crate) struct HttpError { + message: String, + status: axum::http::StatusCode, +} + +impl HttpError { + pub(crate) fn invalid_input(message: String) -> Self { + Self { + message, + status: axum::http::StatusCode::BAD_REQUEST, + } + } + + pub(crate) fn internal_with_logging(msg: impl Display) -> Self { + tracing::error!("{}", msg.to_string()); + Self::internal() + } + + pub(crate) fn internal() -> Self { + Self { + message: serde_json::json!({"message": "Internal server error"}).to_string(), + status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, + } + } + + pub(crate) fn not_found(msg: impl Display) -> Self { + Self { + message: serde_json::json!({"message": msg.to_string()}).to_string(), + status: axum::http::StatusCode::NOT_FOUND, + } + } +} + +impl axum::response::IntoResponse for HttpError { + fn into_response(self) -> axum::response::Response { + (self.status, self.message).into_response() + } +} diff --git a/nym-node-status-api/src/http/mod.rs b/nym-node-status-api/src/http/mod.rs new file mode 100644 index 0000000000..1cc317337f --- /dev/null +++ b/nym-node-status-api/src/http/mod.rs @@ -0,0 +1,71 @@ +use models::{Gateway, GatewaySkinny, Mixnode, Service}; + +pub(crate) mod api; +pub(crate) mod api_docs; +pub(crate) mod error; +pub(crate) mod models; +pub(crate) mod server; +pub(crate) mod state; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)] +// exclude generic from auto-discovery +#[utoipauto::utoipa_ignore] +// https://docs.rs/utoipa/latest/utoipa/derive.ToSchema.html#generic-schemas-with-aliases +// Generic structs can only be included via aliases, not directly, because they +// it would cause an error in generated Swagger docs. +// Instead, you have to manually monomorphize each generic struct that +// you wish to document +#[aliases( + PagedGateway = PagedResult, + PagedGatewaySkinny = PagedResult, + PagedMixnode = PagedResult, + PagedService = PagedResult, +)] +pub struct PagedResult { + pub page: usize, + pub size: usize, + pub total: usize, + pub items: Vec, +} + +impl PagedResult { + pub fn paginate(pagination: Pagination, res: Vec) -> Self { + let total = res.len(); + let (size, mut page) = pagination.intoto_inner_values(); + + if page * size > total { + page = total / size; + } + + let chunks: Vec<&[T]> = res.chunks(size).collect(); + + PagedResult { + page, + size, + total, + items: chunks.get(page).cloned().unwrap_or_default().into(), + } + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub(crate) struct Pagination { + size: Option, + page: Option, +} + +impl Pagination { + // unwrap stored values or use predefined defaults + pub(crate) fn intoto_inner_values(self) -> (usize, usize) { + const SIZE_DEFAULT: usize = 10; + const SIZE_MAX: usize = 200; + + const PAGE_DEFAULT: usize = 0; + + ( + self.size.unwrap_or(SIZE_DEFAULT).min(SIZE_MAX), + self.page.unwrap_or(PAGE_DEFAULT), + ) + } +} diff --git a/nym-node-status-api/src/http/models.rs b/nym-node-status-api/src/http/models.rs new file mode 100644 index 0000000000..315bf085dc --- /dev/null +++ b/nym-node-status-api/src/http/models.rs @@ -0,0 +1,86 @@ +use crate::db::models::TestRunDto; +use nym_node_requests::api::v1::node::models::NodeDescription; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +pub(crate) use nym_common_models::ns_api::TestrunAssignment; + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct Gateway { + pub gateway_identity_key: String, + pub bonded: bool, + pub blacklisted: bool, + pub performance: u8, + pub self_described: Option, + pub explorer_pretty_bond: Option, + pub description: NodeDescription, + pub last_probe_result: Option, + pub last_probe_log: Option, + pub last_testrun_utc: Option, + pub last_updated_utc: String, + pub routing_score: f32, + pub config_score: u32, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct GatewaySkinny { + pub gateway_identity_key: String, + pub self_described: Option, + pub explorer_pretty_bond: Option, + pub last_probe_result: Option, + pub last_testrun_utc: Option, + pub last_updated_utc: String, + pub routing_score: f32, + pub config_score: u32, + pub performance: u8, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct Mixnode { + pub mix_id: u32, + pub bonded: bool, + pub blacklisted: bool, + pub is_dp_delegatee: bool, + pub total_stake: i64, + pub full_details: Option, + pub self_described: Option, + pub description: NodeDescription, + pub last_updated_utc: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct DailyStats { + pub date_utc: String, + pub total_packets_received: i64, + pub total_packets_sent: i64, + pub total_packets_dropped: i64, + pub total_stake: i64, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct Service { + pub gateway_identity_key: String, + pub last_updated_utc: String, + pub routing_score: f32, + pub service_provider_client_id: Option, + pub ip_address: Option, + pub hostname: Option, + pub mixnet_websockets: Option, + pub last_successful_ping_utc: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub(crate) struct SummaryHistory { + pub date: String, + pub value_json: serde_json::Value, + pub timestamp_utc: String, +} + +impl From for TestrunAssignment { + fn from(value: TestRunDto) -> Self { + Self { + gateway_pk_id: value.gateway_id, + testrun_id: value.id, + } + } +} diff --git a/nym-node-status-api/src/http/server.rs b/nym-node-status-api/src/http/server.rs new file mode 100644 index 0000000000..17d5d64ab0 --- /dev/null +++ b/nym-node-status-api/src/http/server.rs @@ -0,0 +1,93 @@ +use axum::Router; +use core::net::SocketAddr; +use tokio::{net::TcpListener, task::JoinHandle}; +use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; + +use crate::{ + db::DbPool, + http::{api::RouterBuilder, state::AppState}, +}; + +/// Return handles that allow for graceful shutdown of server + awaiting its +/// background tokio task +pub(crate) async fn start_http_api( + db_pool: DbPool, + http_port: u16, + nym_http_cache_ttl: u64, +) -> anyhow::Result { + let router_builder = RouterBuilder::with_default_routes(); + + let state = AppState::new(db_pool, nym_http_cache_ttl); + let router = router_builder.with_state(state); + + // TODO dz do we need this to be configurable? + let bind_addr = format!("0.0.0.0:{}", http_port); + tracing::info!("Binding server to {bind_addr}"); + let server = router.build_server(bind_addr).await?; + + Ok(start_server(server)) +} + +fn start_server(server: HttpServer) -> ShutdownHandles { + // one copy is stored to trigger a graceful shutdown later + let shutdown_button = CancellationToken::new(); + // other copy is given to server to listen for a shutdown + let shutdown_receiver = shutdown_button.clone(); + let shutdown_receiver = shutdown_receiver.cancelled_owned(); + + let server_handle = tokio::spawn(async move { server.run(shutdown_receiver).await }); + + ShutdownHandles { + server_handle, + shutdown_button, + } +} + +pub(crate) struct ShutdownHandles { + server_handle: JoinHandle>, + shutdown_button: CancellationToken, +} + +impl ShutdownHandles { + /// Send graceful shutdown signal to server and wait for server task to complete + pub(crate) async fn shutdown(self) -> anyhow::Result<()> { + self.shutdown_button.cancel(); + + match self.server_handle.await { + Ok(Ok(_)) => { + tracing::info!("HTTP server shut down without errors"); + } + Ok(Err(err)) => { + tracing::error!("HTTP server terminated with: {err}"); + anyhow::bail!(err) + } + Err(err) => { + tracing::error!("Server task panicked: {err}"); + } + }; + + Ok(()) + } +} + +pub(crate) struct HttpServer { + router: Router, + listener: TcpListener, +} + +impl HttpServer { + pub(crate) fn new(router: Router, listener: TcpListener) -> Self { + Self { router, listener } + } + + pub(crate) async fn run(self, receiver: WaitForCancellationFutureOwned) -> std::io::Result<()> { + // 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::(), + ) + .with_graceful_shutdown(receiver) + .await + } +} diff --git a/nym-node-status-api/src/http/state.rs b/nym-node-status-api/src/http/state.rs new file mode 100644 index 0000000000..6bccea39a1 --- /dev/null +++ b/nym-node-status-api/src/http/state.rs @@ -0,0 +1,223 @@ +use std::{sync::Arc, time::Duration}; + +use moka::{future::Cache, Entry}; +use tokio::sync::RwLock; + +use crate::{ + db::DbPool, + http::models::{DailyStats, Gateway, Mixnode, SummaryHistory}, +}; + +#[derive(Debug, Clone)] +pub(crate) struct AppState { + db_pool: DbPool, + cache: HttpCache, +} + +impl AppState { + pub(crate) fn new(db_pool: DbPool, cache_ttl: u64) -> Self { + Self { + db_pool, + cache: HttpCache::new(cache_ttl), + } + } + + pub(crate) fn db_pool(&self) -> &DbPool { + &self.db_pool + } + + pub(crate) fn cache(&self) -> &HttpCache { + &self.cache + } +} + +static GATEWAYS_LIST_KEY: &str = "gateways"; +static MIXNODES_LIST_KEY: &str = "mixnodes"; +static MIXSTATS_LIST_KEY: &str = "mixstats"; +static SUMMARY_HISTORY_LIST_KEY: &str = "summary-history"; + +#[derive(Debug, Clone)] +pub(crate) struct HttpCache { + gateways: Cache>>>, + mixnodes: Cache>>>, + mixstats: Cache>>>, + history: Cache>>>, +} + +impl HttpCache { + pub fn new(ttl_seconds: u64) -> Self { + HttpCache { + gateways: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + mixnodes: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + mixstats: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + history: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + } + } + + pub async fn upsert_gateway_list( + &self, + new_gateway_list: Vec, + ) -> Entry>>> { + self.gateways + .entry_by_ref(GATEWAYS_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = new_gateway_list; + v.clone() + } else { + Arc::new(RwLock::new(new_gateway_list)) + } + }) + .await + } + + pub async fn get_gateway_list(&self, db: &DbPool) -> Vec { + match self.gateways.get(GATEWAYS_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.clone() + } + None => { + // the key is missing so populate it + tracing::warn!("No gateways in cache, refreshing cache from DB..."); + + let gateways = crate::db::queries::get_all_gateways(db) + .await + .unwrap_or_default(); + self.upsert_gateway_list(gateways.clone()).await; + + if gateways.is_empty() { + tracing::warn!("Database contains 0 gateways"); + } + + gateways + } + } + } + + pub async fn upsert_mixnode_list( + &self, + new_mixnode_list: Vec, + ) -> Entry>>> { + self.mixnodes + .entry_by_ref(MIXNODES_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = new_mixnode_list; + v.clone() + } else { + Arc::new(RwLock::new(new_mixnode_list)) + } + }) + .await + } + + pub async fn get_mixnodes_list(&self, db: &DbPool) -> Vec { + match self.mixnodes.get(MIXNODES_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.clone() + } + None => { + tracing::warn!("No mixnodes in cache, refreshing cache from DB..."); + + let mixnodes = crate::db::queries::get_all_mixnodes(db) + .await + .unwrap_or_default(); + self.upsert_mixnode_list(mixnodes.clone()).await; + + if mixnodes.is_empty() { + tracing::warn!("Database contains 0 mixnodes"); + } + + mixnodes + } + } + } + + pub async fn upsert_mixnode_stats( + &self, + mixnode_stats: Vec, + ) -> Entry>>> { + self.mixstats + .entry_by_ref(MIXSTATS_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = mixnode_stats; + v.clone() + } else { + Arc::new(RwLock::new(mixnode_stats)) + } + }) + .await + } + + pub async fn get_mixnode_stats(&self, db: &DbPool) -> Vec { + match self.mixstats.get(MIXSTATS_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.to_vec() + } + None => { + let mixnode_stats = crate::db::queries::get_daily_stats(db) + .await + .unwrap_or_default(); + self.upsert_mixnode_stats(mixnode_stats.clone()).await; + mixnode_stats + } + } + } + + pub async fn get_summary_history(&self, db: &DbPool) -> Vec { + match self.history.get(SUMMARY_HISTORY_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.to_vec() + } + None => { + let summary_history = crate::db::queries::get_summary_history(db) + .await + .unwrap_or(vec![]); + self.upsert_summary_history(summary_history.clone()).await; + summary_history + } + } + } + + pub async fn upsert_summary_history( + &self, + summary_history: Vec, + ) -> Entry>>> { + self.history + .entry_by_ref(SUMMARY_HISTORY_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = summary_history; + v.clone() + } else { + Arc::new(RwLock::new(summary_history)) + } + }) + .await + } +} diff --git a/nym-node-status-api/src/logging.rs b/nym-node-status-api/src/logging.rs new file mode 100644 index 0000000000..6bdad3bae5 --- /dev/null +++ b/nym-node-status-api/src/logging.rs @@ -0,0 +1,69 @@ +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{filter::Directive, EnvFilter}; + +// TODO dz you can get the tracing-subscriber via basic-tracing feature on nym-bin-common +pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> { + fn directive_checked(directive: impl Into) -> anyhow::Result { + directive.into().parse().map_err(From::from) + } + + let log_builder = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .compact() + // Display source code file paths + .with_file(true) + // Display source code line numbers + .with_line_number(true) + .with_thread_ids(true) + // Don't display the event's target (module path) + .with_target(false); + + let mut filter = EnvFilter::builder() + // if RUST_LOG isn't set, set default level + .with_default_directive(LevelFilter::DEBUG.into()) + .from_env_lossy(); + + // these crates are more granularly filtered + let warn_crates = [ + "reqwest", + "rustls", + "hyper", + "sqlx", + "h2", + "tendermint_rpc", + "tower_http", + "axum", + ]; + for crate_name in warn_crates { + filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?); + } + + let log_level_hint = filter.max_level_hint(); + + // debug or higher granularity (e.g. trace) + let debug_or_higher = std::cmp::max( + log_level_hint.unwrap_or(LevelFilter::DEBUG), + LevelFilter::DEBUG, + ); + filter = filter.add_directive(directive_checked(format!( + "nym_bin_common={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_explorer_client={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_network_defaults={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_validator_client={}", + debug_or_higher + ))?); + + log_builder.with_env_filter(filter).init(); + tracing::info!("Log level: {:?}", log_level_hint); + + Ok(()) +} diff --git a/nym-node-status-api/src/main.rs b/nym-node-status-api/src/main.rs new file mode 100644 index 0000000000..b2c68b391b --- /dev/null +++ b/nym-node-status-api/src/main.rs @@ -0,0 +1,53 @@ +use clap::Parser; +use nym_task::signal::wait_for_signal; + +mod cli; +mod db; +mod http; +mod logging; +mod monitor; +mod testruns; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + logging::setup_tracing_logger()?; + + let args = cli::Cli::parse(); + + let connection_url = args.database_url.clone(); + tracing::debug!("Using config:\n{:#?}", args); + + let storage = db::Storage::init(connection_url).await?; + let db_pool = storage.pool_owned(); + let args_clone = args.clone(); + tokio::spawn(async move { + monitor::spawn_in_background( + db_pool, + args_clone.explorer_client_timeout, + args_clone.nym_api_client_timeout, + &args_clone.nyxd_addr, + args_clone.monitor_refresh_interval, + ) + .await; + tracing::info!("Started monitor task"); + }); + testruns::spawn(storage.pool_owned(), args.testruns_refresh_interval).await; + + let shutdown_handles = http::server::start_http_api( + storage.pool_owned(), + args.http_port, + args.nym_http_cache_ttl, + ) + .await + .expect("Failed to start server"); + + tracing::info!("Started HTTP server on port {}", args.http_port); + + wait_for_signal().await; + + if let Err(err) = shutdown_handles.shutdown().await { + tracing::error!("{err}"); + }; + + Ok(()) +} diff --git a/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/src/monitor/mod.rs new file mode 100644 index 0000000000..680a9ce312 --- /dev/null +++ b/nym-node-status-api/src/monitor/mod.rs @@ -0,0 +1,466 @@ +use crate::db::models::{ + gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT, + GATEWAYS_BONDED_COUNT, GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT, + MIXNODES_BLACKLISTED_COUNT, MIXNODES_BONDED_ACTIVE, MIXNODES_BONDED_COUNT, + MIXNODES_BONDED_INACTIVE, MIXNODES_BONDED_RESERVE, MIXNODES_HISTORICAL_COUNT, +}; +use crate::db::{queries, DbPool}; +use anyhow::anyhow; +use cosmwasm_std::Decimal; +use nym_explorer_client::{ExplorerClient, PrettyDetailedGatewayBond}; +use nym_network_defaults::NymNetworkDetails; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::models::{DescribedGateway, DescribedMixNode, MixNodeBondAnnotated}; +use nym_validator_client::nym_nodes::SkimmedNode; +use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient; +use nym_validator_client::nyxd::{AccountId, NyxdClient}; +use nym_validator_client::NymApiClient; +use reqwest::Url; +use std::collections::HashSet; +use std::str::FromStr; +use tokio::time::Duration; +use tracing::instrument; + +// TODO dz should be configurable +const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); + +static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw"; + +// TODO dz: query many NYM APIs: +// multiple instances running directory cache, ask sachin +#[instrument(level = "debug", name = "data_monitor", skip_all)] +pub(crate) async fn spawn_in_background( + db_pool: DbPool, + explorer_client_timeout: Duration, + nym_api_client_timeout: Duration, + nyxd_addr: &Url, + refresh_interval: Duration, +) { + let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); + + loop { + tracing::info!("Refreshing node info..."); + + if let Err(e) = run( + &db_pool, + &network_defaults, + explorer_client_timeout, + nym_api_client_timeout, + nyxd_addr, + ) + .await + { + tracing::error!( + "Monitor run failed: {e}, retrying in {}s...", + FAILURE_RETRY_DELAY.as_secs() + ); + // TODO dz implement some sort of backoff + tokio::time::sleep(FAILURE_RETRY_DELAY).await; + } else { + tracing::info!( + "Info successfully collected, sleeping for {}s...", + refresh_interval.as_secs() + ); + tokio::time::sleep(refresh_interval).await; + } + } +} + +async fn run( + pool: &DbPool, + network_details: &NymNetworkDetails, + explorer_client_timeout: Duration, + nym_api_client_timeout: Duration, + nyxd_addr: &Url, +) -> anyhow::Result<()> { + let default_api_url = network_details + .endpoints + .first() + .expect("rust sdk mainnet default incorrectly configured") + .api_url() + .clone() + .expect("rust sdk mainnet default missing api_url"); + let default_explorer_url = network_details.explorer_api.clone().map(|url| { + url.parse() + .expect("rust sdk mainnet default explorer url not parseable") + }); + + // TODO dz replace explorer api with ipinfo.io + let default_explorer_url = + default_explorer_url.expect("explorer url missing in network config"); + let explorer_client = + ExplorerClient::new_with_timeout(default_explorer_url, explorer_client_timeout)?; + let explorer_gateways = explorer_client + .get_gateways() + .await + .log_error("get_gateways")?; + + let api_client = NymApiClient::new_with_timeout(default_api_url, nym_api_client_timeout); + let gateways = api_client + .get_cached_described_gateways() + .await + .log_error("get_described_gateways")?; + tracing::debug!("Fetched {} gateways", gateways.len()); + let skimmed_gateways = api_client + .get_basic_gateways(None) + .await + .log_error("get_basic_gateways")?; + + let mixnodes = api_client + .get_cached_mixnodes() + .await + .log_error("get_cached_mixnodes")?; + tracing::debug!("Fetched {} mixnodes", mixnodes.len()); + + let gateways_blacklisted = skimmed_gateways + .iter() + .filter_map(|gw| { + if gw.performance.round_to_integer() <= 50 { + Some(gw.ed25519_identity_pubkey.to_owned()) + } else { + None + } + }) + .collect::>(); + + // Cached mixnodes don't include blacklisted nodes + // We need that to calculate the total locked tokens later + let mixnodes = api_client + .nym_api + .get_mixnodes_detailed_unfiltered() + .await + .log_error("get_mixnodes_detailed_unfiltered")?; + let mixnodes_described = api_client + .nym_api + .get_mixnodes_described() + .await + .log_error("get_mixnodes_described")?; + let mixnodes_active = api_client + .nym_api + .get_active_mixnodes() + .await + .log_error("get_active_mixnodes")?; + let delegation_program_members = + get_delegation_program_details(network_details, nyxd_addr).await?; + + // keep stats for later + let count_bonded_mixnodes = mixnodes.len(); + let count_bonded_gateways = gateways.len(); + let count_explorer_gateways = explorer_gateways.len(); + let count_bonded_mixnodes_active = mixnodes_active.len(); + + let gateway_records = prepare_gateway_data( + &gateways, + &gateways_blacklisted, + explorer_gateways, + skimmed_gateways, + )?; + queries::insert_gateways(pool, gateway_records) + .await + .map(|_| { + tracing::debug!("Gateway info written to DB!"); + })?; + + // instead of counting blacklisted GWs returned from API cache, count from the active set + let count_gateways_blacklisted = gateways + .iter() + .filter(|gw| { + let gw_identity = gw.bond.identity(); + gateways_blacklisted.contains(gw_identity) + }) + .count(); + + if count_gateways_blacklisted > 0 { + queries::write_blacklisted_gateways_to_db(pool, gateways_blacklisted.iter()) + .await + .map(|_| { + tracing::debug!( + "Gateway blacklist info written to DB! {} blacklisted by Nym API", + count_gateways_blacklisted + ) + })?; + } + + let mixnode_records = + prepare_mixnode_data(&mixnodes, mixnodes_described, delegation_program_members)?; + queries::insert_mixnodes(pool, mixnode_records) + .await + .map(|_| { + tracing::debug!("Mixnode info written to DB!"); + })?; + + let count_mixnodes_blacklisted = mixnodes.iter().filter(|elem| elem.blacklisted).count(); + + let recently_unbonded_gateways = queries::ensure_gateways_still_bonded(pool, &gateways).await?; + let recently_unbonded_mixnodes = queries::ensure_mixnodes_still_bonded(pool, &mixnodes).await?; + + let count_bonded_mixnodes_reserve = 0; // TODO: NymAPI doesn't report the reserve set size + let count_bonded_mixnodes_inactive = count_bonded_mixnodes - count_bonded_mixnodes_active; + + let (all_historical_gateways, all_historical_mixnodes) = calculate_stats(pool).await?; + + // + // write summary keys and values to table + // + + let nodes_summary = vec![ + (MIXNODES_BONDED_COUNT, &count_bonded_mixnodes), + (MIXNODES_BONDED_ACTIVE, &count_bonded_mixnodes_active), + (MIXNODES_BONDED_INACTIVE, &count_bonded_mixnodes_inactive), + (MIXNODES_BONDED_RESERVE, &count_bonded_mixnodes_reserve), + (MIXNODES_BLACKLISTED_COUNT, &count_mixnodes_blacklisted), + (GATEWAYS_BONDED_COUNT, &count_bonded_gateways), + (GATEWAYS_EXPLORER_COUNT, &count_explorer_gateways), + (MIXNODES_HISTORICAL_COUNT, &all_historical_mixnodes), + (GATEWAYS_HISTORICAL_COUNT, &all_historical_gateways), + (GATEWAYS_BLACKLISTED_COUNT, &count_gateways_blacklisted), + ]; + + let last_updated = chrono::offset::Utc::now(); + let last_updated_utc = last_updated.timestamp().to_string(); + let network_summary = NetworkSummary { + mixnodes: mixnode::MixnodeSummary { + bonded: mixnode::MixnodeSummaryBonded { + count: count_bonded_mixnodes.cast_checked()?, + active: count_bonded_mixnodes_active.cast_checked()?, + inactive: count_bonded_mixnodes_inactive.cast_checked()?, + reserve: count_bonded_mixnodes_reserve.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + blacklisted: mixnode::MixnodeSummaryBlacklisted { + count: count_mixnodes_blacklisted.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + historical: mixnode::MixnodeSummaryHistorical { + count: all_historical_mixnodes.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + }, + gateways: gateway::GatewaySummary { + bonded: gateway::GatewaySummaryBonded { + count: count_bonded_gateways.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + blacklisted: gateway::GatewaySummaryBlacklisted { + count: count_gateways_blacklisted.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + historical: gateway::GatewaySummaryHistorical { + count: all_historical_gateways.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + explorer: gateway::GatewaySummaryExplorer { + count: count_explorer_gateways.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + }, + }; + + queries::insert_summaries(pool, &nodes_summary, &network_summary, last_updated).await?; + + let mut log_lines: Vec = vec![]; + for (key, value) in nodes_summary.iter() { + log_lines.push(format!("{} = {}", key, value)); + } + log_lines.push(format!( + "recently_unbonded_mixnodes = {}", + recently_unbonded_mixnodes + )); + log_lines.push(format!( + "recently_unbonded_gateways = {}", + recently_unbonded_gateways + )); + + tracing::info!("Directory summary: \n{}", log_lines.join("\n")); + + Ok(()) +} + +fn prepare_gateway_data( + gateways: &[DescribedGateway], + gateways_blacklisted: &HashSet, + explorer_gateways: Vec, + skimmed_gateways: Vec, +) -> anyhow::Result> { + let mut gateway_records = Vec::new(); + + for gateway in gateways { + let identity_key = gateway.bond.identity(); + let bonded = true; + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let blacklisted = gateways_blacklisted.contains(identity_key); + + let self_described = gateway + .self_described + .as_ref() + .and_then(|v| serde_json::to_string(&v).ok()); + + let explorer_pretty_bond = explorer_gateways + .iter() + .find(|g| g.gateway.identity_key.eq(identity_key)); + let explorer_pretty_bond = explorer_pretty_bond.and_then(|g| serde_json::to_string(g).ok()); + + let performance = skimmed_gateways + .iter() + .find(|g| g.ed25519_identity_pubkey.eq(identity_key)) + .map(|g| g.performance) + .unwrap_or_default() + .round_to_integer(); + + gateway_records.push(GatewayRecord { + identity_key: identity_key.to_owned(), + bonded, + blacklisted, + self_described, + explorer_pretty_bond, + last_updated_utc, + performance, + }); + } + + Ok(gateway_records) +} + +fn prepare_mixnode_data( + mixnodes: &[MixNodeBondAnnotated], + mixnodes_described: Vec, + delegation_program_members: Vec, +) -> anyhow::Result> { + let mut mixnode_records = Vec::new(); + + for mixnode in mixnodes { + let mix_id = mixnode.mix_id(); + let identity_key = mixnode.identity_key(); + let bonded = true; + let total_stake = decimal_to_i64(mixnode.mixnode_details.total_stake()); + let blacklisted = mixnode.blacklisted; + let node_info = mixnode.mix_node(); + let host = node_info.host.clone(); + let http_port = node_info.http_api_port; + // Contains all the information including what's above + let full_details = serde_json::to_string(&mixnode)?; + + let mixnode_described = mixnodes_described.iter().find(|m| m.bond.mix_id == mix_id); + let self_described = mixnode_described.and_then(|v| serde_json::to_string(v).ok()); + let is_dp_delegatee = delegation_program_members.contains(&mix_id); + + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + + mixnode_records.push(MixnodeRecord { + mix_id, + identity_key: identity_key.to_owned(), + bonded, + total_stake, + host, + http_port, + blacklisted, + full_details, + self_described, + last_updated_utc, + is_dp_delegatee, + }); + } + + Ok(mixnode_records) +} + +// TODO dz is there a common monorepo place this can be put? +pub trait NumericalCheckedCast +where + T: TryFrom, + >::Error: std::error::Error, + Self: std::fmt::Display + Copy, +{ + fn cast_checked(self) -> anyhow::Result { + T::try_from(self).map_err(|e| { + anyhow::anyhow!( + "Couldn't cast {} to {}: {}", + self, + std::any::type_name::(), + e + ) + }) + } +} + +impl NumericalCheckedCast for T +where + U: TryFrom, + >::Error: std::error::Error, + T: std::fmt::Display + Copy, +{ +} + +async fn calculate_stats(pool: &DbPool) -> anyhow::Result<(usize, usize)> { + let mut conn = pool.acquire().await?; + + let all_historical_gateways = sqlx::query_scalar!(r#"SELECT count(id) FROM gateways"#) + .fetch_one(&mut *conn) + .await? + .cast_checked()?; + + let all_historical_mixnodes = sqlx::query_scalar!(r#"SELECT count(id) FROM mixnodes"#) + .fetch_one(&mut *conn) + .await? + .cast_checked()?; + + Ok((all_historical_gateways, all_historical_mixnodes)) +} + +async fn get_delegation_program_details( + network_details: &NymNetworkDetails, + nyxd_addr: &Url, +) -> anyhow::Result> { + let config = nym_validator_client::nyxd::Config::try_from_nym_network_details(network_details)?; + + let client = NyxdClient::connect(config, nyxd_addr.as_str()) + .map_err(|err| anyhow::anyhow!("Couldn't connect: {}", err))?; + + let account_id = AccountId::from_str(DELEGATION_PROGRAM_WALLET) + .map_err(|e| anyhow!("Invalid bech32 address: {}", e))?; + + let delegations = client.get_all_delegator_delegations(&account_id).await?; + + let mix_ids: Vec = delegations + .iter() + .map(|delegation| delegation.mix_id) + .collect(); + + Ok(mix_ids) +} + +fn decimal_to_i64(decimal: Decimal) -> i64 { + // Convert the underlying Uint128 to a u128 + let atomics = decimal.atomics().u128(); + let precision = 1_000_000_000_000_000_000u128; + + // Get the fractional part + let fractional = atomics % precision; + + // Get the integer part + let integer = atomics / precision; + + // Combine them into a float + let float_value = integer as f64 + (fractional as f64 / 1_000_000_000_000_000_000_f64); + + // Limit to 6 decimal places + let rounded_value = (float_value * 1_000_000.0).round() / 1_000_000.0; + + rounded_value as i64 +} + +trait LogError { + fn log_error(self, msg: &str) -> Result; +} + +impl LogError for anyhow::Result +where + E: std::error::Error, +{ + fn log_error(self, msg: &str) -> Result { + if let Err(e) = &self { + tracing::error!("[{msg}]:\t{e}"); + } + self + } +} diff --git a/nym-node-status-api/src/testruns/mod.rs b/nym-node-status-api/src/testruns/mod.rs new file mode 100644 index 0000000000..86b36cf48e --- /dev/null +++ b/nym-node-status-api/src/testruns/mod.rs @@ -0,0 +1,76 @@ +use crate::db::models::GatewayIdentityDto; +use crate::db::DbPool; +use futures_util::TryStreamExt; +use std::time::Duration; +use tracing::instrument; + +pub(crate) mod models; +mod queue; +pub(crate) use queue::now_utc; + +pub(crate) async fn spawn(pool: DbPool, refresh_interval: Duration) { + tokio::spawn(async move { + loop { + tracing::info!("Spawning testruns..."); + + if let Err(e) = run(&pool).await { + tracing::error!("Cron job failed: {}", e); + } + tracing::debug!("Sleeping for {}s...", refresh_interval.as_secs()); + tokio::time::sleep(refresh_interval).await; + } + }); +} + +// TODO dz make number of max agents configurable + +// TODO dz periodically clean up stale pending testruns +#[instrument(level = "debug", name = "testrun_queue", skip_all)] +async fn run(pool: &DbPool) -> anyhow::Result<()> { + if pool.is_closed() { + tracing::debug!("DB pool closed, returning early"); + return Ok(()); + } + + let mut conn = pool.acquire().await?; + + let gateways = sqlx::query_as!( + GatewayIdentityDto, + r#"SELECT + gateway_identity_key as "gateway_identity_key!", + bonded as "bonded: bool" + FROM gateways + ORDER BY last_testrun_utc"#, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + // TODO dz this filtering could be done in SQL + let gateways: Vec = gateways.into_iter().filter(|g| g.bonded).collect(); + + tracing::debug!("Trying to queue {} testruns", gateways.len()); + let mut testruns_created = 0; + for gateway in gateways { + if let Err(e) = queue::try_queue_testrun( + &mut conn, + gateway.gateway_identity_key.clone(), + // TODO dz read from config + "127.0.0.1".to_string(), + ) + .await + // TODO dz measure how many were actually inserted and how many were skipped + { + tracing::debug!( + "Skipping test for identity {} with error {}", + &gateway.gateway_identity_key, + e + ); + } else { + testruns_created += 1; + } + } + tracing::debug!("{} testruns queued in total", testruns_created); + + Ok(()) +} diff --git a/nym-node-status-api/src/testruns/models.rs b/nym-node-status-api/src/testruns/models.rs new file mode 100644 index 0000000000..fe4b33384c --- /dev/null +++ b/nym-node-status-api/src/testruns/models.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros +#[derive(Debug, Clone)] +pub struct GatewayIdentityDto { + pub gateway_identity_key: String, + pub bonded: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)] +pub struct TestRun { + pub id: u32, + pub identity_key: String, + pub status: String, + pub log: String, +} diff --git a/nym-node-status-api/src/testruns/queue.rs b/nym-node-status-api/src/testruns/queue.rs new file mode 100644 index 0000000000..aa0ce32fa2 --- /dev/null +++ b/nym-node-status-api/src/testruns/queue.rs @@ -0,0 +1,118 @@ +use crate::db::models::{GatewayInfoDto, TestRunDto, TestRunStatus}; +use crate::testruns::models::TestRun; +use anyhow::anyhow; +use chrono::DateTime; +use futures_util::TryStreamExt; +use sqlx::pool::PoolConnection; +use sqlx::Sqlite; +use std::time::SystemTime; + +pub(crate) async fn try_queue_testrun( + conn: &mut PoolConnection, + identity_key: String, + ip_address: String, +) -> anyhow::Result { + let timestamp = now_utc().timestamp(); + let timestamp_pretty = now_utc_as_rfc3339(); + + let items = sqlx::query_as!( + GatewayInfoDto, + r#"SELECT + id as "id!", + gateway_identity_key as "gateway_identity_key!", + self_described as "self_described?", + explorer_pretty_bond as "explorer_pretty_bond?" + FROM gateways + WHERE gateway_identity_key = ? + ORDER BY gateway_identity_key + LIMIT 1"#, + identity_key, + ) + // TODO dz shoudl call .fetch_one + // TODO dz replace this in other queries as well + .fetch(conn.as_mut()) + .try_collect::>() + .await?; + + let gateway = items + .iter() + .find(|g| g.gateway_identity_key == identity_key); + + // TODO dz if let Some() = gateway.first() ... + if gateway.is_none() { + return Err(anyhow!("Unknown gateway {identity_key}")); + } + let gateway_id = gateway.unwrap().id; + + // + // check if there is already a test run for this gateway + // + let items = sqlx::query_as!( + TestRunDto, + r#"SELECT + id as "id!", + gateway_id as "gateway_id!", + status as "status!", + timestamp_utc as "timestamp_utc!", + ip_address as "ip_address!", + log as "log!" + FROM testruns + WHERE gateway_id = ? AND status != 2 + ORDER BY id DESC + LIMIT 1"#, + gateway_id, + ) + .fetch(conn.as_mut()) + .try_collect::>() + .await?; + + if !items.is_empty() { + let testrun = items.first().unwrap(); + return Ok(TestRun { + id: testrun.id as u32, + identity_key, + status: format!( + "{}", + TestRunStatus::from_repr(testrun.status as u8).unwrap() + ), + log: testrun.log.clone(), + }); + } + + // + // save test run + // + let status = TestRunStatus::Pending as u32; + let log = format!( + "Test for {identity_key} requested at {} UTC\n\n", + timestamp_pretty + ); + + let id = sqlx::query!( + "INSERT INTO testruns (gateway_id, status, ip_address, timestamp_utc, log) VALUES (?, ?, ?, ?, ?)", + gateway_id, + status, + ip_address, + timestamp, + log, + ) + .execute(conn.as_mut()) + .await? + .last_insert_rowid(); + + Ok(TestRun { + id: id as u32, + identity_key, + status: format!("{}", TestRunStatus::Pending), + log, + }) +} + +// TODO dz do we need these? +pub fn now_utc() -> DateTime { + SystemTime::now().into() +} + +pub fn now_utc_as_rfc3339() -> String { + now_utc().to_rfc3339() +}