diff --git a/.github/workflows/ci-check-ns-api-version.yml b/.github/workflows/ci-check-ns-api-version.yml index 322024c0a5..42eded2c86 100644 --- a/.github/workflows/ci-check-ns-api-version.yml +++ b/.github/workflows/ci-check-ns-api-version.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v4 - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/ci-check-nym-stats-api-version.yml b/.github/workflows/ci-check-nym-stats-api-version.yml index cfdddf2b25..ac46925ed3 100644 --- a/.github/workflows/ci-check-nym-stats-api-version.yml +++ b/.github/workflows/ci-check-nym-stats-api-version.yml @@ -10,13 +10,13 @@ env: jobs: check-if-tag-exists: - runs-on: arc-ubuntu-22.04-dind + runs-on: arc-linux-latest-dind steps: - name: Checkout repo uses: actions/checkout@v4 - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/ci-sdk-wasm.yml b/.github/workflows/ci-sdk-wasm.yml index 2bb2c52d83..3f8d57ad4b 100644 --- a/.github/workflows/ci-sdk-wasm.yml +++ b/.github/workflows/ci-sdk-wasm.yml @@ -31,7 +31,7 @@ jobs: components: rustfmt, clippy - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: "1.24.6" diff --git a/.github/workflows/ci-sonar.yml b/.github/workflows/ci-sonar.yml index a06986dfcd..4715aa1bd5 100644 --- a/.github/workflows/ci-sonar.yml +++ b/.github/workflows/ci-sonar.yml @@ -14,6 +14,6 @@ jobs: with: fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - name: SonarQube Scan - uses: SonarSource/sonarqube-scan-action@v5 + uses: SonarSource/sonarqube-scan-action@v6 env: SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} diff --git a/.github/workflows/deploy-github-pages.yml b/.github/workflows/deploy-github-pages.yml index f1e47a3ed8..6b98e9e33f 100644 --- a/.github/workflows/deploy-github-pages.yml +++ b/.github/workflows/deploy-github-pages.yml @@ -34,7 +34,7 @@ jobs: - name: Setup Pages uses: actions/configure-pages@v5 - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 with: # Upload entire repository path: './ppa' diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml index de25d37594..a6e746c65b 100644 --- a/.github/workflows/greetings.yml +++ b/.github/workflows/greetings.yml @@ -8,6 +8,6 @@ jobs: steps: - uses: actions/first-interaction@v3 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: 'Thank you for raising this issue' - pr-message: 'Thank you for making this first PR' + repo_token: ${{ secrets.GITHUB_TOKEN }} + issue_message: 'Thank you for raising this issue' + pr_message: 'Thank you for making this first PR' diff --git a/.github/workflows/push-credential-proxy.yaml b/.github/workflows/push-credential-proxy.yaml index 0c9b7952ac..b8337a58bb 100644 --- a/.github/workflows/push-credential-proxy.yaml +++ b/.github/workflows/push-credential-proxy.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml diff --git a/.github/workflows/push-data-observatory.yaml b/.github/workflows/push-data-observatory.yaml index bd999f45d5..201708d3bd 100644 --- a/.github/workflows/push-data-observatory.yaml +++ b/.github/workflows/push-data-observatory.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-network-monitor.yaml b/.github/workflows/push-network-monitor.yaml index 8baa371411..48990a6789 100644 --- a/.github/workflows/push-network-monitor.yaml +++ b/.github/workflows/push-network-monitor.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml diff --git a/.github/workflows/push-nym-api.yaml b/.github/workflows/push-nym-api.yaml index 79f5fe9ade..7539343f4e 100644 --- a/.github/workflows/push-nym-api.yaml +++ b/.github/workflows/push-nym-api.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml diff --git a/.github/workflows/push-nym-node.yaml b/.github/workflows/push-nym-node.yaml index 05ad829163..3397171354 100644 --- a/.github/workflows/push-nym-node.yaml +++ b/.github/workflows/push-nym-node.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-nym-statistics-api.yaml b/.github/workflows/push-nym-statistics-api.yaml index 3bb9256dfa..c51e3f59e7 100644 --- a/.github/workflows/push-nym-statistics-api.yaml +++ b/.github/workflows/push-nym-statistics-api.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-nyx-chain-watcher.yaml b/.github/workflows/push-nyx-chain-watcher.yaml index c156a6ecde..5fb76e89af 100644 --- a/.github/workflows/push-nyx-chain-watcher.yaml +++ b/.github/workflows/push-nyx-chain-watcher.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/.github/workflows/push-validator-rewarder.yaml b/.github/workflows/push-validator-rewarder.yaml index 7fcb4b166e..9f8e7ab32c 100644 --- a/.github/workflows/push-validator-rewarder.yaml +++ b/.github/workflows/push-validator-rewarder.yaml @@ -26,7 +26,7 @@ jobs: git config --global user.name "Lawrence Stalder" - name: Get version from cargo.toml - uses: mikefarah/yq@v4.47.1 + uses: mikefarah/yq@v4.48.1 id: get_version with: cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index 2033d13fe5..388dc6d5d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,62 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2026.1-niolo] (2026-01-13) + +- bugfix: mozzarella -> niolo config migration ([#6259]) +- chore: remove run DKG migration ([#6253]) +- bugfix: reexposed 'derive_extended_private_key' ([#6247]) +- Bump js-yaml from 3.14.1 to 3.14.2 in /sdk/typescript/codegen/contract-clients ([#6231]) +- Statistics API v2 ([#6227]) +- Bump golang.org/x/crypto from 0.39.0 to 0.45.0 in /nym-gateway-probe/netstack_ping ([#6220]) +- Update chain registry link ([#6219]) +- Bump glob from 10.3.4 to 10.5.0 in /documentation/scripts/post-process ([#6216]) +- Bump js-yaml from 4.1.0 to 4.1.1 in /sdk/typescript/tests/integration-tests/mix-fetch ([#6215]) +- gateway-probe fixes for run-local ([#6212]) +- chore: updated default endpoint for retrieving attestation.json ([#6207]) +- chore: remove support for legacy mixnode within the performance contract ([#6205]) +- feat: upgrade mode: VPN adjustments ([#6189]) +- Bump min-document from 2.19.0 to 2.19.1 ([#6181]) +- Bump next from 15.4.1 to 15.4.7 in /nym-node-status-api/nym-node-status-ui ([#6180]) +- feat: merge intermediate upgrade mode changes ([#6174]) +- Add weighted scoring to NS API ([#6144]) +- build(deps): bump mikefarah/yq from 4.47.1 to 4.48.1 ([#6107]) +- build(deps): bump SonarSource/sonarqube-scan-action from 5 to 6 in /.github/workflows ([#6068]) +- build(deps): bump tar-fs from 3.0.9 to 3.1.1 in /sdk/typescript/tests/integration-tests/mix-fetch ([#6063]) +- build(deps): bump ammonia from 4.1.1 to 4.1.2 ([#6057]) +- build(deps): bump tower-http from 0.5.2 to 0.6.6 ([#6030]) +- build(deps): bump actions/setup-go from 5 to 6 ([#6013]) +- build(deps): bump next from 14.2.28 to 14.2.32 ([#5996]) +- build(deps): bump tracing-subscriber from 0.3.19 to 0.3.20 ([#5993]) +- build(deps): bump actions/upload-pages-artifact from 3 to 4 ([#5992]) + +[#6259]: https://github.com/nymtech/nym/pull/6259 +[#6253]: https://github.com/nymtech/nym/pull/6253 +[#6247]: https://github.com/nymtech/nym/pull/6247 +[#6231]: https://github.com/nymtech/nym/pull/6231 +[#6227]: https://github.com/nymtech/nym/pull/6227 +[#6220]: https://github.com/nymtech/nym/pull/6220 +[#6219]: https://github.com/nymtech/nym/pull/6219 +[#6216]: https://github.com/nymtech/nym/pull/6216 +[#6215]: https://github.com/nymtech/nym/pull/6215 +[#6212]: https://github.com/nymtech/nym/pull/6212 +[#6207]: https://github.com/nymtech/nym/pull/6207 +[#6205]: https://github.com/nymtech/nym/pull/6205 +[#6189]: https://github.com/nymtech/nym/pull/6189 +[#6181]: https://github.com/nymtech/nym/pull/6181 +[#6180]: https://github.com/nymtech/nym/pull/6180 +[#6174]: https://github.com/nymtech/nym/pull/6174 +[#6144]: https://github.com/nymtech/nym/pull/6144 +[#6107]: https://github.com/nymtech/nym/pull/6107 +[#6068]: https://github.com/nymtech/nym/pull/6068 +[#6063]: https://github.com/nymtech/nym/pull/6063 +[#6057]: https://github.com/nymtech/nym/pull/6057 +[#6030]: https://github.com/nymtech/nym/pull/6030 +[#6013]: https://github.com/nymtech/nym/pull/6013 +[#5996]: https://github.com/nymtech/nym/pull/5996 +[#5993]: https://github.com/nymtech/nym/pull/5993 +[#5992]: https://github.com/nymtech/nym/pull/5992 + ## [2025.21-mozzarella] (2025-11-25) - [bugfix] Tunnel not waiting on MixnetClient to shut down cleanly ([#6225]) diff --git a/Cargo.lock b/Cargo.lock index 784ed126c4..3a9d8e0676 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1283,7 +1283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" dependencies = [ "lazy_static", - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] @@ -3965,7 +3965,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -4374,11 +4374,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -4741,6 +4741,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -4825,7 +4834,7 @@ dependencies = [ [[package]] name = "nym-api" -version = "1.1.70" +version = "1.1.71" dependencies = [ "anyhow", "async-trait", @@ -4896,7 +4905,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", - "tower-http 0.5.2", + "tower-http", "tracing", "ts-rs", "url", @@ -4988,6 +4997,7 @@ dependencies = [ "nym-network-defaults", "nym-service-provider-requests-common", "nym-sphinx", + "nym-test-utils", "nym-wireguard-types", "rand 0.8.5", "semver 1.0.26", @@ -4995,6 +5005,7 @@ dependencies = [ "sha2 0.10.9", "strum_macros", "thiserror 2.0.12", + "tracing", "x25519-dalek", ] @@ -5003,21 +5014,16 @@ name = "nym-bandwidth-controller" version = "0.1.0" dependencies = [ "async-trait", - "bip39", "log", "nym-credential-storage", "nym-credentials", "nym-credentials-interface", "nym-crypto", - "nym-ecash-contract-common", "nym-ecash-time", - "nym-network-defaults", "nym-task", "nym-validator-client", "rand 0.8.5", "thiserror 2.0.12", - "url", - "zeroize", ] [[package]] @@ -5051,7 +5057,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.67" +version = "1.1.68" dependencies = [ "anyhow", "base64 0.22.1", @@ -5134,7 +5140,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.67" +version = "1.1.68" dependencies = [ "bs58", "clap", @@ -5481,7 +5487,7 @@ dependencies = [ "time", "tokio", "tokio-util", - "tower-http 0.5.2", + "tower-http", "tracing", "url", "utoipa", @@ -5570,6 +5576,7 @@ dependencies = [ "sqlx", "sqlx-pool-guard", "thiserror 2.0.12", + "time", "tokio", "zeroize", ] @@ -5605,12 +5612,13 @@ dependencies = [ "nym-api-requests", "nym-credentials", "nym-credentials-interface", + "nym-crypto", "nym-ecash-contract-common", "nym-gateway-requests", "nym-gateway-storage", "nym-task", + "nym-upgrade-mode-check", "nym-validator-client", - "rand 0.8.5", "si-scale", "thiserror 2.0.12", "time", @@ -5650,6 +5658,7 @@ dependencies = [ "nym-compact-ecash", "nym-ecash-time", "nym-network-defaults", + "nym-upgrade-mode-check", "rand 0.8.5", "serde", "strum", @@ -5800,7 +5809,6 @@ dependencies = [ name = "nym-gateway" version = "1.1.36" dependencies = [ - "anyhow", "async-trait", "bincode", "bip39", @@ -5811,7 +5819,6 @@ dependencies = [ "futures", "ipnetwork", "mock_instant", - "nym-api-requests", "nym-authenticator-requests", "nym-client-core", "nym-credential-verification", @@ -5824,7 +5831,6 @@ dependencies = [ "nym-id", "nym-ip-packet-router", "nym-mixnet-client", - "nym-mixnode-common", "nym-network-defaults", "nym-network-requester", "nym-node-metrics", @@ -5834,20 +5840,18 @@ dependencies = [ "nym-statistics-common", "nym-task", "nym-topology", - "nym-types", + "nym-upgrade-mode-check", "nym-validator-client", "nym-wireguard", "nym-wireguard-private-metadata-server", "nym-wireguard-types", "rand 0.8.5", "serde", - "sha2 0.10.9", "thiserror 2.0.12", "time", "tokio", "tokio-stream", "tokio-tungstenite", - "tokio-util", "tracing", "url", "zeroize", @@ -6363,7 +6367,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.68" +version = "1.1.69" dependencies = [ "addr", "anyhow", @@ -6413,7 +6417,7 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.22.0" +version = "1.23.0" dependencies = [ "anyhow", "arc-swap", @@ -6444,6 +6448,7 @@ dependencies = [ "nym-bin-common", "nym-client-core-config-types", "nym-config", + "nym-credential-verification", "nym-crypto", "nym-gateway", "nym-gateway-stats-storage", @@ -6486,7 +6491,7 @@ dependencies = [ "tokio-stream", "tokio-util", "toml 0.8.23", - "tower-http 0.5.2", + "tower-http", "tracing", "tracing-indicatif", "tracing-subscriber", @@ -6516,13 +6521,13 @@ version = "0.1.0" dependencies = [ "async-trait", "celes", - "humantime", "humantime-serde", "nym-bin-common", "nym-crypto", "nym-exit-policy", "nym-http-api-client", "nym-noise-keys", + "nym-upgrade-mode-check", "nym-wireguard-types", "rand_chacha 0.3.1", "schemars 0.8.22", @@ -6533,6 +6538,7 @@ dependencies = [ "thiserror 2.0.12", "time", "tokio", + "url", "utoipa", ] @@ -6555,7 +6561,7 @@ dependencies = [ [[package]] name = "nym-node-status-api" -version = "4.0.11-rc1" +version = "4.0.12" dependencies = [ "ammonia", "anyhow", @@ -6603,7 +6609,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", - "tower-http 0.5.2", + "tower-http", "tracing", "tracing-log 0.2.0", "tracing-subscriber", @@ -6940,7 +6946,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.67" +version = "1.1.68" dependencies = [ "bs58", "clap", @@ -7200,7 +7206,7 @@ dependencies = [ [[package]] name = "nym-statistics-api" -version = "0.2.1" +version = "0.3.0" dependencies = [ "anyhow", "axum", @@ -7214,13 +7220,12 @@ dependencies = [ "nym-statistics-common", "nym-task", "nym-validator-client", - "serde", "serde_json", "sqlx", "time", "tokio", "tokio-util", - "tower-http 0.5.2", + "tower-http", "tracing", "tracing-subscriber", "url", @@ -7401,6 +7406,7 @@ dependencies = [ name = "nym-validator-client" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "base64 0.22.1", "bip32", @@ -7574,17 +7580,10 @@ dependencies = [ name = "nym-wireguard" version = "0.1.0" dependencies = [ - "async-trait", "base64 0.22.1", - "bincode", - "chrono", - "dashmap", "defguard_wireguard_rs", - "dyn-clone", "futures", "ip_network", - "log", - "nym-authenticator-requests", "nym-credential-verification", "nym-credentials-interface", "nym-crypto", @@ -7595,11 +7594,9 @@ dependencies = [ "nym-task", "nym-wireguard-types", "thiserror 2.0.12", - "time", "tokio", "tokio-stream", "tracing", - "x25519-dalek", ] [[package]] @@ -7627,7 +7624,7 @@ dependencies = [ "nym-wireguard-private-metadata-shared", "tokio", "tokio-util", - "tower-http 0.5.2", + "tower-http", "utoipa", "utoipa-swagger-ui", ] @@ -7651,16 +7648,21 @@ version = "1.0.0" dependencies = [ "async-trait", "axum", + "futures", "nym-credential-verification", "nym-credentials-interface", + "nym-crypto", "nym-http-api-client", "nym-http-api-common", + "nym-upgrade-mode-check", "nym-wireguard", "nym-wireguard-private-metadata-client", "nym-wireguard-private-metadata-server", "nym-wireguard-private-metadata-shared", + "time", "tokio", - "tower-http 0.5.2", + "tower 0.5.2", + "tower-http", "utoipa", ] @@ -7669,10 +7671,7 @@ name = "nym-wireguard-types" version = "0.1.0" dependencies = [ "base64 0.22.1", - "log", - "nym-config", "nym-crypto", - "nym-network-defaults", "rand 0.8.5", "serde", "thiserror 2.0.12", @@ -7681,7 +7680,7 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.32" +version = "0.1.33" dependencies = [ "anyhow", "bytes", @@ -7732,7 +7731,7 @@ dependencies = [ "time", "tokio", "tokio-util", - "tower-http 0.5.2", + "tower-http", "tracing", "tracing-subscriber", "utoipa", @@ -8660,7 +8659,7 @@ dependencies = [ "once_cell", "socket2 0.5.10", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -8821,17 +8820,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -8842,15 +8832,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -8930,7 +8914,7 @@ dependencies = [ "tokio-rustls 0.26.2", "tokio-util", "tower 0.5.2", - "tower-http 0.6.6", + "tower-http", "tower-service", "url", "wasm-bindgen", @@ -9130,7 +9114,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -10446,7 +10430,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -11065,9 +11049,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.5.2" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "async-compression", "bitflags 2.9.1", @@ -11079,33 +11063,17 @@ dependencies = [ "http-body-util", "http-range-header", "httpdate", + "iri-string", "mime", "mime_guess", "percent-encoding", "pin-project-lite", "tokio", "tokio-util", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.9.1", - "bytes", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "iri-string", - "pin-project-lite", "tower 0.5.2", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -11213,14 +11181,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", - "nu-ansi-term", + "nu-ansi-term 0.50.1", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -11256,7 +11224,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ec6adcab41b1391b08a308cc6302b79f8095d1673f6947c2dc65ffb028b0b2d" dependencies = [ - "nu-ansi-term", + "nu-ansi-term 0.46.0", "tracing-core", "tracing-log 0.1.4", "tracing-subscriber", @@ -12258,7 +12226,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c07db1f4ba..eaad10bbc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -171,6 +171,7 @@ members = [ default-members = [ "clients/native", "clients/socks5", + "nym-authenticator-client", "nym-api", "nym-credential-proxy/nym-credential-proxy", "nym-node", @@ -346,11 +347,11 @@ tokio-tungstenite = { version = "0.20.1" } tokio-util = "0.7.15" toml = "0.8.22" tower = "0.5.2" -tower-http = "0.5.2" +tower-http = "0.6.6" tracing = "0.1.41" tracing-log = "0.2" tracing-opentelemetry = "0.19.0" -tracing-subscriber = "0.3.19" +tracing-subscriber = "0.3.20" tracing-tree = "0.2.2" tracing-indicatif = "0.3.9" tracing-test = "0.2.5" diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index e53458609b..026f483626 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.67" +version = "1.1.68" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 91d6591411..500dd6cc6a 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.67" +version = "1.1.68" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" diff --git a/common/authenticator-requests/Cargo.toml b/common/authenticator-requests/Cargo.toml index 60ff6826ba..6126a18f80 100644 --- a/common/authenticator-requests/Cargo.toml +++ b/common/authenticator-requests/Cargo.toml @@ -16,6 +16,7 @@ serde = { workspace = true, features = ["derive"] } semver = { workspace = true } strum_macros = { workspace = true } thiserror = { workspace = true } +tracing = { workspace = true } nym-credentials-interface = { path = "../credentials-interface" } nym-crypto = { path = "../crypto", features = ["asymmetric"] } @@ -29,7 +30,13 @@ hmac = { workspace = true, optional = true } sha2 = { workspace = true, optional = true } x25519-dalek = { workspace = true, features = ["static_secrets"] } +[dev-dependencies] +nym-test-utils = { path = "../test-utils" } + [features] default = ["verify"] # this is moved to a separate feature as we really need clients to import it (especially, *cough*, wasm) verify = ["hmac", "sha2"] + +[lints] +workspace = true \ No newline at end of file diff --git a/common/authenticator-requests/src/client_message.rs b/common/authenticator-requests/src/client_message.rs index 06a910b9b9..45f7481a46 100644 --- a/common/authenticator-requests/src/client_message.rs +++ b/common/authenticator-requests/src/client_message.rs @@ -6,9 +6,11 @@ use nym_wireguard_types::PeerPublicKey; use crate::{ AuthenticatorVersion, Error, - latest::registration::IpPair, - traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, Versionable}, - v2, v3, v4, v5, + traits::{ + FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, UpgradeModeMessage, + Versionable, + }, + v2, v3, v4, v5, v6, }; // This is very redundant with AuthenticatorRequest and I reckon they could be smooshed. @@ -19,6 +21,293 @@ pub enum ClientMessage { Final(Box), Query(Box), TopUp(Box), + UpgradeModeCheck(Box), +} + +pub struct SerialisedRequest { + pub bytes: Vec, + pub request_id: u64, +} + +impl SerialisedRequest { + pub fn new(bytes: Vec, request_id: u64) -> Self { + Self { bytes, request_id } + } +} + +impl ClientMessage { + fn serialise_v1(&self) -> Result { + Err(Error::UnsupportedVersion) + } + + fn serialise_v2(&self, reply_to: Recipient) -> Result { + use v2::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ip: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)? + .into(), + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message + .credential() + .and_then(|c| c.credential.into_zk_nym()) + .map(|c| *c), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = + AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + _ => Err(Error::UnsupportedMessage), + } + } + + fn serialise_v3(&self, reply_to: Recipient) -> Result { + use v3::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ip: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)? + .into(), + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message + .credential() + .and_then(|c| c.credential.into_zk_nym()) + .map(|c| *c), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = + AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request( + TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + _ => Err(Error::UnsupportedMessage), + } + } + + fn serialise_v4(&self, reply_to: Recipient) -> Result { + use v4::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request( + InitMessage { + pub_key: init_message.pub_key(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request( + FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ips: IpPair { + ipv4: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)?, + ipv6: final_message + .gateway_client_ipv6() + .ok_or(Error::UnsupportedMessage)?, + }, + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message + .credential() + .and_then(|c| c.credential.into_zk_nym()) + .map(|c| *c), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = + AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request( + TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }, + reply_to, + ); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + _ => Err(Error::UnsupportedMessage), + } + } + + fn serialise_v5(&self) -> Result { + use v5::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, + request::AuthenticatorRequest, + topup::TopUpMessage, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage { + pub_key: init_message.pub_key(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ips: IpPair { + ipv4: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)?, + ipv6: final_message + .gateway_client_ipv6() + .ok_or(Error::UnsupportedMessage)?, + }, + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message + .credential() + .and_then(|c| c.credential.into_zk_nym()) + .map(|c| *c), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = AuthenticatorRequest::new_query_request(query_message.pub_key()); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + _ => Err(Error::UnsupportedMessage), + } + } + + fn serialise_v6(&self) -> Result { + use v6::{ + registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, + request::AuthenticatorRequest, + topup::TopUpMessage, + upgrade_mode_check::UpgradeModeCheckRequest, + }; + match self { + ClientMessage::Initial(init_message) => { + let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage { + pub_key: init_message.pub_key(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Final(final_message) => { + let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage { + gateway_client: GatewayClient { + pub_key: final_message.gateway_client_pub_key(), + private_ips: IpPair { + ipv4: final_message + .gateway_client_ipv4() + .ok_or(Error::UnsupportedMessage)?, + ipv6: final_message + .gateway_client_ipv6() + .ok_or(Error::UnsupportedMessage)?, + }, + mac: ClientMac::new(final_message.gateway_client_mac()), + }, + credential: final_message.credential(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::Query(query_message) => { + let (req, id) = AuthenticatorRequest::new_query_request(query_message.pub_key()); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::TopUp(top_up_message) => { + let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage { + pub_key: top_up_message.pub_key(), + credential: top_up_message.credential(), + }); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + ClientMessage::UpgradeModeCheck(upgrade_mode_check) => { + // currently JWT is the only emergency credential option + let Some(upgrade_mode_jwt) = + upgrade_mode_check.upgrade_mode_global_attestation_jwt() + else { + return Err(Error::conversion( + "no valid known upgrade mode check variants", + )); + }; + let msg = UpgradeModeCheckRequest::UpgradeModeJwt { + token: upgrade_mode_jwt, + }; + + let (req, id) = AuthenticatorRequest::new_upgrade_mode_check_request(msg); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } + } + } } impl ClientMessage { @@ -27,7 +316,7 @@ impl ClientMessage { match self { Self::Final(msg) => msg.credential().is_some(), Self::TopUp(_) => true, - Self::Initial(_) | Self::Query(_) => false, + Self::Initial(_) | Self::Query(_) | Self::UpgradeModeCheck(_) => false, } } @@ -37,208 +326,18 @@ impl ClientMessage { ClientMessage::Final(msg) => msg.version(), ClientMessage::Query(msg) => msg.version(), ClientMessage::TopUp(msg) => msg.version(), + ClientMessage::UpgradeModeCheck(msg) => msg.version(), } } - pub fn bytes(&self, reply_to: Recipient) -> Result<(Vec, u64), Error> { + pub fn bytes(&self, reply_to: Recipient) -> Result { match self.version() { - AuthenticatorVersion::V1 => Err(Error::UnsupportedVersion), - AuthenticatorVersion::V2 => { - use v2::{ - registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, - request::AuthenticatorRequest, - }; - match self { - ClientMessage::Initial(init_message) => { - let (req, id) = AuthenticatorRequest::new_initial_request( - InitMessage { - pub_key: init_message.pub_key(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Final(final_message) => { - let (req, id) = AuthenticatorRequest::new_final_request( - FinalMessage { - gateway_client: GatewayClient { - pub_key: final_message.gateway_client_pub_key(), - private_ip: final_message - .gateway_client_ipv4() - .ok_or(Error::UnsupportedMessage)? - .into(), - mac: ClientMac::new(final_message.gateway_client_mac()), - }, - credential: final_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Query(query_message) => { - let (req, id) = AuthenticatorRequest::new_query_request( - query_message.pub_key(), - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - _ => Err(Error::UnsupportedMessage), - } - } - AuthenticatorVersion::V3 => { - use v3::{ - registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, - request::AuthenticatorRequest, - topup::TopUpMessage, - }; - match self { - ClientMessage::Initial(init_message) => { - let (req, id) = AuthenticatorRequest::new_initial_request( - InitMessage { - pub_key: init_message.pub_key(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Final(final_message) => { - let (req, id) = AuthenticatorRequest::new_final_request( - FinalMessage { - gateway_client: GatewayClient { - pub_key: final_message.gateway_client_pub_key(), - private_ip: final_message - .gateway_client_ipv4() - .ok_or(Error::UnsupportedMessage)? - .into(), - mac: ClientMac::new(final_message.gateway_client_mac()), - }, - credential: final_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Query(query_message) => { - let (req, id) = AuthenticatorRequest::new_query_request( - query_message.pub_key(), - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::TopUp(top_up_message) => { - let (req, id) = AuthenticatorRequest::new_topup_request( - TopUpMessage { - pub_key: top_up_message.pub_key(), - credential: top_up_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - } - } - AuthenticatorVersion::V4 => { - use v4::{ - registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, - request::AuthenticatorRequest, - topup::TopUpMessage, - }; - match self { - ClientMessage::Initial(init_message) => { - let (req, id) = AuthenticatorRequest::new_initial_request( - InitMessage { - pub_key: init_message.pub_key(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Final(final_message) => { - let (req, id) = AuthenticatorRequest::new_final_request( - FinalMessage { - gateway_client: GatewayClient { - pub_key: final_message.gateway_client_pub_key(), - private_ips: IpPair { - ipv4: final_message - .gateway_client_ipv4() - .ok_or(Error::UnsupportedMessage)?, - ipv6: final_message - .gateway_client_ipv6() - .ok_or(Error::UnsupportedMessage)?, - } - .into(), - mac: ClientMac::new(final_message.gateway_client_mac()), - }, - credential: final_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Query(query_message) => { - let (req, id) = AuthenticatorRequest::new_query_request( - query_message.pub_key(), - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - ClientMessage::TopUp(top_up_message) => { - let (req, id) = AuthenticatorRequest::new_topup_request( - TopUpMessage { - pub_key: top_up_message.pub_key(), - credential: top_up_message.credential(), - }, - reply_to, - ); - Ok((req.to_bytes()?, id)) - } - } - } - AuthenticatorVersion::V5 => { - use v5::{ - registration::{ClientMac, FinalMessage, GatewayClient, InitMessage}, - request::AuthenticatorRequest, - topup::TopUpMessage, - }; - match self { - ClientMessage::Initial(init_message) => { - let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage { - pub_key: init_message.pub_key(), - }); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Final(final_message) => { - let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage { - gateway_client: GatewayClient { - pub_key: final_message.gateway_client_pub_key(), - private_ips: IpPair { - ipv4: final_message - .gateway_client_ipv4() - .ok_or(Error::UnsupportedMessage)?, - ipv6: final_message - .gateway_client_ipv6() - .ok_or(Error::UnsupportedMessage)?, - }, - mac: ClientMac::new(final_message.gateway_client_mac()), - }, - credential: final_message.credential(), - }); - Ok((req.to_bytes()?, id)) - } - ClientMessage::Query(query_message) => { - let (req, id) = - AuthenticatorRequest::new_query_request(query_message.pub_key()); - Ok((req.to_bytes()?, id)) - } - ClientMessage::TopUp(top_up_message) => { - let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage { - pub_key: top_up_message.pub_key(), - credential: top_up_message.credential(), - }); - Ok((req.to_bytes()?, id)) - } - } - } + AuthenticatorVersion::V1 => self.serialise_v1(), + AuthenticatorVersion::V2 => self.serialise_v2(reply_to), + AuthenticatorVersion::V3 => self.serialise_v3(reply_to), + AuthenticatorVersion::V4 => self.serialise_v4(reply_to), + AuthenticatorVersion::V5 => self.serialise_v5(), + AuthenticatorVersion::V6 => self.serialise_v6(), AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion), } } @@ -247,7 +346,7 @@ impl ClientMessage { use AuthenticatorVersion::*; match self.version() { V1 | V2 | V3 | V4 => false, - V5 => true, + V5 | V6 => true, UNKNOWN => true, } } diff --git a/common/authenticator-requests/src/error.rs b/common/authenticator-requests/src/error.rs index d940bd538d..cbf0cde523 100644 --- a/common/authenticator-requests/src/error.rs +++ b/common/authenticator-requests/src/error.rs @@ -1,6 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::fmt::Display; use thiserror::Error; #[derive(Debug, Error)] @@ -37,3 +38,13 @@ pub enum Error { #[error(transparent)] Bincode(#[from] bincode::Error), } + +impl Error { + pub fn conversion(msg: impl Into) -> Self { + Error::Conversion(msg.into()) + } + + pub fn conversion_display(msg: impl Display) -> Self { + Error::Conversion(msg.to_string()) + } +} diff --git a/common/authenticator-requests/src/lib.rs b/common/authenticator-requests/src/lib.rs index b1b0159da3..226c78adee 100644 --- a/common/authenticator-requests/src/lib.rs +++ b/common/authenticator-requests/src/lib.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod client_message; +pub mod models; pub mod request; pub mod response; pub mod traits; @@ -10,13 +11,14 @@ pub mod v2; pub mod v3; pub mod v4; pub mod v5; +pub mod v6; mod error; mod util; mod version; pub use error::Error; -pub use v5 as latest; +pub use v6 as latest; pub use version::AuthenticatorVersion; pub const CURRENT_VERSION: u8 = latest::VERSION; diff --git a/common/authenticator-requests/src/models.rs b/common/authenticator-requests/src/models.rs new file mode 100644 index 0000000000..c01eec98f4 --- /dev/null +++ b/common/authenticator-requests/src/models.rs @@ -0,0 +1,58 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::{ + BandwidthCredential, CredentialSpendingData, TicketType, UnknownTicketType, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] +pub enum CurrentUpgradeModeStatus { + Enabled, + Disabled, + // everything pre-v6 + Unknown, +} + +impl CurrentUpgradeModeStatus { + pub fn is_enabled(&self) -> bool { + matches!(self, CurrentUpgradeModeStatus::Enabled) + } +} + +impl From for CurrentUpgradeModeStatus { + fn from(value: bool) -> Self { + if value { + CurrentUpgradeModeStatus::Enabled + } else { + CurrentUpgradeModeStatus::Disabled + } + } +} + +impl From for Option { + fn from(value: CurrentUpgradeModeStatus) -> Self { + match value { + CurrentUpgradeModeStatus::Enabled => Some(true), + CurrentUpgradeModeStatus::Disabled => Some(false), + CurrentUpgradeModeStatus::Unknown => None, + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct BandwidthClaim { + pub credential: BandwidthCredential, + pub kind: TicketType, +} + +impl TryFrom for BandwidthClaim { + type Error = UnknownTicketType; + + fn try_from(credential: CredentialSpendingData) -> Result { + Ok(BandwidthClaim { + kind: TicketType::try_from_encoded(credential.payment.t_type)?, + credential: BandwidthCredential::from(credential), + }) + } +} diff --git a/common/authenticator-requests/src/request.rs b/common/authenticator-requests/src/request.rs index 3d98a8ed5e..c1585b9bb6 100644 --- a/common/authenticator-requests/src/request.rs +++ b/common/authenticator-requests/src/request.rs @@ -4,8 +4,10 @@ use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::addressing::Recipient; -use crate::traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage}; -use crate::{v1, v2, v3, v4, v5}; +use crate::traits::{ + FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, UpgradeModeMessage, +}; +use crate::{v1, v2, v3, v4, v5, v6}; #[derive(Debug)] pub enum AuthenticatorRequest { @@ -33,6 +35,11 @@ pub enum AuthenticatorRequest { reply_to: Option, request_id: u64, }, + CheckUpgradeMode { + msg: Box, + protocol: Protocol, + request_id: u64, + }, } impl From for AuthenticatorRequest { @@ -202,3 +209,45 @@ impl From for AuthenticatorRequest { } } } + +impl From for AuthenticatorRequest { + fn from(value: v6::request::AuthenticatorRequest) -> Self { + match value.data { + v6::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + }, + v6::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + }, + v6::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + } + } + v6::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + Self::TopUpBandwidth { + msg: top_up_message, + protocol: value.protocol, + reply_to: None, + request_id: value.request_id, + } + } + v6::request::AuthenticatorRequestData::CheckUpgradeMode(upgrade_mode_check_msg) => { + Self::CheckUpgradeMode { + msg: Box::new(upgrade_mode_check_msg), + protocol: value.protocol, + request_id: value.request_id, + } + } + } + } +} diff --git a/common/authenticator-requests/src/response.rs b/common/authenticator-requests/src/response.rs index 8e196a7b72..84b6ae8526 100644 --- a/common/authenticator-requests/src/response.rs +++ b/common/authenticator-requests/src/response.rs @@ -1,11 +1,12 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::models::CurrentUpgradeModeStatus; use crate::traits::{ Id, PendingRegistrationResponse, RegisteredResponse, RemainingBandwidthResponse, - TopUpBandwidthResponse, + TopUpBandwidthResponse, UpgradeModeStatus, }; -use crate::{v2, v3, v4, v5}; +use crate::{v2, v3, v4, v5, v6}; #[derive(Debug)] pub enum AuthenticatorResponse { @@ -13,6 +14,29 @@ pub enum AuthenticatorResponse { Registered(Box), RemainingBandwidth(Box), TopUpBandwidth(Box), + UpgradeMode(Box), +} + +impl UpgradeModeStatus for AuthenticatorResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + match self { + AuthenticatorResponse::PendingRegistration(pending_registration_response) => { + pending_registration_response.upgrade_mode_status() + } + AuthenticatorResponse::Registered(registered_response) => { + registered_response.upgrade_mode_status() + } + AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => { + remaining_bandwidth_response.upgrade_mode_status() + } + AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { + top_up_bandwidth_response.upgrade_mode_status() + } + AuthenticatorResponse::UpgradeMode(upgrade_mode_response) => { + upgrade_mode_response.upgrade_mode_status() + } + } + } } impl Id for AuthenticatorResponse { @@ -28,6 +52,7 @@ impl Id for AuthenticatorResponse { AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { top_up_bandwidth_response.id() } + AuthenticatorResponse::UpgradeMode(upgrade_mode_response) => upgrade_mode_response.id(), } } } @@ -104,3 +129,25 @@ impl From for AuthenticatorResponse { } } } + +impl From for AuthenticatorResponse { + fn from(value: v6::response::AuthenticatorResponse) -> Self { + match value.data { + v6::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(Box::new(pending_registration_response)), + v6::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(Box::new(registered_response)) + } + v6::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)), + v6::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => { + Self::TopUpBandwidth(Box::new(top_up_bandwidth_response)) + } + v6::response::AuthenticatorResponseData::UpgradeMode(upgrade_mode_check_response) => { + Self::UpgradeMode(Box::new(upgrade_mode_check_response)) + } + } + } +} diff --git a/common/authenticator-requests/src/traits.rs b/common/authenticator-requests/src/traits.rs index 36e999383d..e16c0d96ba 100644 --- a/common/authenticator-requests/src/traits.rs +++ b/common/authenticator-requests/src/traits.rs @@ -1,15 +1,15 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::latest::registration::IpPair; +use crate::models::{BandwidthClaim, CurrentUpgradeModeStatus}; +use crate::{AuthenticatorVersion, Error, v1, v2, v3, v4, v5, v6}; +use nym_credentials_interface::CredentialSpendingData; +use nym_crypto::asymmetric::x25519; +use nym_wireguard_types::PeerPublicKey; use std::fmt; use std::net::{Ipv4Addr, Ipv6Addr}; - -use nym_credentials_interface::CredentialSpendingData; -use nym_crypto::asymmetric::x25519::PrivateKey; -use nym_wireguard_types::PeerPublicKey; - -use crate::latest::registration::IpPair; -use crate::{AuthenticatorVersion, Error, v1, v2, v3, v4, v5}; +use tracing::error; pub trait Versionable { fn version(&self) -> AuthenticatorVersion; @@ -51,6 +51,12 @@ impl Versionable for v5::registration::InitMessage { } } +impl Versionable for v6::registration::InitMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V6 + } +} + impl Versionable for v2::registration::FinalMessage { fn version(&self) -> AuthenticatorVersion { AuthenticatorVersion::V2 @@ -75,6 +81,12 @@ impl Versionable for v5::registration::FinalMessage { } } +impl Versionable for v6::registration::FinalMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V6 + } +} + impl Versionable for PeerPublicKey { fn version(&self) -> AuthenticatorVersion { AuthenticatorVersion::V3 @@ -98,6 +110,158 @@ impl Versionable for v5::topup::TopUpMessage { AuthenticatorVersion::V5 } } +impl Versionable for v6::topup::TopUpMessage { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V6 + } +} + +impl Versionable for v6::upgrade_mode_check::UpgradeModeCheckRequest { + fn version(&self) -> AuthenticatorVersion { + AuthenticatorVersion::V6 + } +} + +pub trait UpgradeModeStatus: Id + fmt::Debug { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus; +} + +impl UpgradeModeStatus for v1::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v1::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v1::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v2::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v2::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v2::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} +impl UpgradeModeStatus for v3::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v3::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v3::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v3::response::TopUpBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v4::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v4::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v4::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v4::response::TopUpBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v5::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v5::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v5::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v5::response::TopUpBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + CurrentUpgradeModeStatus::Unknown + } +} + +impl UpgradeModeStatus for v6::response::PendingRegistrationResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} + +impl UpgradeModeStatus for v6::response::RegisteredResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} + +impl UpgradeModeStatus for v6::response::RemainingBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} + +impl UpgradeModeStatus for v6::response::TopUpBandwidthResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} + +impl UpgradeModeStatus for v6::response::UpgradeModeResponse { + fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus { + self.upgrade_mode_enabled.into() + } +} pub trait InitMessage: Versionable + fmt::Debug { fn pub_key(&self) -> PeerPublicKey; @@ -133,14 +297,20 @@ impl InitMessage for v5::registration::InitMessage { } } +impl InitMessage for v6::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + pub trait FinalMessage: Versionable + fmt::Debug { fn gateway_client_pub_key(&self) -> PeerPublicKey; - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>; + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error>; fn private_ips(&self) -> IpPair; fn gateway_client_ipv4(&self) -> Option; fn gateway_client_ipv6(&self) -> Option; fn gateway_client_mac(&self) -> Vec; - fn credential(&self) -> Option; + fn credential(&self) -> Option; } impl FinalMessage for v1::GatewayClient { @@ -148,7 +318,7 @@ impl FinalMessage for v1::GatewayClient { self.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.verify(private_key, nonce) } @@ -171,7 +341,7 @@ impl FinalMessage for v1::GatewayClient { self.mac.to_vec() } - fn credential(&self) -> Option { + fn credential(&self) -> Option { None } } @@ -181,7 +351,7 @@ impl FinalMessage for v2::registration::FinalMessage { self.gateway_client.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.gateway_client.verify(private_key, nonce) } @@ -204,8 +374,12 @@ impl FinalMessage for v2::registration::FinalMessage { self.gateway_client.mac.to_vec() } - fn credential(&self) -> Option { - self.credential.clone() + fn credential(&self) -> Option { + self.credential.clone().and_then(|c| { + c.try_into() + .inspect_err(|err| error!("credential conversion error: {err}")) + .ok() + }) } } @@ -214,7 +388,7 @@ impl FinalMessage for v3::registration::FinalMessage { self.gateway_client.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.gateway_client.verify(private_key, nonce) } @@ -237,8 +411,12 @@ impl FinalMessage for v3::registration::FinalMessage { self.gateway_client.mac.to_vec() } - fn credential(&self) -> Option { - self.credential.clone() + fn credential(&self) -> Option { + self.credential.clone().and_then(|c| { + c.try_into() + .inspect_err(|err| error!("credential conversion error: {err}")) + .ok() + }) } } @@ -247,7 +425,42 @@ impl FinalMessage for v4::registration::FinalMessage { self.gateway_client.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { + self.gateway_client.verify(private_key, nonce) + } + + fn private_ips(&self) -> IpPair { + // v4 -> v5 -> v6 + v5::registration::IpPair::from(self.gateway_client.private_ips).into() + } + + fn gateway_client_ipv4(&self) -> Option { + Some(self.gateway_client.private_ips.ipv4) + } + + fn gateway_client_ipv6(&self) -> Option { + Some(self.gateway_client.private_ips.ipv6) + } + + fn gateway_client_mac(&self) -> Vec { + self.gateway_client.mac.to_vec() + } + + fn credential(&self) -> Option { + self.credential.clone().and_then(|c| { + c.try_into() + .inspect_err(|err| error!("credential conversion error: {err}")) + .ok() + }) + } +} + +impl FinalMessage for v5::registration::FinalMessage { + fn gateway_client_pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.gateway_client.verify(private_key, nonce) } @@ -267,17 +480,21 @@ impl FinalMessage for v4::registration::FinalMessage { self.gateway_client.mac.to_vec() } - fn credential(&self) -> Option { - self.credential.clone() + fn credential(&self) -> Option { + self.credential.clone().and_then(|c| { + c.try_into() + .inspect_err(|err| error!("credential conversion error: {err}")) + .ok() + }) } } -impl FinalMessage for v5::registration::FinalMessage { +impl FinalMessage for v6::registration::FinalMessage { fn gateway_client_pub_key(&self) -> PeerPublicKey { self.gateway_client.pub_key } - fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> { self.gateway_client.verify(private_key, nonce) } @@ -297,7 +514,7 @@ impl FinalMessage for v5::registration::FinalMessage { self.gateway_client.mac.to_vec() } - fn credential(&self) -> Option { + fn credential(&self) -> Option { self.credential.clone() } } @@ -347,10 +564,42 @@ impl TopUpMessage for v5::topup::TopUpMessage { } } +impl TopUpMessage for v6::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + +pub trait UpgradeModeMessage: Versionable + fmt::Debug { + // the idea is to expose different types of emergency credentials here, + // like upgrade mode JWT, emergency threshold credential issued by signers, etc. + fn upgrade_mode_global_attestation_jwt(&self) -> Option; +} + +impl UpgradeModeMessage for v6::upgrade_mode_check::UpgradeModeCheckRequest { + fn upgrade_mode_global_attestation_jwt(&self) -> Option { + use v6::upgrade_mode_check::UpgradeModeCheckRequest; + + match self { + UpgradeModeCheckRequest::UpgradeModeJwt { token } => Some(token.clone()), + } + } +} + pub trait Id { fn id(&self) -> u64; } +impl Id for v1::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + impl Id for v2::response::PendingRegistrationResponse { fn id(&self) -> u64 { self.request_id @@ -375,6 +624,18 @@ impl Id for v5::response::PendingRegistrationResponse { } } +impl Id for v6::response::PendingRegistrationResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v1::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + impl Id for v2::response::RegisteredResponse { fn id(&self) -> u64 { self.request_id @@ -399,6 +660,18 @@ impl Id for v5::response::RegisteredResponse { } } +impl Id for v6::response::RegisteredResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v1::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + impl Id for v2::response::RemainingBandwidthResponse { fn id(&self) -> u64 { self.request_id @@ -423,6 +696,12 @@ impl Id for v5::response::RemainingBandwidthResponse { } } +impl Id for v6::response::RemainingBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + impl Id for v3::response::TopUpBandwidthResponse { fn id(&self) -> u64 { self.request_id @@ -441,11 +720,28 @@ impl Id for v5::response::TopUpBandwidthResponse { } } -pub trait PendingRegistrationResponse: Id + fmt::Debug { +impl Id for v6::response::TopUpBandwidthResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +impl Id for v6::response::UpgradeModeResponse { + fn id(&self) -> u64 { + self.request_id + } +} + +pub trait PendingRegistrationResponse: Id + UpgradeModeStatus + fmt::Debug { fn nonce(&self) -> u64; - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error>; + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error>; fn pub_key(&self) -> PeerPublicKey; fn private_ips(&self) -> IpPair; + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box; } impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse { @@ -453,7 +749,7 @@ impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse { self.reply.nonce } - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> { + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { self.reply.gateway_data.verify(gateway_key, self.nonce()) } @@ -464,6 +760,22 @@ impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse { fn private_ips(&self) -> IpPair { self.reply.gateway_data.private_ip.into() } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v2::registration::FinalMessage { + gateway_client: v2::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.private_ips().ipv4.into(), + self.nonce(), + ), + credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)), + }) + } } impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse { @@ -471,7 +783,7 @@ impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse { self.reply.nonce } - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> { + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { self.reply.gateway_data.verify(gateway_key, self.nonce()) } @@ -482,6 +794,22 @@ impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse { fn private_ips(&self) -> IpPair { self.reply.gateway_data.private_ip.into() } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v3::registration::FinalMessage { + gateway_client: v3::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.private_ips().ipv4.into(), + self.nonce(), + ), + credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)), + }) + } } impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse { @@ -489,7 +817,42 @@ impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse { self.reply.nonce } - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> { + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { + self.reply.gateway_data.verify(gateway_key, self.nonce()) + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.gateway_data.pub_key + } + + fn private_ips(&self) -> IpPair { + // v4 -> v5 -> v6 + v5::registration::IpPair::from(self.reply.gateway_data.private_ips).into() + } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v4::registration::FinalMessage { + gateway_client: v4::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.reply.gateway_data.private_ips, + self.nonce(), + ), + credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)), + }) + } +} + +impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse { + fn nonce(&self) -> u64 { + self.reply.nonce + } + + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { self.reply.gateway_data.verify(gateway_key, self.nonce()) } @@ -500,14 +863,30 @@ impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse { fn private_ips(&self) -> IpPair { self.reply.gateway_data.private_ips.into() } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v5::registration::FinalMessage { + gateway_client: v5::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.reply.gateway_data.private_ips, + self.nonce(), + ), + credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)), + }) + } } -impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse { +impl PendingRegistrationResponse for v6::response::PendingRegistrationResponse { fn nonce(&self) -> u64 { self.reply.nonce } - fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> { + fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> { self.reply.gateway_data.verify(gateway_key, self.nonce()) } @@ -518,9 +897,25 @@ impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse { fn private_ips(&self) -> IpPair { self.reply.gateway_data.private_ips } + + fn finalise_registration( + &self, + private_key: &x25519::PrivateKey, + credential: Option, + ) -> Box { + Box::new(v6::registration::FinalMessage { + gateway_client: v6::registration::GatewayClient::new( + private_key, + self.pub_key().inner(), + self.reply.gateway_data.private_ips, + self.nonce(), + ), + credential, + }) + } } -pub trait RegisteredResponse: Id + fmt::Debug { +pub trait RegisteredResponse: Id + UpgradeModeStatus + fmt::Debug { fn private_ips(&self) -> IpPair; fn pub_key(&self) -> PeerPublicKey; fn wg_port(&self) -> u16; @@ -555,7 +950,8 @@ impl RegisteredResponse for v3::response::RegisteredResponse { } impl RegisteredResponse for v4::response::RegisteredResponse { fn private_ips(&self) -> IpPair { - self.reply.private_ips.into() + // v4 -> v5 -> v6 + v5::registration::IpPair::from(self.reply.private_ips).into() } fn pub_key(&self) -> PeerPublicKey { @@ -568,6 +964,20 @@ impl RegisteredResponse for v4::response::RegisteredResponse { } impl RegisteredResponse for v5::response::RegisteredResponse { + fn private_ips(&self) -> IpPair { + self.reply.private_ips.into() + } + + fn pub_key(&self) -> PeerPublicKey { + self.reply.pub_key + } + + fn wg_port(&self) -> u16 { + self.reply.wg_port + } +} + +impl RegisteredResponse for v6::response::RegisteredResponse { fn private_ips(&self) -> IpPair { self.reply.private_ips } @@ -581,7 +991,7 @@ impl RegisteredResponse for v5::response::RegisteredResponse { } } -pub trait RemainingBandwidthResponse: Id + fmt::Debug { +pub trait RemainingBandwidthResponse: Id + UpgradeModeStatus + fmt::Debug { fn available_bandwidth(&self) -> Option; } @@ -609,7 +1019,13 @@ impl RemainingBandwidthResponse for v5::response::RemainingBandwidthResponse { } } -pub trait TopUpBandwidthResponse: Id + fmt::Debug { +impl RemainingBandwidthResponse for v6::response::RemainingBandwidthResponse { + fn available_bandwidth(&self) -> Option { + self.reply.as_ref().map(|r| r.available_bandwidth) + } +} + +pub trait TopUpBandwidthResponse: Id + UpgradeModeStatus + fmt::Debug { fn available_bandwidth(&self) -> i64; } @@ -630,3 +1046,9 @@ impl TopUpBandwidthResponse for v5::response::TopUpBandwidthResponse { self.reply.available_bandwidth } } + +impl TopUpBandwidthResponse for v6::response::TopUpBandwidthResponse { + fn available_bandwidth(&self) -> i64 { + self.reply.available_bandwidth + } +} diff --git a/common/authenticator-requests/src/v1/registration.rs b/common/authenticator-requests/src/v1/registration.rs index 6807325921..aa79910972 100644 --- a/common/authenticator-requests/src/v1/registration.rs +++ b/common/authenticator-requests/src/v1/registration.rs @@ -48,7 +48,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ip: IpAddr, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v1/response.rs b/common/authenticator-requests/src/v1/response.rs index 4e7ba61eb4..f4ff8f5da3 100644 --- a/common/authenticator-requests/src/v1/response.rs +++ b/common/authenticator-requests/src/v1/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_sphinx::addressing::Recipient; use serde::{Deserialize, Serialize}; @@ -34,7 +34,7 @@ impl AuthenticatorResponse { } pub fn new_registered( - registred_data: RegistredData, + registred_data: RegisteredData, reply_to: Recipient, request_id: u64, ) -> Self { @@ -108,7 +108,7 @@ pub struct PendingRegistrationResponse { pub struct RegisteredResponse { pub request_id: u64, pub reply_to: Recipient, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/common/authenticator-requests/src/v2/conversion.rs b/common/authenticator-requests/src/v2/conversion.rs index b8e16ac4ba..36dceaf3ff 100644 --- a/common/authenticator-requests/src/v2/conversion.rs +++ b/common/authenticator-requests/src/v2/conversion.rs @@ -154,8 +154,8 @@ impl From for v1::registration::Registration } } -impl From for v1::registration::RegistredData { - fn from(value: v2::registration::RegistredData) -> Self { +impl From for v1::registration::RegisteredData { + fn from(value: v2::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ip: value.private_ip, diff --git a/common/authenticator-requests/src/v2/registration.rs b/common/authenticator-requests/src/v2/registration.rs index a8d5f5e089..34d3b7f4e0 100644 --- a/common/authenticator-requests/src/v2/registration.rs +++ b/common/authenticator-requests/src/v2/registration.rs @@ -58,7 +58,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ip: IpAddr, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v2/response.rs b/common/authenticator-requests/src/v2/response.rs index 1b389de43f..33da1b975d 100644 --- a/common/authenticator-requests/src/v2/response.rs +++ b/common/authenticator-requests/src/v2/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::addressing::Recipient; use serde::{Deserialize, Serialize}; @@ -38,7 +38,7 @@ impl AuthenticatorResponse { } pub fn new_registered( - registred_data: RegistredData, + registred_data: RegisteredData, reply_to: Recipient, request_id: u64, ) -> Self { @@ -118,7 +118,7 @@ pub struct PendingRegistrationResponse { pub struct RegisteredResponse { pub request_id: u64, pub reply_to: Recipient, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/common/authenticator-requests/src/v3/conversion.rs b/common/authenticator-requests/src/v3/conversion.rs index 5a49c771cd..d04bb6134f 100644 --- a/common/authenticator-requests/src/v3/conversion.rs +++ b/common/authenticator-requests/src/v3/conversion.rs @@ -299,8 +299,8 @@ impl From for v3::registration::Registration } } -impl From for v2::registration::RegistredData { - fn from(value: v3::registration::RegistredData) -> Self { +impl From for v2::registration::RegisteredData { + fn from(value: v3::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ip: value.private_ip, @@ -309,8 +309,8 @@ impl From for v2::registration::RegistredData { } } -impl From for v3::registration::RegistredData { - fn from(value: v2::registration::RegistredData) -> Self { +impl From for v3::registration::RegisteredData { + fn from(value: v2::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ip: value.private_ip, @@ -674,7 +674,7 @@ mod tests { let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); let private_ip = IpAddr::from_str("10.10.10.10").unwrap(); let wg_port = 51822; - let registred_data = v2::registration::RegistredData { + let registred_data = v2::registration::RegisteredData { pub_key, private_ip, wg_port, @@ -701,7 +701,7 @@ mod tests { v3::response::AuthenticatorResponseData::Registered(v3::response::RegisteredResponse { request_id, reply_to, - reply: v3::registration::RegistredData { + reply: v3::registration::RegisteredData { wg_port, pub_key, private_ip @@ -715,7 +715,7 @@ mod tests { let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); let private_ip = IpAddr::from_str("10.10.10.10").unwrap(); let wg_port = 51822; - let registred_data = v3::registration::RegistredData { + let registred_data = v3::registration::RegisteredData { pub_key, private_ip, wg_port, @@ -742,7 +742,7 @@ mod tests { v2::response::AuthenticatorResponseData::Registered(v2::response::RegisteredResponse { request_id, reply_to, - reply: v2::registration::RegistredData { + reply: v2::registration::RegisteredData { wg_port, pub_key, private_ip diff --git a/common/authenticator-requests/src/v3/registration.rs b/common/authenticator-requests/src/v3/registration.rs index 00cb146772..66b02c5ed2 100644 --- a/common/authenticator-requests/src/v3/registration.rs +++ b/common/authenticator-requests/src/v3/registration.rs @@ -58,7 +58,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ip: IpAddr, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v3/response.rs b/common/authenticator-requests/src/v3/response.rs index ca44fb19f6..4fd0a9729b 100644 --- a/common/authenticator-requests/src/v3/response.rs +++ b/common/authenticator-requests/src/v3/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::addressing::Recipient; use serde::{Deserialize, Serialize}; @@ -38,7 +38,7 @@ impl AuthenticatorResponse { } pub fn new_registered( - registred_data: RegistredData, + registred_data: RegisteredData, reply_to: Recipient, request_id: u64, ) -> Self { @@ -139,7 +139,7 @@ pub struct PendingRegistrationResponse { pub struct RegisteredResponse { pub request_id: u64, pub reply_to: Recipient, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/common/authenticator-requests/src/v4/conversion.rs b/common/authenticator-requests/src/v4/conversion.rs index 8731222446..3b2cf9a8f2 100644 --- a/common/authenticator-requests/src/v4/conversion.rs +++ b/common/authenticator-requests/src/v4/conversion.rs @@ -262,8 +262,8 @@ impl From for v3::response::TopUpBandwidth } } -impl From for v4::registration::RegistredData { - fn from(value: v3::registration::RegistredData) -> Self { +impl From for v4::registration::RegisteredData { + fn from(value: v3::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ips: value.private_ip.into(), @@ -272,8 +272,8 @@ impl From for v4::registration::RegistredData { } } -impl From for v3::registration::RegistredData { - fn from(value: v4::registration::RegistredData) -> Self { +impl From for v3::registration::RegisteredData { + fn from(value: v4::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ip: value.private_ips.ipv4.into(), @@ -565,7 +565,7 @@ mod tests { let private_ips = v4::registration::IpPair::new(ipv4, Ipv6Addr::from_str("fc01::a0a").unwrap()); let wg_port = 51822; - let registred_data = v3::registration::RegistredData { + let registred_data = v3::registration::RegisteredData { pub_key, private_ip: ipv4.into(), wg_port, @@ -592,7 +592,7 @@ mod tests { v4::response::AuthenticatorResponseData::Registered(v4::response::RegisteredResponse { request_id, reply_to, - reply: v4::registration::RegistredData { + reply: v4::registration::RegisteredData { wg_port, pub_key, private_ips @@ -608,7 +608,7 @@ mod tests { let private_ips = v4::registration::IpPair::new(ipv4, Ipv6Addr::from_str("fc01::10").unwrap()); let wg_port = 51822; - let registred_data = v4::registration::RegistredData { + let registred_data = v4::registration::RegisteredData { pub_key, private_ips, wg_port, @@ -635,7 +635,7 @@ mod tests { v3::response::AuthenticatorResponseData::Registered(v3::response::RegisteredResponse { request_id, reply_to, - reply: v3::registration::RegistredData { + reply: v3::registration::RegisteredData { wg_port, pub_key, private_ip: ipv4.into() diff --git a/common/authenticator-requests/src/v4/registration.rs b/common/authenticator-requests/src/v4/registration.rs index a383b79beb..b1ee074dfd 100644 --- a/common/authenticator-requests/src/v4/registration.rs +++ b/common/authenticator-requests/src/v4/registration.rs @@ -110,7 +110,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ips: IpPair, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v4/response.rs b/common/authenticator-requests/src/v4/response.rs index 9743e8db43..1bbf4557e9 100644 --- a/common/authenticator-requests/src/v4/response.rs +++ b/common/authenticator-requests/src/v4/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use nym_sphinx::addressing::Recipient; use serde::{Deserialize, Serialize}; @@ -38,7 +38,7 @@ impl AuthenticatorResponse { } pub fn new_registered( - registred_data: RegistredData, + registred_data: RegisteredData, reply_to: Recipient, request_id: u64, ) -> Self { @@ -139,7 +139,7 @@ pub struct PendingRegistrationResponse { pub struct RegisteredResponse { pub request_id: u64, pub reply_to: Recipient, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/common/authenticator-requests/src/v5/conversion.rs b/common/authenticator-requests/src/v5/conversion.rs index 77ed294323..f2287c03bd 100644 --- a/common/authenticator-requests/src/v5/conversion.rs +++ b/common/authenticator-requests/src/v5/conversion.rs @@ -186,8 +186,8 @@ impl From for v5::response::TopUpBandwidth } } -impl From for v5::registration::RegistredData { - fn from(value: v4::registration::RegistredData) -> Self { +impl From for v5::registration::RegisteredData { + fn from(value: v4::registration::RegisteredData) -> Self { Self { pub_key: value.pub_key, private_ips: value.private_ips.into(), @@ -405,7 +405,7 @@ mod tests { let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); let private_ips = v4::registration::IpPair::new(ipv4, ipv6); let wg_port = 51822; - let registred_data = v4::registration::RegistredData { + let registred_data = v4::registration::RegisteredData { pub_key, private_ips, wg_port, @@ -431,7 +431,7 @@ mod tests { upgraded_msg.data, v5::response::AuthenticatorResponseData::Registered(v5::response::RegisteredResponse { request_id, - reply: v5::registration::RegistredData { + reply: v5::registration::RegisteredData { wg_port, pub_key, private_ips: v5::registration::IpPair::new(ipv4, ipv6) diff --git a/common/authenticator-requests/src/v5/registration.rs b/common/authenticator-requests/src/v5/registration.rs index 151401da97..5154400f93 100644 --- a/common/authenticator-requests/src/v5/registration.rs +++ b/common/authenticator-requests/src/v5/registration.rs @@ -108,7 +108,7 @@ pub struct RegistrationData { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct RegistredData { +pub struct RegisteredData { pub pub_key: PeerPublicKey, pub private_ips: IpPair, pub wg_port: u16, diff --git a/common/authenticator-requests/src/v5/response.rs b/common/authenticator-requests/src/v5/response.rs index 044b803d0d..b26fcf4627 100644 --- a/common/authenticator-requests/src/v5/response.rs +++ b/common/authenticator-requests/src/v5/response.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; use serde::{Deserialize, Serialize}; @@ -32,7 +32,7 @@ impl AuthenticatorResponse { } } - pub fn new_registered(registred_data: RegistredData, request_id: u64) -> Self { + pub fn new_registered(registred_data: RegisteredData, request_id: u64) -> Self { Self { protocol: Protocol { service_provider_type: ServiceProviderType::Authenticator, @@ -116,7 +116,7 @@ pub struct PendingRegistrationResponse { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct RegisteredResponse { pub request_id: u64, - pub reply: RegistredData, + pub reply: RegisteredData, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/common/authenticator-requests/src/v6/conversion.rs b/common/authenticator-requests/src/v6/conversion.rs new file mode 100644 index 0000000000..8bcc204d7c --- /dev/null +++ b/common/authenticator-requests/src/v6/conversion.rs @@ -0,0 +1,441 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{v5, v6}; + +impl TryFrom for v6::request::AuthenticatorRequest { + type Error = crate::Error; + + fn try_from( + authenticator_request: v5::request::AuthenticatorRequest, + ) -> Result { + Ok(Self { + protocol: v6::PROTOCOL, + data: authenticator_request.data.try_into()?, + request_id: authenticator_request.request_id, + }) + } +} + +impl TryFrom for v6::request::AuthenticatorRequestData { + type Error = crate::Error; + + fn try_from( + authenticator_request_data: v5::request::AuthenticatorRequestData, + ) -> Result { + match authenticator_request_data { + v5::request::AuthenticatorRequestData::Initial(init_msg) => Ok( + v6::request::AuthenticatorRequestData::Initial(init_msg.into()), + ), + v5::request::AuthenticatorRequestData::Final(final_msg) => Ok( + v6::request::AuthenticatorRequestData::Final(Box::new((*final_msg).try_into()?)), + ), + v5::request::AuthenticatorRequestData::QueryBandwidth(pub_key) => Ok( + v6::request::AuthenticatorRequestData::QueryBandwidth(pub_key), + ), + v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => Ok( + v6::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message.into()), + ), + } + } +} + +impl From for v6::registration::InitMessage { + fn from(init_msg: v5::registration::InitMessage) -> Self { + Self { + pub_key: init_msg.pub_key, + } + } +} + +impl TryFrom for v6::registration::FinalMessage { + type Error = crate::Error; + + fn try_from(final_msg: v5::registration::FinalMessage) -> Result { + Ok(Self { + gateway_client: final_msg.gateway_client.into(), + credential: final_msg + .credential + .map(TryInto::try_into) + .transpose() + .map_err(Self::Error::conversion_display)?, + }) + } +} + +impl From for v6::registration::GatewayClient { + fn from(gateway_client: v5::registration::GatewayClient) -> Self { + Self { + pub_key: gateway_client.pub_key, + private_ips: gateway_client.private_ips.into(), + mac: gateway_client.mac.into(), + } + } +} + +impl From for v5::registration::GatewayClient { + fn from(gateway_client: v6::registration::GatewayClient) -> Self { + Self { + pub_key: gateway_client.pub_key, + private_ips: gateway_client.private_ips.into(), + mac: gateway_client.mac.into(), + } + } +} + +impl From for v6::registration::ClientMac { + fn from(client_mac: v5::registration::ClientMac) -> Self { + Self::new((*client_mac).clone()) + } +} + +impl From for v5::registration::ClientMac { + fn from(client_mac: v6::registration::ClientMac) -> Self { + Self::new((*client_mac).clone()) + } +} + +impl From> for Box { + fn from(top_up_message: Box) -> Self { + Box::new(v6::topup::TopUpMessage { + pub_key: top_up_message.pub_key, + credential: top_up_message.credential, + }) + } +} + +impl From for v6::response::AuthenticatorResponse { + fn from(value: v5::response::AuthenticatorResponse) -> Self { + Self { + protocol: v6::PROTOCOL, + data: value.data.into(), + } + } +} + +impl From for v6::response::AuthenticatorResponseData { + fn from(authenticator_response_data: v5::response::AuthenticatorResponseData) -> Self { + match authenticator_response_data { + v5::response::AuthenticatorResponseData::PendingRegistration(pending_response) => { + v6::response::AuthenticatorResponseData::PendingRegistration( + pending_response.into(), + ) + } + v5::response::AuthenticatorResponseData::Registered(registered_response) => { + v6::response::AuthenticatorResponseData::Registered(registered_response.into()) + } + v5::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => v6::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response.into(), + ), + v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response) => { + v6::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response.into()) + } + } + } +} + +impl From for v6::response::RegisteredResponse { + fn from(value: v5::response::RegisteredResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + upgrade_mode_enabled: false, + } + } +} + +impl From for v6::response::PendingRegistrationResponse { + fn from(value: v5::response::PendingRegistrationResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + upgrade_mode_enabled: false, + } + } +} + +impl From for v6::registration::RegistrationData { + fn from(value: v5::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v5::registration::RegistrationData { + fn from(value: v6::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v6::response::RemainingBandwidthResponse { + fn from(value: v5::response::RemainingBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.map(Into::into), + upgrade_mode_enabled: false, + } + } +} + +impl From for v6::response::TopUpBandwidthResponse { + fn from(value: v5::response::TopUpBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply: value.reply.into(), + upgrade_mode_enabled: false, + } + } +} + +impl From for v6::registration::RegisteredData { + fn from(value: v5::registration::RegisteredData) -> Self { + Self { + pub_key: value.pub_key, + private_ips: value.private_ips.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v6::registration::RemainingBandwidthData { + fn from(value: v5::registration::RemainingBandwidthData) -> Self { + Self { + available_bandwidth: value.available_bandwidth, + } + } +} + +impl From for v6::registration::IpPair { + fn from(value: v5::registration::IpPair) -> Self { + Self { + ipv4: value.ipv4, + ipv6: value.ipv6, + } + } +} + +impl From for v5::registration::IpPair { + fn from(value: v6::registration::IpPair) -> Self { + Self { + ipv4: value.ipv4, + ipv6: value.ipv6, + } + } +} + +#[cfg(test)] +mod tests { + use std::{ + net::{Ipv4Addr, Ipv6Addr}, + str::FromStr, + }; + + use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData, TicketType}; + use nym_crypto::asymmetric::x25519::PrivateKey; + use nym_wireguard_types::PeerPublicKey; + use x25519_dalek::PublicKey; + + use super::*; + use crate::models::BandwidthClaim; + use crate::{util::tests::CREDENTIAL_BYTES, v5}; + + #[test] + fn upgrade_initial_req() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + + let (msg, _) = v5::request::AuthenticatorRequest::new_initial_request( + v5::registration::InitMessage::new(pub_key), + ); + let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap(); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::request::AuthenticatorRequestData::Initial(v6::registration::InitMessage { + pub_key + }) + ); + } + + #[test] + fn upgrade_final_req() { + let mut rng = rand::thread_rng(); + + let local_secret = PrivateKey::new(&mut rng); + let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let ips = v5::registration::IpPair::new(ipv4, ipv6); + let nonce = 42; + let gateway_client = v5::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + ips, + nonce, + ); + let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(); + let final_message = v5::registration::FinalMessage { + gateway_client: gateway_client.clone(), + credential: Some(credential.clone()), + }; + + let (msg, _) = v5::request::AuthenticatorRequest::new_final_request(final_message); + let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap(); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::request::AuthenticatorRequestData::Final(Box::new( + v6::registration::FinalMessage { + gateway_client: v6::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + v6::registration::IpPair::new(ipv4, ipv6), + nonce + ), + credential: Some(BandwidthClaim { + credential: BandwidthCredential::ZkNym(Box::new(credential)), + kind: TicketType::V1MixnetEntry, + }) + } + )) + ); + } + + #[test] + fn upgrade_query_req() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + + let (msg, _) = v5::request::AuthenticatorRequest::new_query_request(pub_key); + let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap(); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::request::AuthenticatorRequestData::QueryBandwidth(pub_key) + ); + } + + #[test] + fn upgrade_pending_reg_resp() { + let mut rng = rand::thread_rng(); + + let local_secret = PrivateKey::new(&mut rng); + let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let ips = v5::registration::IpPair::new(ipv4, ipv6); + let nonce = 42; + let wg_port = 51822; + let gateway_data = v5::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + ips, + nonce, + ); + let registration_data = v5::registration::RegistrationData { + nonce, + gateway_data, + wg_port, + }; + let request_id = 123; + + let msg = v5::response::AuthenticatorResponse::new_pending_registration_success( + registration_data, + request_id, + ); + let upgraded_msg = v6::response::AuthenticatorResponse::from(msg); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + + assert_eq!( + upgraded_msg.data, + v6::response::AuthenticatorResponseData::PendingRegistration( + v6::response::PendingRegistrationResponse { + request_id, + reply: v6::registration::RegistrationData { + nonce, + gateway_data: v6::registration::GatewayClient::new( + &local_secret, + (&remote_secret).into(), + v6::registration::IpPair::new(ipv4, ipv6), + nonce + ), + wg_port + }, + upgrade_mode_enabled: false, + } + ) + ); + } + + #[test] + fn upgrade_registered_resp() { + let pub_key = PeerPublicKey::new(PublicKey::from([0; 32])); + let ipv4 = Ipv4Addr::from_str("10.1.10.10").unwrap(); + let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap(); + let private_ips = v5::registration::IpPair::new(ipv4, ipv6); + let wg_port = 51822; + let registered_data = v5::registration::RegisteredData { + pub_key, + private_ips, + wg_port, + }; + let request_id = 123; + + let msg = v5::response::AuthenticatorResponse::new_registered(registered_data, request_id); + let upgraded_msg = v6::response::AuthenticatorResponse::from(msg); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::response::AuthenticatorResponseData::Registered(v6::response::RegisteredResponse { + request_id, + reply: v6::registration::RegisteredData { + wg_port, + pub_key, + private_ips: v6::registration::IpPair::new(ipv4, ipv6) + }, + upgrade_mode_enabled: false, + }) + ); + } + + #[test] + fn upgrade_remaining_bandwidth_resp() { + let available_bandwidth = 42; + let remaining_bandwidth_data = Some(v5::registration::RemainingBandwidthData { + available_bandwidth, + }); + let request_id = 123; + + let msg = v5::response::AuthenticatorResponse::new_remaining_bandwidth( + remaining_bandwidth_data, + request_id, + ); + let upgraded_msg = v6::response::AuthenticatorResponse::from(msg); + + assert_eq!(upgraded_msg.protocol, v6::PROTOCOL); + assert_eq!( + upgraded_msg.data, + v6::response::AuthenticatorResponseData::RemainingBandwidth( + v6::response::RemainingBandwidthResponse { + request_id, + reply: Some(v6::registration::RemainingBandwidthData { + available_bandwidth, + }), + upgrade_mode_enabled: false, + } + ) + ); + } +} diff --git a/common/authenticator-requests/src/v6/mod.rs b/common/authenticator-requests/src/v6/mod.rs new file mode 100644 index 0000000000..6fbc095ae9 --- /dev/null +++ b/common/authenticator-requests/src/v6/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; + +pub mod conversion; +pub mod registration; +pub mod request; +pub mod response; +pub mod topup; +pub mod upgrade_mode_check; + +pub const VERSION: u8 = 6; + +pub const PROTOCOL: Protocol = Protocol::new(VERSION, ServiceProviderType::Authenticator); diff --git a/common/authenticator-requests/src/v6/registration.rs b/common/authenticator-requests/src/v6/registration.rs new file mode 100644 index 0000000000..11fcf34116 --- /dev/null +++ b/common/authenticator-requests/src/v6/registration.rs @@ -0,0 +1,287 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Error; +use crate::models::BandwidthClaim; +use base64::{Engine, engine::general_purpose}; +use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6}; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use std::time::SystemTime; +use std::{fmt, ops::Deref, str::FromStr}; + +#[cfg(feature = "verify")] +use hmac::{Hmac, Mac}; +#[cfg(feature = "verify")] +use nym_crypto::asymmetric::x25519::{PrivateKey, PublicKey}; +#[cfg(feature = "verify")] +use sha2::Sha256; + +pub type PendingRegistrations = HashMap; +pub type PrivateIPs = HashMap; + +#[cfg(feature = "verify")] +pub type HmacSha256 = Hmac; + +pub type Nonce = u64; +pub type Taken = Option; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct IpPair { + pub ipv4: Ipv4Addr, + pub ipv6: Ipv6Addr, +} + +impl IpPair { + pub fn new(ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Self { + IpPair { ipv4, ipv6 } + } +} + +impl From<(Ipv4Addr, Ipv6Addr)> for IpPair { + fn from((ipv4, ipv6): (Ipv4Addr, Ipv6Addr)) -> Self { + IpPair { ipv4, ipv6 } + } +} + +impl fmt::Display for IpPair { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "({}, {})", self.ipv4, self.ipv6) + } +} + +impl From for IpPair { + fn from(value: IpAddr) -> Self { + let (before_last_byte, last_byte) = match value { + IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]), + IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]), + }; + let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16; + let ipv4 = Ipv4Addr::new( + WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0], + WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1], + before_last_byte, + last_byte, + ); + let ipv6 = Ipv6Addr::new( + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5], + WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6], + last_bytes, + ); + IpPair::new(ipv4, ipv6) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InitMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, +} + +impl InitMessage { + pub fn new(pub_key: PeerPublicKey) -> Self { + InitMessage { pub_key } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct FinalMessage { + /// Gateway client data + pub gateway_client: GatewayClient, + + /// Ecash credential + pub credential: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RegistrationData { + pub nonce: u64, + pub gateway_data: GatewayClient, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RegisteredData { + pub pub_key: PeerPublicKey, + pub private_ips: IpPair, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct RemainingBandwidthData { + pub available_bandwidth: i64, +} + +/// Client that wants to register sends its PublicKey bytes mac digest encrypted with a DH shared secret. +/// Gateway/Nym node can then verify pub_key payload using the same process +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct GatewayClient { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Assigned private IPs (v4 and v6) + pub private_ips: IpPair, + + /// Sha256 hmac on the data (alongside the prior nonce) + pub mac: ClientMac, +} + +impl GatewayClient { + #[cfg(feature = "verify")] + pub fn new( + local_secret: &PrivateKey, + remote_public: x25519_dalek::PublicKey, + private_ips: IpPair, + nonce: u64, + ) -> Self { + let local_public = PublicKey::from(local_secret); + let remote_public = PublicKey::from(remote_public); + + let dh = local_secret.diffie_hellman(&remote_public); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(&dh[..]) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(local_public.as_bytes()); + mac.update(private_ips.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + GatewayClient { + pub_key: PeerPublicKey::new(local_public.into()), + private_ips, + mac: ClientMac(mac.finalize().into_bytes().to_vec()), + } + } + + // Reusable secret should be gateways Wireguard PK + // Client should perform this step when generating its payload, using its own WG PK + #[cfg(feature = "verify")] + pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + // use gateways key as a ref to an x25519_dalek key + let dh = gateway_key.inner().diffie_hellman(&self.pub_key); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(self.pub_key.as_bytes()); + mac.update(self.private_ips.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + mac.verify_slice(&self.mac) + .map_err(|source| Error::FailedClientMacVerification { + client: self.pub_key.to_string(), + source, + }) + } + + pub fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +// TODO: change the inner type into generic array of size HmacSha256::OutputSize +// TODO2: rely on our internal crypto/hmac +#[derive(Debug, Clone, PartialEq)] +pub struct ClientMac(Vec); + +impl fmt::Display for ClientMac { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", general_purpose::STANDARD.encode(&self.0)) + } +} + +impl From> for ClientMac { + fn from(v: Vec) -> Self { + ClientMac(v) + } +} + +impl ClientMac { + #[allow(dead_code)] + pub fn new(mac: Vec) -> Self { + ClientMac(mac) + } +} + +impl Deref for ClientMac { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for ClientMac { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mac_bytes: Vec = + general_purpose::STANDARD + .decode(s) + .map_err(|source| Error::MalformedClientMac { + mac: s.to_string(), + source, + })?; + + Ok(ClientMac(mac_bytes)) + } +} + +impl Serialize for ClientMac { + fn serialize(&self, serializer: S) -> Result { + let encoded_key = general_purpose::STANDARD.encode(self.0.clone()); + serializer.serialize_str(&encoded_key) + } +} + +impl<'de> Deserialize<'de> for ClientMac { + fn deserialize>(deserializer: D) -> Result { + let encoded_key = String::deserialize(deserializer)?; + ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_crypto::asymmetric::x25519; + use nym_test_utils::helpers::deterministic_rng; + + #[test] + fn create_ip_pair() { + let ipv4: IpAddr = Ipv4Addr::from_str("10.1.10.50").unwrap().into(); + let ipv6: IpAddr = Ipv6Addr::from_str("fc01::0a32").unwrap().into(); + + assert_eq!(IpPair::from(ipv4), IpPair::from(ipv6)); + } + + #[test] + #[cfg(feature = "verify")] + fn client_request_roundtrip() { + let mut rng = deterministic_rng(); + + let gateway_key_pair = x25519::KeyPair::new(&mut rng); + let client_key_pair = x25519::KeyPair::new(&mut rng); + + let nonce = 1234567890; + + let client = GatewayClient::new( + client_key_pair.private_key(), + x25519_dalek::PublicKey::from(gateway_key_pair.public_key().to_bytes()), + IpPair::new("10.0.0.42".parse().unwrap(), "fc00::42".parse().unwrap()), + nonce, + ); + assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok()) + } +} diff --git a/common/authenticator-requests/src/v6/request.rs b/common/authenticator-requests/src/v6/request.rs new file mode 100644 index 0000000000..3bc8140b74 --- /dev/null +++ b/common/authenticator-requests/src/v6/request.rs @@ -0,0 +1,135 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{ + PROTOCOL, + registration::{FinalMessage, InitMessage}, + topup::TopUpMessage, + upgrade_mode_check::UpgradeModeCheckRequest, +}; +use nym_service_provider_requests_common::Protocol; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +fn generate_random() -> u64 { + use rand::RngCore; + let mut rng = rand::rngs::OsRng; + rng.next_u64() +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AuthenticatorRequest { + pub protocol: Protocol, + pub data: AuthenticatorRequestData, + pub request_id: u64, +} + +impl AuthenticatorRequest { + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn new_initial_request(init_message: InitMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::Initial(init_message), + request_id, + }, + request_id, + ) + } + + pub fn new_final_request(final_message: FinalMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::Final(Box::new(final_message)), + request_id, + }, + request_id, + ) + } + + pub fn new_query_request(peer_public_key: PeerPublicKey) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::QueryBandwidth(peer_public_key), + request_id, + }, + request_id, + ) + } + + pub fn new_topup_request(top_up_message: TopUpMessage) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::TopUpBandwidth(Box::new(top_up_message)), + request_id, + }, + request_id, + ) + } + + pub fn new_upgrade_mode_check_request(message: UpgradeModeCheckRequest) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: PROTOCOL, + data: AuthenticatorRequestData::CheckUpgradeMode(message), + request_id, + }, + request_id, + ) + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum AuthenticatorRequestData { + Initial(InitMessage), + Final(Box), + QueryBandwidth(PeerPublicKey), + TopUpBandwidth(Box), + CheckUpgradeMode(UpgradeModeCheckRequest), +} + +#[cfg(test)] +mod tests { + use super::super::VERSION; + use super::*; + use nym_service_provider_requests_common::ServiceProviderType; + use std::str::FromStr; + + #[test] + fn check_first_bytes_protocol() { + let version = VERSION; + let data = AuthenticatorRequest { + protocol: Protocol { + version, + service_provider_type: ServiceProviderType::Authenticator, + }, + data: AuthenticatorRequestData::Initial(InitMessage::new( + PeerPublicKey::from_str("yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ=").unwrap(), + )), + request_id: 1, + }; + let bytes = *data.to_bytes().unwrap().first_chunk::<2>().unwrap(); + assert_eq!(bytes, [version, ServiceProviderType::Authenticator as u8]); + } +} diff --git a/common/authenticator-requests/src/v6/response.rs b/common/authenticator-requests/src/v6/response.rs new file mode 100644 index 0000000000..c93c34e1c3 --- /dev/null +++ b/common/authenticator-requests/src/v6/response.rs @@ -0,0 +1,153 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData}; +use nym_service_provider_requests_common::Protocol; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +use super::PROTOCOL; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AuthenticatorResponse { + pub protocol: Protocol, + pub data: AuthenticatorResponseData, +} + +impl AuthenticatorResponse { + pub fn new_pending_registration_success( + registration_data: RegistrationData, + request_id: u64, + upgrade_mode_enabled: bool, + ) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::PendingRegistration(PendingRegistrationResponse { + reply: registration_data, + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn new_registered( + registered_data: RegisteredData, + request_id: u64, + upgrade_mode_enabled: bool, + ) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::Registered(RegisteredResponse { + reply: registered_data, + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn new_remaining_bandwidth( + remaining_bandwidth_data: Option, + request_id: u64, + upgrade_mode_enabled: bool, + ) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::RemainingBandwidth(RemainingBandwidthResponse { + reply: remaining_bandwidth_data, + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn new_topup_bandwidth( + remaining_bandwidth_data: RemainingBandwidthData, + request_id: u64, + upgrade_mode_enabled: bool, + ) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::TopUpBandwidth(TopUpBandwidthResponse { + reply: remaining_bandwidth_data, + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn new_upgrade_mode_check(request_id: u64, upgrade_mode_enabled: bool) -> Self { + Self { + protocol: PROTOCOL, + data: AuthenticatorResponseData::UpgradeMode(UpgradeModeResponse { + request_id, + upgrade_mode_enabled, + }), + } + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } + + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn id(&self) -> Option { + match &self.data { + AuthenticatorResponseData::PendingRegistration(response) => Some(response.request_id), + AuthenticatorResponseData::Registered(response) => Some(response.request_id), + AuthenticatorResponseData::RemainingBandwidth(response) => Some(response.request_id), + AuthenticatorResponseData::TopUpBandwidth(response) => Some(response.request_id), + AuthenticatorResponseData::UpgradeMode(response) => Some(response.request_id), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum AuthenticatorResponseData { + PendingRegistration(PendingRegistrationResponse), + Registered(RegisteredResponse), + RemainingBandwidth(RemainingBandwidthResponse), + TopUpBandwidth(TopUpBandwidthResponse), + UpgradeMode(UpgradeModeResponse), +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PendingRegistrationResponse { + pub request_id: u64, + pub reply: RegistrationData, + pub upgrade_mode_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RegisteredResponse { + pub request_id: u64, + pub reply: RegisteredData, + pub upgrade_mode_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct RemainingBandwidthResponse { + pub request_id: u64, + pub reply: Option, + pub upgrade_mode_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct TopUpBandwidthResponse { + pub request_id: u64, + pub reply: RemainingBandwidthData, + pub upgrade_mode_enabled: bool, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct UpgradeModeResponse { + pub request_id: u64, + pub upgrade_mode_enabled: bool, +} diff --git a/common/authenticator-requests/src/v6/topup.rs b/common/authenticator-requests/src/v6/topup.rs new file mode 100644 index 0000000000..b5d25a9dbf --- /dev/null +++ b/common/authenticator-requests/src/v6/topup.rs @@ -0,0 +1,15 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct TopUpMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Ecash credential + pub credential: CredentialSpendingData, +} diff --git a/common/authenticator-requests/src/v6/upgrade_mode_check.rs b/common/authenticator-requests/src/v6/upgrade_mode_check.rs new file mode 100644 index 0000000000..ae27af3800 --- /dev/null +++ b/common/authenticator-requests/src/v6/upgrade_mode_check.rs @@ -0,0 +1,12 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[non_exhaustive] +pub enum UpgradeModeCheckRequest { + /// Attempt to request upgrade mode recheck via the JWT issued as the result of + /// global attestation.json being published + UpgradeModeJwt { token: String }, +} diff --git a/common/authenticator-requests/src/version.rs b/common/authenticator-requests/src/version.rs index 4bb8b6d591..d0f2bbf217 100644 --- a/common/authenticator-requests/src/version.rs +++ b/common/authenticator-requests/src/version.rs @@ -1,7 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::{v1, v2, v3, v4, v5}; +use super::{v1, v2, v3, v4, v5, v6}; use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; #[derive(Copy, Clone, Debug, PartialEq, strum_macros::Display)] @@ -22,11 +22,15 @@ pub enum AuthenticatorVersion { /// introduced in dorina-patched release (1.6.1) V5, + /// introduced in niolo release (1.23.0) + V6, + + /// an unknown, future, variant that can be present if running outdated software UNKNOWN, } impl AuthenticatorVersion { - pub const LATEST: Self = Self::V5; + pub const LATEST: Self = Self::V6; pub const fn release_version(&self) -> semver::Version { match self { @@ -35,6 +39,7 @@ impl AuthenticatorVersion { AuthenticatorVersion::V3 => semver::Version::new(1, 1, 10), AuthenticatorVersion::V4 => semver::Version::new(1, 2, 0), AuthenticatorVersion::V5 => semver::Version::new(1, 6, 1), + AuthenticatorVersion::V6 => semver::Version::new(1, 23, 0), AuthenticatorVersion::UNKNOWN => semver::Version::new(0, 0, 0), } } @@ -54,6 +59,8 @@ impl From for AuthenticatorVersion { AuthenticatorVersion::V4 } else if value.version == v5::VERSION { AuthenticatorVersion::V5 + } else if value.version == v6::VERSION { + AuthenticatorVersion::V6 } else { AuthenticatorVersion::UNKNOWN } @@ -72,6 +79,8 @@ impl From for AuthenticatorVersion { AuthenticatorVersion::V4 } else if value == v5::VERSION { AuthenticatorVersion::V5 + } else if value == v6::VERSION { + AuthenticatorVersion::V6 } else { AuthenticatorVersion::UNKNOWN } @@ -126,11 +135,14 @@ impl From for AuthenticatorVersion { if semver < AuthenticatorVersion::V5.release_version() { return Self::V4; } - // if provided version is higher (or equal) to release version of V5, - // we return the latest (i.e. v5) + if semver < AuthenticatorVersion::V6.release_version() { + return Self::V5; + } + // if provided version is higher (or equal) to release version of V6, + // we return the latest (i.e. v6) debug_assert_eq!( - Self::V5, + Self::V6, Self::LATEST, "a new AuthenticatorVersion variant has been introduced without adjusting the `From` trait" ); @@ -191,5 +203,9 @@ mod tests { assert_eq!(AuthenticatorVersion::V5, "1.7.0".into()); assert_eq!(AuthenticatorVersion::V5, "1.16.11".into()); assert_eq!(AuthenticatorVersion::V5, "1.17.0".into()); + assert_eq!(AuthenticatorVersion::V5, "1.22.0".into()); + assert_eq!(AuthenticatorVersion::V6, "1.23.0".into()); + assert_eq!(AuthenticatorVersion::V6, "1.23.1".into()); + assert_eq!(AuthenticatorVersion::V6, "1.24.0".into()); } } diff --git a/common/bandwidth-controller/Cargo.toml b/common/bandwidth-controller/Cargo.toml index 075c1702ae..a94abcc1cb 100644 --- a/common/bandwidth-controller/Cargo.toml +++ b/common/bandwidth-controller/Cargo.toml @@ -7,21 +7,16 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -async-trait = { workspace = true } -bip39 = { workspace = true } +async-trait = { workspace = true } log = { workspace = true } rand = { workspace = true } thiserror = { workspace = true } -url = { workspace = true } -zeroize = { workspace = true } nym-credential-storage = { path = "../credential-storage" } nym-credentials = { path = "../credentials" } nym-credentials-interface = { path = "../credentials-interface" } nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "stream_cipher", "aes", "hashing"] } -nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } nym-ecash-time = { path = "../ecash-time" } -nym-network-defaults = { path = "../network-defaults" } nym-task = { path = "../task" } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } diff --git a/common/bandwidth-controller/src/error.rs b/common/bandwidth-controller/src/error.rs index 4fcff3731a..c5721df092 100644 --- a/common/bandwidth-controller/src/error.rs +++ b/common/bandwidth-controller/src/error.rs @@ -21,6 +21,9 @@ pub enum BandwidthControllerError { #[error("There was a credential storage error - {0}")] CredentialStorageError(Box), + #[error("retrieved upgrade mode token is not a valid String")] + MalformedUpgradeModeToken, + #[error("the credential storage does not contain any usable credentials")] NoCredentialsAvailable, diff --git a/common/bandwidth-controller/src/lib.rs b/common/bandwidth-controller/src/lib.rs index 05be75cbdc..8002e77631 100644 --- a/common/bandwidth-controller/src/lib.rs +++ b/common/bandwidth-controller/src/lib.rs @@ -12,7 +12,7 @@ use crate::utils::{ ApiClientsWrapper, }; use log::error; -use nym_credential_storage::models::RetrievedTicketbook; +use nym_credential_storage::models::{EmergencyCredential, RetrievedTicketbook}; use nym_credential_storage::storage::Storage; use nym_credentials::ecash::bandwidth::CredentialSpendingData; use nym_credentials_interface::{ @@ -220,6 +220,19 @@ impl BandwidthController { } } } + + pub async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, BandwidthControllerError> + where + ::StorageError: Send + Sync + 'static, + { + self.storage + .get_emergency_credential(typ) + .await + .map_err(BandwidthControllerError::credential_storage_error) + } } impl Clone for BandwidthController diff --git a/common/bandwidth-controller/src/traits.rs b/common/bandwidth-controller/src/traits.rs index 8ff508f3f3..d611649004 100644 --- a/common/bandwidth-controller/src/traits.rs +++ b/common/bandwidth-controller/src/traits.rs @@ -11,6 +11,9 @@ use crate::{error::BandwidthControllerError, BandwidthController, PreparedCreden pub const DEFAULT_TICKETS_TO_SPEND: u32 = 1; +// TODO: this does not really belong here +pub const UPGRADE_MODE_JWT_TYPE: &str = "UPGRADE_MODE_JWT"; + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait BandwidthTicketProvider: Send + Sync { @@ -20,6 +23,8 @@ pub trait BandwidthTicketProvider: Send + Sync { gateway_id: ed25519::PublicKey, tickets_to_spend: u32, ) -> Result; + + async fn get_upgrade_mode_token(&self) -> Result, BandwidthControllerError>; } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -39,4 +44,16 @@ where self.prepare_ecash_ticket(ticket_type, gateway_id.to_bytes(), tickets_to_spend) .await } + + async fn get_upgrade_mode_token(&self) -> Result, BandwidthControllerError> { + let Some(emergency_credential) = + self.get_emergency_credential(UPGRADE_MODE_JWT_TYPE).await? + else { + return Ok(None); + }; + // upgrade mode credential is just a simple stringified JWT + let token = String::from_utf8(emergency_credential.data.content) + .map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?; + Ok(Some(token)) + } } diff --git a/common/client-core/src/cli_helpers/client_init.rs b/common/client-core/src/cli_helpers/client_init.rs index feda3ab8d1..57f800c8cf 100644 --- a/common/client-core/src/cli_helpers/client_init.rs +++ b/common/client-core/src/cli_helpers/client_init.rs @@ -81,6 +81,10 @@ pub struct CommonClientInitArgs { #[cfg_attr(feature = "cli", clap(long, hide = true))] pub enabled_credentials_mode: Option, + /// Change the default minimum node performance used during initial node selection filtering. + #[cfg_attr(feature = "cli", clap(long, hide = true))] + pub minimum_gateway_performance: Option, + /// Mostly debug-related option to increase default traffic rate so that you would not need to /// modify config post init #[cfg_attr(feature = "cli", clap(long, hide = true))] @@ -173,10 +177,14 @@ where })?; hardcoded_topology.entry_capable_nodes().cloned().collect() } else { + let minimum_performance = common_args + .minimum_gateway_performance + .unwrap_or(core.debug.topology.minimum_gateway_performance); + crate::init::helpers::gateways_for_init( &core.client.nym_api_urls, user_agent, - core.debug.topology.minimum_gateway_performance, + minimum_performance, core.debug.topology.ignore_ingress_epoch_role, None, ) diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index efc699473a..969d94807e 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -88,3 +88,6 @@ features = ["js"] [features] wasm = [] + +[lints] +workspace = true \ No newline at end of file diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs index 9fd43765bd..304eff6c96 100644 --- a/common/client-libs/gateway-client/src/bandwidth.rs +++ b/common/client-libs/gateway-client/src/bandwidth.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use si_scale::helpers::bibytes2; +use std::fmt::{Display, Formatter}; use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -26,6 +27,39 @@ pub struct ClientBandwidth { inner: Arc, } +// simple helper for logging purposes to accommodate 'unknown' case +pub(crate) enum UpgradeModeEnabledWrapper { + True, + False, + Unknown, +} + +impl From> for UpgradeModeEnabledWrapper { + fn from(value: Option) -> Self { + match value { + Some(true) => UpgradeModeEnabledWrapper::True, + Some(false) => UpgradeModeEnabledWrapper::False, + None => UpgradeModeEnabledWrapper::Unknown, + } + } +} + +impl From for UpgradeModeEnabledWrapper { + fn from(value: bool) -> Self { + Some(value).into() + } +} + +impl Display for UpgradeModeEnabledWrapper { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + UpgradeModeEnabledWrapper::True => write!(f, "true"), + UpgradeModeEnabledWrapper::False => write!(f, "false"), + UpgradeModeEnabledWrapper::Unknown => write!(f, "unknown"), + } + } +} + struct ClientBandwidthInner { /// the actual bandwidth amount (in bytes) available available: AtomicI64, @@ -71,26 +105,41 @@ impl ClientBandwidth { self.inner.available.load(Ordering::Acquire) } - pub(crate) fn maybe_log_bandwidth(&self, now: Option) { + pub(crate) fn maybe_log_bandwidth( + &self, + now: Option, + upgrade_mode: impl Into, + ) { let last = self.last_logged(); let now = now.unwrap_or_else(OffsetDateTime::now_utc); if last + Duration::from_secs(10) < now { - self.log_bandwidth(Some(now)) + self.log_bandwidth(Some(now), upgrade_mode) } } - pub(crate) fn log_bandwidth(&self, now: Option) { + pub(crate) fn log_bandwidth( + &self, + now: Option, + upgrade_mode: impl Into, + ) { let now = now.unwrap_or_else(OffsetDateTime::now_utc); + let upgrade_mode = upgrade_mode.into(); let remaining = self.remaining(); let remaining_bi2 = bibytes2(remaining as f64); if remaining < 0 { - tracing::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}"); + tracing::warn!( + "OUT OF BANDWIDTH. remaining: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}" + ); } else if remaining < 1_000_000 { - tracing::info!("remaining bandwidth: {remaining_bi2}"); + tracing::info!( + "remaining bandwidth: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}" + ); } else { - tracing::debug!("remaining bandwidth: {remaining_bi2}"); + tracing::trace!( + "remaining bandwidth: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}" + ); } self.inner @@ -98,26 +147,35 @@ impl ClientBandwidth { .store(now.unix_timestamp(), Ordering::Relaxed) } - pub(crate) fn update_and_maybe_log(&self, remaining: i64) { + pub(crate) fn update_and_maybe_log( + &self, + remaining: i64, + upgrade_mode: impl Into, + ) { let now = OffsetDateTime::now_utc(); self.inner.available.store(remaining, Ordering::Release); self.inner .last_updated_ts .store(now.unix_timestamp(), Ordering::Relaxed); - self.maybe_log_bandwidth(Some(now)) + self.maybe_log_bandwidth(Some(now), upgrade_mode) } - pub(crate) fn update_and_log(&self, remaining: i64) { + pub(crate) fn update_and_log( + &self, + remaining: i64, + upgrade_mode: impl Into, + ) { let now = OffsetDateTime::now_utc(); self.inner.available.store(remaining, Ordering::Release); self.inner .last_updated_ts .store(now.unix_timestamp(), Ordering::Relaxed); - self.log_bandwidth(Some(now)) + self.log_bandwidth(Some(now), upgrade_mode) } fn last_logged(&self) -> OffsetDateTime { // SAFETY: this value is always populated with valid timestamps + #[allow(clippy::unwrap_used)] OffsetDateTime::from_unix_timestamp(self.inner.last_logged_ts.load(Ordering::Relaxed)) .unwrap() } diff --git a/common/client-libs/gateway-client/src/client/config.rs b/common/client-libs/gateway-client/src/client/config.rs index fd7bfc142d..af24a72b90 100644 --- a/common/client-libs/gateway-client/src/client/config.rs +++ b/common/client-libs/gateway-client/src/client/config.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::GatewayClientError; -use nym_network_defaults::TicketTypeRepr::V1MixnetEntry; +use nym_credentials_interface::DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD; use si_scale::helpers::bibytes2; use std::time::Duration; @@ -103,7 +103,7 @@ impl BandwidthTickets { // 20% of entry ticket value pub const DEFAULT_REMAINING_BANDWIDTH_THRESHOLD: i64 = - (V1MixnetEntry.bandwidth_value() / 5) as i64; + DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD; pub const DEFAULT_CUTOFF_REMAINING_BANDWIDTH_THRESHOLD: Option = None; diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 52e5833eb2..e9d91beeb9 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -20,9 +20,9 @@ use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::ed25519; use nym_gateway_requests::registration::handshake::client_handshake; use nym_gateway_requests::{ - BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt, - GatewayRequestsError, SensitiveServerResponse, ServerResponse, SharedGatewayKey, - SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, + BandwidthResponse, BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersion, + GatewayProtocolVersionExt, GatewayRequestsError, SensitiveServerResponse, ServerResponse, + SharedGatewayKey, SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, }; use nym_sphinx::forwarding::packet::MixPacket; use nym_statistics_common::clients::connection::ConnectionStatsEvent; @@ -101,8 +101,7 @@ pub struct GatewayClient { bandwidth_controller: Option>, stats_reporter: ClientStatsSender, - // currently unused (but populated) - negotiated_protocol: Option, + negotiated_protocol: Option, // Callback on the fd as soon as the connection has been established #[cfg(unix)] @@ -166,10 +165,12 @@ impl GatewayClient { } #[cfg(not(target_arch = "wasm32"))] + #[allow(clippy::unreachable)] async fn _close_connection(&mut self) -> Result<(), GatewayClientError> { match std::mem::replace(&mut self.connection, SocketState::NotConnected) { SocketState::Available(mut socket) => Ok((*socket).close(None).await?), SocketState::PartiallyDelegated(_) => { + // SAFETY: this is only called after the caller has already recovered the connection unreachable!("this branch should have never been reached!") } _ => Ok(()), // no need to do anything in those cases @@ -177,6 +178,7 @@ impl GatewayClient { } #[cfg(target_arch = "wasm32")] + #[allow(clippy::unreachable)] async fn _close_connection(&mut self) -> Result<(), GatewayClientError> { match std::mem::replace(&mut self.connection, SocketState::NotConnected) { SocketState::Available(socket) => { @@ -184,6 +186,7 @@ impl GatewayClient { Ok(()) } SocketState::PartiallyDelegated(_) => { + // SAFETY: this is only called after the caller has already recovered the connection unreachable!("this branch should have never been reached!") } _ => Ok(()), // no need to do anything in those cases @@ -458,43 +461,16 @@ impl GatewayClient { } } - fn check_gateway_protocol( - &self, - gateway_protocol: Option, - ) -> Result<(), GatewayClientError> { - debug!("gateway protocol: {gateway_protocol:?}, ours: {CURRENT_PROTOCOL_VERSION}"); - - // right now there are no failure cases here, but this might change in the future - match gateway_protocol { - None => { - warn!("the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); - // note: in +1.2.0 we will have to return a hard error here - Ok(()) - } - Some(v) if v > CURRENT_PROTOCOL_VERSION => { - let err = GatewayClientError::IncompatibleProtocol { - gateway: Some(v), - current: CURRENT_PROTOCOL_VERSION, - }; - error!("{err}"); - Err(err) - } - - Some(_) => { - debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!"); - Ok(()) - } - } - } - async fn register( &mut self, - derive_aes256_gcm_siv_key: bool, + supported_gateway_protocol: Option, ) -> Result<(), GatewayClientError> { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } + let derive_aes256_gcm_siv_key = supported_gateway_protocol.supports_aes256_gcm_siv(); + debug_assert!(self.connection.is_available()); log::debug!( "registering with gateway. using legacy key derivation: {}", @@ -505,14 +481,13 @@ impl GatewayClient { // and putting it into the GatewayClient struct would be a hassle let mut rng = OsRng; - let shared_key = match &mut self.connection { + let handshake_result = match &mut self.connection { SocketState::Available(ws_stream) => client_handshake( &mut rng, ws_stream, self.local_identity.as_ref(), self.gateway_identity, - self.cfg.bandwidth.require_tickets, - derive_aes256_gcm_siv_key, + supported_gateway_protocol, #[cfg(not(target_arch = "wasm32"))] self.shutdown_token.clone(), ) @@ -521,26 +496,31 @@ impl GatewayClient { _ => return Err(GatewayClientError::ConnectionInInvalidState), }?; - let (authentication_status, gateway_protocol) = match self.read_control_response().await? { + let authentication_status = match self.read_control_response().await? { ServerResponse::Register { - protocol_version, status, - } => (status, protocol_version), + upgrade_mode, + .. + } => { + if upgrade_mode { + warn!("the system is currently undergoing an upgrade. some of its functionalities might be unstable") + } + status + } ServerResponse::Error { message } => { return Err(GatewayClientError::GatewayError(message)) } other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }), }; - self.check_gateway_protocol(gateway_protocol)?; self.authenticated = authentication_status; if self.authenticated { - self.shared_key = Some(Arc::new(shared_key)); + self.shared_key = Some(Arc::new(handshake_result.derived_key)); } // populate the negotiated protocol for future uses - self.negotiated_protocol = gateway_protocol; + self.negotiated_protocol = Some(handshake_result.negotiated_protocol); Ok(()) } @@ -623,13 +603,24 @@ impl GatewayClient { protocol_version, status, bandwidth_remaining, + upgrade_mode, } => { - self.check_gateway_protocol(protocol_version)?; + if protocol_version.is_future_version() { + // SAFETY: future version is always defined + #[allow(clippy::unwrap_used)] + let version = protocol_version.unwrap(); + error!("the gateway insists on using v{version} protocol which is not supported by this client"); + return Err(GatewayClientError::AuthenticationFailure); + } self.authenticated = status; - self.bandwidth.update_and_maybe_log(bandwidth_remaining); + self.bandwidth + .update_and_maybe_log(bandwidth_remaining, upgrade_mode); self.negotiated_protocol = protocol_version; log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}"); + if upgrade_mode { + warn!("the system is currently undergoing an upgrade. some of its functionalities might be unstable") + } Ok(()) } @@ -650,7 +641,7 @@ impl GatewayClient { .public_key() .derive_destination_address(); - let msg = ClientControlRequest::new_authenticate( + let msg = ClientControlRequest::new_legacy_authenticate( self_address, shared_key, self.cfg.bandwidth.require_tickets, @@ -659,25 +650,40 @@ impl GatewayClient { .await } - async fn authenticate_v2(&mut self) -> Result<(), GatewayClientError> { + async fn authenticate_v2( + &mut self, + requested_protocol_version: GatewayProtocolVersion, + ) -> Result<(), GatewayClientError> { debug!("using v2 authentication"); let Some(shared_key) = self.shared_key.as_ref() else { return Err(GatewayClientError::NoSharedKeyAvailable); }; - let msg = ClientControlRequest::new_authenticate_v2(shared_key, &self.local_identity)?; + let msg = ClientControlRequest::new_authenticate_v2( + shared_key, + &self.local_identity, + requested_protocol_version, + )?; self.send_authenticate_request_and_handle_response(msg) .await } - async fn authenticate(&mut self, use_v2: bool) -> Result<(), GatewayClientError> { + async fn authenticate( + &mut self, + supported_gateway_protocol: Option, + ) -> Result<(), GatewayClientError> { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } debug!("authenticating with gateway"); - if use_v2 { - self.authenticate_v2().await + if supported_gateway_protocol.supports_authenticate_v2() { + // use the highest possible protocol version the gateway has announced support for + + // SAFETY: if announced protocol supports auth v2, it means it's properly set + #[allow(clippy::unwrap_used)] + self.authenticate_v2(supported_gateway_protocol.unwrap()) + .await } else { self.authenticate_v1().await } @@ -708,9 +714,12 @@ impl GatewayClient { } }; + debug!("supported gateway protocol: {gw_protocol:?}"); + let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv(); let supports_auth_v2 = gw_protocol.supports_authenticate_v2(); let supports_key_rotation_info = gw_protocol.supports_key_rotation_packet(); + let supports_upgrade_mode = gw_protocol.supports_upgrade_mode(); if !supports_aes_gcm_siv { warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV"); @@ -721,6 +730,16 @@ impl GatewayClient { if !supports_key_rotation_info { warn!("this gateway is on an old version that doesn't support key rotation packets") } + if !supports_upgrade_mode { + warn!("this gateway is on an old version that doesn't support upgrade mode") + } + + let gw_protocol = if gw_protocol.is_future_version() { + warn!("we're running outdated software as gateway is announcing protocol {gw_protocol:?} whilst we're using {}. we're going to attempt to downgrade", GatewayProtocolVersion::CURRENT); + Some(GatewayProtocolVersion::CURRENT) + } else { + gw_protocol + }; if self.authenticated { debug!("Already authenticated"); @@ -735,10 +754,11 @@ impl GatewayClient { } if self.shared_key.is_some() { - self.authenticate(supports_auth_v2).await?; + self.authenticate(gw_protocol).await?; if self.authenticated { // if we are authenticated it means we MUST have an associated shared_key + #[allow(clippy::unwrap_used)] let shared_key = self.shared_key.as_ref().unwrap(); let requires_key_upgrade = shared_key.is_legacy() && supports_aes_gcm_siv; @@ -751,9 +771,10 @@ impl GatewayClient { Err(GatewayClientError::AuthenticationFailure) } } else { - self.register(supports_aes_gcm_siv).await?; + self.register(gw_protocol).await?; // if registration didn't return an error, we MUST have an associated shared key + #[allow(clippy::unwrap_used)] let shared_key = self.shared_key.as_ref().unwrap(); // we're always registering with the highest supported protocol, @@ -783,51 +804,81 @@ impl GatewayClient { } } - async fn claim_ecash_bandwidth( + async fn wait_for_bandwidth_response( &mut self, - credential: CredentialSpendingData, - ) -> Result<(), GatewayClientError> { - let msg = ClientControlRequest::new_enc_ecash_credential( - credential, - self.shared_key.as_ref().unwrap(), - )?; - let bandwidth_remaining = match self + msg: ClientControlRequest, + ) -> Result { + let response = match self .send_websocket_message_with_non_send_response(msg) .await? { - ServerResponse::Bandwidth { available_total } => Ok(available_total), + ServerResponse::Bandwidth(response) => { + if response.upgrade_mode { + info!("the system is currently undergoing an upgrade. our bandwidth shouldn't have been metered") + } + Ok(response) + } ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), ServerResponse::TypedError { error } => { Err(GatewayClientError::TypedGatewayError(error)) } other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), }?; + Ok(response) + } + + async fn claim_ecash_bandwidth( + &mut self, + credential: CredentialSpendingData, + ) -> Result<(), GatewayClientError> { + // SAFETY: claiming ecash bandwidth is called as part of `claim_bandwidth` which + // ensures the shared key is defined + #[allow(clippy::unwrap_used)] + let msg = ClientControlRequest::new_enc_ecash_credential( + credential, + self.shared_key.as_ref().unwrap(), + )?; + let response = self.wait_for_bandwidth_response(msg).await?; // TODO: create tracing span info!("managed to claim ecash bandwidth"); - self.bandwidth.update_and_log(bandwidth_remaining); + self.bandwidth + .update_and_log(response.available_total, response.upgrade_mode); + + Ok(()) + } + + pub async fn send_upgrade_mode_jwt(&mut self, token: String) -> Result<(), GatewayClientError> { + let msg = ClientControlRequest::new_upgrade_mode_jwt(token); + let response = self.wait_for_bandwidth_response(msg).await?; + + // if gateway rejected our jwt, we would have returned an error + info!("gateway has accepted our jwt"); + if !response.upgrade_mode { + error!("but we're not in upgrade mode - something is wrong!"); + return Err(GatewayClientError::UnexpectedUpgradeModeState); + } + + self.bandwidth + .update_and_log(response.available_total, response.upgrade_mode); Ok(()) } async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> { let msg = ClientControlRequest::ClaimFreeTestnetBandwidth; - let bandwidth_remaining = match self - .send_websocket_message_with_non_send_response(msg) - .await? - { - ServerResponse::Bandwidth { available_total } => Ok(available_total), - ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), - other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), - }?; + let response = self.wait_for_bandwidth_response(msg).await?; info!("managed to claim testnet bandwidth"); - self.bandwidth.update_and_log(bandwidth_remaining); + self.bandwidth + .update_and_log(response.available_total, response.upgrade_mode); Ok(()) } fn unchecked_bandwidth_controller(&self) -> &BandwidthController { + // this is an unchecked method + #[allow(clippy::unwrap_used)] self.bandwidth_controller.as_ref().unwrap() } @@ -919,6 +970,7 @@ impl GatewayClient { BinaryRequest::ForwardSphinx { packet } }; + #[allow(clippy::expect_used)] req.into_ws_message( self.shared_key .as_ref() @@ -1025,6 +1077,8 @@ impl GatewayClient { self.send_with_reconnection_on_failure(msg).await } + // SAFETY: this method is only called when the connection is in `PartiallyDelegated` state + #[allow(clippy::unreachable)] async fn recover_socket_connection(&mut self) -> Result<(), GatewayClientError> { if self.connection.is_available() { return Ok(()); @@ -1054,6 +1108,7 @@ impl GatewayClient { return Err(GatewayClientError::ConnectionInInvalidState); } + #[allow(clippy::expect_used)] let partially_delegated = match std::mem::replace(&mut self.connection, SocketState::Invalid) { SocketState::Available(conn) => { @@ -1069,7 +1124,13 @@ impl GatewayClient { self.shutdown_token.clone(), ) } - _ => unreachable!(), + other => { + error!( + "attempted to start mixnet listener whilst the connection is in {} state!", + other.name() + ); + return Err(GatewayClientError::ConnectionInInvalidState); + } }; self.connection = SocketState::PartiallyDelegated(partially_delegated); @@ -1082,8 +1143,7 @@ impl GatewayClient { } // if we're reconnecting, because we lost connection, we need to re-authenticate the connection - self.authenticate(self.negotiated_protocol.supports_authenticate_v2()) - .await?; + self.authenticate(self.negotiated_protocol).await?; // this call is NON-blocking self.start_listening_for_mixnet_messages()?; diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index eaea37c586..9f97b7c7f5 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -128,6 +128,9 @@ pub enum GatewayClientError { "this operation couldn't be completed as the program is in the process of shutting down" )] ShutdownInProgress, + + #[error("the system is an unexpected upgrade mode state")] + UnexpectedUpgradeModeState, } impl From for GatewayClientError { diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs index 7fb863947f..93019564b9 100644 --- a/common/client-libs/gateway-client/src/packet_router.rs +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -35,6 +35,7 @@ impl PacketRouter { } } + #[allow(clippy::panic)] pub fn route_mixnet_messages( &self, received_messages: Vec>, @@ -54,6 +55,7 @@ impl PacketRouter { Ok(()) } + #[allow(clippy::panic)] pub fn route_acks(&self, received_acks: Vec>) -> Result<(), GatewayClientError> { if let Err(err) = self.ack_sender.unbounded_send(received_acks) { // check if the failure is due to the shutdown being in progress and thus the receiver channel diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 4f3009e389..af98d1b797 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::bandwidth::ClientBandwidth; +use crate::client::config::BandwidthTickets; use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; use crate::traits::GatewayPacketRouter; @@ -10,7 +11,9 @@ use futures::channel::oneshot; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use nym_gateway_requests::shared_key::SharedGatewayKey; -use nym_gateway_requests::{SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError}; +use nym_gateway_requests::{ + SendResponse, SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError, +}; use nym_task::ShutdownToken; use si_scale::helpers::bibytes2; use std::os::raw::c_int as RawFd; @@ -154,11 +157,12 @@ impl PartiallyDelegatedRouter { fn handle_text_message(&self, text: String) -> Result<(), GatewayClientError> { // if we fail to deserialise the response, return a hard error. we can't handle garbage match ServerResponse::try_from(text).map_err(|_| GatewayClientError::MalformedResponse)? { - ServerResponse::Send { + ServerResponse::Send(SendResponse { remaining_bandwidth, - } => { + upgrade_mode, + }) => { self.client_bandwidth - .update_and_maybe_log(remaining_bandwidth); + .update_and_maybe_log(remaining_bandwidth, upgrade_mode); Ok(()) } ServerResponse::Error { message } => { @@ -174,7 +178,20 @@ impl PartiallyDelegatedRouter { let available_bi2 = bibytes2(available as f64); let required_bi2 = bibytes2(required as f64); warn!("run out of bandwidth when attempting to send the message! we got {available_bi2} available, but needed at least {required_bi2} to send the previous message"); - self.client_bandwidth.update_and_log(available); + // if we run out of bandwidth (and tried to send reasonable amount of data), + // the upgrade mode is implicitly disabled, as otherwise we would have been + // to proceed + let upgrade_mode = if available + < BandwidthTickets::DEFAULT_REMAINING_BANDWIDTH_THRESHOLD + { + Some(false) + } else { + // we were attempting to send a lot of data at once + // - we have no certainty about upgrade mode at this point + None + }; + self.client_bandwidth + .update_and_log(available, upgrade_mode); // UNIMPLEMENTED: we should stop sending messages until we recover bandwidth Ok(()) } @@ -327,6 +344,7 @@ impl PartiallyDelegatedHandle { Ok(self.sink_half.send_all(&mut send_stream).await?) } + #[allow(clippy::panic)] pub(crate) async fn merge(self) -> Result { let (mut stream_receiver, notify) = self.delegated_stream; @@ -355,8 +373,10 @@ impl PartiallyDelegatedHandle { // in receive_res .map_err(|_| GatewayClientError::ConnectionAbruptlyClosed)?; let stream = stream_results?; + // the error is thrown when trying to reunite sink and stream that did not originate // from the same split which is impossible to happen here + #[allow(clippy::unwrap_used)] Ok(self.sink_half.reunite(stream).unwrap()) } } @@ -387,4 +407,13 @@ impl SocketState { SocketState::Available(_) | SocketState::PartiallyDelegated(_) ) } + + pub(crate) fn name(&self) -> &'static str { + match self { + SocketState::Available(_) => "available", + SocketState::PartiallyDelegated(_) => "partially delegated", + SocketState::NotConnected => "not connected", + SocketState::Invalid => "invalid", + } + } } diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index afb988c98c..ec4eb62bec 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -75,6 +75,7 @@ workspace = true features = ["json", "rustls-tls"] [dev-dependencies] +anyhow = { workspace = true } bip39 = { workspace = true } cosmrs = { workspace = true, features = ["bip32"] } ts-rs = { workspace = true } diff --git a/common/client-libs/validator-client/examples/offline_signing.rs b/common/client-libs/validator-client/examples/offline_signing.rs index 62c160f1a8..c0a8295307 100644 --- a/common/client-libs/validator-client/examples/offline_signing.rs +++ b/common/client-libs/validator-client/examples/offline_signing.rs @@ -7,6 +7,7 @@ use cosmrs::{tx, AccountId, Coin, Denom}; use nym_validator_client::http_client; use nym_validator_client::nyxd::CosmWasmClient; use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet; +use nym_validator_client::signing::signer::OfflineSigner; use nym_validator_client::signing::tx_signer::TxSigner; use nym_validator_client::signing::SignerData; @@ -19,8 +20,8 @@ async fn main() { let validator = "https://rpc.sandbox.nymtech.net"; let to_address: AccountId = "n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa".parse().unwrap(); - let signer = DirectSecp256k1HdWallet::from_mnemonic(prefix, signer_mnemonic); - let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone(); + let signer = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, signer_mnemonic).unwrap(); + let signer_address = signer.signer_addresses()[0].clone(); // local 'client' ONLY signing messages let tx_signer = signer; @@ -57,9 +58,15 @@ async fn main() { 100000u32, ); - let tx_raw = tx_signer - .sign_direct(&signer_address, vec![send_msg], fee, memo, signer_data) - .unwrap(); + let tx_raw = TxSigner::sign_direct( + &tx_signer, + &signer_address, + vec![send_msg], + fee, + memo, + signer_data, + ) + .unwrap(); let tx_bytes = tx_raw.to_bytes().unwrap(); // compare balances from before and after the tx diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 2758369ede..eb23ba60ca 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -5,8 +5,7 @@ use crate::nyxd::{self, NyxdClient}; use crate::signing::direct_wallet::DirectSecp256k1HdWallet; use crate::signing::signer::{NoSigner, OfflineSigner}; use crate::{ - DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, ReqwestRpcClient, - ValidatorClientError, + DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, ValidatorClientError, }; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, @@ -164,7 +163,7 @@ impl Client { ) -> Result { let rpc_client = http_client(config.nyxd_url.as_str())?; let prefix = &config.nyxd_config.chain_details.bech32_account_prefix; - let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic); + let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic)?; Ok(Self::new_signing_with_rpc_client( config, rpc_client, wallet, @@ -177,12 +176,13 @@ impl Client { } } -impl Client { +#[allow(deprecated)] +impl Client { pub fn new_reqwest_signing( config: Config, mnemonic: bip39::Mnemonic, ) -> DirectSigningReqwestRpcValidatorClient { - let rpc_client = ReqwestRpcClient::new(config.nyxd_url.clone()); + let rpc_client = crate::ReqwestRpcClient::new(config.nyxd_url.clone()); let prefix = &config.nyxd_config.chain_details.bech32_account_prefix; let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic); @@ -203,9 +203,10 @@ impl Client { } } -impl Client { +#[allow(deprecated)] +impl Client { pub fn new_reqwest_query(config: Config) -> QueryReqwestRpcValidatorClient { - let rpc_client = ReqwestRpcClient::new(config.nyxd_url.clone()); + let rpc_client = crate::ReqwestRpcClient::new(config.nyxd_url.clone()); Self::new_with_rpc_client(config, rpc_client) } } diff --git a/common/client-libs/validator-client/src/error.rs b/common/client-libs/validator-client/src/error.rs index c3979c0784..09fb7c8171 100644 --- a/common/client-libs/validator-client/src/error.rs +++ b/common/client-libs/validator-client/src/error.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nym_api; +use crate::signing::direct_wallet::DirectSecp256k1HdWalletError; pub use tendermint_rpc::error::Error as TendermintRpcError; use thiserror::Error; @@ -26,6 +27,12 @@ pub enum ValidatorClientError { #[error("No validator API url has been provided")] NoAPIUrlAvailable, + + #[error("failed to derive signing accounts: {source}")] + AccountDerivationFailure { + #[from] + source: DirectSecp256k1HdWalletError, + }, } impl From for ValidatorClientError { diff --git a/common/client-libs/validator-client/src/lib.rs b/common/client-libs/validator-client/src/lib.rs index db86658047..1736616dda 100644 --- a/common/client-libs/validator-client/src/lib.rs +++ b/common/client-libs/validator-client/src/lib.rs @@ -12,6 +12,7 @@ pub mod rpc; pub mod signing; pub use crate::error::ValidatorClientError; +#[allow(deprecated)] pub use crate::rpc::reqwest::ReqwestRpcClient; pub use crate::signing::direct_wallet::DirectSecp256k1HdWallet; pub use client::{Client, Config, EcashApiClient}; @@ -38,9 +39,13 @@ pub type DirectSigningHttpRpcValidatorClient = Client; +#[allow(deprecated)] pub type QueryReqwestRpcValidatorClient = Client; +#[allow(deprecated)] pub type QueryReqwestRpcNyxdClient = nyxd::NyxdClient; +#[allow(deprecated)] pub type DirectSigningReqwestRpcValidatorClient = Client; +#[allow(deprecated)] pub type DirectSigningReqwestRpcNyxdClient = nyxd::NyxdClient; diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_signing_client.rs index 8371f33133..bebc6aab69 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_signing_client.rs @@ -178,7 +178,7 @@ where .ok_or_else(|| NyxdError::unavailable_contract_address("dkg contract"))?; let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute(signer_address, dkg_contract_address, &msg, fee, memo, funds) .await diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs index 4c40a2b2fb..72ba43053b 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/ecash_signing_client.rs @@ -99,7 +99,7 @@ where .ok_or_else(|| NyxdError::unavailable_contract_address("coconut bandwidth contract"))?; let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute( signer_address, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/group_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/group_signing_client.rs index 5ac1954849..8ce18625b0 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/group_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/group_signing_client.rs @@ -95,7 +95,7 @@ where let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute( signer_address, group_contract_address, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs index 7ba4b3cd98..69aaac59d5 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs @@ -667,7 +667,7 @@ where let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); let memo = msg.default_memo(); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute( signer_address, mixnet_contract_address, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs index 715a38d457..eefa56da36 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/multisig_signing_client.rs @@ -133,7 +133,7 @@ where let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute( signer_address, multisig_contract_address, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs index 78b960265a..d0380c0452 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/performance_signing_client.rs @@ -165,7 +165,7 @@ where let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute( signer_address, performance_contract_address, diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs index 2f50016f91..2a46cf04ac 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs @@ -375,7 +375,7 @@ where let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier()))); let memo = msg.name().to_string(); - let signer_address = &self.signer_addresses()?[0]; + let signer_address = &self.signer_addresses()[0]; self.execute( signer_address, vesting_contract_address, diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs index 6516588339..df7f03dd34 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/mod.rs @@ -324,7 +324,7 @@ where { type Error = S::Error; - fn get_accounts(&self) -> Result, Self::Error> { + fn get_accounts(&self) -> &[AccountData] { self.signer.get_accounts() } diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 85b08ab12a..d7768b4bfd 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -19,7 +19,7 @@ use crate::signing::signer::NoSigner; use crate::signing::signer::OfflineSigner; use crate::signing::tx_signer::TxSigner; use crate::signing::AccountData; -use crate::{DirectSigningReqwestRpcNyxdClient, QueryReqwestRpcNyxdClient, ReqwestRpcClient}; +use crate::{DirectSigningReqwestRpcNyxdClient, QueryReqwestRpcNyxdClient}; use async_trait::async_trait; use cosmrs::tendermint::{abci, evidence::Evidence, Genesis}; use cosmrs::tx::{Raw, SignDoc}; @@ -158,12 +158,13 @@ impl NyxdClient { } } -impl NyxdClient { +#[allow(deprecated)] +impl NyxdClient { pub fn connect_reqwest( config: Config, endpoint: Url, ) -> Result { - let client = ReqwestRpcClient::new(endpoint); + let client = crate::ReqwestRpcClient::new(endpoint); Ok(NyxdClient { client: MaybeSigningClient::new(client, (&config).into()), @@ -195,18 +196,19 @@ impl NyxdClient { let client = http_client(endpoint)?; let prefix = &config.chain_details.bech32_account_prefix; - let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic); + let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic)?; Ok(Self::connect_with_signer(config, client, wallet)) } } -impl NyxdClient { +#[allow(deprecated)] +impl NyxdClient { pub fn connect_reqwest_with_mnemonic( config: Config, endpoint: Url, mnemonic: bip39::Mnemonic, ) -> DirectSigningReqwestRpcNyxdClient { - let client = ReqwestRpcClient::new(endpoint); + let client = crate::ReqwestRpcClient::new(endpoint); let prefix = &config.chain_details.bech32_account_prefix; let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic); @@ -391,17 +393,12 @@ where S: OfflineSigner + Send + Sync, NyxdError: From<::Error>, { - pub fn signing_account(&self) -> Result { + pub fn signing_account(&self) -> Result<&AccountData, NyxdError> { Ok(self.find_account(&self.address())?) } pub fn address(&self) -> AccountId { - match self.client.signer_addresses() { - Ok(addresses) => addresses[0].clone(), - Err(_) => { - panic!("key derivation failure") - } - } + self.client.signer_addresses()[0].clone() } pub fn mix_coin(&self, amount: u128) -> Coin { @@ -867,7 +864,7 @@ where { type Error = S::Error; - fn get_accounts(&self) -> Result, Self::Error> { + fn get_accounts(&self) -> &[AccountData] { self.client.get_accounts() } diff --git a/common/client-libs/validator-client/src/rpc/reqwest.rs b/common/client-libs/validator-client/src/rpc/reqwest.rs index 09312b28c3..b4cd98cd2f 100644 --- a/common/client-libs/validator-client/src/rpc/reqwest.rs +++ b/common/client-libs/validator-client/src/rpc/reqwest.rs @@ -42,12 +42,15 @@ macro_rules! perform_with_compat { }}; } +// the separate implementation is now completely redundant +#[deprecated(note = "use HttpClient directly instead")] pub struct ReqwestRpcClient { compat: CompatMode, inner: reqwest::Client, url: Url, } +#[allow(deprecated)] impl ReqwestRpcClient { pub fn new(url: Url) -> Self { ReqwestRpcClient { @@ -131,6 +134,7 @@ impl TendermintRpcErrorMap for reqwest::Error { } } +#[allow(deprecated)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl TendermintRpcClient for ReqwestRpcClient { diff --git a/common/client-libs/validator-client/src/signing/direct_wallet.rs b/common/client-libs/validator-client/src/signing/direct_wallet.rs index 9d44255129..2cc64f8bfa 100644 --- a/common/client-libs/validator-client/src/signing/direct_wallet.rs +++ b/common/client-libs/validator-client/src/signing/direct_wallet.rs @@ -2,18 +2,18 @@ // SPDX-License-Identifier: Apache-2.0 use crate::signing::signer::{OfflineSigner, SigningError}; -use crate::signing::{AccountData, Secp256k1Derivation}; -use cosmrs::bip32::{DerivationPath, XPrv}; -use cosmrs::crypto::secp256k1::SigningKey; -use cosmrs::crypto::PublicKey; +use crate::signing::{ + derive_extended_private_key, derive_keypair, AccountData, Secp256k1Derivation, Secp256k1Keypair, +}; +use bip32::XPrv; +use cosmrs::bip32::DerivationPath; use cosmrs::tx; use cosmrs::tx::SignDoc; use nym_config::defaults; +use std::borrow::Cow; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; -type Secp256k1Keypair = (SigningKey, PublicKey); - #[derive(Debug, Error)] pub enum DirectSecp256k1HdWalletError { #[error(transparent)] @@ -36,28 +36,22 @@ pub enum DirectSecp256k1HdWalletError { } // TODO: maybe lock this one behind feature flag? -#[derive(Debug, Clone, Zeroize, ZeroizeOnDrop)] +#[derive(Zeroize, ZeroizeOnDrop)] pub struct DirectSecp256k1HdWallet { /// Base secret secret: bip39::Mnemonic, - /// BIP39 seed - seed: [u8; 64], - - // An unfortunate result of immature rust async story is that async traits (only available in the separate package) - // can't yet figure out everything and if we stored our derived account data on the struct, - // that would include the secret key which is a dyn EcdsaSigner and hence not Sync making the wallet - // not Sync and if used on the signing client in an async trait, it wouldn't be Send - /// Derivation instructions + /// Derived accounts #[zeroize(skip)] - accounts: Vec, + // unfortunately `dyn EcdsaSigner` does not guarantee Zeroize + accounts: Vec, } impl OfflineSigner for DirectSecp256k1HdWallet { type Error = DirectSecp256k1HdWalletError; - fn get_accounts(&self) -> Result, Self::Error> { - self.try_derive_accounts() + fn get_accounts(&self) -> &[AccountData] { + &self.accounts } fn sign_direct_with_account( @@ -77,55 +71,27 @@ impl DirectSecp256k1HdWallet { } /// Restores a wallet from the given BIP39 mnemonic using default options. + #[deprecated( + note = "this function can potentially panic if accounts can't be derived correctly. please use .checked_from_mnemonic() instead" + )] pub fn from_mnemonic(prefix: &str, mnemonic: bip39::Mnemonic) -> Self { + // unfortunately due to backwards compatibility requirements, + // we can't change signature of this method + #[allow(deprecated)] DirectSecp256k1HdWalletBuilder::new(prefix).build(mnemonic) } + /// Restores a wallet from the given BIP39 mnemonic using default options. + pub fn checked_from_mnemonic( + prefix: &str, + mnemonic: bip39::Mnemonic, + ) -> Result { + DirectSecp256k1HdWalletBuilder::new(prefix).try_build(mnemonic) + } + pub fn generate(prefix: &str, word_count: usize) -> Result { let mneomonic = bip39::Mnemonic::generate(word_count)?; - Ok(Self::from_mnemonic(prefix, mneomonic)) - } - - fn derive_keypair( - &self, - hd_path: &DerivationPath, - ) -> Result { - let extended_private_key = XPrv::derive_from_path(self.seed, hd_path)?; - - let private_key: SigningKey = extended_private_key.into(); - let public_key = private_key.public_key(); - - Ok((private_key, public_key)) - } - - pub fn derive_extended_private_key( - &self, - hd_path: &DerivationPath, - ) -> Result { - Ok(XPrv::derive_from_path(self.seed, hd_path)?) - } - - pub fn try_derive_accounts(&self) -> Result, DirectSecp256k1HdWalletError> { - let mut accounts = Vec::with_capacity(self.accounts.len()); - for derivation_info in &self.accounts { - let keypair = self.derive_keypair(&derivation_info.hd_path)?; - - // it seems this can only fail if the provided account prefix is invalid - let address = keypair - .1 - .account_id(&derivation_info.prefix) - .map_err( - |source| DirectSecp256k1HdWalletError::AccountDerivationError { source }, - )?; - - accounts.push(AccountData { - address, - public_key: keypair.1, - private_key: keypair.0, - }) - } - - Ok(accounts) + Self::checked_from_mnemonic(prefix, mneomonic) } pub fn secret(&self) -> &bip39::Mnemonic { @@ -142,6 +108,43 @@ impl DirectSecp256k1HdWallet { pub fn mnemonic_string(&self) -> Zeroizing { Zeroizing::new(self.secret.to_string()) } + + pub fn account_seed<'a, P: Into>>( + &self, + bip39_password: P, + ) -> Zeroizing<[u8; 64]> { + Zeroizing::new(self.secret.to_seed(bip39_password)) + } + + /// Derive an extended private key from the stored account secret assuming no bip39 password + #[deprecated( + note = "use derive_extended_private_key_with_password to ensure correct derivation if used bip39 password" + )] + pub fn derive_extended_private_key( + &self, + hd_path: &DerivationPath, + ) -> Result { + let seed = self.account_seed(""); + derive_extended_private_key(seed, hd_path) + } + + pub fn derive_keypair<'a, P: Into>>( + &self, + hd_path: &DerivationPath, + bip39_password: P, + ) -> Result { + let seed = self.account_seed(bip39_password); + derive_keypair(seed, hd_path) + } + + pub fn derive_extended_private_key_with_password<'a, P: Into>>( + &self, + hd_path: &DerivationPath, + bip39_password: P, + ) -> Result { + let seed = self.account_seed(bip39_password); + derive_extended_private_key(seed, hd_path) + } } #[must_use] @@ -188,23 +191,39 @@ impl DirectSecp256k1HdWalletBuilder { self } + #[deprecated( + note = "this function can potentially panic if accounts can't be derived correctly. please use .try_build() instead" + )] pub fn build(self, mnemonic: bip39::Mnemonic) -> DirectSecp256k1HdWallet { - let seed = mnemonic.to_seed(&self.bip39_password); + // unfortunately due to backwards compatibility requirements, + // we can't change signature of this method + #[allow(clippy::expect_used)] + self.try_build(mnemonic) + .expect("account derivation failure") + } + + pub fn try_build( + self, + mnemonic: bip39::Mnemonic, + ) -> Result { + let seed = Zeroizing::new(mnemonic.to_seed(&self.bip39_password)); let prefix = self.prefix.clone(); let accounts = self .hd_paths .iter() - .map(|hd_path| Secp256k1Derivation { - hd_path: hd_path.clone(), - prefix: prefix.clone(), + .map(|hd_path| { + Secp256k1Derivation { + hd_path: hd_path.clone(), + prefix: prefix.clone(), + } + .try_derive_account(&seed) }) - .collect(); + .collect::>()?; - DirectSecp256k1HdWallet { + Ok(DirectSecp256k1HdWallet { accounts, - seed, secret: mnemonic, - } + }) } } @@ -215,7 +234,7 @@ mod tests { use super::*; #[test] - fn generating_account_addresses() { + fn generating_account_addresses() -> anyhow::Result<()> { // test vectors produced from our js wallet let mnemonics = ["crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove", "acquire rebel spot skin gun such erupt pull swear must define ill chief turtle today flower chunk truth battle claw rigid detail gym feel", @@ -230,11 +249,10 @@ mod tests { "n17n9flp6jflljg6fp05dsy07wcprf2uuu8g40rf", ]; for (idx, mnemonic) in mnemonics.iter().enumerate() { - let wallet = DirectSecp256k1HdWallet::from_mnemonic(&prefix, mnemonic.parse().unwrap()); - assert_eq!( - wallet.try_derive_accounts().unwrap()[0].address, - addrs[idx].parse().unwrap() - ) + let wallet = + DirectSecp256k1HdWallet::checked_from_mnemonic(&prefix, mnemonic.parse()?)?; + assert_eq!(wallet.signer_addresses()[0], addrs[idx].parse().unwrap()); } + Ok(()) } } diff --git a/common/client-libs/validator-client/src/signing/mod.rs b/common/client-libs/validator-client/src/signing/mod.rs index d9fa0c30e4..ad5ac9f7e5 100644 --- a/common/client-libs/validator-client/src/signing/mod.rs +++ b/common/client-libs/validator-client/src/signing/mod.rs @@ -1,6 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::signing::direct_wallet::DirectSecp256k1HdWalletError; +use bip32::XPrv; use cosmrs::bip32::DerivationPath; use cosmrs::crypto::secp256k1::SigningKey; use cosmrs::crypto::PublicKey; @@ -12,14 +14,64 @@ pub mod direct_wallet; pub mod signer; pub mod tx_signer; +pub(crate) type Secp256k1Keypair = (SigningKey, PublicKey); + /// Derivation information required to derive a keypair and an address from a mnemonic. #[derive(Debug, Clone)] -struct Secp256k1Derivation { +pub(crate) struct Secp256k1Derivation { hd_path: DerivationPath, prefix: String, } -// TODO: is this struct going to be derivable with other signer types? +impl Secp256k1Derivation { + pub(crate) fn try_derive_account( + &self, + seed: S, + ) -> Result + where + S: AsRef<[u8]>, + { + let keypair = derive_keypair(seed, &self.hd_path)?; + + // it seems this can only fail if the provided account prefix is invalid + let address = keypair + .1 + .account_id(&self.prefix) + .map_err(|source| DirectSecp256k1HdWalletError::AccountDerivationError { source })?; + + Ok(AccountData { + address, + public_key: keypair.1, + private_key: keypair.0, + }) + } +} + +pub fn derive_keypair( + seed: S, + hd_path: &DerivationPath, +) -> Result +where + S: AsRef<[u8]>, +{ + let extended_private_key = derive_extended_private_key(seed, hd_path)?; + + let private_key: SigningKey = extended_private_key.into(); + let public_key = private_key.public_key(); + + Ok((private_key, public_key)) +} + +pub fn derive_extended_private_key( + seed: S, + hd_path: &DerivationPath, +) -> Result +where + S: AsRef<[u8]>, +{ + Ok(XPrv::derive_from_path(seed, hd_path)?) +} + pub struct AccountData { pub address: AccountId, diff --git a/common/client-libs/validator-client/src/signing/signer.rs b/common/client-libs/validator-client/src/signing/signer.rs index 4b620291fe..af7fb584e0 100644 --- a/common/client-libs/validator-client/src/signing/signer.rs +++ b/common/client-libs/validator-client/src/signing/signer.rs @@ -33,23 +33,18 @@ pub enum SignerType { pub trait OfflineSigner { type Error: From; - // I really dislike existence of this function because it makes you re-derive your key **twice** for each contract transaction - fn signer_addresses(&self) -> Result, Self::Error> { - let derived_addresses = self - .get_accounts()? - .into_iter() - .map(|account| account.address) - .collect(); - Ok(derived_addresses) + fn signer_addresses(&self) -> Vec { + self.get_accounts() + .iter() + .map(|account| account.address.clone()) + .collect() } - fn get_accounts(&self) -> Result, Self::Error>; + fn get_accounts(&self) -> &[AccountData]; - fn find_account(&self, signer_address: &AccountId) -> Result { - // TODO: we could really use some zeroize action here - let accounts = self.get_accounts()?; - accounts - .into_iter() + fn find_account(&self, signer_address: &AccountId) -> Result<&AccountData, Self::Error> { + self.get_accounts() + .iter() .find(|account| &account.address == signer_address) .ok_or_else(|| { SigningError::AccountNotFound { @@ -76,7 +71,7 @@ pub trait OfflineSigner { message: M, ) -> Result { let signer = self.find_account(signer_address)?; - self.sign_raw_with_account(&signer, message) + self.sign_raw_with_account(signer, message) } fn sign_direct( @@ -85,7 +80,7 @@ pub trait OfflineSigner { sign_doc: SignDoc, ) -> Result { let signer = self.find_account(signer_address)?; - self.sign_direct_with_account(&signer, sign_doc) + self.sign_direct_with_account(signer, sign_doc) } // unless explicitly defined, each signing method is unsupported @@ -122,7 +117,7 @@ pub struct NoSigner; // impl OfflineSigner for NoSigner { // type Error = SignerUnavailable; // -// fn get_accounts(&self) -> Result, Self::Error> { +// fn get_accounts(&self) -> &[AccountData] { // return Err(SignerUnavailable); // } // } diff --git a/common/client-libs/validator-client/src/signing/tx_signer.rs b/common/client-libs/validator-client/src/signing/tx_signer.rs index f04047c39e..59289bbe09 100644 --- a/common/client-libs/validator-client/src/signing/tx_signer.rs +++ b/common/client-libs/validator-client/src/signing/tx_signer.rs @@ -50,7 +50,7 @@ pub trait TxSigner: OfflineSigner { ) .map_err(|source| SigningError::SignDocFailure { source })?; - self.sign_direct_with_account(&account_from_signer, sign_doc) + self.sign_direct_with_account(account_from_signer, sign_doc) } } diff --git a/common/commands/src/validator/account/create.rs b/common/commands/src/validator/account/create.rs index f2cd4eb336..0839ff1279 100644 --- a/common/commands/src/validator/account/create.rs +++ b/common/commands/src/validator/account/create.rs @@ -3,6 +3,7 @@ use clap::Parser; use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet; +use nym_validator_client::signing::signer::OfflineSigner; #[derive(Debug, Parser)] pub struct Args { @@ -15,9 +16,10 @@ pub fn create_account(args: Args, prefix: &str) { let word_count = args.word_count.unwrap_or(24); let mnemonic = bip39::Mnemonic::generate(word_count).expect("failed to generate mnemonic!"); - let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic); + let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic) + .expect("failed to derive accounts!"); // Output address and mnemonics into separate lines for easier parsing println!("{}", wallet.mnemonic_string().as_str()); - println!("{}", wallet.try_derive_accounts().unwrap()[0].address()); + println!("{}", wallet.signer_addresses()[0]); } diff --git a/common/commands/src/validator/account/pubkey.rs b/common/commands/src/validator/account/pubkey.rs index ed419c1101..fc9bf25f97 100644 --- a/common/commands/src/validator/account/pubkey.rs +++ b/common/commands/src/validator/account/pubkey.rs @@ -1,13 +1,13 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::context::QueryClient; +use crate::utils::show_error; use clap::Parser; use log::{error, info}; use nym_validator_client::nyxd::{AccountId, CosmWasmClient}; use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet; - -use crate::context::QueryClient; -use crate::utils::show_error; +use nym_validator_client::signing::signer::OfflineSigner; #[derive(Debug, Parser)] pub struct Args { @@ -50,20 +50,19 @@ pub async fn get_pubkey( } pub fn get_pubkey_from_mnemonic(address: AccountId, prefix: &str, mnemonic: bip39::Mnemonic) { - let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic); - match wallet.try_derive_accounts() { - Ok(accounts) => match accounts.iter().find(|a| *a.address() == address) { - Some(account) => { - println!("{}", account.public_key().to_string()); - } - None => { - error!("Could not derive key that matches {address}") - } - }, - Err(e) => { - error!("Failed to derive accounts. {e}"); + let wallet = match DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic) { + Ok(wallet) => wallet, + Err(err) => { + error!("Failed to derive accounts. {err}"); + return; } - } + }; + + let Ok(account) = wallet.find_account(&address) else { + error!("Could not derive key that matches {address}"); + return; + }; + println!("{}", account.public_key().to_string()); } pub async fn get_pubkey_from_chain(address: AccountId, client: &QueryClient) { diff --git a/common/commands/src/validator/signature/sign.rs b/common/commands/src/validator/signature/sign.rs index 7df1d12eb6..6a2e3254b9 100644 --- a/common/commands/src/validator/signature/sign.rs +++ b/common/commands/src/validator/signature/sign.rs @@ -36,32 +36,35 @@ pub fn sign(args: Args, prefix: &str, mnemonic: Option) { return; } - let wallet = - DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.expect("mnemonic not set")); - match wallet.try_derive_accounts() { - Ok(accounts) => match accounts.first() { - Some(account) => { - let msg = args.message.into_bytes(); - match wallet.sign_raw_with_account(account, msg) { - Ok(signature) => { - let output = SignatureOutputJson { - account_id: account.address().to_string(), - public_key: account.public_key(), - signature_as_hex: signature.to_string(), - }; - println!("{}", json!(output)); - } - Err(e) => { - error!("Failed to sign message. {e}"); - } + let wallet = match DirectSecp256k1HdWallet::checked_from_mnemonic( + prefix, + mnemonic.expect("mnemonic not set"), + ) { + Ok(wallet) => wallet, + Err(err) => { + error!("Could not derive an account key from the mnemonic: {err}"); + return; + } + }; + match wallet.get_accounts().first() { + Some(account) => { + let msg = args.message.into_bytes(); + match wallet.sign_raw_with_account(account, msg) { + Ok(signature) => { + let output = SignatureOutputJson { + account_id: account.address().to_string(), + public_key: account.public_key(), + signature_as_hex: signature.to_string(), + }; + println!("{}", json!(output)); + } + Err(e) => { + error!("Failed to sign message. {e}"); } } - None => { - error!("Could not derive an account key from the mnemonic",) - } - }, - Err(e) => { - error!("Failed to derive accounts. {e}"); + } + None => { + error!("Could not derive an account key from the mnemonic",) } } } diff --git a/common/credential-proxy/src/error.rs b/common/credential-proxy/src/error.rs index d6e3e8d28f..86219fc5bd 100644 --- a/common/credential-proxy/src/error.rs +++ b/common/credential-proxy/src/error.rs @@ -1,6 +1,7 @@ // Copyright 2025 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_crypto::asymmetric::ed25519; use nym_ecash_signer_check::SignerCheckError; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nym_api::{EpochId, error::NymAPIError}; @@ -174,8 +175,18 @@ pub enum CredentialProxyError { )] AttestationCheckUrlNotSet, - #[error("the provided attestation check url is malformed: {source}")] + #[error("the provided attester public key is malformed: {source}")] MalformedAttestationCheckUrl { source: url::ParseError }, + + #[error( + "the attester public key has not been provided through either the CLI nor the default .env config" + )] + AttesterPublicKeyNotSet, + + #[error("the provided attester public key is malformed: {source}")] + MalformedAttesterPublicKey { + source: ed25519::Ed25519RecoveryError, + }, } impl From for CredentialProxyError { diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index 1b0539dda4..9ca0eab6f3 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -14,6 +14,7 @@ bincode = { workspace = true, optional = true } log = { workspace = true } thiserror = { workspace = true } serde = { workspace = true, features = ["derive"], optional = true } +time = { workspace = true } tokio = { workspace = true, features = ["sync"] } zeroize = { workspace = true, features = ["zeroize_derive"] } diff --git a/common/credential-storage/migrations/20251030120000_upgrade_mode_token.sql b/common/credential-storage/migrations/20251030120000_upgrade_mode_token.sql new file mode 100644 index 0000000000..7f4c6f5ec8 --- /dev/null +++ b/common/credential-storage/migrations/20251030120000_upgrade_mode_token.sql @@ -0,0 +1,17 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +CREATE TABLE emergency_credential +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + -- don't define any strict schema on the content as it might be implementation-dependant + content BLOB NOT NULL, + expiration TIMESTAMP WITHOUT TIME ZONE +); + +-- no point in allowing duplicate data +CREATE UNIQUE INDEX emergency_credential_unique_type_content + ON emergency_credential (type, content); diff --git a/common/credential-storage/src/backends/memory.rs b/common/credential-storage/src/backends/memory.rs index 53207a4060..28b99e4196 100644 --- a/common/credential-storage/src/backends/memory.rs +++ b/common/credential-storage/src/backends/memory.rs @@ -1,7 +1,10 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use crate::models::{ + BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent, + RetrievedPendingTicketbook, RetrievedTicketbook, +}; use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; use nym_compact_ecash::VerificationKeyAuth; @@ -22,6 +25,12 @@ pub struct MemoryEcachTicketbookManager { inner: Arc>, } +#[derive(Default)] +struct InternalIdCounters { + next_ticketbook_id: i64, + next_emergency_credential_id: i64, +} + #[derive(Default)] struct EcashCredentialManagerInner { ticketbooks: HashMap, @@ -29,13 +38,22 @@ struct EcashCredentialManagerInner { master_vk: HashMap, coin_indices_sigs: HashMap>, expiration_date_sigs: HashMap<(u64, Date), Vec>, - _next_id: i64, + emergency_credentials: HashMap>, + + // internal counters emulating assignment of an increasing id to new inserted database entries + internal_counters: InternalIdCounters, } impl EcashCredentialManagerInner { - fn next_id(&mut self) -> i64 { - let next = self._next_id; - self._next_id += 1; + fn next_ticketbook_id(&mut self) -> i64 { + let next = self.internal_counters.next_ticketbook_id; + self.internal_counters.next_ticketbook_id += 1; + next + } + + fn next_emergency_credential_id(&mut self) -> i64 { + let next = self.internal_counters.next_emergency_credential_id; + self.internal_counters.next_emergency_credential_id += 1; next } } @@ -170,7 +188,7 @@ impl MemoryEcachTicketbookManager { used_tickets: u32, ) { let mut guard = self.inner.write().await; - let id = guard.next_id(); + let id = guard.next_ticketbook_id(); #[allow(clippy::unwrap_used)] let mut nasty_clone = hack_clone_ticketbook(ticketbook); @@ -277,4 +295,41 @@ impl MemoryEcachTicketbookManager { sigs.signatures.clone(), ); } + + pub(crate) async fn get_emergency_credential(&self, typ: &str) -> Option { + let guard = self.inner.read().await; + + guard.emergency_credentials.get(typ)?.first().cloned() + } + + pub(crate) async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) { + let mut guard = self.inner.write().await; + let id = guard.next_emergency_credential_id(); + + guard + .emergency_credentials + .entry(credential.typ.clone()) + .or_default() + .push(EmergencyCredential { + id, + data: credential.clone(), + }); + } + + pub(crate) async fn remove_emergency_credential(&self, id: i64) { + let mut guard = self.inner.write().await; + + guard.emergency_credentials.retain(|_, credentials| { + credentials.retain(|c| c.id != id); + !credentials.is_empty() + }) + } + + pub(crate) async fn remove_emergency_credentials_of_type(&self, typ: &str) { + let mut guard = self.inner.write().await; + guard.emergency_credentials.remove(typ); + } } diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index d196cad049..cb1cddf980 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::models::{ - BasicTicketbookInformation, RawCoinIndexSignatures, RawExpirationDateSignatures, - RawVerificationKey, StoredIssuedTicketbook, StoredPendingTicketbook, + BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent, + RawCoinIndexSignatures, RawExpirationDateSignatures, RawVerificationKey, + StoredIssuedTicketbook, StoredPendingTicketbook, }; use nym_ecash_time::Date; use sqlx::{Executor, Sqlite, Transaction}; @@ -305,6 +306,74 @@ impl SqliteEcashTicketbookManager { .await?; Ok(()) } + + pub(crate) async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as( + r#" + SELECT * + FROM emergency_credential + WHERE type = ? + AND (expiration IS NULL OR expiration > CURRENT_TIMESTAMP) + ORDER BY expiration DESC NULLS LAST + LIMIT 1 + "#, + ) + .bind(typ) + .fetch_optional(&*self.connection_pool) + .await + } + + pub(crate) async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO emergency_credential + (type, content, expiration) + VALUES (?, ?, ?) + ON CONFLICT(type, content) DO NOTHING; + "#, + credential.typ, + credential.content, + credential.expiration, + ) + .execute(&*self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_emergency_credential(&self, id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM emergency_credential + WHERE id = ? + "#, + id + ) + .execute(&*self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_emergency_credentials_of_type( + &self, + typ: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM emergency_credential + WHERE type = ? + "#, + typ + ) + .execute(&*self.connection_pool) + .await?; + Ok(()) + } } pub(crate) async fn get_next_unspent_ticketbook<'a, E>( diff --git a/common/credential-storage/src/ephemeral_storage.rs b/common/credential-storage/src/ephemeral_storage.rs index 4c11a456e9..cd65bf3cca 100644 --- a/common/credential-storage/src/ephemeral_storage.rs +++ b/common/credential-storage/src/ephemeral_storage.rs @@ -3,7 +3,10 @@ use crate::backends::memory::MemoryEcachTicketbookManager; use crate::error::StorageError; -use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use crate::models::{ + BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent, + RetrievedPendingTicketbook, RetrievedTicketbook, +}; use crate::storage::Storage; use async_trait::async_trait; use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; @@ -218,6 +221,38 @@ impl Storage for EphemeralStorage { .await; Ok(()) } + + async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_emergency_credential(typ).await) + } + + async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_emergency_credential(credential) + .await; + Ok(()) + } + + async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> { + self.storage_manager.remove_emergency_credential(id).await; + Ok(()) + } + + async fn remove_emergency_credentials_of_type( + &self, + typ: &str, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .remove_emergency_credentials_of_type(typ) + .await; + Ok(()) + } } #[cfg(test)] diff --git a/common/credential-storage/src/models.rs b/common/credential-storage/src/models.rs index 5be1c45d29..f98bcd1f09 100644 --- a/common/credential-storage/src/models.rs +++ b/common/credential-storage/src/models.rs @@ -3,6 +3,7 @@ use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; use nym_ecash_time::Date; +use time::OffsetDateTime; use zeroize::{Zeroize, ZeroizeOnDrop}; pub struct RetrievedTicketbook { @@ -78,3 +79,20 @@ pub struct RawVerificationKey { pub serialised_key: Vec, pub serialization_revision: u8, } + +#[derive(Clone, Debug)] +#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +pub struct EmergencyCredential { + pub id: i64, + #[cfg_attr(not(target_arch = "wasm32"), sqlx(flatten))] + pub data: EmergencyCredentialContent, +} + +#[derive(Clone, Debug)] +#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +pub struct EmergencyCredentialContent { + #[cfg_attr(not(target_arch = "wasm32"), sqlx(rename = "type"))] + pub typ: String, + pub content: Vec, + pub expiration: Option, +} diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs index b3302983ee..debef28093 100644 --- a/common/credential-storage/src/persistent_storage/mod.rs +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -3,6 +3,7 @@ mod legacy_helpers; +use crate::models::{EmergencyCredential, EmergencyCredentialContent}; use crate::{ backends::sqlite::{ get_next_unspent_ticketbook, increase_used_ticketbook_tickets, SqliteEcashTicketbookManager, @@ -401,4 +402,36 @@ impl Storage for PersistentStorage { .await?; Ok(()) } + + async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_emergency_credential(typ).await?) + } + + async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_emergency_credential(credential) + .await?; + Ok(()) + } + + async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> { + self.storage_manager.remove_emergency_credential(id).await?; + Ok(()) + } + + async fn remove_emergency_credentials_of_type( + &self, + typ: &str, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .remove_emergency_credentials_of_type(typ) + .await?; + Ok(()) + } } diff --git a/common/credential-storage/src/storage.rs b/common/credential-storage/src/storage.rs index add8131e12..483a4571c5 100644 --- a/common/credential-storage/src/storage.rs +++ b/common/credential-storage/src/storage.rs @@ -1,7 +1,10 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use crate::models::{ + BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent, + RetrievedPendingTicketbook, RetrievedTicketbook, +}; use async_trait::async_trait; use nym_compact_ecash::VerificationKeyAuth; use nym_credentials::ecash::bandwidth::serialiser::keys::EpochVerificationKey; @@ -108,4 +111,21 @@ pub trait Storage: Clone + Send + Sync { &self, signatures: &AggregatedExpirationDateSignatures, ) -> Result<(), Self::StorageError>; + + async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, Self::StorageError>; + + async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) -> Result<(), Self::StorageError>; + + async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError>; + + async fn remove_emergency_credentials_of_type( + &self, + typ: &str, + ) -> Result<(), Self::StorageError>; } diff --git a/common/credential-verification/Cargo.toml b/common/credential-verification/Cargo.toml index e85589eaae..06c45efdc5 100644 --- a/common/credential-verification/Cargo.toml +++ b/common/credential-verification/Cargo.toml @@ -17,7 +17,6 @@ cosmwasm-std = { workspace = true } cw-utils = { workspace = true } dyn-clone = { workspace = true } futures = { workspace = true } -rand = { workspace = true } si-scale = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } @@ -27,8 +26,10 @@ tracing = { workspace = true } nym-api-requests = { path = "../../nym-api/nym-api-requests" } nym-credentials = { path = "../credentials" } nym-credentials-interface = { path = "../credentials-interface" } +nym-crypto = { path = "../crypto", features = ["asymmetric"] } nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } nym-gateway-requests = { path = "../gateway-requests" } nym-gateway-storage = { path = "../gateway-storage" } nym-task = { path = "../task" } nym-validator-client = { path = "../client-libs/validator-client" } +nym-upgrade-mode-check = { path = "../upgrade-mode-check" } diff --git a/common/credential-verification/src/bandwidth_storage_manager.rs b/common/credential-verification/src/bandwidth_storage_manager.rs index 7286229e68..9a78b9f95e 100644 --- a/common/credential-verification/src/bandwidth_storage_manager.rs +++ b/common/credential-verification/src/bandwidth_storage_manager.rs @@ -6,7 +6,6 @@ use crate::ClientBandwidth; use crate::error::*; use nym_credentials::ecash::utils::ecash_today; use nym_credentials_interface::Bandwidth; -use nym_gateway_requests::ServerResponse; use nym_gateway_storage::traits::BandwidthGatewayStorage; use si_scale::helpers::bibytes2; use time::OffsetDateTime; @@ -66,7 +65,7 @@ impl BandwidthStorageManager { Ok(()) } - pub async fn handle_claim_testnet_bandwidth(&mut self) -> Result { + pub async fn handle_claim_testnet_bandwidth(&mut self) -> Result { debug!("handling testnet bandwidth request"); if self.only_coconut_credentials { @@ -76,8 +75,7 @@ impl BandwidthStorageManager { self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE, ecash_today()) .await?; let available_total = self.client_bandwidth.available().await; - - Ok(ServerResponse::Bandwidth { available_total }) + Ok(available_total) } #[instrument(skip_all)] @@ -96,7 +94,7 @@ impl BandwidthStorageManager { let available_bi2 = bibytes2(available_bandwidth as f64); let required_bi2 = bibytes2(required_bandwidth as f64); - debug!(available = available_bi2, required = required_bi2); + trace!(available = available_bi2, required = required_bi2); self.consume_bandwidth(required_bandwidth).await?; let remaining_bandwidth = self.client_bandwidth.available().await; diff --git a/common/credential-verification/src/error.rs b/common/credential-verification/src/error.rs index a00ce0e268..23fb47d8c0 100644 --- a/common/credential-verification/src/error.rs +++ b/common/credential-verification/src/error.rs @@ -47,6 +47,25 @@ pub enum Error { UnknownTicketType(#[from] nym_credentials_interface::UnknownTicketType), } +impl Error { + pub fn is_out_of_bandwidth(&self) -> bool { + matches!(self, Error::OutOfBandwidth { .. }) + } +} + +pub trait OutOfBandwidthResultExt { + fn is_out_of_bandwidth(&self) -> bool; +} + +impl OutOfBandwidthResultExt for Result { + fn is_out_of_bandwidth(&self) -> bool { + match &self { + Ok(_) => false, + Err(err) => err.is_out_of_bandwidth(), + } + } +} + impl From for Error { fn from(err: EcashTicketError) -> Self { // don't expose storage issue details to the user diff --git a/common/credential-verification/src/lib.rs b/common/credential-verification/src/lib.rs index f306867d98..430674c991 100644 --- a/common/credential-verification/src/lib.rs +++ b/common/credential-verification/src/lib.rs @@ -13,11 +13,13 @@ use tracing::*; pub use client_bandwidth::*; pub use error::*; +pub use upgrade_mode::UpgradeModeState; pub mod bandwidth_storage_manager; mod client_bandwidth; pub mod ecash; pub mod error; +pub mod upgrade_mode; pub struct CredentialVerifier { credential: CredentialSpendingRequest, diff --git a/common/credential-verification/src/upgrade_mode.rs b/common/credential-verification/src/upgrade_mode.rs new file mode 100644 index 0000000000..7dec1010f7 --- /dev/null +++ b/common/credential-verification/src/upgrade_mode.rs @@ -0,0 +1,293 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use nym_crypto::asymmetric::ed25519; +use nym_upgrade_mode_check::{ + CREDENTIAL_PROXY_JWT_ISSUER, UpgradeModeAttestation, validate_upgrade_mode_jwt, +}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; +use std::time::Duration; +use thiserror::Error; +use time::OffsetDateTime; +use tokio::sync::{Notify, RwLock}; +use tracing::{debug, error, info}; + +#[derive(Debug, Error)] +pub enum UpgradeModeEnableError { + #[error("too soon to perform another upgrade mode attestation check")] + TooManyRecheckRequests, + + #[error("provided upgrade mode JWT is invalid: {0}")] + InvalidUpgradeModeJWT(#[from] nym_upgrade_mode_check::UpgradeModeCheckError), + + #[error("the upgrade mode attestation does not appear to have been published")] + AttestationNotPublished, + + #[error("the provided upgrade mode attestation is different from the published one")] + MismatchedUpgradeModeAttestation, +} + +// the idea behind this is as follows: +// it's been relatively a long time since the watcher last performed its checks (since it's in 'regular' mode) +// and some client has just sent a JWT. we have to retrieve most recent information in case upgrade mode +// has just been enabled, and we haven't learned about it yet +#[derive(Clone)] +pub struct UpgradeModeCheckRequestSender(Option>); + +impl UpgradeModeCheckRequestSender { + pub fn new(sender: UnboundedSender) -> Self { + UpgradeModeCheckRequestSender(Some(sender)) + } + + pub fn new_empty() -> Self { + Self(None) + } + + pub(crate) fn send_request(&self, on_done: Arc) { + let Some(ref inner) = self.0 else { + // make sure the caller gets notified so it doesn't wait forever + on_done.notify_waiters(); + return; + }; + + if let Err(not_sent) = inner.unbounded_send(CheckRequest { on_done }) { + debug!("failed to send upgrade mode check request - {not_sent}"); + // make sure the caller gets notified so it doesn't wait forever + not_sent.into_inner().on_done.notify_waiters(); + } + } +} + +pub type UpgradeModeCheckRequestReceiver = UnboundedReceiver; + +pub struct CheckRequest { + on_done: Arc, +} + +impl CheckRequest { + pub fn finalize(self) { + self.on_done.notify_waiters(); + } +} + +#[derive(Clone, Copy)] +pub struct UpgradeModeCheckConfig { + /// The minimum duration since the last explicit check to allow creation of separate request. + pub min_staleness_recheck: Duration, +} + +/// Full upgrade mode information, that apart from boolean flag indicating the state +/// and the attestation information, includes channel connection to relevant +/// attestation watcher to request state rechecks +#[derive(Clone)] +pub struct UpgradeModeDetails { + pub(crate) config: UpgradeModeCheckConfig, + pub(crate) request_checker: UpgradeModeCheckRequestSender, + pub(crate) state: UpgradeModeState, +} + +impl UpgradeModeDetails { + pub fn new( + config: UpgradeModeCheckConfig, + request_checker: UpgradeModeCheckRequestSender, + state: UpgradeModeState, + ) -> Self { + UpgradeModeDetails { + config, + request_checker, + state, + } + } + + pub fn state(&self) -> &UpgradeModeState { + &self.state + } + + pub fn enabled(&self) -> bool { + self.state.upgrade_mode_enabled() + } + + fn since_last_query(&self) -> Duration { + self.state.since_last_query() + } + + pub fn can_request_recheck(&self) -> bool { + self.since_last_query() > self.config.min_staleness_recheck + } + + // explicitly request state update. this is only called when upgrade mode is NOT enabled, + // and client has sent a JWT instead of ticket + async fn request_recheck(&self) -> bool { + // send request + let on_done = Arc::new(Notify::new()); + self.request_checker.send_request(on_done.clone()); + + // wait for response - note, if we fail to send, notification will be sent regardless, + // so that we wouldn't get stuck in here + on_done.notified().await; + + // check the state again + self.enabled() + } + + pub async fn try_enable_via_received_jwt( + &self, + token: String, + ) -> Result<(), UpgradeModeEnableError> { + // see if it's viable to perform another expedited check + if !self.can_request_recheck() { + return Err(UpgradeModeEnableError::TooManyRecheckRequests); + } + + // first validate whether the received JWT is even valid + let attestation = validate_upgrade_mode_jwt(&token, Some(CREDENTIAL_PROXY_JWT_ISSUER))?; + + // send request to revalidate internal state + // this will, among other things, pull fresh attestation from the configured endpoint + // and also verify required signatures (and pubkeys) + self.request_recheck().await; + + // not strictly necessary, but check if provided attestation actually matches the one retrieved + // (if any) + let Some(retrieved_attestation) = self.state.attestation().await else { + return Err(UpgradeModeEnableError::AttestationNotPublished); + }; + if retrieved_attestation != attestation { + return Err(UpgradeModeEnableError::MismatchedUpgradeModeAttestation); + } + + // note: if attestation has been returned, it means we're definitely in upgrade mode + // (otherwise it wouldn't have existed in the state) + info!("managed to initialise upgrade mode through received JWT"); + + Ok(()) + } +} + +/// Detailed upgrade mode information, that apart from boolean flag, +/// also includes, if applicable, the associated attestation +#[derive(Clone)] +pub struct UpgradeModeState { + inner: Arc, +} + +/// Just a shareable flag to indicate whether upgrade mode is enabled or disabled +#[derive(Clone, Default)] +pub struct UpgradeModeStatus(Arc); + +impl UpgradeModeStatus { + pub fn enabled(&self) -> bool { + self.0.load(Ordering::Acquire) + } + + pub fn enable(&self) { + self.0.store(true, Ordering::Relaxed); + } + + pub fn disable(&self) { + self.0.store(false, Ordering::Release); + } +} + +impl UpgradeModeState { + pub fn new(attester_public_key: ed25519::PublicKey) -> UpgradeModeState { + UpgradeModeState { + inner: Arc::new(UpgradeModeStateInner { + expected_attester_public_key: attester_public_key, + expected_attestation: RwLock::new(None), + last_queried_ts: AtomicI64::new(OffsetDateTime::UNIX_EPOCH.unix_timestamp()), + status: UpgradeModeStatus(Arc::new(AtomicBool::new(false))), + }), + } + } + + pub fn attester_pubkey(&self) -> ed25519::PublicKey { + self.inner.expected_attester_public_key + } + + pub async fn attestation(&self) -> Option { + self.inner.expected_attestation.read().await.clone() + } + + pub async fn try_set_expected_attestation( + &self, + expected_attestation: Option, + ) { + // make sure to only enable upgrade mode flag AFTER we have written the expected value + // (or still hold the exclusive lock as in this instance) + let mut guard = self.inner.expected_attestation.write().await; + + // ensure that the attestation had been signed with the expected key + if let Some(attestation) = expected_attestation.as_ref() { + if attestation.content.attester_public_key != self.inner.expected_attester_public_key { + self.update_last_queried(OffsetDateTime::now_utc()); + return; + } + + self.enable_upgrade_mode() + } else { + self.disable_upgrade_mode() + } + + self.update_last_queried(OffsetDateTime::now_utc()); + *guard = expected_attestation; + } + + pub fn upgrade_mode_status(&self) -> UpgradeModeStatus { + self.inner.status.clone() + } + + pub fn upgrade_mode_enabled(&self) -> bool { + self.inner.status.enabled() + } + + pub fn enable_upgrade_mode(&self) { + self.inner.status.enable() + } + + pub fn disable_upgrade_mode(&self) { + self.inner.status.disable() + } + + pub fn last_queried(&self) -> OffsetDateTime { + // SAFETY: the stored value here is always a valid unix timestamp + #[allow(clippy::unwrap_used)] + OffsetDateTime::from_unix_timestamp(self.inner.last_queried_ts.load(Ordering::Acquire)) + .unwrap() + } + + pub fn update_last_queried(&self, queried_at: OffsetDateTime) { + self.inner + .last_queried_ts + .store(queried_at.unix_timestamp(), Ordering::Release); + } + + pub fn since_last_query(&self) -> Duration { + (OffsetDateTime::now_utc() - self.last_queried()) + .try_into() + .unwrap_or_else(|_| { + error!("somehow our last query for upgrade mode was in the future!"); + Duration::ZERO + }) + } +} + +struct UpgradeModeStateInner { + /// Expected public key of the entity issuing upgrade mode attestations. + expected_attester_public_key: ed25519::PublicKey, + + /// Contents of the published upgrade mode attestation, as queried by this node + expected_attestation: RwLock>, + + /// timestamp indicating last time this node has queried for the current upgrade mode attestation + /// it is used to determine if an additional expedited query should be made in case client sends a JWT + /// whilst this node is not aware of the upgrade mode + last_queried_ts: AtomicI64, + + /// flag indicating whether upgrade mode is currently enabled. this is to perform cheap checks + /// that avoid having to acquire the lock + // (and dealing with the async consequences of that) + status: UpgradeModeStatus, +} diff --git a/common/credentials-interface/Cargo.toml b/common/credentials-interface/Cargo.toml index d6d4ce842b..57a88ea603 100644 --- a/common/credentials-interface/Cargo.toml +++ b/common/credentials-interface/Cargo.toml @@ -23,3 +23,5 @@ rand = { workspace = true } nym-compact-ecash = { path = "../nym_offline_compact_ecash" } nym-ecash-time = { path = "../ecash-time" } nym-network-defaults = { path = "../network-defaults" } +nym-upgrade-mode-check = { path = "../upgrade-mode-check" } + diff --git a/common/credentials-interface/src/lib.rs b/common/credentials-interface/src/lib.rs index 91e08ee770..807016792b 100644 --- a/common/credentials-interface/src/lib.rs +++ b/common/credentials-interface/src/lib.rs @@ -30,6 +30,35 @@ pub use nym_compact_ecash::{ }; pub use nym_ecash_time::{EcashTime, ecash_today}; pub use nym_network_defaults::TicketTypeRepr; +use nym_network_defaults::TicketTypeRepr::V1MixnetEntry; + +/// Default bandwidth amount under which [mixnet] clients will attempt to send additional zk-nyms +/// to increase their allowance. +// currently defined as 20% of entry ticket value +// clients are, of course, free to override this value +pub const DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD: i64 = + (V1MixnetEntry.bandwidth_value() / 5) as i64; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum BandwidthCredential { + ZkNym(Box), + UpgradeModeJWT { token: String }, +} + +impl BandwidthCredential { + pub fn into_zk_nym(self) -> Option> { + match self { + BandwidthCredential::ZkNym(credential) => Some(credential), + _ => None, + } + } +} + +impl From for BandwidthCredential { + fn from(credential: CredentialSpendingData) -> Self { + Self::ZkNym(Box::new(credential)) + } +} #[derive(Debug, Clone)] pub struct CredentialSigningData { diff --git a/common/gateway-requests/Cargo.toml b/common/gateway-requests/Cargo.toml index 83042ead14..65a5704fdc 100644 --- a/common/gateway-requests/Cargo.toml +++ b/common/gateway-requests/Cargo.toml @@ -51,3 +51,6 @@ anyhow = { workspace = true } nym-compact-ecash = { path = "../nym_offline_compact_ecash" } # we need specific imports in tests nym-test-utils = { path = "../test-utils" } tokio = { workspace = true, features = ["full"] } + +[lints] +workspace = true \ No newline at end of file diff --git a/common/gateway-requests/src/lib.rs b/common/gateway-requests/src/lib.rs index bdb9a30a0f..a0f44b20fc 100644 --- a/common/gateway-requests/src/lib.rs +++ b/common/gateway-requests/src/lib.rs @@ -19,7 +19,9 @@ pub use shared_key::{ SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey, }; -pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION; +pub type GatewayProtocolVersion = u8; + +pub const CURRENT_PROTOCOL_VERSION: GatewayProtocolVersion = UPGRADE_MODE_VERSION; /// Defines the current version of the communication protocol between gateway and clients. /// It has to be incremented for any breaking change. @@ -29,35 +31,73 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION; // 3 - change to AES-GCM-SIV and non-zero IVs // 4 - introduction of v2 authentication protocol to prevent reply attacks // 5 - add key rotation information to the serialised mix packet -pub const INITIAL_PROTOCOL_VERSION: u8 = 1; -pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2; -pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3; -pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4; -pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: u8 = 5; +// 6 - support for 'upgrade mode' +pub const INITIAL_PROTOCOL_VERSION: GatewayProtocolVersion = 1; +pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: GatewayProtocolVersion = 2; +pub const AES_GCM_SIV_PROTOCOL_VERSION: GatewayProtocolVersion = 3; +pub const AUTHENTICATE_V2_PROTOCOL_VERSION: GatewayProtocolVersion = 4; +pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: GatewayProtocolVersion = 5; +pub const UPGRADE_MODE_VERSION: GatewayProtocolVersion = 6; // TODO: could using `Mac` trait here for OutputSize backfire? // Should hmac itself be exposed, imported and used instead? pub type LegacyGatewayMacSize = ::OutputSize; pub trait GatewayProtocolVersionExt { + const CURRENT: GatewayProtocolVersion = CURRENT_PROTOCOL_VERSION; + fn supports_aes256_gcm_siv(&self) -> bool; fn supports_authenticate_v2(&self) -> bool; fn supports_key_rotation_packet(&self) -> bool; + fn supports_upgrade_mode(&self) -> bool; + fn is_future_version(&self) -> bool; } -impl GatewayProtocolVersionExt for Option { +impl GatewayProtocolVersionExt for Option { fn supports_aes256_gcm_siv(&self) -> bool { - let Some(protocol) = *self else { return false }; - protocol >= AES_GCM_SIV_PROTOCOL_VERSION + let Some(protocol) = self else { return false }; + protocol.supports_aes256_gcm_siv() } fn supports_authenticate_v2(&self) -> bool { - let Some(protocol) = *self else { return false }; - protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION + let Some(protocol) = self else { return false }; + protocol.supports_authenticate_v2() } fn supports_key_rotation_packet(&self) -> bool { - let Some(protocol) = *self else { return false }; - protocol >= EMBEDDED_KEY_ROTATION_INFO_VERSION + let Some(protocol) = self else { return false }; + protocol.supports_key_rotation_packet() + } + + fn supports_upgrade_mode(&self) -> bool { + let Some(protocol) = self else { return false }; + protocol.supports_upgrade_mode() + } + + fn is_future_version(&self) -> bool { + let Some(protocol) = self else { return false }; + protocol.is_future_version() + } +} + +impl GatewayProtocolVersionExt for GatewayProtocolVersion { + fn supports_aes256_gcm_siv(&self) -> bool { + *self >= AES_GCM_SIV_PROTOCOL_VERSION + } + + fn supports_authenticate_v2(&self) -> bool { + *self >= AUTHENTICATE_V2_PROTOCOL_VERSION + } + + fn supports_key_rotation_packet(&self) -> bool { + *self >= EMBEDDED_KEY_ROTATION_INFO_VERSION + } + + fn supports_upgrade_mode(&self) -> bool { + *self >= UPGRADE_MODE_VERSION + } + + fn is_future_version(&self) -> bool { + *self > CURRENT_PROTOCOL_VERSION } } diff --git a/common/gateway-requests/src/registration/handshake/client.rs b/common/gateway-requests/src/registration/handshake/client.rs index 549cddca39..7f2a3ccb19 100644 --- a/common/gateway-requests/src/registration/handshake/client.rs +++ b/common/gateway-requests/src/registration/handshake/client.rs @@ -3,10 +3,12 @@ use crate::registration::handshake::messages::{Finalization, GatewayMaterialExchange}; use crate::registration::handshake::state::State; -use crate::registration::handshake::SharedGatewayKey; +use crate::registration::handshake::HandshakeResult; use crate::registration::handshake::{error::HandshakeError, WsItem}; +use crate::{GatewayProtocolVersionExt, INITIAL_PROTOCOL_VERSION}; use futures::{Sink, Stream}; use rand::{CryptoRng, RngCore}; +use tracing::info; use tungstenite::Message as WsMessage; impl State<'_, S, R> { @@ -25,10 +27,26 @@ impl State<'_, S, R> { // 2. wait for response with remote x25519 pubkey as well as encrypted signature // <- g^y || AES(k, sig(gate_priv, (g^y || g^x)) || MAYBE_NONCE - let mid_res = self + let (mid_res, gateway_protocol) = self .receive_handshake_message::() .await?; + // NEGOTIATE PROTOCOL + if gateway_protocol.is_future_version() { + // SAFETY: future version means it's greater than CURRENT, which is always a `Some` + #[allow(clippy::unwrap_used)] + return Err(HandshakeError::UnsupportedProtocol { + version: gateway_protocol.unwrap(), + }); + } + let gateway_protocol = gateway_protocol.unwrap_or(INITIAL_PROTOCOL_VERSION); + + // that should never happen, but we're fine with that outcome + if Some(gateway_protocol) != self.proposed_protocol_version() { + info!("the gateway insists on protocol version different from the one we suggested. it wants {gateway_protocol} whilst we wanted {:?}, however, we can support it", self.proposed_protocol_version()); + self.set_protocol_version(gateway_protocol); + } + // 3. derive shared keys locally // hkdf::::(g^xy) self.derive_shared_key(&mid_res.ephemeral_dh, maybe_hkdf_salt.as_deref()); @@ -42,14 +60,14 @@ impl State<'_, S, R> { self.send_handshake_data(materials).await?; // 6. wait for remote confirmation of finalizing the handshake - let finalization = self.receive_handshake_message::().await?; + let (finalization, _) = self.receive_handshake_message::().await?; finalization.ensure_success()?; Ok(()) } pub(crate) async fn perform_client_handshake( mut self, - ) -> Result + ) -> Result where S: Stream + Sink + Unpin, R: CryptoRng + RngCore, diff --git a/common/gateway-requests/src/registration/handshake/error.rs b/common/gateway-requests/src/registration/handshake/error.rs index 8cc9cf1c0d..67dc02d009 100644 --- a/common/gateway-requests/src/registration/handshake/error.rs +++ b/common/gateway-requests/src/registration/handshake/error.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::shared_key::SharedKeyUsageError; +use crate::GatewayProtocolVersion; +use crate::GatewayProtocolVersionExt; use thiserror::Error; #[derive(Debug, Error)] @@ -34,4 +36,10 @@ pub enum HandshakeError { #[error("timed out waiting for a handshake message")] Timeout, + + #[error("Connection is in an invalid state - please send a bug report")] + ConnectionInInvalidState, + + #[error("the gateway requests protocol version that's not supported by this client. it wants to use v{version} whilst we only understand up to v{}", GatewayProtocolVersion::CURRENT)] + UnsupportedProtocol { version: GatewayProtocolVersion }, } diff --git a/common/gateway-requests/src/registration/handshake/gateway.rs b/common/gateway-requests/src/registration/handshake/gateway.rs index 5fec717c46..cea8fd67de 100644 --- a/common/gateway-requests/src/registration/handshake/gateway.rs +++ b/common/gateway-requests/src/registration/handshake/gateway.rs @@ -5,9 +5,11 @@ use crate::registration::handshake::messages::{ HandshakeMessage, Initialisation, MaterialExchange, }; use crate::registration::handshake::state::State; -use crate::registration::handshake::SharedGatewayKey; +use crate::registration::handshake::HandshakeResult; use crate::registration::handshake::{error::HandshakeError, WsItem}; +use crate::{GatewayProtocolVersion, GatewayProtocolVersionExt}; use futures::{Sink, Stream}; +use tracing::{debug, warn}; use tungstenite::Message as WsMessage; impl State<'_, S, R> { @@ -18,11 +20,39 @@ impl State<'_, S, R> { where S: Stream + Sink + Unpin, { + // NEGOTIATE PROTOCOL + // old clients were sending protocol version as defined by the following: + /* + fn request_protocol_version(&self) -> u8 { + if self.derive_aes256_gcm_siv_key { + AES_GCM_SIV_PROTOCOL_VERSION + } else if self.expects_credential_usage { + CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION + } else { + INITIAL_PROTOCOL_VERSION + } + } + */ + // meaning the highest possible value they could have sent was `4` (AUTHENTICATE_V2_PROTOCOL_VERSION) + // so if we received anything higher than that, it means they understand negotiation. + // currently not strictly needed as we just blindly accept what they proposed, + // but will be needed in the future. + if self.proposed_protocol_version().is_future_version() { + // this should never happen in a non-malicious client as it should use at most whatever version this gateway has announced + self.set_protocol_version(GatewayProtocolVersion::CURRENT) + } else { + // currently we accept all protocols, i.e. legacy keys, aes128, etc. so we downgrade to whatever + // the client has proposed. this will change in the future + debug!( + "using the protocol version proposed by the client: {:?}", + self.proposed_protocol_version() + ) + } + // 1. receive remote ed25519 pubkey alongside ephemeral x25519 pubkey and maybe a flag indicating non-legacy client // LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY let init_message = Initialisation::try_from_bytes(&raw_init_message)?; self.update_remote_identity(init_message.identity); - self.set_aes256_gcm_siv_key_derivation(!init_message.is_legacy()); // 2. derive shared keys locally // hkdf::::(g^xy) @@ -39,7 +69,12 @@ impl State<'_, S, R> { self.send_handshake_data(material).await?; // 4. wait for the remote response with their own encrypted signature - let materials = self.receive_handshake_message::().await?; + let (materials, client_protocol) = + self.receive_handshake_message::().await?; + if client_protocol != self.proposed_protocol_version() { + warn!("the client hasn't accepted our proposed protocol version. we suggested {:?} while it returned {client_protocol:?}", self.proposed_protocol_version()); + // TBD what to do here + } // 5. verify the received signature using the locally derived keys self.verify_remote_key_material(&materials, &init_message.ephemeral_dh)?; @@ -54,7 +89,7 @@ impl State<'_, S, R> { pub(crate) async fn perform_gateway_handshake( mut self, raw_init_message: Vec, - ) -> Result + ) -> Result where S: Stream + Sink + Unpin, { diff --git a/common/gateway-requests/src/registration/handshake/messages.rs b/common/gateway-requests/src/registration/handshake/messages.rs index 10dda7edaf..9958c17ba1 100644 --- a/common/gateway-requests/src/registration/handshake/messages.rs +++ b/common/gateway-requests/src/registration/handshake/messages.rs @@ -24,13 +24,6 @@ pub struct Initialisation { pub initiator_salt: Option>, } -impl Initialisation { - #[cfg(not(target_arch = "wasm32"))] - pub fn is_legacy(&self) -> bool { - self.initiator_salt.is_none() - } -} - #[derive(Debug)] pub struct MaterialExchange { pub signature_ciphertext: Vec, @@ -99,8 +92,9 @@ impl HandshakeMessage for Initialisation { let identity = ed25519::PublicKey::from_bytes(&bytes[..ed25519::PUBLIC_KEY_LENGTH]) .map_err(|_| HandshakeError::MalformedRequest)?; - // this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE + // SAFETY: this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE // which is impossible + #[allow(clippy::unwrap_used)] let ephemeral_dh = x25519::PublicKey::from_bytes(&bytes[ed25519::PUBLIC_KEY_LENGTH..legacy_len]).unwrap(); @@ -194,6 +188,7 @@ impl HandshakeMessage for GatewayMaterialExchange { // this can only fail if the provided bytes have len different from PUBLIC_KEY_SIZE // which is impossible + #[allow(clippy::unwrap_used)] let ephemeral_dh = x25519::PublicKey::from_bytes(&bytes[..x25519::PUBLIC_KEY_SIZE]).unwrap(); let materials = MaterialExchange::try_from_bytes(&bytes[x25519::PUBLIC_KEY_SIZE..])?; diff --git a/common/gateway-requests/src/registration/handshake/mod.rs b/common/gateway-requests/src/registration/handshake/mod.rs index 2daed81b48..8b1972f921 100644 --- a/common/gateway-requests/src/registration/handshake/mod.rs +++ b/common/gateway-requests/src/registration/handshake/mod.rs @@ -3,7 +3,7 @@ use self::error::HandshakeError; use crate::registration::handshake::state::State; -use crate::SharedGatewayKey; +use crate::{GatewayProtocolVersion, SharedGatewayKey}; use futures::future::BoxFuture; use futures::{Sink, Stream}; use nym_crypto::asymmetric::ed25519; @@ -34,24 +34,29 @@ pub const KDF_SALT_LENGTH: usize = 16; // we do not need to worry about that. pub struct GatewayHandshake<'a> { - handshake_future: BoxFuture<'a, Result>, + handshake_future: BoxFuture<'a, Result>, } impl Future for GatewayHandshake<'_> { - type Output = Result; + type Output = Result; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { Pin::new(&mut self.handshake_future).poll(cx) } } +#[derive(Debug, PartialEq)] +pub struct HandshakeResult { + pub negotiated_protocol: GatewayProtocolVersion, + pub derived_key: SharedGatewayKey, +} + pub fn client_handshake<'a, S, R>( rng: &'a mut R, ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, gateway_pubkey: ed25519::PublicKey, - expects_credential_usage: bool, - derive_aes256_gcm_siv_key: bool, + gateway_protocol: Option, #[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken, ) -> GatewayHandshake<'a> where @@ -63,11 +68,10 @@ where ws_stream, identity, Some(gateway_pubkey), + gateway_protocol, #[cfg(not(target_arch = "wasm32"))] shutdown_token, - ) - .with_credential_usage(expects_credential_usage) - .with_aes256_gcm_siv_key(derive_aes256_gcm_siv_key); + ); GatewayHandshake { handshake_future: Box::pin(state.perform_client_handshake()), @@ -80,13 +84,21 @@ pub fn gateway_handshake<'a, S, R>( ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, received_init_payload: Vec, + requested_client_protocol: Option, shutdown_token: ShutdownToken, ) -> GatewayHandshake<'a> where S: Stream + Sink + Unpin + Send + 'a, R: CryptoRng + RngCore + Send, { - let state = State::new(rng, ws_stream, identity, None, shutdown_token); + let state = State::new( + rng, + ws_stream, + identity, + None, + requested_client_protocol, + shutdown_token, + ); GatewayHandshake { handshake_future: Box::pin(state.perform_gateway_handshake(received_init_payload)), } @@ -113,7 +125,8 @@ DONE(status) #[cfg(test)] mod tests { use super::*; - use crate::ClientControlRequest; + use crate::{ClientControlRequest, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION}; + use anyhow::{bail, Context}; use futures::StreamExt; use nym_test_utils::helpers::u64_seeded_rng; use nym_test_utils::mocks::stream_sink::mock_streams; @@ -121,10 +134,53 @@ mod tests { use tokio::join; use tungstenite::Message; - #[tokio::test] - async fn basic_handshake() -> anyhow::Result<()> { - use anyhow::Context as _; + trait ClientControlRequestExt { + async fn get_handshake_init_data(&mut self) -> anyhow::Result> { + let ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version: _, + data, + } = self.get_control_request().await? + else { + bail!("unexpected ClientControlRequest") + }; + Ok(data) + } + async fn get_control_request(&mut self) -> anyhow::Result; + } + impl ClientControlRequestExt for T + where + T: Stream + Unpin, + { + async fn get_control_request(&mut self) -> anyhow::Result { + let msg = self + .next() + .timeboxed() + .await + .context("timeout")? + .context("no message!")?? + .into_text()? + .parse::()?; + Ok(msg) + } + } + + struct Party { + rng: &'static mut R, + keys: &'static mut ed25519::KeyPair, + socket: &'static mut S, + } + + fn setup() -> ( + Party< + impl CryptoRng + RngCore + Send, + impl Stream + Sink + Unpin, + >, + Party< + impl CryptoRng + RngCore + Send, + impl Stream + Sink + Unpin, + >, + ) { // solve the lifetime issue by just leaking the contents of the boxes // which is perfectly fine in test let client_rng = u64_seeded_rng(42).leak(); @@ -142,51 +198,139 @@ mod tests { let client_ws = client_ws.leak(); let gateway_ws = gateway_ws.leak(); + ( + Party { + rng: client_rng, + keys: client_keys, + socket: client_ws, + }, + Party { + rng: gateway_rng, + keys: gateway_keys, + socket: gateway_ws, + }, + ) + } + + #[tokio::test] + async fn basic_handshake() -> anyhow::Result<()> { + let (client, gateway) = setup(); + let handshake_client = client_handshake( - client_rng, - client_ws, - client_keys, - *gateway_keys.public_key(), - false, - true, + client.rng, + client.socket, + client.keys, + *gateway.keys.public_key(), + Some(CURRENT_PROTOCOL_VERSION), ShutdownToken::default(), ); let client_fut = handshake_client.spawn_timeboxed(); // we need to receive the first message so that it could be propagated to the gateway side of the handshake - let ClientControlRequest::RegisterHandshakeInitRequest { - protocol_version: _, - data, - } = (gateway_ws.next()) - .timeboxed() - .await - .context("timeout")? - .context("no message!")?? - .into_text()? - .parse::()? - else { - panic!("bad message") - }; - - let init_msg = data; + let init_msg = gateway.socket.get_handshake_init_data().await?; let handshake_gateway = gateway_handshake( - gateway_rng, - gateway_ws, - gateway_keys, + gateway.rng, + gateway.socket, + gateway.keys, init_msg, + Some(CURRENT_PROTOCOL_VERSION), ShutdownToken::default(), ); let gateway_fut = handshake_gateway.spawn_timeboxed(); let (client, gateway) = join!(client_fut, gateway_fut); - let client_key = client???; - let gateway_key = gateway???; + let client_res = client???; + let gateway_res = gateway???; // ensure the created keys are the same - assert_eq!(client_key, gateway_key); + assert_eq!(client_res, gateway_res); + assert_eq!(client_res.negotiated_protocol, CURRENT_PROTOCOL_VERSION); + + Ok(()) + } + + #[tokio::test] + async fn protocol_downgrade() -> anyhow::Result<()> { + let (client, gateway) = setup(); + + let handshake_client = client_handshake( + client.rng, + client.socket, + client.keys, + *gateway.keys.public_key(), + Some(CURRENT_PROTOCOL_VERSION + 42), + ShutdownToken::default(), + ); + + let client_fut = handshake_client.spawn_timeboxed(); + // we need to receive the first message so that it could be propagated to the gateway side of the handshake + let init_msg = gateway.socket.get_handshake_init_data().await?; + + let handshake_gateway = gateway_handshake( + gateway.rng, + gateway.socket, + gateway.keys, + init_msg, + Some(CURRENT_PROTOCOL_VERSION + 42), + ShutdownToken::default(), + ); + + let gateway_fut = handshake_gateway.spawn_timeboxed(); + let (client, gateway) = join!(client_fut, gateway_fut); + + let client_res = client???; + let gateway_res = gateway???; + + // ensure the created keys are the same + assert_eq!(client_res, gateway_res); + + // and the protocol got downgraded for both parties + assert_eq!(client_res.negotiated_protocol, CURRENT_PROTOCOL_VERSION); + + Ok(()) + } + + #[tokio::test] + async fn protocol_upgrade() -> anyhow::Result<()> { + let (client, gateway) = setup(); + + let handshake_client = client_handshake( + client.rng, + client.socket, + client.keys, + *gateway.keys.public_key(), + None, + ShutdownToken::default(), + ); + + let client_fut = handshake_client.spawn_timeboxed(); + + // we need to receive the first message so that it could be propagated to the gateway side of the handshake + let init_msg = gateway.socket.get_handshake_init_data().await?; + + let handshake_gateway = gateway_handshake( + gateway.rng, + gateway.socket, + gateway.keys, + init_msg, + None, + ShutdownToken::default(), + ); + + let gateway_fut = handshake_gateway.spawn_timeboxed(); + let (client, gateway) = join!(client_fut, gateway_fut); + + let client_res = client???; + let gateway_res = gateway???; + + // ensure the created keys are the same + assert_eq!(client_res, gateway_res); + + // and the protocol got upgraded to the first known version + assert_eq!(client_res.negotiated_protocol, INITIAL_PROTOCOL_VERSION); Ok(()) } diff --git a/common/gateway-requests/src/registration/handshake/state.rs b/common/gateway-requests/src/registration/handshake/state.rs index 3d7b29e3f9..1e51fda37b 100644 --- a/common/gateway-requests/src/registration/handshake/state.rs +++ b/common/gateway-requests/src/registration/handshake/state.rs @@ -5,11 +5,11 @@ use crate::registration::handshake::error::HandshakeError; use crate::registration::handshake::messages::{ HandshakeMessage, Initialisation, MaterialExchange, }; -use crate::registration::handshake::{SharedGatewayKey, WsItem, KDF_SALT_LENGTH}; +use crate::registration::handshake::{HandshakeResult, SharedGatewayKey, WsItem, KDF_SALT_LENGTH}; use crate::shared_key::SharedKeySize; use crate::{ - types, LegacySharedKeySize, LegacySharedKeys, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, - CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, + types, GatewayProtocolVersion, GatewayProtocolVersionExt, LegacySharedKeySize, + LegacySharedKeys, SharedSymmetricKey, INITIAL_PROTOCOL_VERSION, }; use futures::{Sink, SinkExt, Stream, StreamExt}; use nym_crypto::asymmetric::{ed25519, x25519}; @@ -54,12 +54,11 @@ pub(crate) struct State<'a, S, R> { /// Ideally it would always be known before the handshake was initiated. remote_pubkey: Option, - // this field is really out of place here, however, we need to propagate this information somehow - // in order to establish correct protocol for backwards compatibility reasons - expects_credential_usage: bool, - - /// Specifies whether the end product should be an AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current) - derive_aes256_gcm_siv_key: bool, + /// Version of the protocol to use during the handshake that also implicitly specifies + /// additional features such as the type of derived shared keys, i.e. + /// AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current) + /// the above is decided by whether the specified protocol version supports the new variant or not. + protocol_version: Option, // channel to receive shutdown signal #[cfg(not(target_arch = "wasm32"))] @@ -72,6 +71,7 @@ impl<'a, S, R> State<'a, S, R> { ws_stream: &'a mut S, identity: &'a ed25519::KeyPair, remote_pubkey: Option, + protocol_version: Option, #[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken, ) -> Self where @@ -84,40 +84,31 @@ impl<'a, S, R> State<'a, S, R> { ephemeral_keypair, identity, remote_pubkey, + protocol_version, derived_shared_keys: None, - // later on this should become the default - expects_credential_usage: false, - derive_aes256_gcm_siv_key: false, #[cfg(not(target_arch = "wasm32"))] shutdown_token, } } - pub(crate) fn with_credential_usage(mut self, expects_credential_usage: bool) -> Self { - self.expects_credential_usage = expects_credential_usage; - self - } - - pub(crate) fn with_aes256_gcm_siv_key(mut self, derive_aes256_gcm_siv_key: bool) -> Self { - self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key; - self - } - - #[cfg(not(target_arch = "wasm32"))] - pub(crate) fn set_aes256_gcm_siv_key_derivation(&mut self, derive_aes256_gcm_siv_key: bool) { - self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key; - } - #[cfg(not(target_arch = "wasm32"))] pub(crate) fn local_ephemeral_key(&self) -> &x25519::PublicKey { self.ephemeral_keypair.public_key() } + pub(crate) fn proposed_protocol_version(&self) -> Option { + self.protocol_version + } + + pub(crate) fn set_protocol_version(&mut self, protocol_version: GatewayProtocolVersion) { + self.protocol_version = Some(protocol_version); + } + pub(crate) fn maybe_generate_initiator_salt(&mut self) -> Option> where R: CryptoRng + RngCore, { - if self.derive_aes256_gcm_siv_key { + if self.protocol_version.supports_aes256_gcm_siv() { let mut salt = vec![0u8; KDF_SALT_LENGTH]; self.rng.fill_bytes(&mut salt); Some(salt) @@ -154,13 +145,14 @@ impl<'a, S, R> State<'a, S, R> { .private_key() .diffie_hellman(remote_ephemeral_key); - let key_size = if self.derive_aes256_gcm_siv_key { + let key_size = if self.protocol_version.supports_aes256_gcm_siv() { SharedKeySize::to_usize() } else { LegacySharedKeySize::to_usize() }; - // there is no reason for this to fail as our okm is expected to be only 16 bytes + // SAFETY: there is no reason for this to fail as our okm is expected to be only 16 bytes + #[allow(clippy::expect_used)] let okm = hkdf::extract_then_expand::( initiator_salt, &dh_result, @@ -169,11 +161,14 @@ impl<'a, S, R> State<'a, S, R> { ) .expect("somehow too long okm was provided"); - let shared_key = if self.derive_aes256_gcm_siv_key { + // SAFETY: the okm has been expanded to the length expected by the corresponding keys + let shared_key = if self.protocol_version.supports_aes256_gcm_siv() { + #[allow(clippy::expect_used)] let current_key = SharedSymmetricKey::try_from_bytes(&okm) .expect("okm was expanded to incorrect length!"); SharedGatewayKey::Current(current_key) } else { + #[allow(clippy::expect_used)] let legacy_key = LegacySharedKeys::try_from_bytes(&okm) .expect("okm was expanded to incorrect length!"); SharedGatewayKey::Legacy(legacy_key) @@ -196,7 +191,7 @@ impl<'a, S, R> State<'a, S, R> { .collect(); let signature = self.identity.private_key().sign(plaintext); - let nonce = if self.derive_aes256_gcm_siv_key { + let nonce = if self.protocol_version.supports_aes256_gcm_siv() { let mut rng = thread_rng(); Some(random_nonce::(&mut rng).to_vec()) } else { @@ -204,6 +199,7 @@ impl<'a, S, R> State<'a, S, R> { }; // SAFETY: this function is only called after the local key has already been derived + #[allow(clippy::expect_used)] let signature_ciphertext = self .derived_shared_keys .as_ref() @@ -222,13 +218,14 @@ impl<'a, S, R> State<'a, S, R> { remote_ephemeral_key: &x25519::PublicKey, ) -> Result<(), HandshakeError> { // SAFETY: this function is only called after the local key has already been derived + #[allow(clippy::expect_used)] let derived_shared_key = self .derived_shared_keys .as_ref() .expect("shared key was not derived!"); // if the [client] init message contained non-legacy flag, the associated nonce MUST be present - if self.derive_aes256_gcm_siv_key && remote_response.nonce.is_none() { + if self.protocol_version.supports_aes256_gcm_siv() && remote_response.nonce.is_none() { return Err(HandshakeError::MissingNonceForCurrentKey); } @@ -249,6 +246,7 @@ impl<'a, S, R> State<'a, S, R> { .chain(self.ephemeral_keypair.public_key().to_bytes()) .collect(); + #[allow(clippy::unwrap_used)] self.remote_pubkey .as_ref() .unwrap() @@ -261,7 +259,10 @@ impl<'a, S, R> State<'a, S, R> { self.remote_pubkey = Some(remote_pubkey) } - fn on_wg_msg(msg: Option) -> Result>, HandshakeError> { + #[allow(clippy::complexity)] + fn on_wg_msg( + msg: Option, + ) -> Result, Option)>, HandshakeError> { let Some(msg) = msg else { return Err(HandshakeError::ClosedStream); }; @@ -277,9 +278,10 @@ impl<'a, S, R> State<'a, S, R> { // hehe, that's a bit disgusting that the type system requires we explicitly ignore the // protocol_version field that we actually never attach at this point // yet another reason for the overdue refactor - types::RegistrationHandshake::HandshakePayload { data, .. } => { - Ok(Some(data)) - } + types::RegistrationHandshake::HandshakePayload { + protocol_version, + data, + } => Ok(Some((data, protocol_version))), types::RegistrationHandshake::HandshakeError { message } => { Err(HandshakeError::RemoteError(message)) } @@ -299,7 +301,9 @@ impl<'a, S, R> State<'a, S, R> { } #[cfg(not(target_arch = "wasm32"))] - async fn _receive_handshake_message_bytes(&mut self) -> Result, HandshakeError> + async fn _receive_handshake_message_bytes( + &mut self, + ) -> Result<(Vec, Option), HandshakeError> where S: Stream + Unpin, { @@ -318,7 +322,9 @@ impl<'a, S, R> State<'a, S, R> { } #[cfg(target_arch = "wasm32")] - async fn _receive_handshake_message_bytes(&mut self) -> Result, HandshakeError> + async fn _receive_handshake_message_bytes( + &mut self, + ) -> Result<(Vec, Option), HandshakeError> where S: Stream + Unpin, { @@ -331,20 +337,22 @@ impl<'a, S, R> State<'a, S, R> { } } - pub(crate) async fn receive_handshake_message(&mut self) -> Result + pub(crate) async fn receive_handshake_message( + &mut self, + ) -> Result<(M, Option), HandshakeError> where S: Stream + Unpin, M: HandshakeMessage, { // TODO: make timeout duration configurable - let bytes = timeout( + let (bytes, protocol) = timeout( Duration::from_secs(5), self._receive_handshake_message_bytes(), ) .await .map_err(|_| HandshakeError::Timeout)??; - M::try_from_bytes(&bytes) + M::try_from_bytes(&bytes).map(|msg| (msg, protocol)) } // upon receiving this, the receiver should terminate the handshake @@ -357,21 +365,11 @@ impl<'a, S, R> State<'a, S, R> { { let handshake_message = types::RegistrationHandshake::new_error(message); self.ws_stream - .send(WsMessage::Text(handshake_message.try_into().unwrap())) + .send(WsMessage::Text(handshake_message.into())) .await .map_err(|_| HandshakeError::ClosedStream) } - fn request_protocol_version(&self) -> u8 { - if self.derive_aes256_gcm_siv_key { - AES_GCM_SIV_PROTOCOL_VERSION - } else if self.expects_credential_usage { - CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION - } else { - INITIAL_PROTOCOL_VERSION - } - } - pub(crate) async fn send_handshake_data( &mut self, inner_message: M, @@ -384,18 +382,25 @@ impl<'a, S, R> State<'a, S, R> { let handshake_message = types::RegistrationHandshake::new_payload( inner_message.into_bytes(), - self.request_protocol_version(), + self.protocol_version, ); self.ws_stream - .send(WsMessage::Text(handshake_message.try_into().unwrap())) + .send(WsMessage::Text(handshake_message.into())) .await .map_err(|_| HandshakeError::ClosedStream) } /// Finish the handshake, yielding the derived shared key and implicitly dropping all borrowed /// values. - pub(crate) fn finalize_handshake(self) -> SharedGatewayKey { - self.derived_shared_keys.unwrap() + pub(crate) fn finalize_handshake(self) -> HandshakeResult { + // SAFETY: handshake can't be finalised without deriving the shared keys + #[allow(clippy::unwrap_used)] + HandshakeResult { + negotiated_protocol: self + .proposed_protocol_version() + .unwrap_or(INITIAL_PROTOCOL_VERSION), + derived_key: self.derived_shared_keys.unwrap(), + } } // If any step along the way failed (that are non-network related), diff --git a/common/gateway-requests/src/shared_key/legacy.rs b/common/gateway-requests/src/shared_key/legacy.rs index 8fcf286697..6f40fcd2ff 100644 --- a/common/gateway-requests/src/shared_key/legacy.rs +++ b/common/gateway-requests/src/shared_key/legacy.rs @@ -43,6 +43,7 @@ impl LegacySharedKeys { rng.fill_bytes(&mut salt); let legacy_bytes = Zeroizing::new(self.to_bytes()); + #[allow(clippy::expect_used)] let okm = hkdf::extract_then_expand::( Some(&salt), &legacy_bytes, @@ -51,6 +52,7 @@ impl LegacySharedKeys { ) .expect("somehow too long okm was provided"); + #[allow(clippy::expect_used)] let key = SharedSymmetricKey::try_from_bytes(&okm) .expect("okm was expanded to incorrect length!"); (key, salt) @@ -62,6 +64,7 @@ impl LegacySharedKeys { expected_digest: &[u8], ) -> Option { let legacy_bytes = Zeroizing::new(self.to_bytes()); + #[allow(clippy::expect_used)] let okm = hkdf::extract_then_expand::( Some(salt), &legacy_bytes, @@ -69,6 +72,8 @@ impl LegacySharedKeys { SharedKeySize::to_usize(), ) .expect("somehow too long okm was provided"); + + #[allow(clippy::expect_used)] let key = SharedSymmetricKey::try_from_bytes(&okm) .expect("okm was expanded to incorrect length!"); if key.digest() != expected_digest { diff --git a/common/gateway-requests/src/shared_key/mod.rs b/common/gateway-requests/src/shared_key/mod.rs index b424fd49bf..c0a72135a3 100644 --- a/common/gateway-requests/src/shared_key/mod.rs +++ b/common/gateway-requests/src/shared_key/mod.rs @@ -47,6 +47,8 @@ impl SharedGatewayKey { } } + // it is responsibility of the caller to ensure the correct variant is present + #[allow(clippy::panic)] pub fn unwrap_legacy(&self) -> &LegacySharedKeys { match self { SharedGatewayKey::Current(_) => panic!("expected legacy key"), diff --git a/common/gateway-requests/src/types/error.rs b/common/gateway-requests/src/types/error.rs index edd8e41b22..bdbfe6fbac 100644 --- a/common/gateway-requests/src/types/error.rs +++ b/common/gateway-requests/src/types/error.rs @@ -13,7 +13,7 @@ use thiserror::Error; use time::OffsetDateTime; // specific errors (that should not be nested!!) for clients to match on -#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize)] +#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "snake_case")] pub enum SimpleGatewayRequestsError { #[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")] diff --git a/common/gateway-requests/src/types/registration_handshake_wrapper.rs b/common/gateway-requests/src/types/registration_handshake_wrapper.rs index 0dc6daa567..be75af675b 100644 --- a/common/gateway-requests/src/types/registration_handshake_wrapper.rs +++ b/common/gateway-requests/src/types/registration_handshake_wrapper.rs @@ -1,6 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::GatewayProtocolVersion; use serde::{Deserialize, Serialize}; use std::str::FromStr; @@ -9,7 +10,7 @@ use std::str::FromStr; pub enum RegistrationHandshake { HandshakePayload { #[serde(default)] - protocol_version: Option, + protocol_version: Option, data: Vec, }, HandshakeError { @@ -18,9 +19,9 @@ pub enum RegistrationHandshake { } impl RegistrationHandshake { - pub fn new_payload(data: Vec, protocol_version: u8) -> Self { + pub fn new_payload(data: Vec, protocol_version: Option) -> Self { RegistrationHandshake::HandshakePayload { - protocol_version: Some(protocol_version), + protocol_version, data, } } @@ -48,11 +49,11 @@ impl TryFrom for RegistrationHandshake { } } -impl TryInto for RegistrationHandshake { - type Error = serde_json::Error; - - fn try_into(self) -> Result { - serde_json::to_string(&self) +impl From for String { + fn from(value: RegistrationHandshake) -> Self { + // SAFETY: we have infallible serde implementation + #[allow(clippy::unwrap_used)] + serde_json::to_string(&value).unwrap() } } @@ -79,7 +80,7 @@ mod tests { assert_eq!(protocol_version, Some(42)); assert_eq!(data, handshake_data) } - _ => unreachable!("this branch shouldn't have been reached!"), + _ => panic!("this branch shouldn't have been reached!"), } let handshake_payload_without_protocol = RegistrationHandshake::HandshakePayload { @@ -97,7 +98,7 @@ mod tests { assert!(protocol_version.is_none()); assert_eq!(data, handshake_data) } - _ => unreachable!("this branch shouldn't have been reached!"), + _ => panic!("this branch shouldn't have been reached!"), } } } diff --git a/common/gateway-requests/src/types/text_request/authenticate.rs b/common/gateway-requests/src/types/text_request/authenticate.rs index 6c4c884957..65dacb3f00 100644 --- a/common/gateway-requests/src/types/text_request/authenticate.rs +++ b/common/gateway-requests/src/types/text_request/authenticate.rs @@ -1,7 +1,9 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::{AuthenticationFailure, GatewayRequestsError, SharedGatewayKey}; +use crate::{ + AuthenticationFailure, GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey, +}; use nym_crypto::asymmetric::ed25519; use serde::{Deserialize, Serialize}; use std::iter; @@ -20,7 +22,7 @@ pub struct AuthenticateRequest { impl AuthenticateRequest { pub fn new( - protocol_version: u8, + protocol_version: GatewayProtocolVersion, shared_key: &SharedGatewayKey, identity_keys: &ed25519::KeyPair, ) -> Result { @@ -98,7 +100,7 @@ impl AuthenticateRequest { #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct AuthenticateRequestContent { - pub protocol_version: u8, + pub protocol_version: GatewayProtocolVersion, // this is identical to the client's address pub client_identity: ed25519::PublicKey, diff --git a/common/gateway-requests/src/types/text_request/mod.rs b/common/gateway-requests/src/types/text_request/mod.rs index 342bc9adfd..b15a27bf93 100644 --- a/common/gateway-requests/src/types/text_request/mod.rs +++ b/common/gateway-requests/src/types/text_request/mod.rs @@ -4,9 +4,8 @@ use crate::models::CredentialSpendingRequest; use crate::text_request::authenticate::AuthenticateRequest; use crate::{ - GatewayRequestsError, SharedGatewayKey, SymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION, - AUTHENTICATE_V2_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, - INITIAL_PROTOCOL_VERSION, + GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey, SymmetricKey, + AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION, }; use nym_credentials_interface::CredentialSpendingData; use nym_crypto::asymmetric::ed25519; @@ -46,6 +45,7 @@ impl ClientRequest { // - the schema is self-describing which simplifies deserialisation // SAFETY: the trait has been derived correctly with no weird variants + #[allow(clippy::unwrap_used)] let plaintext = serde_json::to_vec(self).unwrap(); let nonce = key.random_nonce_or_iv(); let ciphertext = key.encrypt(&plaintext, Some(&nonce))?; @@ -72,7 +72,7 @@ pub enum ClientControlRequest { // have the shared key derived? Authenticate { #[serde(default)] - protocol_version: Option, + protocol_version: Option, address: String, enc_address: String, iv: String, @@ -83,7 +83,7 @@ pub enum ClientControlRequest { #[serde(alias = "handshakePayload")] RegisterHandshakeInitRequest { #[serde(default)] - protocol_version: Option, + protocol_version: Option, data: Vec, }, BandwidthCredential { @@ -98,6 +98,10 @@ pub enum ClientControlRequest { enc_credential: Vec, iv: Vec, }, + UpgradeModeJWT { + // no need to encrypt it as it's public anyway + token: String, + }, ClaimFreeTestnetBandwidth, EncryptedRequest { ciphertext: Vec, @@ -108,12 +112,14 @@ pub enum ClientControlRequest { } impl ClientControlRequest { - pub fn new_authenticate( + pub fn new_legacy_authenticate( address: DestinationAddressBytes, shared_key: &SharedGatewayKey, uses_credentials: bool, ) -> Result { // if we're encrypting with non-legacy key, the remote must support AES256-GCM-SIV + // since we are using legacy authentication, the gateway definitely doesn't understand the protocol downgrade, + // so use the lowest possible version we can let protocol_version = if !shared_key.is_legacy() { Some(AES_GCM_SIV_PROTOCOL_VERSION) } else if uses_credentials { @@ -138,10 +144,8 @@ impl ClientControlRequest { pub fn new_authenticate_v2( shared_key: &SharedGatewayKey, identity_keys: &ed25519::KeyPair, + protocol_version: GatewayProtocolVersion, ) -> Result { - // if we're using v2 authentication, we must announce at least that protocol version - let protocol_version = AUTHENTICATE_V2_PROTOCOL_VERSION; - Ok(ClientControlRequest::AuthenticateV2(Box::new( AuthenticateRequest::new(protocol_version, shared_key, identity_keys)?, ))) @@ -159,6 +163,7 @@ impl ClientControlRequest { "BandwidthCredentialV2".to_string() } ClientControlRequest::EcashCredential { .. } => "EcashCredential".to_string(), + ClientControlRequest::UpgradeModeJWT { .. } => "UpgradeModeJWT".to_string(), ClientControlRequest::ClaimFreeTestnetBandwidth => { "ClaimFreeTestnetBandwidth".to_string() } @@ -192,12 +197,16 @@ impl ClientControlRequest { CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice()) .map_err(|_| GatewayRequestsError::MalformedEncryption) } + + pub fn new_upgrade_mode_jwt(token: String) -> Self { + ClientControlRequest::UpgradeModeJWT { token } + } } impl From for Message { fn from(req: ClientControlRequest) -> Self { - // it should be safe to call `unwrap` here as the message is generated by the server - // so if it fails (and consequently panics) it's a bug that should be resolved + // SAFETY: all of the enum variants have valid (for json) serde impl + #[allow(clippy::unwrap_used)] let str_req = serde_json::to_string(&req).unwrap(); Message::Text(str_req) } diff --git a/common/gateway-requests/src/types/text_response.rs b/common/gateway-requests/src/types/text_response.rs index c3418649cf..140aa26bbd 100644 --- a/common/gateway-requests/src/types/text_response.rs +++ b/common/gateway-requests/src/types/text_response.rs @@ -1,7 +1,9 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey}; +use crate::{ + GatewayProtocolVersion, GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey, +}; use serde::{Deserialize, Serialize}; use tungstenite::Message; @@ -26,6 +28,7 @@ impl SensitiveServerResponse { // - the schema is self-describing which simplifies deserialisation // SAFETY: the trait has been derived correctly with no weird variants + #[allow(clippy::unwrap_used)] let plaintext = serde_json::to_vec(self).unwrap(); let nonce = key.random_nonce_or_iv(); let ciphertext = key.encrypt(&plaintext, Some(&nonce))?; @@ -43,31 +46,57 @@ impl SensitiveServerResponse { } } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct BandwidthResponse { + pub available_total: i64, + + /// Flag indicating whether the gateway has detected the system is undergoing the upgrade + /// (thus it will not meter bandwidth) + #[serde(default)] + pub upgrade_mode: bool, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] +pub struct SendResponse { + pub remaining_bandwidth: i64, + + /// Flag indicating whether the gateway has detected the system is undergoing the upgrade + /// (thus it will not meter bandwidth) + #[serde(default)] + pub upgrade_mode: bool, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(tag = "type", rename_all = "camelCase")] #[non_exhaustive] pub enum ServerResponse { Authenticate { #[serde(default)] - protocol_version: Option, + protocol_version: Option, status: bool, bandwidth_remaining: i64, + + /// Flag indicating whether the gateway has detected the system is undergoing the upgrade + /// (thus it will not meter bandwidth) + #[serde(default)] + upgrade_mode: bool, }, Register { #[serde(default)] - protocol_version: Option, + protocol_version: Option, status: bool, + + /// Flag indicating whether the gateway has detected the system is undergoing the upgrade + /// (thus it will not meter bandwidth) + #[serde(default)] + upgrade_mode: bool, }, EncryptedResponse { ciphertext: Vec, nonce: Vec, }, - Bandwidth { - available_total: i64, - }, - Send { - remaining_bandwidth: i64, - }, + Bandwidth(BandwidthResponse), + Send(SendResponse), SupportedProtocol { version: u8, }, @@ -122,6 +151,7 @@ impl From for Message { fn from(res: ServerResponse) -> Self { // it should be safe to call `unwrap` here as the message is generated by the server // so if it fails (and consequently panics) it's a bug that should be resolved + #[allow(clippy::unwrap_used)] let str_res = serde_json::to_string(&res).unwrap(); Message::Text(str_res) } @@ -134,3 +164,79 @@ impl TryFrom for ServerResponse { serde_json::from_str(&msg) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn server_response_serde_compat() { + // make sure new serialisation is identical and compatible + #[derive(Serialize, Deserialize, Debug, PartialEq)] + #[serde(tag = "type", rename_all = "camelCase")] + #[non_exhaustive] + pub enum OldServerResponse { + Bandwidth { available_total: i64 }, + Send { remaining_bandwidth: i64 }, + } + + // OLD => NEW + let old_bandwidth = OldServerResponse::Bandwidth { + available_total: 100, + }; + let old_send = OldServerResponse::Send { + remaining_bandwidth: 100, + }; + + let old_bandwidth_str = serde_json::to_string(&old_bandwidth).unwrap(); + let old_send_str = serde_json::to_string(&old_send).unwrap(); + + let recovered_bandwidth = ServerResponse::try_from(old_bandwidth_str).unwrap(); + assert_eq!( + recovered_bandwidth, + ServerResponse::Bandwidth(BandwidthResponse { + available_total: 100, + upgrade_mode: false + }) + ); + + let recovered_send = ServerResponse::try_from(old_send_str).unwrap(); + assert_eq!( + recovered_send, + ServerResponse::Send(SendResponse { + remaining_bandwidth: 100, + upgrade_mode: false + }) + ); + + // NEW => OLD + let new_bandwidth = ServerResponse::Bandwidth(BandwidthResponse { + available_total: 100, + upgrade_mode: false, + }); + let new_send = ServerResponse::Send(SendResponse { + remaining_bandwidth: 100, + upgrade_mode: false, + }); + + let new_bandwidth_str = serde_json::to_string(&new_bandwidth).unwrap(); + let new_send_str = serde_json::to_string(&new_send).unwrap(); + + let recovered_bandwidth: OldServerResponse = + serde_json::from_str(&new_bandwidth_str).unwrap(); + assert_eq!( + recovered_bandwidth, + OldServerResponse::Bandwidth { + available_total: 100 + } + ); + + let recovered_send: OldServerResponse = serde_json::from_str(&new_send_str).unwrap(); + assert_eq!( + recovered_send, + OldServerResponse::Send { + remaining_bandwidth: 100 + } + ); + } +} diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index 291f397482..8e253a77b7 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -55,7 +55,8 @@ pub const NYM_APIS: &[ApiUrlConst] = &[ pub const NYM_VPN_API: &str = "https://nymvpn.com/api/"; -pub const UPGRADE_MODE_ATTESTATION_URL: &str = "https://nym.com/upgrade-mode/attestation.json"; +pub const UPGRADE_MODE_ATTESTATION_URL: &str = + "https://nymtech.net/.wellknown/upgrade-mode/attestation.json"; pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str = "3bgffBYcfFkTTXc2npNNn9MkddFZ3H2LrPjXDmnJzrqd"; diff --git a/common/service-provider-requests-common/src/lib.rs b/common/service-provider-requests-common/src/lib.rs index 9e392dff19..44e54dc2af 100644 --- a/common/service-provider-requests-common/src/lib.rs +++ b/common/service-provider-requests-common/src/lib.rs @@ -82,6 +82,17 @@ pub struct Protocol { pub service_provider_type: ServiceProviderType, } +impl Protocol { + pub const fn new(version: u8, service_provider_type: ServiceProviderType) -> Self { + Self { + version, + service_provider_type, + } + } +} + +// NOTE: this only works under the assumption of using bincode for serialisation +// with the current field layout impl TryFrom<&[u8; 2]> for Protocol { type Error = ProtocolError; diff --git a/common/statistics/Cargo.toml b/common/statistics/Cargo.toml index 18a8ca8fc9..b0f122646c 100644 --- a/common/statistics/Cargo.toml +++ b/common/statistics/Cargo.toml @@ -17,7 +17,7 @@ serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } thiserror = { workspace = true } -time = { workspace = true } +time = { workspace = true, features = ["serde"] } tokio = { workspace = true } si-scale = { workspace = true } strum = { workspace = true } diff --git a/common/statistics/src/report/vpn_client.rs b/common/statistics/src/report/vpn_client.rs index ab1260cae5..3a4cf5d8a8 100644 --- a/common/statistics/src/report/vpn_client.rs +++ b/common/statistics/src/report/vpn_client.rs @@ -2,9 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use serde::{Deserialize, Serialize}; +use time::Date; -const KIND: &str = "vpn_client_stats_report"; -const VERSION: &str = "v1"; +const BASIC_REPORT_KIND: &str = "vpn_client_stats_report"; +const SESSION_REPORT_KIND: &str = "vpn_client_session_report"; +const VERSION_1: &str = "v1"; +const VERSION_2: &str = "v2"; #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] @@ -13,15 +16,14 @@ pub struct VpnClientStatsReport { pub api_version: String, pub stats_id: String, pub static_information: StaticInformationReport, - //SW called it basic so we can swap it easily down the line for more data pub basic_usage: Option, } impl VpnClientStatsReport { pub fn new(stats_id: String, static_information: StaticInformationReport) -> Self { VpnClientStatsReport { - kind: KIND.into(), - api_version: VERSION.into(), + kind: BASIC_REPORT_KIND.into(), + api_version: VERSION_1.into(), stats_id, static_information, basic_usage: None, @@ -34,6 +36,34 @@ impl VpnClientStatsReport { self } } + +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VpnClientStatsReportV2 { + pub kind: String, + pub api_version: String, + pub stats_id: String, + + pub static_information: StaticInformationReport, + pub session_report: SessionReport, +} + +impl VpnClientStatsReportV2 { + pub fn new( + stats_id: String, + static_information: StaticInformationReport, + session_report: SessionReport, + ) -> Self { + VpnClientStatsReportV2 { + kind: SESSION_REPORT_KIND.into(), + api_version: VERSION_2.into(), + stats_id, + static_information, + session_report, + } + } +} + #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StaticInformationReport { @@ -49,3 +79,17 @@ pub struct UsageReport { pub connection_time_ms: Option, pub two_hop: bool, } + +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SessionReport { + pub start_day_utc: Date, + pub connection_time_ms: i32, + pub tunnel_type: String, + pub retry_attempt: i32, + pub session_duration_min: i32, + pub disconnection_time_ms: i32, + pub exit_id: String, + pub follow_up_id: Option, + pub error: Option, +} diff --git a/common/types/src/error.rs b/common/types/src/error.rs index b72a4a18d6..e8d803b077 100644 --- a/common/types/src/error.rs +++ b/common/types/src/error.rs @@ -1,6 +1,7 @@ use nym_mixnet_contract_common::ContractsCommonError; use nym_validator_client::error::TendermintRpcError; use nym_validator_client::nym_api::error::NymAPIError; +use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWalletError; use nym_validator_client::{nyxd::error::NyxdError, ValidatorClientError}; use serde::{Serialize, Serializer}; use std::io; @@ -59,6 +60,8 @@ pub enum TypesError { #[from] source: cosmwasm_std::DecimalRangeExceeded, }, + #[error(transparent)] + AccountDerivationFailure(#[from] DirectSecp256k1HdWalletError), #[error("No nym API URL configured")] NoNymApiUrlConfigured, #[error("{0} is not a valid amount string")] @@ -113,6 +116,7 @@ impl From for TypesError { ValidatorClientError::InconsistentPagedMetadata => { TypesError::InconsistentPagedMetadata } + ValidatorClientError::AccountDerivationFailure { source } => source.into(), } } } diff --git a/common/upgrade-mode-check/src/error.rs b/common/upgrade-mode-check/src/error.rs index 0edf4201e8..acb9ec198c 100644 --- a/common/upgrade-mode-check/src/error.rs +++ b/common/upgrade-mode-check/src/error.rs @@ -6,9 +6,12 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum UpgradeModeCheckError { - #[error("failed to decode jwt metadata")] + #[error("failed to decode jwt metadata: {source}")] TokenMetadataDecodeFailure { source: jwt_simple::Error }, + #[error("the upgrade mode JWT is malformed")] + MalformedToken, + #[error("the jwt metadata didn't contain explicit public key")] MissingTokenPublicKey, @@ -21,6 +24,6 @@ pub enum UpgradeModeCheckError { #[error("failed to verify the jwt: {source}")] JwtVerificationFailure { source: jwt_simple::Error }, - #[error("failed to retrieve attestation from {url}:{source}")] + #[error("failed to retrieve attestation from {url}: {source}")] AttestationRetrievalFailure { url: String, source: reqwest::Error }, } diff --git a/common/upgrade-mode-check/src/jwt.rs b/common/upgrade-mode-check/src/jwt.rs index 060546f0bb..42329f80a5 100644 --- a/common/upgrade-mode-check/src/jwt.rs +++ b/common/upgrade-mode-check/src/jwt.rs @@ -4,12 +4,17 @@ use crate::{UpgradeModeAttestation, UpgradeModeCheckError}; use jwt_simple::claims::Claims; use jwt_simple::common::{KeyMetadata, VerificationOptions}; -use jwt_simple::prelude::{EdDSAKeyPairLike, EdDSAPublicKeyLike}; +use jwt_simple::prelude::{ + Base64UrlSafeNoPadding, EdDSAKeyPairLike, EdDSAPublicKeyLike, JWTClaims, +}; +use jwt_simple::reexports::ct_codecs::Decoder; use jwt_simple::token::Token; use nym_crypto::asymmetric::ed25519; use std::collections::HashSet; use std::time::Duration; +pub const CREDENTIAL_PROXY_JWT_ISSUER: &str = "nym-credential-proxy"; + // for now use static issuer such as "nym-credential-proxy" pub fn generate_jwt_for_upgrade_mode_attestation( attestation: UpgradeModeAttestation, @@ -74,12 +79,26 @@ pub fn validate_upgrade_mode_jwt( Ok(attestation) } +/// Attempt to extract the upgrade mode JWT payload from the provided token +pub fn try_decode_upgrade_mode_jwt_claims( + token: &str, +) -> Result, UpgradeModeCheckError> { + let mut parts = token.split('.'); + let _header = parts.next().ok_or(UpgradeModeCheckError::MalformedToken)?; + let claims_b64 = parts.next().ok_or(UpgradeModeCheckError::MalformedToken)?; + let claims_bytes = Base64UrlSafeNoPadding::decode_to_vec(claims_b64, None) + .map_err(|_| UpgradeModeCheckError::MalformedToken)?; + + serde_json::from_slice(&claims_bytes).map_err(|_| UpgradeModeCheckError::MalformedToken) +} + #[cfg(test)] mod tests { use super::*; use crate::generate_new_attestation; use nym_crypto::asymmetric::ed25519; use nym_test_utils::helpers::deterministic_rng; + use time::OffsetDateTime; #[test] fn generate_and_validate_jwt() { @@ -109,11 +128,11 @@ mod tests { attestation.clone(), Duration::from_secs(60 * 60), &unauthorised_jwt_keys, - Some("nym-credential-proxy"), + Some(CREDENTIAL_PROXY_JWT_ISSUER), ); // we expect 'nym-credential-proxy' issuer - assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("nym-credential-proxy")).is_ok()); + assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some(CREDENTIAL_PROXY_JWT_ISSUER)).is_ok()); // we don't care about issuer assert!(validate_upgrade_mode_jwt(&jwt_issuer, None).is_ok()); @@ -133,9 +152,65 @@ mod tests { None, ); // we expect 'nym-credential-proxy' issuer - assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, Some("nym-credential-proxy")).is_err()); + assert!( + validate_upgrade_mode_jwt(&jwt_no_issuer, Some(CREDENTIAL_PROXY_JWT_ISSUER)).is_err() + ); // we don't care about issuer assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, None).is_ok()); } + + #[test] + fn decode_upgrade_mode_claims() { + let invalid_jwts = [ + "", + "invalidSections", + "also.invalid.sections", + "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0.eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0.eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0", + ]; + + let attestation_key = ed25519::PrivateKey::from_bytes(&[ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ]) + .unwrap(); + let jwt_key = ed25519::PrivateKey::from_bytes(&[ + 152, 17, 144, 255, 213, 219, 246, 208, 109, 33, 100, 73, 1, 141, 32, 63, 141, 89, 167, + 2, 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32, + ]) + .unwrap(); + let jwt_keys = ed25519::KeyPair::from(jwt_key); + + let validity = Duration::from_secs(60 * 60); + let attestation = generate_new_attestation(&attestation_key, vec![*jwt_keys.public_key()]); + let valid_jwt = generate_jwt_for_upgrade_mode_attestation( + attestation.clone(), + validity, + &jwt_keys, + Some("nym-credential-proxy"), + ); + + for invalid in invalid_jwts { + assert!(try_decode_upgrade_mode_jwt_claims(invalid).is_err()) + } + + let decoded = try_decode_upgrade_mode_jwt_claims(&valid_jwt).unwrap(); + assert_eq!(decoded.issuer.unwrap(), "nym-credential-proxy"); + assert_eq!(decoded.custom, attestation); + + // unfortunately we can't inject current time when constructing the JWT so the best we can do is ensure its within error margin + let margin = Duration::from_secs(10); + let now = OffsetDateTime::now_utc(); + let min = now - margin; + let max = now + margin; + let issued = decoded.issued_at.unwrap(); + let issued_time = OffsetDateTime::from_unix_timestamp(issued.as_secs() as i64).unwrap(); + assert!(issued_time >= min && issued_time <= max); + + let min = now - margin + validity; + let max = now + margin + validity; + let expires = decoded.expires_at.unwrap(); + let expires_time = OffsetDateTime::from_unix_timestamp(expires.as_secs() as i64).unwrap(); + assert!(expires_time >= min && expires_time <= max); + } } diff --git a/common/upgrade-mode-check/src/lib.rs b/common/upgrade-mode-check/src/lib.rs index 36cbf43c91..7a2e246e2d 100644 --- a/common/upgrade-mode-check/src/lib.rs +++ b/common/upgrade-mode-check/src/lib.rs @@ -9,7 +9,12 @@ pub use attestation::{ UpgradeModeAttestation, generate_new_attestation, generate_new_attestation_with_starting_time, }; pub use error::UpgradeModeCheckError; -pub use jwt::{generate_jwt_for_upgrade_mode_attestation, validate_upgrade_mode_jwt}; +pub use jwt::{ + CREDENTIAL_PROXY_JWT_ISSUER, generate_jwt_for_upgrade_mode_attestation, + try_decode_upgrade_mode_jwt_claims, validate_upgrade_mode_jwt, +}; #[cfg(not(target_arch = "wasm32"))] pub use attestation::attempt_retrieve_attestation; + +pub const UPGRADE_MODE_CREDENTIAL_TYPE: &str = "upgrade_mode_jwt"; diff --git a/common/wireguard-private-metadata/client/src/lib.rs b/common/wireguard-private-metadata/client/src/lib.rs index 58d78fb6c6..3dd884518c 100644 --- a/common/wireguard-private-metadata/client/src/lib.rs +++ b/common/wireguard-private-metadata/client/src/lib.rs @@ -46,6 +46,23 @@ pub trait WireguardMetadataApiClient: ApiClient { ) .await } + + #[instrument(level = "debug", skip(self, request_body))] + async fn request_upgrade_mode_check( + &self, + request_body: &Request, + ) -> Result { + self.post_json( + &[ + routes::V1_API_VERSION, + routes::NETWORK, + routes::UPGRADE_MODE_CHECK, + ], + NO_PARAMS, + request_body, + ) + .await + } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/common/wireguard-private-metadata/server/src/http/router.rs b/common/wireguard-private-metadata/server/src/http/router.rs index 099e1d5724..e76df7d328 100644 --- a/common/wireguard-private-metadata/server/src/http/router.rs +++ b/common/wireguard-private-metadata/server/src/http/router.rs @@ -15,7 +15,7 @@ use utoipa_swagger_ui::SwaggerUi; use crate::http::openapi::ApiDoc; use crate::http::state::AppState; -use crate::network::bandwidth_routes; +use crate::network::{bandwidth_routes, network_routes}; /// Wrapper around `axum::Router` which ensures correct [order of layers][order]. /// Add new routes as if you were working directly with `axum`. @@ -35,7 +35,12 @@ impl RouterBuilder { let default_routes = Router::new() .merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", ApiDoc::openapi())) .route("/", get(|| async { Redirect::to("/swagger") })) - .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())); + .nest( + "/v1", + Router::new() + .nest("/bandwidth", bandwidth_routes()) + .nest("/network", network_routes()), + ); Self { unfinished_router: default_routes, } diff --git a/common/wireguard-private-metadata/server/src/http/state.rs b/common/wireguard-private-metadata/server/src/http/state.rs index 06916bb35f..3e913d7073 100644 --- a/common/wireguard-private-metadata/server/src/http/state.rs +++ b/common/wireguard-private-metadata/server/src/http/state.rs @@ -1,35 +1,137 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use nym_credential_verification::upgrade_mode::UpgradeModeDetails; +use nym_credentials_interface::BandwidthCredential; +use std::cmp::max; use std::net::IpAddr; -use nym_credentials_interface::CredentialSpendingData; - use crate::transceiver::PeerControllerTransceiver; use nym_wireguard_private_metadata_shared::error::MetadataError; +use nym_wireguard_private_metadata_shared::interface::{ResponseData, UpgradeModeCheckRequestType}; + +// we need to be above MINIMUM_REMAINING_BANDWIDTH (500MB) plus we also have to trick the client +// its depletion is low enough to not require sending new tickets +const DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024 * 1024; #[derive(Clone, axum::extract::FromRef)] pub struct AppState { transceiver: PeerControllerTransceiver, + #[from_ref(skip)] + upgrade_mode: UpgradeModeDetails, } impl AppState { - pub fn new(transceiver: PeerControllerTransceiver) -> Self { - Self { transceiver } + pub fn new(transceiver: PeerControllerTransceiver, upgrade_mode: UpgradeModeDetails) -> Self { + Self { + transceiver, + upgrade_mode, + } } - pub async fn available_bandwidth(&self, ip: IpAddr) -> Result { - self.transceiver.query_bandwidth(ip).await + fn upgrade_mode_bandwidth(&self, true_bandwidth: i64) -> i64 { + // if we're undergoing upgrade mode, we don't meter bandwidth, + // we simply return MAX of clients current bandwidth and minimum bandwidth before default + // client would have attempted to send new ticket (hopefully) + // the latter is to support older clients that will ignore `upgrade_mode` field in the response + // as they're not aware of its existence + max(DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD, true_bandwidth) + } + + pub async fn available_bandwidth(&self, ip: IpAddr) -> Result { + let upgrade_mode = self.upgrade_mode.enabled(); + + let true_bandwidth = self.transceiver.query_bandwidth(ip).await?; + let available_bandwidth = if upgrade_mode { + self.upgrade_mode_bandwidth(true_bandwidth) + } else { + true_bandwidth + }; + + Ok(ResponseData::AvailableBandwidth { + amount: available_bandwidth, + upgrade_mode, + }) } // Top up with a credential and return the afterwards available bandwidth pub async fn topup_bandwidth( &self, ip: IpAddr, - credential: CredentialSpendingData, - ) -> Result { - self.transceiver - .topup_bandwidth(ip, Box::new(credential)) - .await + claim: Box, + ) -> Result { + match *claim { + BandwidthCredential::ZkNym(zk_nym) => { + // if we got zk-nym, we just try to verify it + let available_bandwidth = self.transceiver.topup_bandwidth(ip, zk_nym).await?; + + // however, we still follow the same upgrade-mode logic, + // so that the client would not attempt to needlessly send more credentials + let upgrade_mode = self.upgrade_mode.enabled(); + let available_bandwidth = if upgrade_mode { + self.upgrade_mode_bandwidth(available_bandwidth) + } else { + available_bandwidth + }; + + Ok(ResponseData::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + }) + } + BandwidthCredential::UpgradeModeJWT { token } => { + // if we're already in the upgrade mode, don't bother validating the token + if self.upgrade_mode.enabled() { + let true_bandwidth = self.transceiver.query_bandwidth(ip).await?; + return Ok(ResponseData::TopUpBandwidth { + available_bandwidth: self.upgrade_mode_bandwidth(true_bandwidth), + upgrade_mode: true, + }); + } + + // if the token is valid, try to check if we're behind + // and have to update our internal state + self.upgrade_mode + .try_enable_via_received_jwt(token) + .await + .map_err(|err| MetadataError::JWTVerification { + message: err.to_string(), + })?; + + // if we didn't return an error, it means token got accepted + // and we have transitioned into the upgrade mode + let true_bandwidth = self.transceiver.query_bandwidth(ip).await?; + + Ok(ResponseData::TopUpBandwidth { + available_bandwidth: self.upgrade_mode_bandwidth(true_bandwidth), + upgrade_mode: true, + }) + } + } + } + + pub async fn upgrade_mode_check( + &self, + request: UpgradeModeCheckRequestType, + ) -> Result { + // if we're already in the upgrade mode - no need to do anything + if self.upgrade_mode.enabled() { + return Ok(ResponseData::UpgradeMode { upgrade_mode: true }); + } + + match request { + UpgradeModeCheckRequestType::UpgradeModeJwt { token } => { + self.upgrade_mode + .try_enable_via_received_jwt(token) + .await + .map_err(|err| MetadataError::JWTVerification { + message: err.to_string(), + })?; + } + } + + // if we didn't return an error, it means token got accepted + // and we have transitioned into the upgrade mode + Ok(ResponseData::UpgradeMode { upgrade_mode: true }) } } diff --git a/common/wireguard-private-metadata/server/src/network.rs b/common/wireguard-private-metadata/server/src/network.rs index 8e27eca5cd..f1ad08a108 100644 --- a/common/wireguard-private-metadata/server/src/network.rs +++ b/common/wireguard-private-metadata/server/src/network.rs @@ -9,8 +9,7 @@ use axum::{ }; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_wireguard_private_metadata_shared::{ - AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, - interface::{RequestData, ResponseData}, + AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, interface::RequestData, latest, }; use tower_http::compression::CompressionLayer; @@ -25,6 +24,15 @@ pub(crate) fn bandwidth_routes() -> Router { .layer(CompressionLayer::new()) } +pub(crate) fn network_routes() -> Router { + Router::new() + .route( + "/upgrade-mode-check", + axum::routing::post(upgrade_mode_check), + ) + .layer(CompressionLayer::new()) +} + #[utoipa::path( tag = "bandwidth", get, @@ -59,20 +67,17 @@ async fn available_bandwidth( ) -> AxumResult> { let output = output.output.unwrap_or_default(); - let (RequestData::AvailableBandwidth(_), version) = + let (RequestData::AvailableBandwidth, version) = request.extract().map_err(AxumErrorResponse::bad_request)? else { return Err(AxumErrorResponse::bad_request("incorrect request type")); }; - let available_bandwidth = state + let available_bandwidth_response = state .available_bandwidth(addr.ip()) .await .map_err(AxumErrorResponse::bad_request)?; - let response = Response::construct( - ResponseData::AvailableBandwidth(available_bandwidth), - version, - ) - .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(available_bandwidth_response, version) + .map_err(AxumErrorResponse::bad_request)?; Ok(output.to_response(response)) } @@ -96,16 +101,49 @@ async fn topup_bandwidth( ) -> AxumResult> { let output = output.output.unwrap_or_default(); - let (RequestData::TopUpBandwidth(credential), version) = + let (RequestData::TopUpBandwidth { credential }, version) = request.extract().map_err(AxumErrorResponse::bad_request)? else { return Err(AxumErrorResponse::bad_request("incorrect request type")); }; - let available_bandwidth = state - .topup_bandwidth(addr.ip(), *credential) + let top_up_bandwidth_response = state + .topup_bandwidth(addr.ip(), credential) .await .map_err(AxumErrorResponse::bad_request)?; - let response = Response::construct(ResponseData::TopUpBandwidth(available_bandwidth), version) + let response = Response::construct(top_up_bandwidth_response, version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) +} + +#[utoipa::path( + tag = "network", + post, + request_body = Request, + path = "/v1/network/upgrade-mode-check", + responses( + (status = 200, content( + (Response = "application/bincode") + )) + ), +)] +async fn upgrade_mode_check( + Query(output): Query, + State(state): State, + Json(request): Json, +) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::UpgradeModeCheck { typ }, version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let upgrade_mode_check_response = state + .upgrade_mode_check(typ) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(upgrade_mode_check_response, version) .map_err(AxumErrorResponse::bad_request)?; Ok(output.to_response(response)) diff --git a/common/wireguard-private-metadata/server/src/transceiver.rs b/common/wireguard-private-metadata/server/src/transceiver.rs index cbe77126cf..5614e7f8f7 100644 --- a/common/wireguard-private-metadata/server/src/transceiver.rs +++ b/common/wireguard-private-metadata/server/src/transceiver.rs @@ -37,12 +37,12 @@ impl PeerControllerTransceiver { }) } - pub(crate) async fn query_bandwidth(&self, ip: IpAddr) -> Result { + pub async fn query_bandwidth(&self, ip: IpAddr) -> Result { Ok(self.get_client_bandwidth(ip).await?.available().await) } // Top up with a credential and return the afterwards available bandwidth - pub(crate) async fn topup_bandwidth( + pub async fn topup_bandwidth( &self, ip: IpAddr, credential: Box, diff --git a/common/wireguard-private-metadata/shared/src/conversion_helpers.rs b/common/wireguard-private-metadata/shared/src/conversion_helpers.rs new file mode 100644 index 0000000000..117f9afb02 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/conversion_helpers.rs @@ -0,0 +1,218 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +/// A simple macro that given `TryFrom<&A> for B`, implements `TryFrom for B` +/// using the former implementation +#[macro_export] +macro_rules! impl_tryfrom_ref { + ($src:ty, $dst:ty, $err:ty) => { + impl TryFrom<$src> for $dst { + // can't use type Error = >::Error; + // due to lifetime interference within macros + type Error = $err; + + fn try_from(value: $src) -> Result { + >::try_from(&value) + } + } + }; +} + +/// A simple macro that implements all required variants of `TryFrom` +/// between particular versioned `VersionedRequest` and given request variant +/// using default bincode serializer +#[macro_export] +macro_rules! impl_default_bincode_request_query_conversions { + // limitation of macros - need to pass the same underlying type twice, + // once as pattern and once as expression + ($top_req_type:ty, $inner_req_type:ty, $query_type_pat:pat, $query_type_expr:expr) => { + $crate::impl_query_conversions!( + $crate::Request, + $top_req_type, + $inner_req_type, + $query_type_pat, + $query_type_expr + ); + }; +} + +/// A simple macro that implements all required variants of `TryFrom` +/// between particular versioned `VersionedResponse` and given response variant +/// using default bincode serializer +#[macro_export] +macro_rules! impl_default_bincode_response_query_conversions { + // limitation of macros - need to pass the same underlying type twice, + // once as pattern and once as expression + ($top_resp_type:ty, $inner_resp_type:ty, $query_type_pat:pat, $query_type_expr:expr) => { + $crate::impl_query_conversions!( + $crate::Response, + $top_resp_type, + $inner_resp_type, + $query_type_pat, + $query_type_expr + ); + }; +} + +/// A simple macro that implements all required variants of `TryFrom` +/// between [crate::models::Request] and corresponding versioned `VersionedRequest` +/// using default bincode serializer +#[macro_export] +macro_rules! impl_default_bincode_request_conversions { + ($req_type:ty, $version:expr) => { + $crate::impl_versioned_conversions!($crate::Request, $req_type, $version); + }; +} + +/// A simple macro that implements all required variants of `TryFrom` +/// between [crate::models::Response] and corresponding versioned `VersionedResponse` +/// using default bincode serializer +#[macro_export] +macro_rules! impl_default_bincode_response_conversions { + ($req_type:ty, $version:expr) => { + $crate::impl_versioned_conversions!($crate::Response, $req_type, $version); + }; +} + +#[macro_export] +macro_rules! impl_versioned_conversions { + ( + // is it Request or Response + $main_type_ty:ty, + + // e.g. VersionedResponse + $top_type:ty, + + // request/response version type + $version:expr + ) => { + impl TryFrom<&$top_type> for $main_type_ty { + type Error = $crate::models::error::Error; + + fn try_from(value: &$top_type) -> Result { + use ::bincode::Options; + let data = $crate::make_bincode_serializer().serialize(value)?; + Ok(<$main_type_ty>::new($version, data)) + } + } + + // automatically generate `impl TryFrom<$top_type> for $main_type` + $crate::impl_tryfrom_ref!($top_type, $main_type_ty, $crate::models::error::Error); + + impl TryFrom<&$main_type_ty> for $top_type { + type Error = $crate::models::error::Error; + + fn try_from(value: &$main_type_ty) -> Result { + use ::bincode::Options; + if value.version != $version { + return Err($crate::models::error::Error::InvalidVersion { + source_version: value.version, + target_version: $version, + }); + } + Ok($crate::make_bincode_serializer().deserialize(&value.inner)?) + } + } + + // automatically generate `impl TryFrom<$main_type> for $top_type` + $crate::impl_tryfrom_ref!($main_type_ty, $top_type, $crate::models::error::Error); + }; +} + +#[macro_export] +macro_rules! impl_query_conversions { + // limitation of macros - need to pass the same underlying type twice, + // once as pattern and once as expression + ( + // is it Request or Response + $main_type:ty, + + // e.g. VersionedResponse + $top_type:ty, + + // e.g. InnerTopUpResponse + $inner_type:ty, + + // e.g. QueryType::TopUpBandwidth, + $query_type_pat:pat, + + // e.g. QueryType::TopUpBandwidth, + $query_type_expr:expr + ) => { + // conversion from the versioned type into the particular typ, + // e.g. TryFrom<&VersionedResponse> for InnerTopUpResponse + impl TryFrom<&$top_type> for $inner_type { + type Error = $crate::models::error::Error; + + fn try_from(value: &$top_type) -> Result { + use ::bincode::Options; + match value.query_type { + $query_type_pat => { + Ok($crate::make_bincode_serializer().deserialize(&value.inner)?) + } + other => Err($crate::models::error::Error::InvalidQueryType { + source_query_type: other.to_string(), + target_query_type: stringify!($query_type_pat).to_string(), + }), + } + } + } + // implementation of conversion without the referenced type, i.e. + // e.g. TryFrom for InnerTopUpResponse + $crate::impl_tryfrom_ref!($top_type, $inner_type, $crate::models::error::Error); + + // conversion back from the particular type into the versioned type, i.e. + // e.g. TryFrom<&InnerTopUpResponse> for VersionedResponse + impl TryFrom<&$inner_type> for $top_type { + type Error = $crate::models::error::Error; + + fn try_from(value: &$inner_type) -> Result { + use ::bincode::Options; + Ok(Self { + query_type: $query_type_expr, + inner: $crate::make_bincode_serializer().serialize(value)?, + }) + } + } + + // implementation of conversion without the referenced type, i.e. + // e.g. TryFrom for VersionedResponse + $crate::impl_tryfrom_ref!($inner_type, $top_type, $crate::models::error::Error); + + // conversion from the'main' type (Request/Response) into the particular type + // e.g. TryFrom<&Response> for InnerTopUpResponse + impl TryFrom<&$main_type> for $inner_type { + type Error = $crate::error::MetadataError; + + fn try_from(value: &$main_type) -> Result { + <$top_type>::try_from(value)?.try_into().map_err( + |err: $crate::models::error::Error| $crate::error::MetadataError::Models { + message: err.to_string(), + }, + ) + } + } + + // implementation of conversion without the referenced type, i.e. + // e.g. TryFrom for InnerTopUpResponse + $crate::impl_tryfrom_ref!($main_type, $inner_type, $crate::error::MetadataError); + + // conversion from the particular type into the 'main' type (Request/Response) + // e.g. TryFrom<&InnerTopUpResponse> for Response + impl TryFrom<&$inner_type> for $main_type { + type Error = $crate::error::MetadataError; + + fn try_from(value: &$inner_type) -> Result { + <$top_type>::try_from(value)?.try_into().map_err( + |err: $crate::models::error::Error| $crate::error::MetadataError::Models { + message: err.to_string(), + }, + ) + } + } + + // implementation of conversion without the referenced type, i.e. + // e.g. TryFrom for Response + $crate::impl_tryfrom_ref!($inner_type, $main_type, $crate::error::MetadataError); + }; +} diff --git a/common/wireguard-private-metadata/shared/src/error.rs b/common/wireguard-private-metadata/shared/src/error.rs index 3783462a4d..f8db19c304 100644 --- a/common/wireguard-private-metadata/shared/src/error.rs +++ b/common/wireguard-private-metadata/shared/src/error.rs @@ -17,6 +17,9 @@ pub enum MetadataError { #[error("Credential verification error: {message}")] CredentialVerification { message: String }, + + #[error("Upgrade Mode JWT verification error: {message}")] + JWTVerification { message: String }, } impl From for MetadataError { diff --git a/common/wireguard-private-metadata/shared/src/lib.rs b/common/wireguard-private-metadata/shared/src/lib.rs index 54041fd8c7..6a8eadcc24 100644 --- a/common/wireguard-private-metadata/shared/src/lib.rs +++ b/common/wireguard-private-metadata/shared/src/lib.rs @@ -1,6 +1,7 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub(crate) mod conversion_helpers; pub mod error; mod models; pub mod routes; @@ -9,7 +10,9 @@ pub mod routes; pub use models::v0; pub use models::{ AxumErrorResponse, AxumResult, Construct, ErrorResponse, Extract, Request, Response, Version, - error::Error as ModelError, interface, latest, v1, + error::Error as ModelError, + interface::{self, AvailableBandwidth}, + latest, v1, v2, }; fn make_bincode_serializer() -> impl bincode::Options { diff --git a/common/wireguard-private-metadata/shared/src/models/error.rs b/common/wireguard-private-metadata/shared/src/models/error.rs index 45dc88617d..aef1ac5ae6 100644 --- a/common/wireguard-private-metadata/shared/src/models/error.rs +++ b/common/wireguard-private-metadata/shared/src/models/error.rs @@ -15,7 +15,7 @@ pub enum Error { }, #[error( - "trying to deserialize from query type {source_query_type} query type {target_query_type}" + "trying to deserialize from query type {source_query_type} into query type {target_query_type}" )] InvalidQueryType { source_query_type: String, diff --git a/common/wireguard-private-metadata/shared/src/models/interface.rs b/common/wireguard-private-metadata/shared/src/models/interface.rs index 9d5a786c53..317e6680ca 100644 --- a/common/wireguard-private-metadata/shared/src/models/interface.rs +++ b/common/wireguard-private-metadata/shared/src/models/interface.rs @@ -1,61 +1,90 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_credentials_interface::CredentialSpendingData; +use nym_credentials_interface::BandwidthCredential; #[cfg(feature = "testing")] use crate::models::v0; -use crate::models::{Construct, Extract, Request, Response, Version, v1}; +use crate::models::{Construct, Extract, Request, Response, Version, latest, v1, v2}; + +pub use latest::check_upgrade_mode::request::UpgradeModeCheckRequestType; pub enum RequestData { - AvailableBandwidth(()), - TopUpBandwidth(Box), + AvailableBandwidth, + TopUpBandwidth { + credential: Box, + }, + UpgradeModeCheck { + typ: UpgradeModeCheckRequestType, + }, } -impl From for RequestData { - fn from(value: super::latest::interface::RequestData) -> Self { +impl From for RequestData { + fn from(value: latest::interface::RequestData) -> Self { match value { - super::latest::interface::RequestData::AvailableBandwidth(inner) => { - Self::AvailableBandwidth(inner) + latest::interface::RequestData::AvailableBandwidth => Self::AvailableBandwidth, + latest::interface::RequestData::TopUpBandwidth { credential } => { + Self::TopUpBandwidth { credential } } - super::latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) + latest::interface::RequestData::UpgradeModeCheck { typ } => { + Self::UpgradeModeCheck { typ } } } } } -impl From for super::latest::interface::RequestData { +impl From for latest::interface::RequestData { fn from(value: RequestData) -> Self { match value { - RequestData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), - RequestData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) - } + RequestData::AvailableBandwidth => Self::AvailableBandwidth, + RequestData::TopUpBandwidth { credential } => Self::TopUpBandwidth { credential }, + RequestData::UpgradeModeCheck { typ } => Self::UpgradeModeCheck { typ }, } } } -impl From for ResponseData { - fn from(value: super::latest::interface::ResponseData) -> Self { +impl From for ResponseData { + fn from(value: latest::interface::ResponseData) -> Self { match value { - super::latest::interface::ResponseData::AvailableBandwidth(inner) => { - Self::AvailableBandwidth(inner) - } - super::latest::interface::ResponseData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) + latest::interface::ResponseData::AvailableBandwidth { + amount, + upgrade_mode, + } => Self::AvailableBandwidth { + amount, + upgrade_mode, + }, + latest::interface::ResponseData::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + } => Self::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + }, + latest::interface::ResponseData::UpgradeMode { upgrade_mode } => { + Self::UpgradeMode { upgrade_mode } } } } } -impl From for super::latest::interface::ResponseData { +impl From for latest::interface::ResponseData { fn from(value: ResponseData) -> Self { match value { - ResponseData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), - ResponseData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) - } + ResponseData::AvailableBandwidth { + amount, + upgrade_mode, + } => Self::AvailableBandwidth { + amount, + upgrade_mode, + }, + ResponseData::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + } => Self::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + }, + ResponseData::UpgradeMode { upgrade_mode } => Self::UpgradeMode { upgrade_mode }, } } } @@ -65,13 +94,26 @@ impl Construct for Request { match version { #[cfg(feature = "testing")] Version::V0 => { - let translate_info = super::latest::interface::RequestData::from(info); - let downgrade_info = v0::interface::RequestData::try_from(translate_info)?; - let versioned_request = v0::VersionedRequest::construct(downgrade_info, version)?; + // attempt to go through conversion chain for `info`: v2 => v1 => v0 + let v2_info = v2::interface::RequestData::from(info); + let v1_info = v1::interface::RequestData::try_from(v2_info)?; + let v0_info = v0::interface::RequestData::try_from(v1_info)?; + + let versioned_request = v0::VersionedRequest::construct(v0_info, version)?; Ok(versioned_request.try_into()?) } Version::V1 => { - let versioned_request = v1::VersionedRequest::construct(info.into(), version)?; + // attempt to go through conversion chain for `info`: v2 => v1 + let v2_info = v2::interface::RequestData::from(info); + let v1_info = v1::interface::RequestData::try_from(v2_info)?; + + let versioned_request = v1::VersionedRequest::construct(v1_info, version)?; + Ok(versioned_request.try_into()?) + } + Version::V2 => { + let v2_info = v2::interface::RequestData::from(info); + + let versioned_request = v2::VersionedRequest::construct(v2_info, version)?; Ok(versioned_request.try_into()?) } } @@ -84,24 +126,45 @@ impl Extract for Request { #[cfg(feature = "testing")] super::Version::V0 => { let versioned_request = v0::VersionedRequest::try_from(self.clone())?; - let (request, version) = versioned_request.extract()?; + let (extracted_v0_info, version) = versioned_request.extract()?; - let upgrade_request = super::latest::interface::RequestData::try_from(request)?; + let v1_info = v1::interface::RequestData::try_from(extracted_v0_info)?; + let v2_info = v2::interface::RequestData::try_from(v1_info)?; - Ok((upgrade_request.into(), version)) + let request_data = RequestData::from(v2_info); + Ok((request_data, version)) } super::Version::V1 => { - let versioned_request = v1::VersionedRequest::try_from(self.clone())?; - let (extracted, version) = versioned_request.extract()?; - Ok((extracted.into(), version)) + let versioned_request = v1::VersionedRequest::try_from(self)?; + let (extracted_v1_info, version) = versioned_request.extract()?; + let v2_info = v2::interface::RequestData::try_from(extracted_v1_info)?; + + let request_data = RequestData::from(v2_info); + Ok((request_data, version)) + } + super::Version::V2 => { + let versioned_request = v2::VersionedRequest::try_from(self)?; + let (extracted_v2_info, version) = versioned_request.extract()?; + + let request_data = RequestData::from(extracted_v2_info); + Ok((request_data, version)) } } } } pub enum ResponseData { - AvailableBandwidth(i64), - TopUpBandwidth(i64), + AvailableBandwidth { + amount: i64, + upgrade_mode: bool, + }, + TopUpBandwidth { + available_bandwidth: i64, + upgrade_mode: bool, + }, + UpgradeMode { + upgrade_mode: bool, + }, } impl Construct for Response { @@ -109,14 +172,26 @@ impl Construct for Response { match version { #[cfg(feature = "testing")] super::Version::V0 => { - let translate_response = super::latest::interface::ResponseData::from(info); - let downgrade_response = v0::interface::ResponseData::try_from(translate_response)?; - let versioned_response = - v0::VersionedResponse::construct(downgrade_response, version)?; + // attempt to go through conversion chain for `info`: v2 => v1 => v0 + let v2_info = v2::interface::ResponseData::from(info); + let v1_info = v1::interface::ResponseData::try_from(v2_info)?; + let v0_info = v0::interface::ResponseData::try_from(v1_info)?; + + let versioned_response = v0::VersionedResponse::construct(v0_info, version)?; Ok(versioned_response.try_into()?) } Version::V1 => { - let versioned_response = v1::VersionedResponse::construct(info.into(), version)?; + // attempt to go through conversion chain for `info`: v2 => v1 + let v2_info = v2::interface::ResponseData::from(info); + let v1_info = v1::interface::ResponseData::try_from(v2_info)?; + + let versioned_response = v1::VersionedResponse::construct(v1_info, version)?; + Ok(versioned_response.try_into()?) + } + Version::V2 => { + let v2_info = v2::interface::ResponseData::from(info); + + let versioned_response = v2::VersionedResponse::construct(v2_info, version)?; Ok(versioned_response.try_into()?) } } @@ -129,17 +204,70 @@ impl Extract for Response { #[cfg(feature = "testing")] super::Version::V0 => { let versioned_response = v0::VersionedResponse::try_from(self.clone())?; - let (response, version) = versioned_response.extract()?; + let (extracted_v0_info, version) = versioned_response.extract()?; + let v1_info = v1::interface::ResponseData::try_from(extracted_v0_info)?; + let v2_info = v2::interface::ResponseData::try_from(v1_info)?; - let upgrade_response = super::latest::interface::ResponseData::try_from(response)?; - - Ok((upgrade_response.into(), version)) + let response_data = ResponseData::from(v2_info); + Ok((response_data, version)) } super::Version::V1 => { let versioned_response = v1::VersionedResponse::try_from(self.clone())?; - let (extracted, version) = versioned_response.extract()?; - Ok((extracted.into(), version)) + let (extracted_v1_info, version) = versioned_response.extract()?; + let v2_info = v2::interface::ResponseData::try_from(extracted_v1_info)?; + + let response_data = ResponseData::from(v2_info); + Ok((response_data, version)) + } + super::Version::V2 => { + let versioned_response = v2::VersionedResponse::try_from(self.clone())?; + let (extracted_v2_info, version) = versioned_response.extract()?; + + let response_data = ResponseData::from(extracted_v2_info); + Ok((response_data, version)) } } } } + +#[derive(Debug, Clone, Copy)] +pub struct AvailableBandwidth { + pub bandwidth_bytes: i64, + pub upgrade_mode: Option, +} + +impl From for AvailableBandwidth { + fn from(value: v1::AvailableBandwidthResponse) -> Self { + AvailableBandwidth { + bandwidth_bytes: value.available_bandwidth, + upgrade_mode: None, + } + } +} + +impl From for AvailableBandwidth { + fn from(value: v2::AvailableBandwidthResponse) -> Self { + AvailableBandwidth { + bandwidth_bytes: value.available_bandwidth, + upgrade_mode: Some(value.upgrade_mode), + } + } +} + +impl From for AvailableBandwidth { + fn from(value: v1::TopUpResponse) -> Self { + AvailableBandwidth { + bandwidth_bytes: value.available_bandwidth, + upgrade_mode: None, + } + } +} + +impl From for AvailableBandwidth { + fn from(value: v2::TopUpResponse) -> Self { + AvailableBandwidth { + bandwidth_bytes: value.available_bandwidth, + upgrade_mode: Some(value.upgrade_mode), + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/mod.rs b/common/wireguard-private-metadata/shared/src/models/mod.rs index e408d7c5df..ae39c890ac 100644 --- a/common/wireguard-private-metadata/shared/src/models/mod.rs +++ b/common/wireguard-private-metadata/shared/src/models/mod.rs @@ -14,7 +14,10 @@ pub mod interface; pub mod v0; // dummy version, only for filling boilerplate code for update/downgrade and testing pub mod v1; -pub use v1 as latest; +// adds upgrade mode information to bandwidth response +pub mod v2; + +pub use v2 as latest; use crate::models::error::Error; @@ -24,6 +27,7 @@ pub enum Version { /// only used for testing purposes, don't include it in your matching arms V0, V1, + V2, } impl From for Version { @@ -35,6 +39,7 @@ impl From for Version { match value { 0 => zero_version, 1 => Version::V1, + 2 => Version::V2, _ => latest::VERSION, // if unknown, it means we're behind, so we can use the latest we know about } } @@ -47,6 +52,7 @@ impl From for u64 { #[cfg(feature = "testing")] Version::V0 => 0, Version::V1 => 1, + Version::V2 => 2, } } } @@ -57,12 +63,24 @@ pub struct Request { pub(crate) inner: Vec, } -#[derive(Clone, Serialize, Deserialize, ToSchema)] +impl Request { + pub fn new(version: Version, inner: Vec) -> Self { + Request { version, inner } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] pub struct Response { pub version: Version, pub(crate) inner: Vec, } +impl Response { + pub fn new(version: Version, inner: Vec) -> Self { + Response { version, inner } + } +} + pub trait Extract { fn extract(&self) -> Result<(T, Version), Error>; } diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs index 78dfdec7b5..c9cef56089 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/request.rs @@ -1,66 +1,29 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Request}; - -use super::super::{Error, QueryType, VersionedRequest}; +use super::super::{QueryType, VersionedRequest}; +use crate::impl_default_bincode_request_query_conversions; #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct InnerAvailableBandwidthRequest {} -impl TryFrom for InnerAvailableBandwidthRequest { - type Error = Error; - - fn try_from(value: VersionedRequest) -> Result { - match value.query_type { - QueryType::AvailableBandwidth => { - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } - QueryType::TopupBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::AvailableBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedRequest { - type Error = Error; - - fn try_from(value: InnerAvailableBandwidthRequest) -> Result { - Ok(Self { - query_type: QueryType::AvailableBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerAvailableBandwidthRequest { - type Error = crate::error::MetadataError; - - fn try_from(value: Request) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Request { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerAvailableBandwidthRequest) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerAvailableBandwidthRequest +// - TryFrom for InnerAvailableBandwidthRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerAvailableBandwidthRequest, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); #[cfg(test)] mod tests { diff --git a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs index 5243e312ee..d822a72e60 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/available_bandwidth/response.rs @@ -1,66 +1,30 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Response}; +use crate::impl_default_bincode_response_query_conversions; -use super::super::{Error, QueryType, VersionedResponse}; +use super::super::{QueryType, VersionedResponse}; #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct InnerAvailableBandwidthResponse {} -impl TryFrom for InnerAvailableBandwidthResponse { - type Error = Error; - - fn try_from(value: VersionedResponse) -> Result { - match value.query_type { - QueryType::AvailableBandwidth => { - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } - QueryType::TopupBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::AvailableBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedResponse { - type Error = Error; - - fn try_from(value: InnerAvailableBandwidthResponse) -> Result { - Ok(Self { - query_type: QueryType::AvailableBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerAvailableBandwidthResponse { - type Error = crate::error::MetadataError; - - fn try_from(value: Response) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Response { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerAvailableBandwidthResponse) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerAvailableBandwidthResponse, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); #[cfg(test)] mod tests { diff --git a/common/wireguard-private-metadata/shared/src/models/v0/interface.rs b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs index f52daba30e..9c6fc462b3 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/interface.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/interface.rs @@ -9,6 +9,7 @@ use super::{ topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, }; use crate::models::{Construct, Extract, Version, error::Error}; +use crate::{Request, Response}; #[derive(Debug, Clone, PartialEq)] pub enum RequestData { @@ -35,11 +36,11 @@ impl Extract for VersionedRequest { fn extract(&self) -> Result<(RequestData, Version), Error> { match self.query_type { QueryType::AvailableBandwidth => { - let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; + let _req = InnerAvailableBandwidthRequest::try_from(self)?; Ok((RequestData::AvailableBandwidth(()), VERSION)) } - QueryType::TopupBandwidth => { - let _req = InnerTopUpRequest::try_from(self.clone())?; + QueryType::TopUpBandwidth => { + let _req = InnerTopUpRequest::try_from(self)?; Ok((RequestData::TopUpBandwidth(()), VERSION)) } } @@ -61,13 +62,46 @@ impl Extract for VersionedResponse { fn extract(&self) -> Result<(ResponseData, Version), Error> { match self.query_type { QueryType::AvailableBandwidth => { - let _resp = InnerAvailableBandwidthResponse::try_from(self.clone())?; + let _resp = InnerAvailableBandwidthResponse::try_from(self)?; Ok((ResponseData::AvailableBandwidth(()), VERSION)) } - QueryType::TopupBandwidth => { - let _resp = InnerTopUpResponse::try_from(self.clone())?; + QueryType::TopUpBandwidth => { + let _resp = InnerTopUpResponse::try_from(self)?; Ok((ResponseData::TopUpBandwidth(()), VERSION)) } } } } + +#[cfg(feature = "testing")] +impl Extract for Request { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.version { + Version::V0 => { + let versioned_request = VersionedRequest::try_from(self)?; + versioned_request.extract() + } + _ => Err(Error::UpdateNotPossible { + from: self.version, + to: VERSION, + }), + } + } +} + +#[cfg(feature = "testing")] +impl Construct for Response { + fn construct(info: ResponseData, version: Version) -> Result { + match version { + Version::V0 => { + let translate_response = info; + let versioned_response = VersionedResponse::construct(translate_response, version)?; + Ok(versioned_response.try_into()?) + } + _ => Err(Error::DowngradeNotPossible { + from: version, + to: VERSION, + }), + } + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v0/mod.rs b/common/wireguard-private-metadata/shared/src/models/v0/mod.rs index 997fbf6f57..fd90ac124a 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/mod.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/mod.rs @@ -3,15 +3,13 @@ use std::fmt::Display; -use bincode::Options; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use super::error::Error; use crate::{ - make_bincode_serializer, - models::{Request, Response, Version}, + impl_default_bincode_request_conversions, impl_default_bincode_response_conversions, + models::Version, }; pub(crate) mod available_bandwidth; @@ -31,7 +29,7 @@ pub use topup_bandwidth::{ #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] pub enum QueryType { AvailableBandwidth, - TopupBandwidth, + TopUpBandwidth, } impl Display for QueryType { @@ -45,62 +43,24 @@ pub struct VersionedRequest { query_type: QueryType, inner: Vec, } - -impl TryFrom for Request { - type Error = Error; - - fn try_from(value: VersionedRequest) -> Result { - Ok(Request { - version: VERSION, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for VersionedRequest { - type Error = Error; - - fn try_from(value: Request) -> Result { - if value.version != VERSION { - return Err(Error::InvalidVersion { - source_version: value.version, - target_version: VERSION, - }); - } - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for Request +// - TryFrom for Request +// - TryFrom<&Request> for VersionedRequest +// - TryFrom for VersionedRequest +impl_default_bincode_request_conversions!(VersionedRequest, VERSION); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct VersionedResponse { query_type: QueryType, inner: Vec, } - -impl TryFrom for Response { - type Error = Error; - - fn try_from(value: VersionedResponse) -> Result { - Ok(Response { - version: VERSION, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for VersionedResponse { - type Error = Error; - - fn try_from(value: Response) -> Result { - if value.version != VERSION { - return Err(Error::InvalidVersion { - source_version: value.version, - target_version: VERSION, - }); - } - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for Response +// - TryFrom for Response +// - TryFrom<&Response> for VersionedResponse +// - TryFrom for VersionedResponse +impl_default_bincode_response_conversions!(VersionedResponse, VERSION); #[cfg(test)] mod tests { @@ -110,8 +70,10 @@ mod tests { }, topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, }; - use super::*; + use crate::make_bincode_serializer; + use crate::{Request, Response}; + use bincode::Options; #[test] fn serde_request_av_bw() { @@ -146,7 +108,7 @@ mod tests { #[test] fn serde_request_topup() { let req = VersionedRequest { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: make_bincode_serializer() .serialize(&InnerTopUpRequest {}) .unwrap(), @@ -161,7 +123,7 @@ mod tests { #[test] fn serde_response_topup() { let resp = VersionedResponse { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: make_bincode_serializer() .serialize(&InnerTopUpResponse {}) .unwrap(), diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs index 9c333478d2..3cb15b5ee8 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/request.rs @@ -1,64 +1,30 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Request}; +use crate::impl_default_bincode_request_query_conversions; -use super::super::{Error, QueryType, VersionedRequest}; +use super::super::{QueryType, VersionedRequest}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct InnerTopUpRequest {} -impl TryFrom for InnerTopUpRequest { - type Error = Error; - - fn try_from(value: VersionedRequest) -> Result { - match value.query_type { - QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), - QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::TopupBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedRequest { - type Error = Error; - - fn try_from(value: InnerTopUpRequest) -> Result { - Ok(Self { - query_type: QueryType::TopupBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerTopUpRequest { - type Error = crate::error::MetadataError; - - fn try_from(value: Request) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Request { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerTopUpRequest) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerTopUpRequest, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); #[cfg(test)] mod tests { @@ -68,7 +34,7 @@ mod tests { fn serde() { let req = InnerTopUpRequest {}; let ser = VersionedRequest::try_from(req.clone()).unwrap(); - assert_eq!(QueryType::TopupBandwidth, ser.query_type); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); let de = InnerTopUpRequest::try_from(ser).unwrap(); assert_eq!(req, de); } @@ -76,7 +42,7 @@ mod tests { #[test] fn empty_content() { let future_req = VersionedRequest { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: vec![], }; assert!(InnerTopUpRequest::try_from(future_req).is_ok()); diff --git a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs index cd934b6e7e..346792ce73 100644 --- a/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs +++ b/common/wireguard-private-metadata/shared/src/models/v0/topup_bandwidth/response.rs @@ -1,64 +1,30 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Response}; +use crate::impl_default_bincode_response_query_conversions; -use super::super::{Error, QueryType, VersionedResponse}; +use super::super::{QueryType, VersionedResponse}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct InnerTopUpResponse {} -impl TryFrom for InnerTopUpResponse { - type Error = Error; - - fn try_from(value: VersionedResponse) -> Result { - match value.query_type { - QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), - QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::TopupBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedResponse { - type Error = Error; - - fn try_from(value: InnerTopUpResponse) -> Result { - Ok(Self { - query_type: QueryType::TopupBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerTopUpResponse { - type Error = crate::error::MetadataError; - - fn try_from(value: Response) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Response { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerTopUpResponse) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerTopUpResponse, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); #[cfg(test)] mod tests { @@ -68,7 +34,7 @@ mod tests { fn serde() { let resp = InnerTopUpResponse {}; let ser = VersionedResponse::try_from(resp.clone()).unwrap(); - assert_eq!(QueryType::TopupBandwidth, ser.query_type); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); let de = InnerTopUpResponse::try_from(ser).unwrap(); assert_eq!(resp, de); } @@ -76,7 +42,7 @@ mod tests { #[test] fn empty_content() { let future_resp = VersionedResponse { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: vec![], }; assert!(InnerTopUpResponse::try_from(future_resp).is_ok()); diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs index 78dfdec7b5..702c06377a 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/request.rs @@ -1,66 +1,30 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Request}; +use crate::impl_default_bincode_request_query_conversions; -use super::super::{Error, QueryType, VersionedRequest}; +use super::super::{QueryType, VersionedRequest}; #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct InnerAvailableBandwidthRequest {} -impl TryFrom for InnerAvailableBandwidthRequest { - type Error = Error; - - fn try_from(value: VersionedRequest) -> Result { - match value.query_type { - QueryType::AvailableBandwidth => { - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } - QueryType::TopupBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::AvailableBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedRequest { - type Error = Error; - - fn try_from(value: InnerAvailableBandwidthRequest) -> Result { - Ok(Self { - query_type: QueryType::AvailableBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerAvailableBandwidthRequest { - type Error = crate::error::MetadataError; - - fn try_from(value: Request) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Request { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerAvailableBandwidthRequest) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerAvailableBandwidthRequest +// - TryFrom for InnerAvailableBandwidthRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerAvailableBandwidthRequest, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); #[cfg(test)] mod tests { diff --git a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs index f5addd609f..a65ae64aed 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/available_bandwidth/response.rs @@ -1,68 +1,32 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Response}; +use crate::impl_default_bincode_response_query_conversions; -use super::super::{Error, QueryType, VersionedResponse}; +use super::super::{QueryType, VersionedResponse}; #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct InnerAvailableBandwidthResponse { pub available_bandwidth: i64, } -impl TryFrom for InnerAvailableBandwidthResponse { - type Error = Error; - - fn try_from(value: VersionedResponse) -> Result { - match value.query_type { - QueryType::AvailableBandwidth => { - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } - QueryType::TopupBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::AvailableBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedResponse { - type Error = Error; - - fn try_from(value: InnerAvailableBandwidthResponse) -> Result { - Ok(Self { - query_type: QueryType::AvailableBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerAvailableBandwidthResponse { - type Error = crate::error::MetadataError; - - fn try_from(value: Response) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Response { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerAvailableBandwidthResponse) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerAvailableBandwidthResponse, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); #[cfg(test)] mod tests { diff --git a/common/wireguard-private-metadata/shared/src/models/v1/interface.rs b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs index 763222e04a..7a02463956 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/interface.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/interface.rs @@ -5,6 +5,8 @@ use nym_credentials_interface::CredentialSpendingData; #[cfg(feature = "testing")] use super::super::v0 as previous; +#[cfg(feature = "testing")] +use crate::{Request, Response, v0}; use super::{ QueryType, VERSION, VersionedRequest, VersionedResponse, @@ -46,7 +48,7 @@ impl Extract for VersionedRequest { let _req = InnerAvailableBandwidthRequest::try_from(self.clone())?; Ok((RequestData::AvailableBandwidth(()), VERSION)) } - QueryType::TopupBandwidth => { + QueryType::TopUpBandwidth => { let req = InnerTopUpRequest::try_from(self.clone())?; Ok(( RequestData::TopUpBandwidth(Box::new(req.credential)), @@ -84,7 +86,7 @@ impl Extract for VersionedResponse { VERSION, )) } - QueryType::TopupBandwidth => { + QueryType::TopUpBandwidth => { let resp = InnerTopUpResponse::try_from(self.clone())?; Ok(( ResponseData::TopUpBandwidth(resp.available_bandwidth), @@ -98,7 +100,7 @@ impl Extract for VersionedResponse { // this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions #[cfg(feature = "testing")] impl TryFrom for RequestData { - type Error = super::Error; + type Error = crate::models::error::Error; fn try_from(value: previous::interface::RequestData) -> Result { match value { @@ -106,7 +108,7 @@ impl TryFrom for RequestData { Ok(Self::AvailableBandwidth(inner)) } previous::interface::RequestData::TopUpBandwidth(_) => { - Err(super::Error::UpdateNotPossible { + Err(crate::models::Error::UpdateNotPossible { from: previous::VERSION, to: VERSION, }) @@ -118,7 +120,7 @@ impl TryFrom for RequestData { // this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions #[cfg(feature = "testing")] impl TryFrom for previous::interface::RequestData { - type Error = super::Error; + type Error = crate::models::error::Error; fn try_from(value: RequestData) -> Result { match value { @@ -131,18 +133,18 @@ impl TryFrom for previous::interface::RequestData { // this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions #[cfg(feature = "testing")] impl TryFrom for ResponseData { - type Error = super::Error; + type Error = crate::models::error::Error; fn try_from(value: previous::interface::ResponseData) -> Result { match value { previous::interface::ResponseData::AvailableBandwidth(_) => { - Err(super::Error::UpdateNotPossible { + Err(crate::models::error::Error::UpdateNotPossible { from: previous::VERSION, to: VERSION, }) } previous::interface::ResponseData::TopUpBandwidth(_) => { - Err(super::Error::UpdateNotPossible { + Err(crate::models::error::Error::UpdateNotPossible { from: previous::VERSION, to: VERSION, }) @@ -154,7 +156,7 @@ impl TryFrom for ResponseData { // this should be with #[cfg(feature = "testing")] only coming from v0, don't copy this for future versions #[cfg(feature = "testing")] impl TryFrom for previous::interface::ResponseData { - type Error = super::Error; + type Error = crate::models::error::Error; fn try_from(value: ResponseData) -> Result { match value { @@ -164,13 +166,64 @@ impl TryFrom for previous::interface::ResponseData { } } +#[cfg(feature = "testing")] +impl Extract for Request { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.version { + Version::V0 => { + let versioned_request = v0::VersionedRequest::try_from(self)?; + let (extracted_v0_info, version) = versioned_request.extract()?; + + let v1_info = RequestData::try_from(extracted_v0_info)?; + Ok((v1_info, version)) + } + Version::V1 => { + let versioned_request = VersionedRequest::try_from(self)?; + versioned_request.extract() + } + // a v1 server does not have any code for downgrading v2 into v1 + _ => Err(Error::DowngradeNotPossible { + from: self.version, + to: VERSION, + }), + } + } +} + +#[cfg(feature = "testing")] +impl Construct for Response { + fn construct(info: ResponseData, version: Version) -> Result { + match version { + Version::V0 => { + let v1_info = info; + let v0_info = v0::interface::ResponseData::try_from(v1_info)?; + + let versioned_response = v0::VersionedResponse::construct(v0_info, version)?; + Ok(versioned_response.try_into()?) + } + Version::V1 => { + let translate_response = info; + let versioned_response = VersionedResponse::construct(translate_response, version)?; + Ok(versioned_response.try_into()?) + } + // a v1 server does not have any code for downgrading v2 into v1 + _ => Err(Error::DowngradeNotPossible { + from: version, + to: VERSION, + }), + } + } +} + #[cfg(test)] mod test { + #[cfg(feature = "testing")] + use super::*; + #[cfg(feature = "testing")] use crate::models::tests::CREDENTIAL_BYTES; - use super::*; - #[test] + #[cfg(feature = "testing")] fn request_upgrade() { assert_eq!( RequestData::try_from(previous::interface::RequestData::AvailableBandwidth(())) @@ -183,6 +236,7 @@ mod test { } #[test] + #[cfg(feature = "testing")] fn response_upgrade() { assert!( ResponseData::try_from(previous::interface::ResponseData::AvailableBandwidth(())) @@ -194,6 +248,7 @@ mod test { } #[test] + #[cfg(feature = "testing")] fn request_downgrade() { assert_eq!( previous::interface::RequestData::try_from(RequestData::AvailableBandwidth(())) @@ -210,6 +265,7 @@ mod test { } #[test] + #[cfg(feature = "testing")] fn response_downgrade() { assert_eq!( previous::interface::ResponseData::try_from(ResponseData::AvailableBandwidth(42)) diff --git a/common/wireguard-private-metadata/shared/src/models/v1/mod.rs b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs index 787a74f671..050b97caa1 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/mod.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/mod.rs @@ -3,15 +3,13 @@ use std::fmt::Display; -use bincode::Options; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use super::error::Error; use crate::{ - make_bincode_serializer, - models::{Request, Response, Version}, + impl_default_bincode_request_conversions, impl_default_bincode_response_conversions, + models::Version, }; pub use available_bandwidth::{ @@ -31,7 +29,7 @@ pub const VERSION: Version = Version::V1; #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] pub enum QueryType { AvailableBandwidth, - TopupBandwidth, + TopUpBandwidth, } impl Display for QueryType { @@ -45,82 +43,44 @@ pub struct VersionedRequest { query_type: QueryType, inner: Vec, } - -impl TryFrom for Request { - type Error = Error; - - fn try_from(value: VersionedRequest) -> Result { - Ok(Request { - version: VERSION, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for VersionedRequest { - type Error = Error; - - fn try_from(value: Request) -> Result { - if value.version != VERSION { - return Err(Error::InvalidVersion { - source_version: value.version, - target_version: VERSION, - }); - } - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for Request +// - TryFrom for Request +// - TryFrom<&Request> for VersionedRequest +// - TryFrom for VersionedRequest +impl_default_bincode_request_conversions!(VersionedRequest, VERSION); #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct VersionedResponse { query_type: QueryType, inner: Vec, } - -impl TryFrom for Response { - type Error = Error; - - fn try_from(value: VersionedResponse) -> Result { - Ok(Response { - version: VERSION, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for VersionedResponse { - type Error = Error; - - fn try_from(value: Response) -> Result { - if value.version != VERSION { - return Err(Error::InvalidVersion { - source_version: value.version, - target_version: VERSION, - }); - } - Ok(make_bincode_serializer().deserialize(&value.inner)?) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for Response +// - TryFrom for Response +// - TryFrom<&Response> for VersionedResponse +// - TryFrom for VersionedResponse +impl_default_bincode_response_conversions!(VersionedResponse, VERSION); #[cfg(test)] mod tests { - - use nym_credentials_interface::CredentialSpendingData; - - use crate::models::tests::CREDENTIAL_BYTES; - use self::{ available_bandwidth::{ request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, }, topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, }; + use crate::models::error::Error; + use crate::models::tests::CREDENTIAL_BYTES; + use crate::{Request, Response, make_bincode_serializer}; + use bincode::Options; + use nym_credentials_interface::CredentialSpendingData; use super::*; #[test] fn mismatched_request_version() { - let version = Version::V0; + let version = Version::V2; let future_bw = Request { version, inner: vec![], @@ -139,7 +99,7 @@ mod tests { #[test] fn mismatched_response_version() { - let version = Version::V0; + let version = Version::V2; let future_bw = Response { version, inner: vec![], @@ -191,7 +151,7 @@ mod tests { #[test] fn serde_request_topup() { let req = VersionedRequest { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: make_bincode_serializer() .serialize(&InnerTopUpRequest { credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), @@ -208,7 +168,7 @@ mod tests { #[test] fn serde_response_topup() { let resp = VersionedResponse { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: make_bincode_serializer() .serialize(&InnerTopUpResponse { available_bandwidth: 42, diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs index 871cc127ef..3a3de39689 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/request.rs @@ -1,13 +1,12 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use nym_credentials_interface::CredentialSpendingData; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Request}; +use crate::impl_default_bincode_request_query_conversions; -use super::super::{Error, QueryType, VersionedRequest}; +use super::super::{QueryType, VersionedRequest}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct InnerTopUpRequest { @@ -15,54 +14,21 @@ pub struct InnerTopUpRequest { pub credential: CredentialSpendingData, } -impl TryFrom for InnerTopUpRequest { - type Error = Error; - - fn try_from(value: VersionedRequest) -> Result { - match value.query_type { - QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), - QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::TopupBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedRequest { - type Error = Error; - - fn try_from(value: InnerTopUpRequest) -> Result { - Ok(Self { - query_type: QueryType::TopupBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerTopUpRequest { - type Error = crate::error::MetadataError; - - fn try_from(value: Request) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Request { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerTopUpRequest) -> Result { - VersionedRequest::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerTopUpRequest, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); #[cfg(test)] mod tests { @@ -76,7 +42,7 @@ mod tests { credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), }; let ser = VersionedRequest::try_from(req.clone()).unwrap(); - assert_eq!(QueryType::TopupBandwidth, ser.query_type); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); let de = InnerTopUpRequest::try_from(ser).unwrap(); assert_eq!(req, de); } @@ -84,7 +50,7 @@ mod tests { #[test] fn invalid_content() { let future_req = VersionedRequest { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: vec![], }; assert!(InnerTopUpRequest::try_from(future_req).is_err()); diff --git a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs index 08e0ef111f..dccc735b8f 100644 --- a/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs +++ b/common/wireguard-private-metadata/shared/src/models/v1/topup_bandwidth/response.rs @@ -1,66 +1,32 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use bincode::Options; use serde::{Deserialize, Serialize}; -use crate::{make_bincode_serializer, models::Response}; +use crate::impl_default_bincode_response_query_conversions; -use super::super::{Error, QueryType, VersionedResponse}; +use super::super::{QueryType, VersionedResponse}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct InnerTopUpResponse { pub available_bandwidth: i64, } -impl TryFrom for InnerTopUpResponse { - type Error = Error; - - fn try_from(value: VersionedResponse) -> Result { - match value.query_type { - QueryType::TopupBandwidth => Ok(make_bincode_serializer().deserialize(&value.inner)?), - QueryType::AvailableBandwidth => Err(Error::InvalidQueryType { - source_query_type: value.query_type.to_string(), - target_query_type: QueryType::TopupBandwidth.to_string(), - }), - } - } -} - -impl TryFrom for VersionedResponse { - type Error = Error; - - fn try_from(value: InnerTopUpResponse) -> Result { - Ok(Self { - query_type: QueryType::TopupBandwidth, - inner: make_bincode_serializer().serialize(&value)?, - }) - } -} - -impl TryFrom for InnerTopUpResponse { - type Error = crate::error::MetadataError; - - fn try_from(value: Response) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} - -impl TryFrom for Response { - type Error = crate::error::MetadataError; - - fn try_from(value: InnerTopUpResponse) -> Result { - VersionedResponse::try_from(value)? - .try_into() - .map_err(|err: Error| crate::error::MetadataError::Models { - message: err.to_string(), - }) - } -} +// Implements: +// - TryFrom<&VersionedResponse> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerTopUpResponse, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); #[cfg(test)] mod tests { @@ -72,7 +38,7 @@ mod tests { available_bandwidth: 42, }; let ser = VersionedResponse::try_from(resp.clone()).unwrap(); - assert_eq!(QueryType::TopupBandwidth, ser.query_type); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); let de = InnerTopUpResponse::try_from(ser).unwrap(); assert_eq!(resp, de); } @@ -80,7 +46,7 @@ mod tests { #[test] fn invalid_content() { let future_resp = VersionedResponse { - query_type: QueryType::TopupBandwidth, + query_type: QueryType::TopUpBandwidth, inner: vec![], }; assert!(InnerTopUpResponse::try_from(future_resp).is_err()); diff --git a/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/request.rs new file mode 100644 index 0000000000..702c06377a --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/request.rs @@ -0,0 +1,50 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_request_query_conversions; + +use super::super::{QueryType, VersionedRequest}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthRequest {} + +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerAvailableBandwidthRequest +// - TryFrom for InnerAvailableBandwidthRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerAvailableBandwidthRequest, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerAvailableBandwidthRequest {}; + let ser = VersionedRequest::try_from(req).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn empty_content() { + let future_req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthRequest::try_from(future_req).is_ok()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/response.rs new file mode 100644 index 0000000000..0da8eb0a2b --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/available_bandwidth/response.rs @@ -0,0 +1,56 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_response_query_conversions; + +use super::super::{QueryType, VersionedResponse}; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct InnerAvailableBandwidthResponse { + pub available_bandwidth: i64, + pub upgrade_mode: bool, +} + +// Implements: +// - TryFrom<&VersionedResponse> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerAvailableBandwidthResponse +// - TryFrom for InnerAvailableBandwidthResponse +// - TryFrom<&InnerAvailableBandwidthResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerAvailableBandwidthResponse, + QueryType::AvailableBandwidth, + QueryType::AvailableBandwidth +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerAvailableBandwidthResponse { + available_bandwidth: 42, + upgrade_mode: false, + }; + let ser = VersionedResponse::try_from(resp).unwrap(); + assert_eq!(QueryType::AvailableBandwidth, ser.query_type); + let de = InnerAvailableBandwidthResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: vec![], + }; + assert!(InnerAvailableBandwidthResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/mod.rs b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/request.rs b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/request.rs new file mode 100644 index 0000000000..2baba00f8a --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/request.rs @@ -0,0 +1,76 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_request_query_conversions; + +use super::super::{QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub enum UpgradeModeCheckRequestType { + /// Attempt to request upgrade mode recheck via the JWT issued as the result of + /// global attestation.json being published + UpgradeModeJwt { token: String }, +} + +// each versioned variant should always be a subset of the latest one defined in the interface +// so a From trait should always be implementable (as opposed to having to do TryFrom) +// (but this is not applicable in this instance as this IS the latest (05.11.25) +// impl From for crate::models::interface::UpgradeModeCheckRequestType { +// fn from(typ: UpgradeModeCheckRequestType) -> Self { +// match typ { +// UpgradeModeCheckRequestType::UpgradeModeJwt { token } => { +// crate::models::interface::UpgradeModeCheckRequestType::UpgradeModeJwt { token } +// } +// } +// } +// } + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerUpgradeModeCheckRequest { + pub request_type: UpgradeModeCheckRequestType, +} + +// Implements: +// - TryFrom<&VersionedRequest> for InnerUpgradeModeCheckRequest +// - TryFrom for InnerUpgradeModeCheckRequest +// - TryFrom<&InnerUpgradeModeCheckRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerUpgradeModeCheckRequest +// - TryFrom for InnerUpgradeModeCheckRequest +// - TryFrom<&InnerUpgradeModeCheckRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerUpgradeModeCheckRequest, + QueryType::UpgradeModeCheck, + QueryType::UpgradeModeCheck +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let req = InnerUpgradeModeCheckRequest { + request_type: UpgradeModeCheckRequestType::UpgradeModeJwt { + token: "dummy.jwt.token".to_string(), + }, + }; + let ser = VersionedRequest::try_from(req.clone()).unwrap(); + assert_eq!(QueryType::UpgradeModeCheck, ser.query_type); + let de = InnerUpgradeModeCheckRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn invalid_content() { + let future_req = VersionedRequest { + query_type: QueryType::UpgradeModeCheck, + inner: vec![], + }; + assert!(InnerUpgradeModeCheckRequest::try_from(future_req).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/response.rs b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/response.rs new file mode 100644 index 0000000000..3b83ae69eb --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/check_upgrade_mode/response.rs @@ -0,0 +1,52 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_response_query_conversions; + +use super::super::{QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerUpgradeModeCheckResponse { + pub upgrade_mode: bool, +} + +// Implements: +// - TryFrom<&VersionedResponse> for InnerUpgradeModeCheckResponse +// - TryFrom for InnerUpgradeModeCheckResponse +// - TryFrom<&InnerUpgradeModeCheckResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerUpgradeModeCheckResponse +// - TryFrom for InnerUpgradeModeCheckResponse +// - TryFrom<&InnerUpgradeModeCheckResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerUpgradeModeCheckResponse, + QueryType::UpgradeModeCheck, + QueryType::UpgradeModeCheck +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerUpgradeModeCheckResponse { upgrade_mode: true }; + let ser = VersionedResponse::try_from(resp.clone()).unwrap(); + assert_eq!(QueryType::UpgradeModeCheck, ser.query_type); + let de = InnerUpgradeModeCheckResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::UpgradeModeCheck, + inner: vec![], + }; + assert!(InnerUpgradeModeCheckResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/interface.rs b/common/wireguard-private-metadata/shared/src/models/v2/interface.rs new file mode 100644 index 0000000000..9a5e79ff61 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/interface.rs @@ -0,0 +1,304 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::BandwidthCredential; + +use super::super::v1 as previous; + +use super::{ + QueryType, VERSION, VersionedRequest, VersionedResponse, + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + check_upgrade_mode::{ + request::{InnerUpgradeModeCheckRequest, UpgradeModeCheckRequestType}, + response::InnerUpgradeModeCheckResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, +}; +use crate::models::{Construct, Extract, Version, error::Error}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RequestData { + AvailableBandwidth, + TopUpBandwidth { + credential: Box, + }, + UpgradeModeCheck { + typ: UpgradeModeCheckRequestType, + }, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum ResponseData { + AvailableBandwidth { + amount: i64, + upgrade_mode: bool, + }, + TopUpBandwidth { + available_bandwidth: i64, + upgrade_mode: bool, + }, + UpgradeMode { + upgrade_mode: bool, + }, +} + +impl Construct for VersionedRequest { + fn construct(info: RequestData, _version: Version) -> Result { + match info { + RequestData::AvailableBandwidth => Ok(InnerAvailableBandwidthRequest {}.try_into()?), + RequestData::TopUpBandwidth { credential } => Ok(InnerTopUpRequest { + credential: *credential, + } + .try_into()?), + RequestData::UpgradeModeCheck { typ } => { + Ok(InnerUpgradeModeCheckRequest { request_type: typ }.try_into()?) + } + } + } +} + +impl Extract for VersionedRequest { + fn extract(&self) -> Result<(RequestData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let _req = InnerAvailableBandwidthRequest::try_from(self)?; + Ok((RequestData::AvailableBandwidth, VERSION)) + } + QueryType::TopUpBandwidth => { + let req = InnerTopUpRequest::try_from(self)?; + Ok(( + RequestData::TopUpBandwidth { + credential: Box::new(req.credential), + }, + VERSION, + )) + } + QueryType::UpgradeModeCheck => { + let req = InnerUpgradeModeCheckRequest::try_from(self)?; + Ok(( + RequestData::UpgradeModeCheck { + typ: req.request_type, + }, + VERSION, + )) + } + } + } +} + +impl Construct for VersionedResponse { + fn construct(info: ResponseData, _version: Version) -> Result { + match info { + ResponseData::AvailableBandwidth { + amount, + upgrade_mode, + } => Ok(InnerAvailableBandwidthResponse { + available_bandwidth: amount, + upgrade_mode, + } + .try_into()?), + ResponseData::TopUpBandwidth { + available_bandwidth, + upgrade_mode, + } => Ok(InnerTopUpResponse { + available_bandwidth, + upgrade_mode, + } + .try_into()?), + ResponseData::UpgradeMode { upgrade_mode } => { + Ok(InnerUpgradeModeCheckResponse { upgrade_mode }.try_into()?) + } + } + } +} + +impl Extract for VersionedResponse { + fn extract(&self) -> Result<(ResponseData, Version), Error> { + match self.query_type { + QueryType::AvailableBandwidth => { + let resp = InnerAvailableBandwidthResponse::try_from(self)?; + Ok(( + ResponseData::AvailableBandwidth { + amount: resp.available_bandwidth, + upgrade_mode: resp.upgrade_mode, + }, + VERSION, + )) + } + QueryType::TopUpBandwidth => { + let resp = InnerTopUpResponse::try_from(self)?; + Ok(( + ResponseData::TopUpBandwidth { + available_bandwidth: resp.available_bandwidth, + upgrade_mode: resp.upgrade_mode, + }, + VERSION, + )) + } + QueryType::UpgradeModeCheck => { + let resp = InnerUpgradeModeCheckResponse::try_from(self)?; + Ok(( + ResponseData::UpgradeMode { + upgrade_mode: resp.upgrade_mode, + }, + VERSION, + )) + } + } + } +} + +impl TryFrom for RequestData { + type Error = super::Error; + + fn try_from(value: previous::interface::RequestData) -> Result { + match value { + previous::interface::RequestData::AvailableBandwidth(_) => Ok(Self::AvailableBandwidth), + previous::interface::RequestData::TopUpBandwidth(zk_nym) => Ok(Self::TopUpBandwidth { + credential: Box::new((*zk_nym).into()), + }), + } + } +} + +impl TryFrom for previous::interface::RequestData { + type Error = super::Error; + + fn try_from(value: RequestData) -> Result { + match value { + RequestData::AvailableBandwidth => Ok(Self::AvailableBandwidth(())), + RequestData::TopUpBandwidth { credential } => match *credential { + BandwidthCredential::ZkNym(zk_nym) => Ok(Self::TopUpBandwidth(zk_nym)), + BandwidthCredential::UpgradeModeJWT { .. } => { + Err(super::Error::DowngradeNotPossible { + from: VERSION, + to: previous::VERSION, + }) + } + }, + RequestData::UpgradeModeCheck { .. } => Err(super::Error::DowngradeNotPossible { + from: VERSION, + to: previous::VERSION, + }), + } + } +} + +impl TryFrom for ResponseData { + type Error = super::Error; + + fn try_from(value: previous::interface::ResponseData) -> Result { + match value { + previous::interface::ResponseData::AvailableBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + previous::interface::ResponseData::TopUpBandwidth(_) => { + Err(super::Error::UpdateNotPossible { + from: previous::VERSION, + to: VERSION, + }) + } + } + } +} + +impl TryFrom for previous::interface::ResponseData { + type Error = super::Error; + + fn try_from(value: ResponseData) -> Result { + match value { + ResponseData::AvailableBandwidth { amount, .. } => Ok(Self::AvailableBandwidth(amount)), + ResponseData::TopUpBandwidth { + available_bandwidth, + .. + } => Ok(Self::TopUpBandwidth(available_bandwidth)), + ResponseData::UpgradeMode { .. } => Err(super::Error::DowngradeNotPossible { + from: VERSION, + to: previous::VERSION, + }), + } + } +} + +#[cfg(test)] +mod test { + use crate::models::tests::CREDENTIAL_BYTES; + use nym_credentials_interface::CredentialSpendingData; + + use super::*; + + #[test] + fn request_upgrade() { + assert_eq!( + RequestData::try_from(previous::interface::RequestData::AvailableBandwidth(())) + .unwrap(), + RequestData::AvailableBandwidth + ); + assert_eq!( + RequestData::try_from(previous::interface::RequestData::TopUpBandwidth(Box::new( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + ))) + .unwrap(), + RequestData::TopUpBandwidth { + credential: Box::new(BandwidthCredential::ZkNym(Box::new( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + ))), + } + ); + } + + #[test] + fn response_upgrade() { + assert!( + ResponseData::try_from(previous::interface::ResponseData::AvailableBandwidth(42)) + .is_err() + ); + assert!( + ResponseData::try_from(previous::interface::ResponseData::TopUpBandwidth(42)).is_err() + ); + } + + #[test] + fn request_downgrade() { + assert_eq!( + previous::interface::RequestData::try_from(RequestData::AvailableBandwidth).unwrap(), + previous::interface::RequestData::AvailableBandwidth(()) + ); + assert_eq!( + previous::interface::RequestData::try_from(RequestData::TopUpBandwidth { + credential: Box::new(BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + )) + }) + .unwrap(), + previous::interface::RequestData::TopUpBandwidth(Box::new( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap() + )) + ); + } + + #[test] + fn response_downgrade() { + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::AvailableBandwidth { + amount: 42, + upgrade_mode: true + }) + .unwrap(), + previous::interface::ResponseData::AvailableBandwidth(42) + ); + assert_eq!( + previous::interface::ResponseData::try_from(ResponseData::TopUpBandwidth { + available_bandwidth: 42, + upgrade_mode: true, + }) + .unwrap(), + previous::interface::ResponseData::TopUpBandwidth(42) + ); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/mod.rs b/common/wireguard-private-metadata/shared/src/models/v2/mod.rs new file mode 100644 index 0000000000..6eea7d0713 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/mod.rs @@ -0,0 +1,197 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::fmt::Display; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use super::error::Error; +use crate::{ + impl_default_bincode_request_conversions, impl_default_bincode_response_conversions, + models::Version, +}; + +pub use available_bandwidth::{ + request::InnerAvailableBandwidthRequest as AvailableBandwidthRequest, + response::InnerAvailableBandwidthResponse as AvailableBandwidthResponse, +}; +pub use check_upgrade_mode::{ + request::{ + InnerUpgradeModeCheckRequest as UpgradeModeCheckRequest, UpgradeModeCheckRequestType, + }, + response::InnerUpgradeModeCheckResponse as UpgradeModeCheckResponse, +}; +pub use topup_bandwidth::{ + request::InnerTopUpRequest as TopUpRequest, response::InnerTopUpResponse as TopUpResponse, +}; + +pub(crate) mod available_bandwidth; +pub(crate) mod check_upgrade_mode; +pub mod interface; +pub(crate) mod topup_bandwidth; + +pub const VERSION: Version = Version::V2; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub enum QueryType { + AvailableBandwidth, + TopUpBandwidth, + UpgradeModeCheck, +} + +impl Display for QueryType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{self:?}") + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedRequest { + query_type: QueryType, + inner: Vec, +} +// Implements: +// - TryFrom<&VersionedRequest> for Request +// - TryFrom for Request +// - TryFrom<&Request> for VersionedRequest +// - TryFrom for VersionedRequest +impl_default_bincode_request_conversions!(VersionedRequest, VERSION); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct VersionedResponse { + query_type: QueryType, + inner: Vec, +} +// Implements: +// - TryFrom<&VersionedResponse> for Response +// - TryFrom for Response +// - TryFrom<&Response> for VersionedResponse +// - TryFrom for VersionedResponse +impl_default_bincode_response_conversions!(VersionedResponse, VERSION); + +#[cfg(test)] +mod tests { + + use self::{ + available_bandwidth::{ + request::InnerAvailableBandwidthRequest, response::InnerAvailableBandwidthResponse, + }, + topup_bandwidth::{request::InnerTopUpRequest, response::InnerTopUpResponse}, + }; + use crate::models::tests::CREDENTIAL_BYTES; + use crate::{Request, Response, make_bincode_serializer}; + use bincode::Options; + use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData}; + + use super::*; + + #[test] + fn mismatched_request_version() { + let version = Version::V1; + let future_bw = Request { + version, + inner: vec![], + }; + if let Err(Error::InvalidVersion { + source_version, + target_version, + }) = VersionedRequest::try_from(future_bw) + { + assert_eq!(source_version, version); + assert_eq!(target_version, VERSION); + } else { + panic!("failed"); + }; + } + + #[test] + fn mismatched_response_version() { + let version = Version::V1; + let future_bw = Response { + version, + inner: vec![], + }; + if let Err(Error::InvalidVersion { + source_version, + target_version, + }) = VersionedResponse::try_from(future_bw) + { + assert_eq!(source_version, version); + assert_eq!(target_version, VERSION); + } else { + panic!("failed"); + }; + } + + #[test] + fn serde_request_av_bw() { + let req = VersionedRequest { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthResponse { + available_bandwidth: 42, + upgrade_mode: true, + }) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_av_bw() { + let resp = VersionedResponse { + query_type: QueryType::AvailableBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerAvailableBandwidthRequest {}) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn serde_request_topup() { + let req = VersionedRequest { + query_type: QueryType::TopUpBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + }) + .unwrap(), + }; + + let ser = Request::try_from(req.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn serde_response_topup() { + let resp = VersionedResponse { + query_type: QueryType::TopUpBandwidth, + inner: make_bincode_serializer() + .serialize(&InnerTopUpResponse { + available_bandwidth: 42, + upgrade_mode: true, + }) + .unwrap(), + }; + + let ser = Response::try_from(resp.clone()).unwrap(); + assert_eq!(VERSION, ser.version); + let de = VersionedResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/mod.rs b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/mod.rs new file mode 100644 index 0000000000..10698cca2d --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod request; +pub mod response; diff --git a/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/request.rs b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/request.rs new file mode 100644 index 0000000000..a029b3920e --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/request.rs @@ -0,0 +1,61 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::BandwidthCredential; +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_request_query_conversions; + +use super::super::{QueryType, VersionedRequest}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpRequest { + /// Ecash credential + pub credential: BandwidthCredential, +} + +// Implements: +// - TryFrom<&VersionedRequest> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for VersionedRequest +// - TryFrom for VersionedRequest +// - TryFrom<&Request> for InnerTopUpRequest +// - TryFrom for InnerTopUpRequest +// - TryFrom<&InnerTopUpRequest> for Request +// - TryFrom for Request +impl_default_bincode_request_query_conversions!( + VersionedRequest, + InnerTopUpRequest, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); + +#[cfg(test)] +mod tests { + use crate::models::tests::CREDENTIAL_BYTES; + use nym_credentials_interface::CredentialSpendingData; + + use super::*; + + #[test] + fn serde() { + let req = InnerTopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + }; + let ser = VersionedRequest::try_from(req.clone()).unwrap(); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); + let de = InnerTopUpRequest::try_from(ser).unwrap(); + assert_eq!(req, de); + } + + #[test] + fn invalid_content() { + let future_req = VersionedRequest { + query_type: QueryType::TopUpBandwidth, + inner: vec![], + }; + assert!(InnerTopUpRequest::try_from(future_req).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/response.rs b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/response.rs new file mode 100644 index 0000000000..0cbf3b00d8 --- /dev/null +++ b/common/wireguard-private-metadata/shared/src/models/v2/topup_bandwidth/response.rs @@ -0,0 +1,56 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; + +use crate::impl_default_bincode_response_query_conversions; + +use super::super::{QueryType, VersionedResponse}; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct InnerTopUpResponse { + pub available_bandwidth: i64, + pub upgrade_mode: bool, +} + +// Implements: +// - TryFrom<&VersionedResponse> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for VersionedResponse +// - TryFrom for VersionedResponse +// - TryFrom<&Response> for InnerTopUpResponse +// - TryFrom for InnerTopUpResponse +// - TryFrom<&InnerTopUpResponse> for Response +// - TryFrom for Response +impl_default_bincode_response_query_conversions!( + VersionedResponse, + InnerTopUpResponse, + QueryType::TopUpBandwidth, + QueryType::TopUpBandwidth +); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn serde() { + let resp = InnerTopUpResponse { + available_bandwidth: 42, + upgrade_mode: true, + }; + let ser = VersionedResponse::try_from(resp.clone()).unwrap(); + assert_eq!(QueryType::TopUpBandwidth, ser.query_type); + let de = InnerTopUpResponse::try_from(ser).unwrap(); + assert_eq!(resp, de); + } + + #[test] + fn invalid_content() { + let future_resp = VersionedResponse { + query_type: QueryType::TopUpBandwidth, + inner: vec![], + }; + assert!(InnerTopUpResponse::try_from(future_resp).is_err()); + } +} diff --git a/common/wireguard-private-metadata/shared/src/routes.rs b/common/wireguard-private-metadata/shared/src/routes.rs index bda615fe1c..2919908e5d 100644 --- a/common/wireguard-private-metadata/shared/src/routes.rs +++ b/common/wireguard-private-metadata/shared/src/routes.rs @@ -4,7 +4,9 @@ pub const V1_API_VERSION: &str = "v1"; pub const BANDWIDTH: &str = "bandwidth"; +pub const NETWORK: &str = "network"; pub const VERSION: &str = "version"; pub const AVAILABLE: &str = "available"; pub const TOPUP: &str = "topup"; +pub const UPGRADE_MODE_CHECK: &str = "upgrade-mode-check"; diff --git a/common/wireguard-private-metadata/tests/Cargo.toml b/common/wireguard-private-metadata/tests/Cargo.toml index 6827c16f60..074d088f15 100644 --- a/common/wireguard-private-metadata/tests/Cargo.toml +++ b/common/wireguard-private-metadata/tests/Cargo.toml @@ -11,16 +11,21 @@ license.workspace = true [dependencies] async-trait = { workspace = true } axum = { workspace = true, features = ["tokio", "macros"] } +futures = { workspace = true } nym-credential-verification = { path = "../../credential-verification" } nym-credentials-interface = { path = "../../credentials-interface" } +nym-crypto = { path = "../../crypto", features = ["asymmetric"] } nym-http-api-client = { path = "../../http-api-client" } nym-http-api-common = { path = "../../http-api-common", features = [ "middleware", "utoipa", "output", ] } +nym-upgrade-mode-check = { path = "../../upgrade-mode-check" } nym-wireguard = { path = "../../wireguard" } +time = { workspace = true, features = ["macros"] } tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } +tower = { workspace = true } tower-http = { workspace = true, features = [ "cors", "trace", @@ -37,5 +42,3 @@ nym-wireguard-private-metadata-shared = { path = "../shared", features = [ ] } nym-wireguard-private-metadata-server = { path = "../server" } -[lints] -workspace = true diff --git a/common/wireguard-private-metadata/tests/src/lib.rs b/common/wireguard-private-metadata/tests/src/lib.rs index 7c8cbec81d..6f3d5ca20f 100644 --- a/common/wireguard-private-metadata/tests/src/lib.rs +++ b/common/wireguard-private-metadata/tests/src/lib.rs @@ -1,20 +1,86 @@ #[cfg(test)] mod v0; +#[cfg(test)] +mod v1; +#[cfg(test)] +mod v2; + +// TODO: we might possibly want to move it to some common crate +// so that it could be re-used by other tests (if needed) +#[cfg(test)] +pub(crate) mod mock_connect_info; #[cfg(test)] mod tests { - use std::net::SocketAddr; - + use crate::v2::peer_controller::PeerControlRequestTypeV2; + use nym_credential_verification::upgrade_mode::UpgradeModeEnableError; use nym_credential_verification::{ClientBandwidth, TicketVerifier}; - use nym_credentials_interface::CredentialSpendingData; - use nym_http_api_client::Client; - use nym_wireguard::{CONTROL_CHANNEL_SIZE, peer_controller::PeerControlRequest}; - use nym_wireguard_private_metadata_client::WireguardMetadataApiClient; - use nym_wireguard_private_metadata_server::{ - AppState, PeerControllerTransceiver, RouterBuilder, + use nym_credentials_interface::{ + AvailableBandwidth, BandwidthCredential, CredentialSpendingData, }; - use nym_wireguard_private_metadata_shared::{latest, v0, v1}; - use tokio::{net::TcpListener, sync::mpsc}; + use nym_crypto::asymmetric::ed25519; + use nym_http_api_client::HttpClientError; + use nym_upgrade_mode_check::{ + CREDENTIAL_PROXY_JWT_ISSUER, UpgradeModeAttestation, + generate_jwt_for_upgrade_mode_attestation, generate_new_attestation_with_starting_time, + }; + use nym_wireguard_private_metadata_client::WireguardMetadataApiClient; + use nym_wireguard_private_metadata_shared::{v0, v1, v2}; + use std::net::IpAddr; + use std::time::Duration; + use time::OffsetDateTime; + use time::macros::datetime; + + fn unchecked_ip>(raw: S) -> IpAddr { + raw.into().parse().unwrap() + } + + const HIGH_BANDWIDTH: i64 = 20000000000000; + + const DUMMY_JWT_ISSUER_ED25519_PRIVATE_KEY: [u8; 32] = [ + 152, 17, 144, 255, 213, 219, 246, 208, 109, 33, 100, 73, 1, 141, 32, 63, 141, 89, 167, 2, + 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32, + ]; + + pub(crate) fn dummy_jwt_issuer_public_key() -> ed25519::PublicKey { + let private_key = + ed25519::PrivateKey::from_bytes(&DUMMY_JWT_ISSUER_ED25519_PRIVATE_KEY).unwrap(); + private_key.public_key() + } + + const DUMMY_ATTESTER_ED25519_PRIVATE_KEY: [u8; 32] = [ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ]; + + pub(crate) fn dummy_attester_public_key() -> ed25519::PublicKey { + let private_key = + ed25519::PrivateKey::from_bytes(&DUMMY_ATTESTER_ED25519_PRIVATE_KEY).unwrap(); + private_key.public_key() + } + + fn high_bandwidth() -> Result { + bandwidth_response(HIGH_BANDWIDTH) + } + + fn low_bandwidth() -> Result { + bandwidth_response(0) + } + + fn bandwidth_response(amount: i64) -> Result { + Ok::<_, nym_wireguard::Error>(ClientBandwidth::new(AvailableBandwidth { + bytes: amount, + expiration: OffsetDateTime::from_unix_timestamp(2000000000).unwrap(), + })) + } + + fn mock_verifier( + bandwidth: i64, + ) -> Result, nym_wireguard::Error> { + Ok::<_, nym_wireguard::Error>( + Box::new(MockVerifier::new(bandwidth)) as Box + ) + } pub(crate) const VERIFIER_AVAILABLE_BANDWIDTH: i64 = 42; pub(crate) const CREDENTIAL_BYTES: [u8; 1245] = [ @@ -82,6 +148,52 @@ mod tests { 0, 0, 0, 0, 0, 1, ]; + pub(crate) fn mock_upgrade_mode_attestation() -> UpgradeModeAttestation { + let starting_time = datetime!(2025-10-20 12:00 UTC); + + // just some random, HARDCODED, key + let key = ed25519::PrivateKey::from_bytes(&DUMMY_ATTESTER_ED25519_PRIVATE_KEY).unwrap(); + + generate_new_attestation_with_starting_time( + &key, + vec![dummy_jwt_issuer_public_key()], + starting_time, + ) + } + + pub(crate) fn mock_different_upgrade_mode_attestation() -> UpgradeModeAttestation { + let starting_time = datetime!(2025-10-30 12:00 UTC); + + // just some random, HARDCODED, key + let key = ed25519::PrivateKey::from_bytes(&[ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ]) + .unwrap(); + + generate_new_attestation_with_starting_time( + &key, + vec![dummy_jwt_issuer_public_key()], + starting_time, + ) + } + + pub(crate) fn mock_upgrade_mode_jwt() -> String { + let jwt_key = + ed25519::PrivateKey::from_bytes(&DUMMY_JWT_ISSUER_ED25519_PRIVATE_KEY).unwrap(); + let keys = ed25519::KeyPair::from(jwt_key); + // sanity check in case hardcoded values were modified inconsistently + debug_assert_eq!(*keys.public_key(), dummy_jwt_issuer_public_key()); + + let attestation = mock_upgrade_mode_attestation(); + generate_jwt_for_upgrade_mode_attestation( + attestation, + Duration::from_secs(60 * 60), + &keys, + Some(CREDENTIAL_PROXY_JWT_ISSUER), + ) + } + pub(crate) struct MockVerifier { ret: i64, } @@ -99,56 +211,12 @@ mod tests { } } - pub(crate) async fn spawn_server_and_create_client() -> Client { - let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); - let addr = listener.local_addr().unwrap(); - let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); - let router = RouterBuilder::with_default_routes() - .with_state(AppState::new(PeerControllerTransceiver::new(request_tx))) - .router; - - tokio::spawn(async move { - loop { - match request_rx.recv().await { - Some(PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx }) => { - response_tx - .send(Ok(ClientBandwidth::new(Default::default()))) - .ok(); - } - Some(PeerControlRequest::GetVerifierByIp { - ip: _, - credential: _, - response_tx, - }) => { - response_tx - .send(Ok(Box::new(MockVerifier::new( - VERIFIER_AVAILABLE_BANDWIDTH, - )))) - .ok(); - } - None => break, - _ => panic!("Not expected"), - } - } - }); - - tokio::spawn(async move { - axum::serve( - listener, - router.into_make_service_with_connect_info::(), - ) - .await - .unwrap(); - }); - Client::new_url(addr.to_string(), None).unwrap() - } - - #[tokio::test] - async fn query_latest_version() { - let client = spawn_server_and_create_client().await; - let version = client.version().await.unwrap(); - assert_eq!(version, latest::VERSION); - } + // #[tokio::test] + // async fn query_latest_version() { + // let client = super::v2::network::test::spawn_server_and_create_client().await; + // let version = client.version().await.unwrap(); + // assert_eq!(version, latest::VERSION); + // } #[tokio::test] async fn query_against_server_v0() { @@ -158,7 +226,7 @@ mod tests { let version = client.version().await.unwrap(); assert_eq!(version, v0::VERSION); - // v0 reqwests + // v0 requests let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); let response = client.available_bandwidth(&request).await.unwrap(); v0::AvailableBandwidthResponse::try_from(response).unwrap(); @@ -167,7 +235,7 @@ mod tests { let response = client.topup_bandwidth(&request).await.unwrap(); v0::TopUpResponse::try_from(response).unwrap(); - // v1 reqwests + // v1 requests let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); assert!(client.available_bandwidth(&request).await.is_err()); @@ -177,17 +245,30 @@ mod tests { .try_into() .unwrap(); assert!(client.topup_bandwidth(&request).await.is_err()); + + // v2 requests + let request = v2::AvailableBandwidthRequest {}.try_into().unwrap(); + assert!(client.available_bandwidth(&request).await.is_err()); + + let request = v2::TopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + } + .try_into() + .unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); } #[tokio::test] async fn query_against_server_v1() { - let client = spawn_server_and_create_client().await; + let client = super::v1::network::test::spawn_server_and_create_client().await; // version check let version = client.version().await.unwrap(); assert_eq!(version, v1::VERSION); - // v0 reqwests + // v0 requests let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); let response = client.available_bandwidth(&request).await.unwrap(); v0::AvailableBandwidthResponse::try_from(response).unwrap(); @@ -195,7 +276,7 @@ mod tests { let request = v0::TopUpRequest {}.try_into().unwrap(); assert!(client.topup_bandwidth(&request).await.is_err()); - // v1 reqwests + // v1 requests let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); let response = client.available_bandwidth(&request).await.unwrap(); let available_bandwidth = v1::AvailableBandwidthResponse::try_from(response) @@ -213,5 +294,272 @@ mod tests { .unwrap() .available_bandwidth; assert_eq!(available_bandwidth, VERIFIER_AVAILABLE_BANDWIDTH); + + // v2 requests + let request = v2::AvailableBandwidthRequest {}.try_into().unwrap(); + assert!(client.available_bandwidth(&request).await.is_err()); + + let request = v2::TopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + } + .try_into() + .unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); + } + + #[tokio::test] + async fn query_against_server_v2() { + let server_test = super::v2::network::test::spawn_server_and_create_client().await; + let client = &server_test.api_client; + + // version check + let version = client.version().await.unwrap(); + assert_eq!(version, v2::VERSION); + + // =========== + // v0 requests + // =========== + let client_ip = unchecked_ip("0.0.0.1"); + server_test.set_client_ip(client_ip); + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetClientBandwidthByIp { ip: client_ip }, + bandwidth_response(0), + ) + .await; + + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetVerifierByIp { ip: client_ip }, + mock_verifier(10), + ) + .await; + + let request = v0::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + v0::AvailableBandwidthResponse::try_from(response).unwrap(); + + let request = v0::TopUpRequest {}.try_into().unwrap(); + assert!(client.topup_bandwidth(&request).await.is_err()); + server_test.reset_registered_responses().await; + + // =========== + // v1 requests + // =========== + let client_ip = unchecked_ip("1.1.1.1"); + server_test.set_client_ip(client_ip); + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetClientBandwidthByIp { ip: client_ip }, + bandwidth_response(0), + ) + .await; + + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetVerifierByIp { ip: client_ip }, + mock_verifier(100), + ) + .await; + + let request = v1::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + let available_bandwidth = v1::AvailableBandwidthResponse::try_from(response) + .unwrap() + .available_bandwidth; + assert_eq!(available_bandwidth, 0); + + let request = v1::TopUpRequest { + credential: CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + + let available_bandwidth = v1::TopUpResponse::try_from(response) + .unwrap() + .available_bandwidth; + assert_eq!(available_bandwidth, 100); + server_test.reset_registered_responses().await; + + // =========== + // v2 requests + // =========== + let client_ip = unchecked_ip("2.2.2.1"); + server_test.set_client_ip(client_ip); + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetClientBandwidthByIp { ip: client_ip }, + bandwidth_response(0), + ) + .await; + + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetVerifierByIp { ip: client_ip }, + mock_verifier(200), + ) + .await; + + let request = v2::AvailableBandwidthRequest {}.try_into().unwrap(); + let response = client.available_bandwidth(&request).await.unwrap(); + let available = v2::AvailableBandwidthResponse::try_from(response).unwrap(); + assert_eq!(available.available_bandwidth, 0); + assert!(!available.upgrade_mode); + + let request = v2::TopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + assert_eq!(top_up.available_bandwidth, 200); + assert!(!top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // upgrade mode test + let upgrade_mode_client = unchecked_ip("2.2.2.2"); + server_test.set_client_ip(upgrade_mode_client); + let good_attestation_alt = mock_different_upgrade_mode_attestation(); + let good_jwt = mock_upgrade_mode_jwt(); + + // 1. send attestation when upgrade mode is not enabled + let request = v2::TopUpRequest { + credential: BandwidthCredential::UpgradeModeJWT { + token: good_jwt.clone(), + }, + } + .try_into() + .unwrap(); + let response_err = client.topup_bandwidth(&request).await.unwrap_err(); + let HttpClientError::EndpointFailure { error, .. } = response_err else { + panic!("unexpected response") + }; + assert!(error.contains(&UpgradeModeEnableError::AttestationNotPublished.to_string())); + server_test.reset_registered_responses().await; + + // 2.1. send attestation when upgrade mode is enabled (low bandwidth) + let request_typ = PeerControlRequestTypeV2::GetClientBandwidthByIp { + ip: upgrade_mode_client, + }; + server_test + .register_peer_controller_response(request_typ, low_bandwidth()) + .await; + server_test.enable_upgrade_mode().await; + let request = v2::TopUpRequest { + credential: BandwidthCredential::UpgradeModeJWT { + token: good_jwt.clone(), + }, + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + // as defined by `DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD` + assert_eq!(top_up.available_bandwidth, 1024 * 1024 * 1024); + assert!(top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // 2.2. send attestation when upgrade mode is enabled (high bandwidth) + let request_typ = PeerControlRequestTypeV2::GetClientBandwidthByIp { + ip: upgrade_mode_client, + }; + server_test + .register_peer_controller_response(request_typ, high_bandwidth()) + .await; + server_test.enable_upgrade_mode().await; + let request = v2::TopUpRequest { + credential: BandwidthCredential::UpgradeModeJWT { + token: good_jwt.clone(), + }, + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + assert_eq!(top_up.available_bandwidth, HIGH_BANDWIDTH); + assert!(top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // 3. send bad attestation when upgrade mode is enabled + // (we don't validate it, so client is let through) + // (the only case where invalid attestation would have been rejected is when server + // is not aware of the UM, and that was meant to trigger a refresh. however, a test for that + // is out of scope for these unit tests) + server_test + .change_upgrade_mode_attestation(good_attestation_alt) + .await; + server_test + .register_peer_controller_response(request_typ, high_bandwidth()) + .await; + let request = v2::TopUpRequest { + credential: BandwidthCredential::UpgradeModeJWT { + token: good_jwt.clone(), + }, + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + assert_eq!(top_up.available_bandwidth, HIGH_BANDWIDTH); + assert!(top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // 4. send zk-nym when upgrade mode is enabled + server_test + .register_peer_controller_response(request_typ, high_bandwidth()) + .await; + server_test + .register_peer_controller_response( + PeerControlRequestTypeV2::GetVerifierByIp { + ip: upgrade_mode_client, + }, + mock_verifier(300), + ) + .await; + let request = v2::TopUpRequest { + credential: BandwidthCredential::from( + CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap(), + ), + } + .try_into() + .unwrap(); + let response = client.topup_bandwidth(&request).await.unwrap(); + let top_up = v2::TopUpResponse::try_from(response).unwrap(); + // as defined by `DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD` + assert_eq!(top_up.available_bandwidth, 1024 * 1024 * 1024); + assert!(top_up.upgrade_mode); + server_test.reset_registered_responses().await; + + // attempt to enable UM with a valid token + // no global attestation + server_test.disable_upgrade_mode().await; + let request = v2::UpgradeModeCheckRequest { + request_type: v2::UpgradeModeCheckRequestType::UpgradeModeJwt { + token: "".to_string(), + }, + } + .try_into() + .unwrap(); + let response = client.request_upgrade_mode_check(&request).await; + assert!(response.is_err()); + + server_test.publish_upgrade_mode_attestation().await; + // global attestation + let request = v2::UpgradeModeCheckRequest { + request_type: v2::UpgradeModeCheckRequestType::UpgradeModeJwt { + token: mock_upgrade_mode_jwt(), + }, + } + .try_into() + .unwrap(); + let response = client.request_upgrade_mode_check(&request).await.unwrap(); + let upgrade_mode = v2::UpgradeModeCheckResponse::try_from(response).unwrap(); + assert!(upgrade_mode.upgrade_mode); } } diff --git a/common/wireguard-private-metadata/tests/src/mock_connect_info.rs b/common/wireguard-private-metadata/tests/src/mock_connect_info.rs new file mode 100644 index 0000000000..95633d9e01 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/mock_connect_info.rs @@ -0,0 +1,121 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use async_trait::async_trait; +use axum::extract::FromRequestParts; +use axum::http::Request; +use axum::http::request::Parts; +use std::fmt::Display; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::sync::Arc; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::task::{Context, Poll}; +use tower::Layer; +use tower::Service; + +#[derive(Clone)] +pub struct DummyConnectInfo { + // store it as atomic i32 to avoid having to use locks to read and set the value + address: Arc, +} + +impl Display for DummyConnectInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.address().fmt(f) + } +} + +impl DummyConnectInfo { + pub fn new() -> Self { + let dummy_ip = Ipv4Addr::new(1, 2, 3, 4); + DummyConnectInfo { + address: Arc::new(AtomicU32::new(dummy_ip.to_bits())), + } + } + + #[allow(clippy::panic)] + pub fn set(&self, address: IpAddr) { + let IpAddr::V4(v4_address) = address else { + // it would be relatively easy to support ipv6 with multiple atomics, + // but I didn't feel it was needed at the time + panic!("ipv6 not supported") + }; + + self.address.store(v4_address.to_bits(), Ordering::Relaxed); + } + + pub fn address(&self) -> SocketAddr { + let bits = self.address.load(Ordering::Relaxed); + let ipv4 = Ipv4Addr::from(bits); + + SocketAddr::new(IpAddr::V4(ipv4), 1791) + } + + pub fn ip(&self) -> IpAddr { + self.address().ip() + } +} + +#[async_trait] +impl FromRequestParts for DummyConnectInfo +where + S: Send + Sync, +{ + type Rejection = std::convert::Infallible; + + #[allow(clippy::panic)] + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + if let Some(info) = parts.extensions.get::() { + Ok(info.clone()) + } else { + // this is a test code so that's fine + panic!("DummyConnectInfo not set") + } + } +} + +#[derive(Clone)] +pub struct MockConnectInfoLayer { + info: DummyConnectInfo, +} + +impl MockConnectInfoLayer { + pub fn new(info: DummyConnectInfo) -> Self { + Self { info } + } +} + +impl Layer for MockConnectInfoLayer { + type Service = MockConnectInfoMiddleware; + + fn layer(&self, inner: S) -> Self::Service { + MockConnectInfoMiddleware { + inner, + info: self.info.clone(), + } + } +} + +#[derive(Clone)] +pub struct MockConnectInfoMiddleware { + inner: S, + info: DummyConnectInfo, +} + +impl Service> for MockConnectInfoMiddleware +where + S: Service>, +{ + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, mut req: Request) -> Self::Future { + req.extensions_mut().insert(self.info.clone()); + self.inner.call(req) + } +} diff --git a/common/wireguard-private-metadata/tests/src/v0/app_state.rs b/common/wireguard-private-metadata/tests/src/v0/app_state.rs new file mode 100644 index 0000000000..45222bda15 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/app_state.rs @@ -0,0 +1,7 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::v1::app_state::AppStateV1; + +// there have been no changes between v0 and v1 so there's no point in redefining it +pub type AppStateV0 = AppStateV1; diff --git a/common/wireguard-private-metadata/tests/src/v0/interface.rs b/common/wireguard-private-metadata/tests/src/v0/interface.rs deleted file mode 100644 index a09ff4307a..0000000000 --- a/common/wireguard-private-metadata/tests/src/v0/interface.rs +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2025 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_wireguard_private_metadata_shared::{ - Construct, Extract, Request, Response, Version, v0 as latest, -}; - -pub enum RequestData { - AvailableBandwidth(()), - TopUpBandwidth(()), -} - -impl From for RequestData { - fn from(value: latest::interface::RequestData) -> Self { - match value { - latest::interface::RequestData::AvailableBandwidth(inner) => { - Self::AvailableBandwidth(inner) - } - latest::interface::RequestData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) - } - } - } -} - -impl From for latest::interface::RequestData { - fn from(value: RequestData) -> Self { - match value { - RequestData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), - RequestData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) - } - } - } -} - -impl From for ResponseData { - fn from(value: latest::interface::ResponseData) -> Self { - match value { - latest::interface::ResponseData::AvailableBandwidth(inner) => { - Self::AvailableBandwidth(inner) - } - latest::interface::ResponseData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) - } - } - } -} - -impl From for latest::interface::ResponseData { - fn from(value: ResponseData) -> Self { - match value { - ResponseData::AvailableBandwidth(inner) => Self::AvailableBandwidth(inner), - ResponseData::TopUpBandwidth(credential_spending_data) => { - Self::TopUpBandwidth(credential_spending_data) - } - } - } -} - -impl Construct for Request { - fn construct( - info: RequestData, - version: Version, - ) -> Result { - match version { - Version::V0 => { - let translate_info = latest::interface::RequestData::from(info); - let versioned_request = - latest::VersionedRequest::construct(translate_info, latest::VERSION)?; - Ok(versioned_request.try_into()?) - } - _ => Err( - nym_wireguard_private_metadata_shared::ModelError::DowngradeNotPossible { - from: version, - to: Version::V0, - }, - ), - } - } -} - -impl Extract for Request { - fn extract( - &self, - ) -> Result<(RequestData, Version), nym_wireguard_private_metadata_shared::ModelError> { - match self.version { - Version::V0 => { - let versioned_request = latest::VersionedRequest::try_from(self.clone())?; - let (request, version) = versioned_request.extract()?; - - Ok((request.into(), version)) - } - _ => Err( - nym_wireguard_private_metadata_shared::ModelError::UpdateNotPossible { - from: self.version, - to: Version::V0, - }, - ), - } - } -} - -pub enum ResponseData { - AvailableBandwidth(()), - TopUpBandwidth(()), -} - -impl Construct for Response { - fn construct( - info: ResponseData, - version: Version, - ) -> Result { - match version { - Version::V0 => { - let translate_response = latest::interface::ResponseData::from(info); - let versioned_response = - latest::VersionedResponse::construct(translate_response, version)?; - Ok(versioned_response.try_into()?) - } - _ => Err( - nym_wireguard_private_metadata_shared::ModelError::DowngradeNotPossible { - from: version, - to: Version::V0, - }, - ), - } - } -} - -impl Extract for Response { - fn extract( - &self, - ) -> Result<(ResponseData, Version), nym_wireguard_private_metadata_shared::ModelError> { - match self.version { - Version::V0 => { - let versioned_response = latest::VersionedResponse::try_from(self.clone())?; - let (response, version) = versioned_response.extract()?; - - Ok((response.into(), version)) - } - _ => Err( - nym_wireguard_private_metadata_shared::ModelError::UpdateNotPossible { - from: self.version, - to: Version::V0, - }, - ), - } - } -} diff --git a/common/wireguard-private-metadata/tests/src/v0/mod.rs b/common/wireguard-private-metadata/tests/src/v0/mod.rs index 7338cc5ae2..2af556497b 100644 --- a/common/wireguard-private-metadata/tests/src/v0/mod.rs +++ b/common/wireguard-private-metadata/tests/src/v0/mod.rs @@ -1,2 +1,3 @@ -pub(crate) mod interface; +pub(crate) mod app_state; pub(crate) mod network; +pub(crate) mod peer_controller; diff --git a/common/wireguard-private-metadata/tests/src/v0/network.rs b/common/wireguard-private-metadata/tests/src/v0/network.rs index b0b7361e84..e45c506b94 100644 --- a/common/wireguard-private-metadata/tests/src/v0/network.rs +++ b/common/wireguard-private-metadata/tests/src/v0/network.rs @@ -5,25 +5,24 @@ pub(crate) mod test { use std::net::SocketAddr; - use crate::{ - tests::{MockVerifier, VERIFIER_AVAILABLE_BANDWIDTH}, - v0::interface::{RequestData, ResponseData}, - }; + use crate::tests::{MockVerifier, VERIFIER_AVAILABLE_BANDWIDTH}; use axum::{Json, Router, extract::Query}; use nym_credential_verification::ClientBandwidth; use nym_http_api_client::Client; use nym_http_api_common::{FormattedResponse, OutputParams}; use nym_wireguard::{CONTROL_CHANNEL_SIZE, peer_controller::PeerControlRequest}; use nym_wireguard_private_metadata_server::PeerControllerTransceiver; + use nym_wireguard_private_metadata_shared::v0::interface::{RequestData, ResponseData}; use nym_wireguard_private_metadata_shared::{ AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, v0 as latest, }; + use tokio::sync::mpsc::Receiver; use tokio::{net::TcpListener, sync::mpsc}; use tower_http::compression::CompressionLayer; - use nym_wireguard_private_metadata_server::AppState; + use crate::v0::app_state::AppStateV0; - fn bandwidth_routes() -> Router { + fn bandwidth_routes() -> Router { Router::new() .route("/version", axum::routing::get(version)) .route("/available", axum::routing::post(available_bandwidth)) @@ -31,32 +30,11 @@ pub(crate) mod test { .layer(CompressionLayer::new()) } - #[utoipa::path( - tag = "bandwidth", - get, - path = "/v1/bandwidth/version", - responses( - (status = 200, content( - (Response = "application/bincode") - )) - ), -)] async fn version(Query(output): Query) -> AxumResult> { let output = output.output.unwrap_or_default(); Ok(output.to_response(latest::VERSION.into())) } - #[utoipa::path( - tag = "bandwidth", - post, - request_body = Request, - path = "/v1/bandwidth/available", - responses( - (status = 200, content( - (Response = "application/bincode") - )) - ), -)] async fn available_bandwidth( Query(output): Query, Json(request): Json, @@ -74,17 +52,6 @@ pub(crate) mod test { Ok(output.to_response(response)) } - #[utoipa::path( - tag = "bandwidth", - post, - request_body = Request, - path = "/v1/bandwidth/topup", - responses( - (status = 200, content( - (Response = "application/bincode") - )) - ), -)] async fn topup_bandwidth( Query(output): Query, Json(request): Json, @@ -102,35 +69,41 @@ pub(crate) mod test { Ok(output.to_response(response)) } + fn spawn_mock_peer_controller(mut request_rx: Receiver) { + tokio::spawn(async move { + while let Some(request) = request_rx.recv().await { + match request { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Ok(Box::new(MockVerifier::new( + VERIFIER_AVAILABLE_BANDWIDTH, + )))) + .ok(); + } + _ => panic!("Not expected"), + } + } + }); + } + pub(crate) async fn spawn_server_and_create_client() -> Client { let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); - let (request_tx, mut request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let (request_tx, request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); let router = Router::new() .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())) - .with_state(AppState::new(PeerControllerTransceiver::new(request_tx))); + .with_state(AppStateV0::new(PeerControllerTransceiver::new(request_tx))); - tokio::spawn(async move { - match request_rx.recv().await.unwrap() { - PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { - response_tx - .send(Ok(ClientBandwidth::new(Default::default()))) - .ok(); - } - PeerControlRequest::GetVerifierByIp { - ip: _, - credential: _, - response_tx, - } => { - response_tx - .send(Ok(Box::new(MockVerifier::new( - VERIFIER_AVAILABLE_BANDWIDTH, - )))) - .ok(); - } - _ => panic!("Not expected"), - } - }); + spawn_mock_peer_controller(request_rx); tokio::spawn(async move { axum::serve( diff --git a/common/wireguard-private-metadata/tests/src/v0/peer_controller.rs b/common/wireguard-private-metadata/tests/src/v0/peer_controller.rs new file mode 100644 index 0000000000..b1f80ad87c --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v0/peer_controller.rs @@ -0,0 +1,9 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![allow(dead_code)] + +use crate::v2::peer_controller::{MockPeerControllerStateV2, MockPeerControllerV2}; + +pub type MockPeerControllerStateV0 = MockPeerControllerStateV2; +pub type MockPeerControllerV0 = MockPeerControllerV2; diff --git a/common/wireguard-private-metadata/tests/src/v1/app_state.rs b/common/wireguard-private-metadata/tests/src/v1/app_state.rs new file mode 100644 index 0000000000..f773165819 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v1/app_state.rs @@ -0,0 +1,33 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard_private_metadata_server::PeerControllerTransceiver; +use nym_wireguard_private_metadata_shared::error::MetadataError; +use std::net::IpAddr; + +#[derive(Clone, axum::extract::FromRef)] +pub struct AppStateV1 { + transceiver: PeerControllerTransceiver, +} + +impl AppStateV1 { + pub fn new(transceiver: PeerControllerTransceiver) -> Self { + Self { transceiver } + } + + pub async fn available_bandwidth(&self, ip: IpAddr) -> Result { + self.transceiver.query_bandwidth(ip).await + } + + // Top up with a credential and return the afterwards available bandwidth + pub async fn topup_bandwidth( + &self, + ip: IpAddr, + credential: CredentialSpendingData, + ) -> Result { + self.transceiver + .topup_bandwidth(ip, Box::new(credential)) + .await + } +} diff --git a/common/wireguard-private-metadata/tests/src/v1/mod.rs b/common/wireguard-private-metadata/tests/src/v1/mod.rs new file mode 100644 index 0000000000..3cf9729866 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v1/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod app_state; +pub(crate) mod network; +pub(crate) mod peer_controller; diff --git a/common/wireguard-private-metadata/tests/src/v1/network.rs b/common/wireguard-private-metadata/tests/src/v1/network.rs new file mode 100644 index 0000000000..07b23fccbc --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v1/network.rs @@ -0,0 +1,134 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub(crate) mod test { + use std::net::SocketAddr; + + use crate::tests::{MockVerifier, VERIFIER_AVAILABLE_BANDWIDTH}; + use crate::v1::app_state::AppStateV1; + use axum::extract::{ConnectInfo, State}; + use axum::{Json, Router, extract::Query}; + use nym_credential_verification::ClientBandwidth; + use nym_http_api_client::Client; + use nym_http_api_common::{FormattedResponse, OutputParams}; + use nym_wireguard::{CONTROL_CHANNEL_SIZE, peer_controller::PeerControlRequest}; + use nym_wireguard_private_metadata_server::PeerControllerTransceiver; + use nym_wireguard_private_metadata_shared::v1::interface::{RequestData, ResponseData}; + use nym_wireguard_private_metadata_shared::{ + AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, v1 as latest, + }; + use tokio::sync::mpsc::Receiver; + use tokio::{net::TcpListener, sync::mpsc}; + use tower_http::compression::CompressionLayer; + + fn bandwidth_routes() -> Router { + Router::new() + .route("/version", axum::routing::get(version)) + .route("/available", axum::routing::post(available_bandwidth)) + .route("/topup", axum::routing::post(topup_bandwidth)) + .layer(CompressionLayer::new()) + } + + async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) + } + + async fn available_bandwidth( + ConnectInfo(addr): ConnectInfo, + Query(output): Query, + State(state): State, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::AvailableBandwidth(_), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth = state + .available_bandwidth(addr.ip()) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct( + ResponseData::AvailableBandwidth(available_bandwidth), + version, + ) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + async fn topup_bandwidth( + ConnectInfo(addr): ConnectInfo, + Query(output): Query, + State(state): State, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::TopUpBandwidth(credential), version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth = state + .topup_bandwidth(addr.ip(), *credential) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = + Response::construct(ResponseData::TopUpBandwidth(available_bandwidth), version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + fn spawn_mock_peer_controller(mut request_rx: Receiver) { + tokio::spawn(async move { + while let Some(request) = request_rx.recv().await { + match request { + PeerControlRequest::GetClientBandwidthByIp { ip: _, response_tx } => { + response_tx + .send(Ok(ClientBandwidth::new(Default::default()))) + .ok(); + } + PeerControlRequest::GetVerifierByIp { + ip: _, + credential: _, + response_tx, + } => { + response_tx + .send(Ok(Box::new(MockVerifier::new( + VERIFIER_AVAILABLE_BANDWIDTH, + )))) + .ok(); + } + _ => panic!("Not expected"), + } + } + }); + } + + pub(crate) async fn spawn_server_and_create_client() -> Client { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let (request_tx, request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + let router = Router::new() + .nest("/v1", Router::new().nest("/bandwidth", bandwidth_routes())) + .with_state(AppStateV1::new(PeerControllerTransceiver::new(request_tx))); + + spawn_mock_peer_controller(request_rx); + + tokio::spawn(async move { + axum::serve( + listener, + router.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); + }); + Client::new_url(addr.to_string(), None).unwrap() + } +} diff --git a/common/wireguard-private-metadata/tests/src/v1/peer_controller.rs b/common/wireguard-private-metadata/tests/src/v1/peer_controller.rs new file mode 100644 index 0000000000..7291d5c5b3 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v1/peer_controller.rs @@ -0,0 +1,9 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#![allow(dead_code)] + +use crate::v2::peer_controller::{MockPeerControllerStateV2, MockPeerControllerV2}; + +pub type MockPeerControllerStateV1 = MockPeerControllerStateV2; +pub type MockPeerControllerV1 = MockPeerControllerV2; diff --git a/common/wireguard-private-metadata/tests/src/v2/app_state.rs b/common/wireguard-private-metadata/tests/src/v2/app_state.rs new file mode 100644 index 0000000000..3ebf886df1 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v2/app_state.rs @@ -0,0 +1,8 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_wireguard_private_metadata_server::AppState; + +// given latest is v2, we just create a type alias +// for any future versions, this would have to be adjusted +pub type AppStateV2 = AppState; diff --git a/common/wireguard-private-metadata/tests/src/v2/mod.rs b/common/wireguard-private-metadata/tests/src/v2/mod.rs new file mode 100644 index 0000000000..3cf9729866 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v2/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod app_state; +pub(crate) mod network; +pub(crate) mod peer_controller; diff --git a/common/wireguard-private-metadata/tests/src/v2/network.rs b/common/wireguard-private-metadata/tests/src/v2/network.rs new file mode 100644 index 0000000000..fd2520e8e5 --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v2/network.rs @@ -0,0 +1,329 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +pub(crate) mod test { + use crate::mock_connect_info::{DummyConnectInfo, MockConnectInfoLayer}; + use crate::tests::{dummy_attester_public_key, mock_upgrade_mode_attestation}; + use crate::v2::app_state::AppStateV2; + use crate::v2::peer_controller::{ + MockPeerControllerStateV2, MockPeerControllerV2, PeerControlRequestTypeV2, + }; + use axum::extract::State; + use axum::{Extension, Json, Router, extract::Query}; + use futures::StreamExt; + use nym_credential_verification::upgrade_mode::{ + CheckRequest, UpgradeModeCheckConfig, UpgradeModeCheckRequestReceiver, + UpgradeModeCheckRequestSender, UpgradeModeDetails, UpgradeModeState, + }; + use nym_http_api_client::Client; + use nym_http_api_common::{FormattedResponse, OutputParams}; + use nym_upgrade_mode_check::UpgradeModeAttestation; + use nym_wireguard::CONTROL_CHANNEL_SIZE; + use nym_wireguard_private_metadata_server::AppState; + use nym_wireguard_private_metadata_server::PeerControllerTransceiver; + use nym_wireguard_private_metadata_shared::interface::RequestData; + use nym_wireguard_private_metadata_shared::{ + AxumErrorResponse, AxumResult, Construct, Extract, Request, Response, v2 as latest, + }; + use std::any::Any; + use std::net::IpAddr; + use std::sync::Arc; + use std::time::Duration; + use tokio::sync::Mutex; + use tokio::task::JoinSet; + use tokio::{net::TcpListener, sync::mpsc}; + use tower_http::compression::CompressionLayer; + + pub struct MockUpgradeModeWatcher { + check_request_receiver: UpgradeModeCheckRequestReceiver, + upgrade_mode_state: UpgradeModeState, + + mock_published_attestation: Arc>>, + } + + impl MockUpgradeModeWatcher { + pub fn new( + check_request_receiver: UpgradeModeCheckRequestReceiver, + upgrade_mode_state: UpgradeModeState, + mock_published_attestation: Arc>>, + ) -> Self { + MockUpgradeModeWatcher { + check_request_receiver, + upgrade_mode_state, + mock_published_attestation, + } + } + + async fn handle_check_request(&mut self, polled_request: CheckRequest) { + let mut requests = vec![polled_request]; + while let Ok(Some(queued_up)) = self.check_request_receiver.try_next() { + requests.push(queued_up); + } + + let published = self.mock_published_attestation.lock().await; + self.upgrade_mode_state + .try_set_expected_attestation(published.clone()) + .await; + + for request in requests { + request.finalize() + } + } + + pub async fn run(&mut self) { + // for now don't do anything apart from notifying the caller + while let Some(polled_request) = self.check_request_receiver.next().await { + self.handle_check_request(polled_request).await + } + } + } + + pub struct ServerTest { + // among other things gives you access to the shared state, so you could toggle the flag + // and thus change server behaviour + upgrade_mode_state: UpgradeModeState, + + // shared state with the mock attestation watcher to make it think new attestation has been published + mock_published_attestation: Arc>>, + + connect_info: DummyConnectInfo, + + // handles to the following tasks: + // - the actual axum server + // - dummy attestation watcher + // - dummy peer controller + _server_tasks: JoinSet<()>, + + peer_controller_state: MockPeerControllerStateV2, + + pub(crate) api_client: Client, + } + + impl ServerTest { + pub(crate) async fn new() -> Self { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + let (request_tx, request_rx) = mpsc::channel(CONTROL_CHANNEL_SIZE); + + let (um_recheck_tx, um_recheck_rx) = futures::channel::mpsc::unbounded(); + let upgrade_mode_state = UpgradeModeState::new(dummy_attester_public_key()); + let upgrade_mode_details = UpgradeModeDetails::new( + UpgradeModeCheckConfig { + // essentially we never want to trigger this in our tests + min_staleness_recheck: Duration::from_nanos(1), + }, + UpgradeModeCheckRequestSender::new(um_recheck_tx), + upgrade_mode_state.clone(), + ); + + let dummy_connect_info = DummyConnectInfo::new(); + + let router = Router::new() + .nest( + "/v1", + Router::new() + .nest("/bandwidth", bandwidth_routes()) + .nest("/network", network_routes()), + ) + .with_state(AppStateV2::new( + PeerControllerTransceiver::new(request_tx), + upgrade_mode_details, + )); + + // register responses for expected requests + let peer_controller_state = MockPeerControllerStateV2::default(); + let mut server_tasks = JoinSet::new(); + + let mut peer_controller = + MockPeerControllerV2::new(peer_controller_state.clone(), request_rx); + + let mock_published_attestation = Arc::new(Mutex::new(None)); + let mut upgrade_mode_watcher = MockUpgradeModeWatcher::new( + um_recheck_rx, + upgrade_mode_state.clone(), + mock_published_attestation.clone(), + ); + + // spawn all the tasks + server_tasks.spawn(async move { + peer_controller.run().await; + }); + server_tasks.spawn(async move { + upgrade_mode_watcher.run().await; + }); + + let connect_info = dummy_connect_info.clone(); + server_tasks.spawn(async move { + axum::serve( + listener, + // router.into_make_service_with_connect_info::(), + router.layer(MockConnectInfoLayer::new(connect_info)), + ) + .await + .unwrap(); + }); + let api_client = Client::new_url(addr.to_string(), None).unwrap(); + + ServerTest { + upgrade_mode_state, + mock_published_attestation, + connect_info: dummy_connect_info, + _server_tasks: server_tasks, + peer_controller_state, + api_client, + } + } + + pub(crate) async fn enable_upgrade_mode(&self) { + self.change_upgrade_mode_attestation(mock_upgrade_mode_attestation()) + .await + } + + pub(crate) async fn change_upgrade_mode_attestation( + &self, + attestation: UpgradeModeAttestation, + ) { + self.upgrade_mode_state + .try_set_expected_attestation(Some(attestation)) + .await + } + + pub(crate) async fn publish_upgrade_mode_attestation(&self) { + *self.mock_published_attestation.lock().await = Some(mock_upgrade_mode_attestation()) + } + + #[allow(dead_code)] + pub(crate) async fn disable_upgrade_mode(&self) { + self.upgrade_mode_state + .try_set_expected_attestation(None) + .await; + } + + pub(crate) fn set_client_ip(&self, ip: IpAddr) { + self.connect_info.set(ip) + } + + #[allow(dead_code)] + pub(crate) fn client_ip(&self) -> IpAddr { + self.connect_info.ip() + } + + // note: it's caller's responsibility to make sure the response type is correct! + pub(crate) async fn register_peer_controller_response( + &self, + request: PeerControlRequestTypeV2, + response: impl Any + Send + Sync + 'static, + ) { + self.peer_controller_state + .register_response(request, response) + .await + } + + pub(crate) async fn reset_registered_responses(&self) { + self.peer_controller_state + .clear_registered_responses() + .await + } + } + + fn bandwidth_routes() -> Router { + Router::new() + .route("/version", axum::routing::get(version)) + .route("/available", axum::routing::post(available_bandwidth)) + .route("/topup", axum::routing::post(topup_bandwidth)) + .layer(CompressionLayer::new()) + } + + fn network_routes() -> Router { + Router::new() + .route( + "/upgrade-mode-check", + axum::routing::post(upgrade_mode_check), + ) + .layer(CompressionLayer::new()) + } + + async fn version(Query(output): Query) -> AxumResult> { + let output = output.output.unwrap_or_default(); + Ok(output.to_response(latest::VERSION.into())) + } + + async fn available_bandwidth( + // ❗ \/ DIFFERENT FROM ACTUAL SERVER \/ ❗ + // we use different ConnectInfo to be able to mock different ip addresses + Extension(addr): Extension, + // ❗ /\ DIFFERENT FROM ACTUAL SERVER /\ ❗ + Query(output): Query, + State(state): State, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::AvailableBandwidth, version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let available_bandwidth_response = state + .available_bandwidth(addr.ip()) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(available_bandwidth_response, version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + async fn topup_bandwidth( + // ❗ \/ DIFFERENT FROM ACTUAL SERVER \/ ❗ + // we use different ConnectInfo to be able to mock different ip addresses + Extension(addr): Extension, + // ❗ /\ DIFFERENT FROM ACTUAL SERVER /\ ❗ + Query(output): Query, + State(state): State, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::TopUpBandwidth { credential }, version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let top_up_bandwidth_response = state + .topup_bandwidth(addr.ip(), credential) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(top_up_bandwidth_response, version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + async fn upgrade_mode_check( + Query(output): Query, + State(state): State, + Json(request): Json, + ) -> AxumResult> { + let output = output.output.unwrap_or_default(); + + let (RequestData::UpgradeModeCheck { typ }, version) = + request.extract().map_err(AxumErrorResponse::bad_request)? + else { + return Err(AxumErrorResponse::bad_request("incorrect request type")); + }; + let upgrade_mode_check_response = state + .upgrade_mode_check(typ) + .await + .map_err(AxumErrorResponse::bad_request)?; + let response = Response::construct(upgrade_mode_check_response, version) + .map_err(AxumErrorResponse::bad_request)?; + + Ok(output.to_response(response)) + } + + pub(crate) async fn spawn_server_and_create_client() -> ServerTest { + ServerTest::new().await + } +} diff --git a/common/wireguard-private-metadata/tests/src/v2/peer_controller.rs b/common/wireguard-private-metadata/tests/src/v2/peer_controller.rs new file mode 100644 index 0000000000..435359efac --- /dev/null +++ b/common/wireguard-private-metadata/tests/src/v2/peer_controller.rs @@ -0,0 +1,177 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// not declared as a 'global' since I can imagine it might change between versions + +use nym_wireguard::peer_controller::PeerControlRequest; +use std::any::Any; +use std::collections::{HashMap, VecDeque}; +use std::net::IpAddr; +use std::sync::Arc; +use tokio::sync::RwLock; +use tokio::sync::mpsc::Receiver; + +#[derive(Hash, PartialOrd, PartialEq, Clone, Debug, Eq, Copy)] +pub enum PeerControlRequestTypeV2 { + AddPeer, + RemovePeer, + QueryPeer, + GetClientBandwidthByKey, + GetClientBandwidthByIp { ip: IpAddr }, + GetVerifierByKey, + GetVerifierByIp { ip: IpAddr }, +} + +impl From<&PeerControlRequest> for PeerControlRequestTypeV2 { + fn from(req: &PeerControlRequest) -> Self { + match req { + PeerControlRequest::AddPeer { .. } => PeerControlRequestTypeV2::AddPeer, + PeerControlRequest::RemovePeer { .. } => PeerControlRequestTypeV2::RemovePeer, + PeerControlRequest::QueryPeer { .. } => PeerControlRequestTypeV2::QueryPeer, + PeerControlRequest::GetClientBandwidthByKey { .. } => { + PeerControlRequestTypeV2::GetClientBandwidthByKey + } + PeerControlRequest::GetClientBandwidthByIp { ip, .. } => { + PeerControlRequestTypeV2::GetClientBandwidthByIp { ip: *ip } + } + PeerControlRequest::GetVerifierByKey { .. } => { + PeerControlRequestTypeV2::GetVerifierByKey + } + PeerControlRequest::GetVerifierByIp { ip, .. } => { + PeerControlRequestTypeV2::GetVerifierByIp { ip: *ip } + } + } + } +} + +// all responses are registered as a queue for particular type +// (this is because the actual type can't be cloned as the `Error` does not implement Clone) +type RegisteredResponses = + HashMap>>; + +#[derive(Clone, Default)] +pub struct MockPeerControllerStateV2 { + registered_responses: Arc>, +} + +impl MockPeerControllerStateV2 { + pub async fn register_response( + &self, + request: PeerControlRequestTypeV2, + response: impl Any + Send + Sync + 'static, + ) { + self.registered_responses + .write() + .await + .entry(request) + .or_default() + .push_back(Box::new(response)); + } + + pub async fn clear_registered_responses(&self) { + self.registered_responses.write().await.clear(); + } +} + +pub struct MockPeerControllerV2 { + state: MockPeerControllerStateV2, + request_rx: Receiver, +} + +impl MockPeerControllerV2 { + pub(crate) fn new( + state: MockPeerControllerStateV2, + request_rx: Receiver, + ) -> Self { + MockPeerControllerV2 { state, request_rx } + } + + async fn handle_request(&mut self, request: PeerControlRequest) { + let typ = PeerControlRequestTypeV2::from(&request); + + let mut guard = self.state.registered_responses.write().await; + let Some(registered_responses) = guard.get_mut(&typ) else { + panic!( + "received a request for {typ:?} but there are no registered responses - this is probably due to a bug in your test setup" + ); + }; + + let Some(response) = registered_responses.pop_front() else { + panic!( + "received a request for {typ:?} but there are no registered responses - this is probably due to a bug in your test setup" + ); + }; + + match request { + PeerControlRequest::AddPeer { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::RemovePeer { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::QueryPeer { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::GetClientBandwidthByKey { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::GetClientBandwidthByIp { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .unwrap(); + } + PeerControlRequest::GetVerifierByKey { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .ok(); + } + PeerControlRequest::GetVerifierByIp { response_tx, .. } => { + response_tx + .send( + *response + .downcast() + .expect("registered response has mismatched type"), + ) + .ok(); + } + } + } + + pub(crate) async fn run(&mut self) { + while let Some(request) = self.request_rx.recv().await { + self.handle_request(request).await; + } + } +} diff --git a/common/wireguard-types/Cargo.toml b/common/wireguard-types/Cargo.toml index e1ffda761c..9a8a783db0 100644 --- a/common/wireguard-types/Cargo.toml +++ b/common/wireguard-types/Cargo.toml @@ -12,14 +12,11 @@ license.workspace = true [dependencies] base64 = { workspace = true } -log = { workspace = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } -nym-config = { path = "../config" } -nym-network-defaults = { path = "../network-defaults" } - x25519-dalek = { workspace = true, features = ["static_secrets"] } +nym-crypto = { path = "../crypto", features = ["asymmetric"] } [dev-dependencies] rand = { workspace = true } diff --git a/common/wireguard-types/src/public_key.rs b/common/wireguard-types/src/public_key.rs index 3b3bbd60d1..755c60d628 100644 --- a/common/wireguard-types/src/public_key.rs +++ b/common/wireguard-types/src/public_key.rs @@ -9,11 +9,24 @@ use std::fmt; use std::ops::Deref; use std::str::FromStr; +use nym_crypto::asymmetric::x25519; use x25519_dalek::PublicKey; #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] pub struct PeerPublicKey(PublicKey); +impl From for PeerPublicKey { + fn from(pk: x25519::PublicKey) -> Self { + PeerPublicKey(pk.into()) + } +} + +impl From<&x25519::PublicKey> for PeerPublicKey { + fn from(pk: &x25519::PublicKey) -> Self { + (*pk).into() + } +} + impl PeerPublicKey { pub fn new(key: PublicKey) -> Self { PeerPublicKey(key) diff --git a/common/wireguard/Cargo.toml b/common/wireguard/Cargo.toml index e98e5fc27d..f2a773d4ec 100644 --- a/common/wireguard/Cargo.toml +++ b/common/wireguard/Cargo.toml @@ -11,27 +11,15 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -async-trait = { workspace = true } base64 = { workspace = true } -bincode = { workspace = true } -chrono = { workspace = true } -dashmap = { workspace = true } defguard_wireguard_rs = { workspace = true } -dyn-clone = { workspace = true } futures = { workspace = true } -# The latest version on crates.io at the time of writing this (6.0.0) has a -# version mismatch with x25519-dalek/curve25519-dalek that is resolved in the -# latest commit. So pick that for now. -x25519-dalek = { workspace = true } ip_network = { workspace = true } -log.workspace = true thiserror = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "net", "io-util"] } tokio-stream = { workspace = true } -time = { workspace = true } tracing = { workspace = true } -nym-authenticator-requests = { path = "../authenticator-requests" } nym-credentials-interface = { path = "../credentials-interface" } nym-credential-verification = { path = "../credential-verification" } nym-crypto = { path = "../crypto", features = ["asymmetric"] } @@ -48,3 +36,6 @@ nym-gateway-storage = { path = "../gateway-storage", features = ["mock"] } [features] default = [] mock = ["nym-gateway-storage/mock"] + +[lints] +workspace = true diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index 6b2c632d22..cf7ff7f32f 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -7,13 +7,15 @@ // #![warn(clippy::unwrap_used)] use defguard_wireguard_rs::{WGApi, WireguardInterfaceApi, host::Peer, key::Key, net::IpAddrMask}; -#[cfg(target_os = "linux")] -use nym_credential_verification::ecash::EcashManager; use nym_crypto::asymmetric::x25519::KeyPair; use nym_wireguard_types::Config; use peer_controller::PeerControlRequest; use std::sync::Arc; use tokio::sync::mpsc::{self, Receiver, Sender}; +use tracing::error; + +#[cfg(target_os = "linux")] +use nym_credential_verification::ecash::EcashManager; #[cfg(target_os = "linux")] use nym_network_defaults::constants::WG_TUN_BASE_NAME; @@ -23,6 +25,8 @@ pub mod peer_controller; pub mod peer_handle; pub mod peer_storage_manager; +pub use error::Error; + pub const CONTROL_CHANNEL_SIZE: usize = 256; pub struct WgApiWrapper { @@ -114,7 +118,7 @@ impl Drop for WgApiWrapper { fn drop(&mut self) { if let Err(e) = defguard_wireguard_rs::WireguardInterfaceApi::remove_interface(&self.inner) { - log::error!("Could not remove the wireguard interface: {e:?}"); + error!("Could not remove the wireguard interface: {e:?}"); } } } @@ -163,6 +167,7 @@ pub async fn start_wireguard( ecash_manager: Arc, metrics: nym_node_metrics::NymNodeMetrics, peers: Vec, + upgrade_mode_status: nym_credential_verification::upgrade_mode::UpgradeModeStatus, shutdown_token: nym_task::ShutdownToken, wireguard_data: WireguardData, ) -> Result, Box> { @@ -250,6 +255,7 @@ pub async fn start_wireguard( peer_bandwidth_managers, wireguard_data.inner.peer_tx.clone(), wireguard_data.peer_rx, + upgrade_mode_status, shutdown_token, ); tokio::spawn(async move { controller.run().await }); diff --git a/common/wireguard/src/peer_controller.rs b/common/wireguard/src/peer_controller.rs index e9e4f78f2d..54b208c5af 100644 --- a/common/wireguard/src/peer_controller.rs +++ b/common/wireguard/src/peer_controller.rs @@ -1,13 +1,18 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::{ + error::{Error, Result}, + peer_handle::SharedBandwidthStorageManager, +}; +use crate::{peer_handle::PeerHandle, peer_storage_manager::CachedPeerManager}; use defguard_wireguard_rs::{ WireguardInterfaceApi, host::{Host, Peer}, key::Key, }; use futures::channel::oneshot; -use log::info; +use nym_credential_verification::upgrade_mode::UpgradeModeStatus; use nym_credential_verification::{ BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier, TicketVerifier, bandwidth_storage_manager::BandwidthStorageManager, ecash::traits::EcashManager, @@ -24,12 +29,7 @@ use std::{ }; use tokio::sync::{RwLock, mpsc}; use tokio_stream::{StreamExt, wrappers::IntervalStream}; - -use crate::{ - error::{Error, Result}, - peer_handle::SharedBandwidthStorageManager, -}; -use crate::{peer_handle::PeerHandle, peer_storage_manager::CachedPeerManager}; +use tracing::{debug, error, info, trace}; pub enum PeerControlRequest { AddPeer { @@ -84,6 +84,10 @@ pub struct PeerController { host_information: Arc>, bw_storage_managers: HashMap, timeout_check_interval: IntervalStream, + + /// Flag indicating whether the system is undergoing an upgrade and thus peers shouldn't be getting + /// their bandwidth metered. + upgrade_mode: UpgradeModeStatus, shutdown_token: nym_task::ShutdownToken, } @@ -97,6 +101,7 @@ impl PeerController { bw_storage_managers: HashMap, request_tx: mpsc::Sender, request_rx: mpsc::Receiver, + upgrade_mode: UpgradeModeStatus, shutdown_token: nym_task::ShutdownToken, ) -> Self { let timeout_check_interval = @@ -110,12 +115,13 @@ impl PeerController { cached_peer_manager, bandwidth_storage_manager.clone(), request_tx.clone(), + upgrade_mode.clone(), &shutdown_token, ); let public_key = public_key.clone(); tokio::spawn(async move { handle.run().await; - log::debug!("Peer handle shut down for {public_key}"); + debug!("Peer handle shut down for {public_key}"); }); } let bw_storage_managers = bw_storage_managers @@ -131,6 +137,7 @@ impl PeerController { request_tx, request_rx, timeout_check_interval, + upgrade_mode, shutdown_token, metrics, } @@ -145,7 +152,7 @@ impl PeerController { self.bw_storage_managers.remove(key); let ret = self.wg_api.remove_peer(key); if ret.is_err() { - log::error!( + error!( "Wireguard peer could not be removed from wireguard kernel module. Process should be restarted so that the interface is reset." ); } @@ -192,6 +199,7 @@ impl PeerController { cached_peer_manager, bandwidth_storage_manager.clone(), self.request_tx.clone(), + self.upgrade_mode.clone(), &self.shutdown_token, ); self.bw_storage_managers @@ -203,7 +211,7 @@ impl PeerController { let public_key = peer.public_key.clone(); tokio::spawn(async move { handle.run().await; - log::debug!("Peer handle shut down for {public_key}"); + debug!("Peer handle shut down for {public_key}"); }); Ok(()) } @@ -357,6 +365,7 @@ impl PeerController { } } + #[allow(clippy::expect_used)] self.metrics.wireguard.update( // if the conversion fails it means we're running not running on a 64bit system // and that's a reason enough for this failure. @@ -377,7 +386,7 @@ impl PeerController { tokio::select! { _ = self.timeout_check_interval.next() => { let Ok(host) = self.wg_api.read_interface_data() else { - log::error!("Can't read wireguard kernel data"); + error!("Can't read wireguard kernel data"); continue; }; self.update_metrics(&host).await; @@ -385,7 +394,7 @@ impl PeerController { *self.host_information.write().await = host; } _ = self.shutdown_token.cancelled() => { - log::trace!("PeerController handler: Received shutdown"); + trace!("PeerController handler: Received shutdown"); break; } msg = self.request_rx.recv() => { @@ -412,7 +421,7 @@ impl PeerController { response_tx.send(self.handle_query_verifier_by_ip(ip, *credential).await).ok(); } None => { - log::trace!("PeerController [main loop]: stopping since channel closed"); + trace!("PeerController [main loop]: stopping since channel closed"); break; } } @@ -429,6 +438,9 @@ struct MockWgApi { } #[cfg(feature = "mock")] +// unwraps, etc. are fine in test code +#[allow(clippy::unwrap_used)] +#[allow(clippy::todo)] impl WireguardInterfaceApi for MockWgApi { fn create_interface( &self, @@ -534,6 +546,7 @@ pub fn start_controller( Default::default(), request_tx, request_rx, + UpgradeModeStatus::default(), shutdown_manager.child_shutdown_token(), ); tokio::spawn(async move { peer_controller.run().await }); @@ -542,12 +555,15 @@ pub fn start_controller( } #[cfg(feature = "mock")] +// unwraps are fine in test code +#[allow(clippy::unwrap_used)] pub async fn stop_controller(mut shutdown_manager: nym_task::ShutdownManager) { shutdown_manager.send_cancellation(); shutdown_manager.run_until_shutdown().await; } #[cfg(test)] +#[cfg(feature = "mock")] mod tests { use super::*; diff --git a/common/wireguard/src/peer_handle.rs b/common/wireguard/src/peer_handle.rs index 57c92d5072..b5dd5d83fc 100644 --- a/common/wireguard/src/peer_handle.rs +++ b/common/wireguard/src/peer_handle.rs @@ -6,12 +6,16 @@ use crate::peer_controller::PeerControlRequest; use crate::peer_storage_manager::{CachedPeerManager, PeerInformation}; use defguard_wireguard_rs::{host::Host, key::Key, net::IpAddrMask}; use futures::channel::oneshot; +use nym_credential_verification::OutOfBandwidthResultExt; use nym_credential_verification::bandwidth_storage_manager::BandwidthStorageManager; +use nym_credential_verification::upgrade_mode::UpgradeModeStatus; use nym_task::ShutdownToken; use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK; +use std::fmt::Display; use std::sync::Arc; use tokio::sync::{RwLock, mpsc}; use tokio_stream::{StreamExt, wrappers::IntervalStream}; +use tracing::{debug, error, trace, warn}; #[derive(Clone)] pub(crate) struct SharedBandwidthStorageManager { @@ -43,9 +47,19 @@ pub struct PeerHandle { bandwidth_storage_manager: SharedBandwidthStorageManager, request_tx: mpsc::Sender, timeout_check_interval: IntervalStream, + + /// Flag indicating whether the system is undergoing an upgrade and thus peers shouldn't be getting + /// their bandwidth metered. + upgrade_mode: UpgradeModeStatus, shutdown_token: ShutdownToken, } +impl Display for PeerHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "peer {}", self.public_key) + } +} + impl PeerHandle { pub(crate) fn new( public_key: Key, @@ -53,11 +67,11 @@ impl PeerHandle { cached_peer: CachedPeerManager, bandwidth_storage_manager: SharedBandwidthStorageManager, request_tx: mpsc::Sender, + upgrade_mode: UpgradeModeStatus, shutdown_token: &ShutdownToken, ) -> Self { - let timeout_check_interval = tokio_stream::wrappers::IntervalStream::new( - tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK), - ); + let timeout_check_interval = + IntervalStream::new(tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK)); let shutdown_token = shutdown_token.clone(); PeerHandle { public_key, @@ -66,10 +80,22 @@ impl PeerHandle { bandwidth_storage_manager, request_tx, timeout_check_interval, + upgrade_mode, shutdown_token, } } + /// Attempt to use the specified amount of bandwidth and update internal cache. + /// Returns the amount of bandwidth remaining + async fn try_use_bandwidth(&self, spent: i64) -> nym_credential_verification::Result { + self.bandwidth_storage_manager + .inner + .write() + .await + .try_use_bandwidth(spent) + .await + } + async fn remove_peer(&self) -> Result { let (response_tx, response_rx) = oneshot::channel(); self.request_tx @@ -87,73 +113,33 @@ impl PeerHandle { Ok(success) } - fn compute_spent_bandwidth( - kernel_peer: PeerInformation, - cached_peer: PeerInformation, - ) -> Option { - let kernel_total = kernel_peer - .rx_bytes - .checked_add(kernel_peer.tx_bytes) - .or_else(|| { - tracing::error!( - "Overflow on kernel adding bytes: {} + {}", - kernel_peer.rx_bytes, - kernel_peer.tx_bytes - ); - None - })?; - let cached_total = cached_peer - .rx_bytes - .checked_add(cached_peer.tx_bytes) - .or_else(|| { - tracing::error!( - "Overflow on cached adding bytes: {} + {}", - cached_peer.rx_bytes, - cached_peer.tx_bytes - ); - None - })?; - - kernel_total.checked_sub(cached_total).or_else(|| { - tracing::error!("Overflow on spent bandwidth subtraction: kernel - cached = {kernel_total} - {cached_total}"); - None - }) - } - async fn active_peer(&mut self, kernel_peer: PeerInformation) -> Result { let Some(cached_peer) = self.cached_peer.get_peer() else { - log::debug!( - "Peer {:?} not in storage anymore, shutting down handle", - self.public_key - ); + debug!("{self} not in storage anymore, shutting down handle"); return Ok(false); }; - let spent_bandwidth = Self::compute_spent_bandwidth(kernel_peer, cached_peer) - .unwrap_or_default() - .try_into() - .inspect_err(|err| tracing::error!("Could not convert from u64 to i64: {err:?}")) - .unwrap_or_default(); - + let spent_bandwidth = kernel_peer.consumed_kernel_bandwidth(&cached_peer); self.cached_peer.update(kernel_peer); - if spent_bandwidth > 0 - && self - .bandwidth_storage_manager - .inner() - .write() - .await + if spent_bandwidth > 0 { + trace!("{self} has used {spent_bandwidth} of bandwidth"); + if self.upgrade_mode.enabled() { + debug!("we're in upgrade mode - {self} is not going to get its bandwidth deducted"); + return Ok(true); + } + + // 'regular' flow + if self .try_use_bandwidth(spent_bandwidth) .await - .is_err() - { - tracing::debug!( - "Peer {} is out of bandwidth, removing it", - self.public_key.to_string() - ); - let success = self.remove_peer().await?; - self.cached_peer.remove_peer(); - return Ok(!success); + .is_out_of_bandwidth() + { + debug!("{self} is out of bandwidth, removing it"); + let success = self.remove_peer().await?; + self.cached_peer.remove_peer(); + return Ok(!success); + } } Ok(true) @@ -169,10 +155,7 @@ impl PeerHandle { .ok_or(Error::MissingClientKernelEntry(self.public_key.to_string()))? .into(); if !self.active_peer(kernel_peer).await? { - log::debug!( - "Peer {:?} is not active anymore, shutting down handle", - self.public_key - ); + debug!("{self} is not active anymore, shutting down handle",); Ok(false) } else { Ok(true) @@ -184,12 +167,12 @@ impl PeerHandle { tokio::select! { biased; _ = self.shutdown_token.cancelled() => { - log::trace!("PeerHandle: Received shutdown"); + trace!("PeerHandle: Received shutdown"); if let Err(e) = self.bandwidth_storage_manager.inner().write().await.sync_storage_bandwidth().await { - log::error!("Storage sync failed - {e}, unaccounted bandwidth might have been consumed"); + error!("Storage sync failed - {e}, unaccounted bandwidth might have been consumed"); } - log::trace!("PeerHandle: Finished shutdown"); + trace!("PeerHandle: Finished shutdown"); break; } _ = self.timeout_check_interval.next() => { @@ -199,11 +182,11 @@ impl PeerHandle { Err(err) => { match self.remove_peer().await { Ok(true) => { - tracing::debug!("Removed peer due to error {err}"); + debug!("Removed peer due to error {err}"); return; } _ => { - tracing::warn!("Could not remove peer yet, we'll try again later. If this message persists, the gateway might need to be restarted"); + warn!("Could not remove peer yet, we'll try again later. If this message persists, the gateway might need to be restarted"); continue; } } diff --git a/common/wireguard/src/peer_storage_manager.rs b/common/wireguard/src/peer_storage_manager.rs index 1675cf6b2f..00c439350d 100644 --- a/common/wireguard/src/peer_storage_manager.rs +++ b/common/wireguard/src/peer_storage_manager.rs @@ -46,7 +46,7 @@ impl CachedPeerManager { pub(crate) fn update(&mut self, kernel_peer: PeerInformation) { if let Some(peer_information) = self.peer_information.as_mut() { - peer_information.update_trx_bytes(kernel_peer); + peer_information.update_tx_rx_bytes(kernel_peer); } } } @@ -67,8 +67,49 @@ impl From<&Peer> for PeerInformation { } impl PeerInformation { - pub(crate) fn update_trx_bytes(&mut self, peer: PeerInformation) { + pub(crate) fn update_tx_rx_bytes(&mut self, peer: PeerInformation) { self.tx_bytes = peer.tx_bytes; self.rx_bytes = peer.rx_bytes; } + + fn rx_tx_total(&self, typ: &'static str) -> Option { + self.rx_bytes.checked_add(self.tx_bytes).or_else(|| { + tracing::error!( + "overflow on {typ} adding bytes: {} + {}", + self.rx_bytes, + self.tx_bytes + ); + None + }) + } + + /// Attempt to determine the amount of consumed bandwidth based on the current peer information + /// and state from the last checkpoint. + pub(crate) fn consumed_bandwidth(kernel: &Self, previous_cached: &Self) -> Option { + let kernel_total = kernel.rx_tx_total("kernel")?; + let cached_total = previous_cached.rx_tx_total("cached")?; + kernel_total.checked_sub(cached_total).or_else(|| { + tracing::error!("Overflow on spent bandwidth subtraction: kernel - cached = {kernel_total} - {cached_total}"); + None + }) + } + + /// Attempt to determine the amount of consumed bandwidth based on the current peer information + /// and state from the last checkpoint. + /// On failures, it will attempt to default to most sensible alternative + /// + /// Note, it is responsibility of the caller to ensure that `self` corresponds to the kernel peer information + pub(crate) fn consumed_kernel_bandwidth(&self, previous_cached: &Self) -> i64 { + let Some(consumed) = Self::consumed_bandwidth(self, previous_cached) else { + // old behaviour of returning the `Default::default()` + return 0; + }; + + // old behaviour would have also returned 0 here, but I'd argue if u64 can't fit in i64, + // it means we're over i64::MAX, thus that's what we should return + consumed + .try_into() + .inspect_err(|err| tracing::error!("Could not convert from u64 to i64: {err:?}")) + .unwrap_or(i64::MAX) + } } diff --git a/contracts/coconut-dkg/src/contract.rs b/contracts/coconut-dkg/src/contract.rs index d5d525dd4c..6f3e331ed3 100644 --- a/contracts/coconut-dkg/src/contract.rs +++ b/contracts/coconut-dkg/src/contract.rs @@ -255,12 +255,10 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result, env: Env, _msg: MigrateMsg) -> Result { +pub fn migrate(deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result { set_build_information!(deps.storage)?; cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; - crate::queued_migrations::introduce_historical_epochs(deps, env)?; - Ok(Response::new()) } diff --git a/contracts/coconut-dkg/src/queued_migrations.rs b/contracts/coconut-dkg/src/queued_migrations.rs index 54c149a518..7e1e3cacd6 100644 --- a/contracts/coconut-dkg/src/queued_migrations.rs +++ b/contracts/coconut-dkg/src/queued_migrations.rs @@ -1,21 +1,2 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 - -use crate::epoch_state::storage::HISTORICAL_EPOCH; -use crate::error::ContractError; -use cosmwasm_std::{DepsMut, Env}; - -pub fn introduce_historical_epochs(deps: DepsMut, env: Env) -> Result<(), ContractError> { - if HISTORICAL_EPOCH.may_load(deps.storage)?.is_some() { - return Err(ContractError::FailedMigration { - comment: "this migration has already been run before".to_string(), - }); - } - - #[allow(deprecated)] - let current = crate::epoch_state::storage::CURRENT_EPOCH.load(deps.storage)?; - // we won't have information on intermediate states prior to now, but that's not the end of the world - HISTORICAL_EPOCH.save(deps.storage, ¤t, env.block.height)?; - - Ok(()) -} diff --git a/contracts/performance/src/helpers.rs b/contracts/performance/src/helpers.rs index 51ccb4f4b2..8219910787 100644 --- a/contracts/performance/src/helpers.rs +++ b/contracts/performance/src/helpers.rs @@ -3,7 +3,7 @@ use cosmwasm_std::{from_json, Binary, CustomQuery, QuerierWrapper, StdError, StdResult}; use cw_storage_plus::{Key, Namespace, Path, PrimaryKey}; -use nym_mixnet_contract_common::{Interval, MixNodeBond, NymNodeBond}; +use nym_mixnet_contract_common::{Interval, NymNodeBond}; use nym_performance_contract_common::{EpochId, NodeId}; use serde::de::DeserializeOwned; use std::ops::Deref; @@ -51,15 +51,10 @@ pub(crate) trait MixnetContractQuerier { fn check_node_existence(&self, address: impl Into, node_id: NodeId) -> StdResult { let mixnet_contract_address = address.into(); - // 1. check if it's a nym-node if let Some(nym_node) = self.query_nymnode_bond(mixnet_contract_address.clone(), node_id)? { return Ok(!nym_node.is_unbonding); } - // 2. try a legacy mixnode - if let Some(nym_node) = self.query_mixnode_bond(mixnet_contract_address, node_id)? { - return Ok(!nym_node.is_unbonding); - } Ok(false) } @@ -78,22 +73,6 @@ pub(crate) trait MixnetContractQuerier { self.query_mixnet_contract_storage_value(address, storage_key) } - - fn query_mixnode_bond( - &self, - address: impl Into, - node_id: NodeId, - ) -> StdResult> { - // construct proper map key - let pk_namespace = "mnn"; - let path: Path = Path::new( - Namespace::from_static_str(pk_namespace).as_slice(), - &node_id.key().iter().map(Key::as_ref).collect::>(), - ); - let storage_key = path.deref(); - - self.query_mixnet_contract_storage_value(address, storage_key) - } } impl MixnetContractQuerier for QuerierWrapper<'_, C> diff --git a/contracts/performance/src/storage.rs b/contracts/performance/src/storage.rs index acba90762b..e392fb78ba 100644 --- a/contracts/performance/src/storage.rs +++ b/contracts/performance/src/storage.rs @@ -1261,28 +1261,6 @@ mod tests { } ); - // bonded legacy mix-node - let node_id = tester.bond_dummy_legacy_mixnode()?; - let perf = NodePerformance { - node_id, - performance: Default::default(), - }; - let res = - storage.submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, perf); - assert!(res.is_ok()); - - // unbonded - tester.unbond_legacy_mixnode(node_id)?; - - let res = storage - .submit_performance_data(tester.deps_mut(), env.clone(), &nm, 0, dummy_perf) - .unwrap_err(); - assert_eq!( - res, - NymPerformanceContractError::NodeNotBonded { - node_id: dummy_perf.node_id - } - ); Ok(()) } } @@ -1984,11 +1962,6 @@ mod tests { tester.unbond_nymnode(nym_node_between)?; let nym_node2 = tester.bond_dummy_nymnode()?; - let mix_node1 = tester.bond_dummy_legacy_mixnode()?; - let mixnode_between = tester.bond_dummy_legacy_mixnode()?; - tester.unbond_legacy_mixnode(mixnode_between)?; - let mix_node2 = tester.bond_dummy_legacy_mixnode()?; - let env = tester.env(); // single id - nothing bonded @@ -2053,84 +2026,6 @@ mod tests { assert_eq!(res.accepted_scores, 2); assert_eq!(res.non_existent_nodes, vec![2, nym_node_between]); - // MIXNODES - - // one bonded mixnode, one not bonded - let res = storage.batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 3, - vec![ - NodePerformance { - node_id: mix_node1, - performance: Default::default(), - }, - NodePerformance { - node_id: 999999, - performance: Default::default(), - }, - ], - )?; - assert_eq!(res.accepted_scores, 1); - assert_eq!(res.non_existent_nodes, vec![999999]); - - // not-bonded, bonded, not-bonded, bonded - let res = storage.batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 4, - vec![ - NodePerformance { - node_id: 2, - performance: Default::default(), - }, - NodePerformance { - node_id: mix_node1, - performance: Default::default(), - }, - NodePerformance { - node_id: mixnode_between, - performance: Default::default(), - }, - NodePerformance { - node_id: mix_node2, - performance: Default::default(), - }, - ], - )?; - assert_eq!(res.accepted_scores, 2); - assert_eq!(res.non_existent_nodes, vec![2, mixnode_between]); - - // nym-node, not bonded, mixnode - let res = storage.batch_submit_performance_results( - tester.deps_mut(), - env.clone(), - &nm, - 5, - vec![ - NodePerformance { - node_id: 3, - performance: Default::default(), - }, - NodePerformance { - node_id: nym_node1, - performance: Default::default(), - }, - NodePerformance { - node_id: nym_node_between, - performance: Default::default(), - }, - NodePerformance { - node_id: mix_node2, - performance: Default::default(), - }, - ], - )?; - assert_eq!(res.accepted_scores, 2); - assert_eq!(res.non_existent_nodes, vec![3, nym_node_between]); - Ok(()) } } diff --git a/contracts/performance/src/testing/mod.rs b/contracts/performance/src/testing/mod.rs index 614982acc3..33e90beaf3 100644 --- a/contracts/performance/src/testing/mod.rs +++ b/contracts/performance/src/testing/mod.rs @@ -6,10 +6,9 @@ use crate::helpers::MixnetContractQuerier; use crate::storage::NYM_PERFORMANCE_CONTRACT_STORAGE; use cosmwasm_std::testing::{message_info, mock_env, MockApi}; use cosmwasm_std::{ - coin, coins, Addr, Binary, ContractInfo, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, - StdError, StdResult, + coin, coins, Addr, ContractInfo, Deps, DepsMut, Env, MessageInfo, QuerierWrapper, StdError, + StdResult, }; -use cw_storage_plus::PrimaryKey; use mixnet_contract::testable_mixnet_contract::MixnetContract; use nym_contracts_common::signing::{ContractMessageContent, MessageSignature}; use nym_contracts_common::Percent; @@ -22,9 +21,8 @@ use nym_contracts_common_testing::{ use nym_crypto::asymmetric::ed25519; use nym_mixnet_contract_common::nym_node::{NodeDetailsResponse, NodeOwnershipResponse, Role}; use nym_mixnet_contract_common::{ - CurrentIntervalResponse, EpochId, Interval, MixNode, MixNodeBond, MixnodeDetailsResponse, - NodeCostParams, NodeRewarding, NymNode, NymNodeBondingPayload, RoleAssignment, - SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + CurrentIntervalResponse, EpochId, Interval, NodeCostParams, NymNode, NymNodeBondingPayload, + RoleAssignment, SignableNymNodeBondingMsg, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, }; use nym_performance_contract_common::constants::storage_keys; @@ -33,7 +31,7 @@ use nym_performance_contract_common::{ NymPerformanceContractError, QueryMsg, }; use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; +use serde::Serialize; use std::str::FromStr; pub struct PerformanceContract; @@ -499,110 +497,6 @@ pub(crate) trait PerformanceContractTesterExt: self.advance_mixnet_epoch()?; Ok(()) } - - fn bond_dummy_legacy_mixnode(&mut self) -> Result { - #[derive(Deserialize, Serialize)] - pub(crate) struct UniqueRef { - // note, we collapse the pk - combining everything under the namespace - even if it is composite - pk: Binary, - value: T, - } - - // there's no proper Execute flow for this anymore, so we have to "hack" the storage a bit, - // ensuring all invariants still hold - let owner = self.generate_account_with_balance(); - - let mixnode = MixNode { - host: "1.2.3.4".to_string(), - mix_port: 123, - verloc_port: 123, - http_api_port: 123, - sphinx_key: "aaaa".to_string(), - identity_key: "bbbbb".to_string(), - version: "ccc".to_string(), - }; - let cost_params = NodeCostParams { - profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) - .unwrap(), - interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_DENOM), - }; - - // adjust node counter - let node_id_counter: u32 = self.read_from_mixnet_contract_storage("nic")?; - let node_id = node_id_counter + 1; - self.write_to_mixnet_contract_storage_value("nic", &node_id)?; - - let current_epoch = self.current_mixnet_epoch()?; - let pledge = coin(100_000000, TEST_DENOM); - let mixnode_rewarding = - NodeRewarding::initialise_new(cost_params, &pledge, current_epoch).unwrap(); - let env = self.env(); - let mixnode_bond = MixNodeBond { - mix_id: node_id, - owner, - original_pledge: pledge, - mix_node: mixnode, - proxy: None, - bonding_height: env.block.height, - is_unbonding: false, - }; - - // save to the main mixnode storage - self.set_contract_map_value( - self.mixnet_contract_address()?, - "mnn", - node_id, - &mixnode_bond, - )?; - // update indices - let pk = node_id.joined_key(); - let unique_ref = UniqueRef { - pk: pk.into(), - value: mixnode_bond.clone(), - }; - - // owner index - let idx = mixnode_bond.owner.clone(); - self.set_contract_map_value(self.mixnet_contract_address()?, "mno", idx, &unique_ref)?; - - // identity key index - let idx = mixnode_bond.mix_node.identity_key.clone(); - self.set_contract_map_value(self.mixnet_contract_address()?, "mni", idx, &unique_ref)?; - - // sphinx key index - let idx = mixnode_bond.mix_node.sphinx_key.clone(); - self.set_contract_map_value(self.mixnet_contract_address()?, "mns", idx, &unique_ref)?; - - // update rewarding data - self.set_contract_map_value( - self.mixnet_contract_address()?, - "mnr", - node_id, - &mixnode_rewarding, - )?; - - Ok(node_id) - } - - fn unbond_legacy_mixnode( - &mut self, - node_id: NodeId, - ) -> Result<(), NymPerformanceContractError> { - let bond: MixnodeDetailsResponse = self.query_arbitrary_contract( - self.mixnet_contract_address()?, - &nym_mixnet_contract_common::QueryMsg::GetMixnodeDetails { mix_id: node_id }, - )?; - - let node_owner = bond.mixnode_details.unwrap().bond_information.owner; - - self.execute_mixnet_contract( - message_info(&node_owner, &[]), - &nym_mixnet_contract_common::ExecuteMsg::UnbondMixnode {}, - )?; - - self.advance_mixnet_epoch()?; - Ok(()) - } } impl PerformanceContractTesterExt for ContractTester {} diff --git a/documentation/docs/components/operators/snippets/quic-bridge-deployment-script-setup.mdx b/documentation/docs/components/operators/snippets/quic-bridge-deployment-script-setup.mdx index 592269d728..0fd62d033f 100644 --- a/documentation/docs/components/operators/snippets/quic-bridge-deployment-script-setup.mdx +++ b/documentation/docs/components/operators/snippets/quic-bridge-deployment-script-setup.mdx @@ -32,6 +32,10 @@ chmod +x quic_bridge_deployment.sh ```sh ./quic_bridge_deployment.sh full_bridge_setup ``` +- If you prefer a non-interactive mode, run the command with this variable (and skip next step): +```sh +NONINTERACTIVE=1 quick_bridge_deployment.sh full_bridge_setup +``` ###### 3. Follow the interactive prompts - Make sure you don't just press enter to insert default values if your setup is different, for example in case of path to the config file diff --git a/documentation/docs/components/operators/snippets/routing-conf.mdx b/documentation/docs/components/operators/snippets/routing-conf.mdx new file mode 100644 index 0000000000..31c7148454 --- /dev/null +++ b/documentation/docs/components/operators/snippets/routing-conf.mdx @@ -0,0 +1,276 @@ +import { Callout } from 'nextra/components'; +import { Tabs } from 'nextra/components'; +import { Steps } from 'nextra/components'; +import { AccordionTemplate } from 'components/accordion-template.tsx'; + +export const ManagerIPOutput = () => ( +
+ Correct ./network-tunnel-manager.sh fetch_and_display_ipv6 output +
+); + +export const ManagerTablesOutput = () => ( +
+ Correct ./network-tunnel-manager.sh check_nymtun_iptables output +
+); + +export const ShowTun = () => ( +
+ Correct ip addr show nymtun0 output +
+); + + + +We recommend operators to configure their `nym-node` with the full routing configuration. + +However, most of the time the packets sent through the Mixnet are IPv4 based. The IPv6 packets are still pretty rare and therefore it's not mandatory from operational point of view to have this configuration implemented if you running only `mixnode` mode. + +If you preparing to run a `nym-node` with all modes enabled in the future, this setup is required. + + + +Networking configuration across different ISPs and various operation systems does not have a generic solution. If the provided configuration setup doesn't solve your problem check out [IPv6 troubleshooting](/operators/troubleshooting/vps-isp.mdx#ipv6-troubleshooting) page. Be aware that you may have to do more research, customised adjustments or contact your ISP to change settings for your VPS. + + +**Network Tunnel Manager ([`network-tunnel-manager.sh`](https://github.com/nymtech/nym/blob/develop/scripts/network_tunnel_manager.sh), NTM) is currently the one tool hadling the configuration of `nym-node` hosting server, according to the required design (node's [functionality](/operators/nodes/nym-node/setup#functionality-mode), WireGuard setup etc).** + +**NTM cand administrate these areas:** + +* IPv4 and IPv6 routing to the internet + +* The `nymtun0` interface (Mixnet / 5-hop): dynamically managed by the `exit-gateway` service. When the service is stopped, `nymtun0` disappears, and when started, `nymtun0` is recreated. + +* The `nymwg` interface (WG / 2-hop): used for creating a secure wireguard tunnel as part of the Nym Network configuration. + +* `iptables` rules specific to `nymwg` to ensure proper routing and forwarding through the wireguard tunnel. The `nymwg` interface needs to be correctly configured and active for the related commands to function properly. This includes applying or removing iptables rules and running connectivity tests through the `nymwg` tunnel. + +* WireGuard exit policy: Mixnet uses a common [exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt), to apply the same for WG, the operators need to set that one up on their server using `iptables` rules. + +* Testing and validating all above + +**Before starting with the following configuration, make sure you have the [latest `nym-node` binary](https://github.com/nymtech/nym/releases) installed and your [VPS setup](../preliminary-steps/vps-setup.mdx) finished properly!** + + +**Run the following steps as root!** + + +**Choose configuration command according your setup** + +
+ New nym-node full configuration, + Existing nym-node full configuration, + Step-by-step or Partial configuration + ]} defaultIndex={0}> + +This design is meant for operators setting up a new node on a fresh machine and it will result with a complete server readiness for routing as Entry Gateway and Exit Gateway in both Mixnet and WireGuard mode. + + +###### 1. Download `network-tunnel-manager.sh`, make executable and run with `--help` command: + +```sh +curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh -o network-tunnel-manager.sh && \ +chmod +x network-tunnel-manager.sh && \ +./network-tunnel-manager.sh --help +``` + +###### 2. Make sure your `nym-node` service is up and running and bonded + +- **If you setting up a new node and not upgrading an existing one, keep it running and [bond](/operators/nodes/nym-node/bonding.mdx) your node now! Then come back here and follow the rest of the configuration.** + +###### 3. Run command for configuration: +- Nodes with **WireGuard enabled**: Configures interfaces (`nymtun0` and `nymwg`), IPv4 and IPv6 routing, WireGuard exit policy and does validation tests +```sh +./network-tunnel-manager.sh complete_networking_configuration +``` +- Nodes with **WireGuard disabled**: Does everything like the command above *without WireGuard exit policy* +```sh +./network-tunnel-manager.sh nym_tunnel_setup +``` + + + +This is meant for operators configuring an existing and bonded node and it will result with a complete server readiness for routing as Entry Gateway and Exit Gateway in both Mixnet and WireGuard mode. + + +###### 1. Download `network-tunnel-manager.sh`, make executable and run with `--help` command: + +```sh +curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh -o network-tunnel-manager.sh && \ +chmod +x network-tunnel-manager.sh && \ +./network-tunnel-manager.sh --help +``` + +###### 2. Run command for configuration: +- Nodes with **WireGuard enabled**: Configures interfaces (`nymtun0` and `nymwg`), IPv4 and IPv6 routing, WireGuard exit policy and does validation tests +```sh +./network-tunnel-manager.sh complete_networking_configuration +``` +- Nodes with **WireGuard disabled**: Does everything like the command above *without WireGuard exit policy* +```sh +./network-tunnel-manager.sh nym_tunnel_setup +``` + + + + +This design is meant for operators who want to do their server configuration step by step or choose only some parts of the setup. + +###### 1. Download `network-tunnel-manager.sh`, make executable and run with `--help` command: + +```sh +curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh -o network-tunnel-manager.sh && \ +chmod +x network-tunnel-manager.sh && \ +./network-tunnel-manager.sh --help +``` + +###### 2. Make sure your `nym-node` service is up and running and bonded + +- **If you setting up a new node and not upgrading an existing one, keep it running and [bond](/operators/nodes/nym-node/bonding.mdx) your node now! Then come back here and follow the rest of the configuration.** + +###### 3. Choose steps according your need + +> You should be certain in your selection when configuring only various parts of the server. + +###### Setup IP tables rules + +- Delete IP tables rules for IPv4 and IPv6 and apply new ones: +```sh +./network-tunnel-manager.sh remove_duplicate_rules nymtun0 + +./network-tunnel-manager.sh apply_iptables_rules +``` + +- The process may prompt you if you want to save current IPv4 and IPv6 rules, choose yes. + +![](/images/operators/ip_table_prompt.png) + +- At this point you should see a `global ipv6` address. +```sh +./network-tunnel-manager.sh fetch_and_display_ipv6 +``` +
+}> +```sh +iptables-persistent is already installed. +Using IPv6 address: 2001:db8:a160::1/112 #the address will be different for you +operation fetch_ipv6_address_nym_tun completed successfully. +``` + + +###### Check Nymtun IP tables: + +```sh +./network-tunnel-manager.sh check_nymtun_iptables +``` + +- If there's no process running it wouldn't return anything. +- In case you see `nymtun0` but not active, this is probably because you are setting up a new (never bonded) node and not upgrading an existing one. + +
+}> +```sh +iptables-persistent is already installed. +network Device: eth0 +--------------------------------------- + +inspecting IPv4 firewall rules... +Chain FORWARD (policy DROP 0 packets, 0 bytes) + 0 0 ufw-reject-forward all -- * * 0.0.0.0/0 0.0.0.0/0 + 0 0 ACCEPT all -- nymtun0 eth0 0.0.0.0/0 0.0.0.0/0 + 0 0 ACCEPT all -- eth0 nymtun0 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED + 0 0 ACCEPT all -- nymtun0 eth0 0.0.0.0/0 0.0.0.0/0 + 0 0 ACCEPT all -- eth0 nymtun0 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED + 0 0 ACCEPT all -- nymtun0 eth0 0.0.0.0/0 0.0.0.0/0 + 0 0 ACCEPT all -- eth0 nymtun0 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED +--------------------------------------- + +inspecting IPv6 firewall rules... +Chain FORWARD (policy DROP 0 packets, 0 bytes) + 0 0 ufw6-reject-forward all * * ::/0 ::/0 + 0 0 ACCEPT all eth0 nymtun0 ::/0 ::/0 state RELATED,ESTABLISHED + 0 0 ACCEPT all nymtun0 eth0 ::/0 ::/0 + 0 0 ACCEPT all eth0 nymtun0 ::/0 ::/0 state RELATED,ESTABLISHED + 0 0 ACCEPT all nymtun0 eth0 ::/0 ::/0 + 0 0 ACCEPT all eth0 nymtun0 ::/0 ::/0 state RELATED,ESTABLISHED + 0 0 ACCEPT all nymtun0 eth0 ::/0 ::/0 +operation check_nymtun_iptables completed successfully. +``` + + +###### Remove old and apply new rules for wireguad routing + +```sh +../network-tunnel-manager.sh remove_duplicate_rules nymwg + +./network-tunnel-manager.sh apply_iptables_rules_wg +``` + +###### Apply rules to configure DNS routing and allow ICMP piung test for node probing (network testing) + +```sh +./network-tunnel-manager.sh configure_dns_and_icmp_wg +``` +###### Adjust and validate IP forwarding + +```sh +./network-tunnel-manager.sh adjust_ip_forwarding + +./network-tunnel-manager.sh check_ipv6_ipv4_forwarding +``` + +###### Check `nymtun0` interface and test routing configuration + +```sh +ip addr show nymtun0 +``` + +
+}> +```sh +# your addresses will be different +8: nymtun0: mtu 1420 qdisc fq_codel state UNKNOWN group default qlen 500 + link/none + inet 10.0.0.1/16 scope global nymtun0 + valid_lft forever preferred_lft forever + inet6 fc00::1/112 scope global + valid_lft forever preferred_lft forever + inet6 fe80::ad08:d167:5700:8c7c/64 scope link stable-privacy + valid_lft forever preferred_lft forever` +``` + + +- Validate your IPv6 and IPv4 networking by running a joke test via Mixnet: +```sh +./network-tunnel-manager.sh joke_through_the_mixnet +``` + +- Validate your tunneling by running a joke test via WG: +```sh +../network-tunnel-manager.sh joke_through_wg_tunnel +``` + +###### Enable wireguard + +Now you can run your node with the `--wireguard-enabled true` flag or add it to your [systemd service config](#systemd). Restart your `nym-node` or [systemd](#2-following-steps-for-nym-nodes-running-as-systemd-service) service (recommended): + +```sh +systemctl daemon-reload && service nym-node restart +``` +- Optionally, you can check if the node is running correctly by monitoring the service logs: + +```sh +journalctl -u nym-node.service -f -n 100 +``` + +
+
+
+ + +Note that the functionality the node runs in is decided by [arguments on the node itself / in node's `config.toml`](/operators/nodes/nym-node/setup#functionality-mode), this tool only prepares the server. + + +Make sure that you get the validation of all connectivity. If there are still any problems, please refer to [troubleshooting section](/operators/troubleshooting/vps-isp.mdx#incorrect-gateway-network-check). diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx new file mode 100644 index 0000000000..7d5ebf3de7 --- /dev/null +++ b/documentation/docs/components/operators/snippets/wg-exit-policy-conf.mdx @@ -0,0 +1,68 @@ +import { Callout } from 'nextra/components'; +import { Tabs } from 'nextra/components'; +import { Steps } from 'nextra/components'; +import { AccordionTemplate } from 'components/accordion-template.tsx'; +import ExitPolicyInstallOutput from 'components/operators/snippets/wg-exit-policy-install-output.mdx'; +import ExitPolicyStatusOutput from 'components/operators/snippets/wg-exit-policy-status-output.mdx'; + + +**In case you had used `network-tunnel-manager.sh` with the command `complete_networking_setup`, your WireGuard exit policy is already setup. You can test it in the next chapter.** + + +Nym Node running as Exit Gateway has contains multiple modules, one of them is Nym Network Requester(NR), routing TCP traffic to the internet. To make sure that the node is not just an open proxy, NR checks agains [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) following these conditions (in this exact order): + +1. Do we explicitly block those IP addresses regardless of ports? +2. Do we allow those specific ports regardless of IPs? +3. Do block EVERYTHING else! + +The exit policy is same for all NRs, the content is shaped by an offchain governance of Nym Node operators on our [forum](https://forum.nym.com/t/poll-a-new-nym-exit-policy-for-exit-gateways-and-the-nym-mixnet-is-inbound/464). + +There is a caveat though. NR is only routing TCP streams and therefore any other type of routing than Mixnet is *not* filtered thorugh the exit policy. To ensure that Nym Nodes follow the same exit policy when routing IP packets through WireGuard and don't act as open proxies, the operators have to set up these rules via IP tables rules. + +**For all routing configuration we provide one tool [`network-tunnel-manager.sh` (NTM)](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh). This tool manages WireGuard exit policy as well.** + +In case you haven't run `network-tunnel-manager.sh` with the command `complete_networking_setup` you need to use NTM for WireGuard exit policy configuration. + +**Folow these steps** + + +**Run the following steps as root!** + + + + +###### 1. Download `network-tunnel-manager.sh`, make executable and run with `--help` command: + +```sh +curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh -o network-tunnel-manager.sh && \ +chmod +x network-tunnel-manager.sh && \ +./network-tunnel-manager.sh --help +``` + +###### 2. Install exit policy + +- Clear old rules and configure new ones: +```sh +./network-tunnel-manager.sh exit_policy_clear +./network-tunnel-manager.sh exit_policy_install +``` +- The output should look like this: + + + + + +###### 3. Check status of your configuration +```sh +./network-tunnel-manager.sh exit_policy_status +``` + +- The output should look like this: + + + + + +Now your WireGuard routing (2-hop) should have same rotuing permissions like [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) used on 5-hop (Mixnet) mode of NymVPN. + + diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-test.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-test.mdx new file mode 100644 index 0000000000..f8a25006f3 --- /dev/null +++ b/documentation/docs/components/operators/snippets/wg-exit-policy-test.mdx @@ -0,0 +1,39 @@ +import { Tabs } from 'nextra/components'; +import { Steps } from 'nextra/components'; + +import ExitPolicyTestServer from 'components/operators/snippets/wg-exit-policy-testing-from-server.mdx'; +import ExitPolicyTestOutside from 'components/operators/snippets/wg-exit-policy-testing-from-outside.mdx'; + +**For all routing configuration we provide one tool [`network-tunnel-manager.sh` (NTM)](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh). This tool manages WireGuard tests all configurations, including WireGuard exit policy as well.** + + +You can use NTM to validate the application of the IP tables routes on your `nym-node` by checking it from the server side as well as from the outside. + +
+ From the server, + From the outside - using NymVPN + ]} defaultIndex={0}> + + + +
+ + +If all works , your node has successfully implemented WireGuard exit policy with the same routing permissions like [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) used on 5-hop (Mixnet) for TCP routing. + + + + + + + + + + + + + + + + diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-outside.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-outside.mdx index ebc11c315c..349a7cd2de 100644 --- a/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-outside.mdx +++ b/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-outside.mdx @@ -1,9 +1,29 @@ +import { Steps } from 'nextra/components'; + +Here are a few ways you can ensre your WireGuard exit policy is working correctly from the outside. + + + +###### 1. Using NymVPN + +- Connect to NymVPN and use your node as an Exit Gateway in dVPN (2-hop) mode + +- While connected to NymVPN, navigate to [`portquiz.net:12345`](http://portquiz.net:12345) and see if you can load the page + +- It shouldn't load, but if you navigate to some of the accepted ports, like[`portquiz.net:443`](http://portquiz.net:443) it should all work + +###### 2. Testing from your local terminal + - Install these dependencies on your local machine: ```shell sudo apt install tcpdump sudo tcpdump -i nymwg -n ``` -- Connect to [NymVPN](https://nym.com) and select your node as an Exit Gateway (after running the exit policy [manager script](#wireguard-exit-policy-configuration)) +- Connect to [NymVPN](https://nym.com) and select your node as an Exit Gateway + - Run the `tcpdump` command before registering -- Have the output of the `echo $BLOCKED_IP` from your node and try to go into your browser or a registered client and try to connect - It will not resolve. + +- Have the output of the `echo $BLOCKED_IP` from your node and try to go into your browser or a registered client and try to connect - It should not resolve + + \ No newline at end of file diff --git a/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-server.mdx b/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-server.mdx index 02fe54adfa..501dcc3876 100644 --- a/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-server.mdx +++ b/documentation/docs/components/operators/snippets/wg-exit-policy-testing-from-server.mdx @@ -1,11 +1,39 @@ -Run this command to define variable `BLOCKED_IP` and try to `ping` it: +import { Steps } from 'nextra/components'; +import { AccordionTemplate } from 'components/accordion-template.tsx'; +import ExitPolicyTestOutput from 'components/operators/snippets/wg-exit-policy-test-output.mdx'; + + + + +###### 1. Make sure to have the latest NTM: +```sh +curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/network-tunnel-manager.sh -o network-tunnel-manager.sh && \ +chmod +x network-tunnel-manager.sh && \ +./network-tunnel-manager.sh --help +``` + +###### 2. Run tests with NTM +```sh +./network-tunnel-manager.sh exit_policy_test_connectivity +./network-tunnel-manager.sh exit_policy_tests +``` + +- The output should look like this: + + + + +###### 3. Optionally you can do some manual sanity checks +- Run this command to define variable `BLOCKED_IP` and try to `ping` it: ```shell BLOCKED_IP=$(grep "ExitPolicy reject" /etc/nym/exit-policy.txt | head -1 | sed -E 's/ExitPolicy reject ([^:]+):.*/\1/' | sed 's/\/.*$//') ping -c 3 $BLOCKED_IP ``` -You should see `100% packet loss` as an outcome. +- You should see `100% packet loss` as an outcome. ```shell telnet $BLOCKED_IP 80 ``` -You should see `telnet: Unable to connect to remote host: Connection timed out`. +- You should see `telnet: Unable to connect to remote host: Connection timed out`. + + \ No newline at end of file diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md index 069e6eb141..858227b92a 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-percent-stake.md @@ -1 +1 @@ -0.77% +0.79% diff --git a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md index 486924e832..6c69256b94 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/nyx-outputs/nyx-total-stake.md @@ -1 +1 @@ -37.305 +36.446 diff --git a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md index bd9b415e4a..a7fdf68bde 100644 --- a/documentation/docs/components/outputs/api-scraping-outputs/time-now.md +++ b/documentation/docs/components/outputs/api-scraping-outputs/time-now.md @@ -1 +1 @@ -Thursday, November 13th 2025, 11:23:33 UTC +Wednesday, November 26th 2025, 12:13:56 UTC diff --git a/documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md b/documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md index 999f3d4a6f..bfa068ddb0 100644 --- a/documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md +++ b/documentation/docs/components/outputs/command-outputs/node-api-check-query-help.md @@ -11,7 +11,7 @@ options: --no_routing_history Display node stats without routing history --no_verloc_metrics Display node stats without verloc metrics -m, --markdown Display results in markdown format - -o, --output [OUTPUT] + -o [OUTPUT], --output [OUTPUT] Save results to file (in current dir or supply with path without filename) ``` diff --git a/documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md b/documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md new file mode 100644 index 0000000000..70baf6f736 --- /dev/null +++ b/documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md @@ -0,0 +1,37 @@ +```sh +usage: nym-node-cli install [-h] [-V] [-d BRANCH] [-v] + [--mode {mixnode,entry-gateway,exit-gateway}] + [--wireguard-enabled {true,false}] + [--hostname HOSTNAME] [--location LOCATION] + [--email EMAIL] [--moniker MONIKER] + [--description DESCRIPTION] + [--public-ip PUBLIC_IP] + [--nym-node-binary NYM_NODE_BINARY] + [--uplink-dev UPLINK_DEV] [--env KEY=VALUE] + +options: + -h, --help show this help message and exit + -V, --version show program's version number and exit + -d BRANCH, --dev BRANCH + Define github branch (default: develop) + -v, --verbose Show full error tracebacks + --mode {mixnode,entry-gateway,exit-gateway} + Node mode: 'mixnode', 'entry-gateway', or 'exit- + gateway' + --wireguard-enabled {true,false} + WireGuard functionality switch: true / false + --hostname HOSTNAME Node domain / hostname + --location LOCATION Node location (country code or name) + --email EMAIL Contact email for the node operator + --moniker MONIKER Public moniker displayed in explorer & NymVPN app + --description DESCRIPTION + Short public description of the node + --public-ip PUBLIC_IP + External IPv4 address (autodetected if omitted) + --nym-node-binary NYM_NODE_BINARY + URL for nym-node binary (autodetected if omitted) + --uplink-dev UPLINK_DEV + Override uplink interface used for NAT/FORWARD (e.g., + 'eth0'; autodetected if omitted) + --env KEY=VALUE (Optional) Extra ENV VARS, e.g. --env CUSTOM_KEY=value +``` diff --git a/documentation/docs/pages/operators/changelog.mdx b/documentation/docs/pages/operators/changelog.mdx index aa1fe30dfe..34c4551975 100644 --- a/documentation/docs/pages/operators/changelog.mdx +++ b/documentation/docs/pages/operators/changelog.mdx @@ -49,6 +49,71 @@ This page displays a full list of all the changes during our release cycle from +## `v2025.21-mozzarella` + +- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.21-mozzarella) +- [`nym-node`](nodes/nym-node.mdx) version `1.22.0` + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2025-11-25T14:26:29.627763948Z +Build Version: 1.22.0 +Commit SHA: 22793bc45ea21561671d6670497ff42bc36b9d76 +Commit Date: 2025-11-25T15:16:42.000000000+01:00 +Commit Branch: HEAD +rustc Version: 1.88.0 +rustc Channel: stable +cargo Profile: release +``` + +### Operators Updates & Tools + + +**We are introducing new `network-tunnel-manager.sh` (NTM), with improved server configuration (see all changes here: [\#6186](https://github.com/nymtech/nym/pull/6186/)). We ask operators to re-run their routing configuration using the new NTM, following [these steps](/operators/nodes/nym-node/configuration#routing-configuration).** + +**NOTE THAT THIS IS A NEW TOOL HANDLING VARIOUS COMPLEX SERVER SETTINGS, PLEASE RUN IT ON A FEW NODES AT A TIME, DO THE TESTING, MONITOR YOUR NODES AND [REPORT ANY ISSUES](https://matrix.to/#/#operators:nymtech.chat).** + +We will keep `nym-node v1.21.1` as the latest version on API side for another 3 days to not penalize operators taking time for a proper implementation. + + +We are proud to announce that there are several improvements on tools aiming to make node operators life easier. Please let us know how do these programs work for you. + +- [**New Nym Node CLI version**](/operators/tools#nym-node-cli): This version introduces arguments, allowing the operator to pass all needed variables (ie. hostname, mode etcetra) and let the program setup everything required for `nym-node` installation, server configuration, nginx, routing, tunnels and bridges, without further prompts for these values. This version also installs [QUIC bridge](/operators/nodes/nym-node/configuration#quic-transport-bridge-deployment). + +- [**New Network Tunnel Manager**](/operators/nodes/nym-node/configuration#routing-configuration): NTM went through an big overhaul ([\#6186](https://github.com/nymtech/nym/pull/6186/)) - this new version combines old NTM with WG scripts. If run with `complete_networking_configuration` the tool does entire routing configuration, creates interfaces, and configures WireGuard exit policy. + +- [**Updated QUIC Deployment Tool**](/operators/nodes/nym-node/configuration#quic-transport-bridge-deployment): The program is lighter, stripped of redundant functions reworking iptables rules. + +### Features + +- [HTTP API resilience enable & domain rotation conditions](https://github.com/nymtech/nym/pull/6200): Changes to make sure fallback domains are updated / enabled only for relevant + +- [Typescript SDK 1.4.1](https://github.com/nymtech/nym/pull/6146): This PR is a new release of the Typescript SDK, `mixFetch` and `WASM` client. It also removes the Harbour Master client from `mixFetch`, replacing it with the Nym API's described endpoint for nym-nodes. + +- [Enable URL rotation and retries for mixnet gateway init](https://github.com/nymtech/nym/pull/6126): + - Multiple URL fallback with configurable retries (defaults to 3) + - Infallible URL conversion per feedback (`Url::from()` instead of `parse().ok()`) + - Non-breaking builder pattern for `BuilderConfig` per feedback + - Reverted redundant node filtering per clarification that API already filters by `supported_roles.entry` + +- [Credential proxy jwt](https://github.com/nymtech/nym/pull/5957): This PR is part of the 'Upgrade Mode' that should allow usage of the network in a situation where ecash credentials are unissuable, because, for example, we have lost signing quorum (i.e. we have fewer than the required number of threshold signers responding to requests). This version is more naive. Instead requesting actual 'emergency credentials' that would have been issued by a subset of ecash signers, the credentials proxy creates a JWT, signed with its key, attesting the upgrade mode has been enabled. + +### Bugfix + +- [Tunnel not waiting on `MixnetClient` to shut down cleanly](https://github.com/nymtech/nym/pull/6225): When there is a wireguard registration error, the mixnet client is signalled to shut down, but the tunnel doesn't wait. Now on errors, the registration client doesn't return until everything is properly stopped + +- [Fix credential proxy upgrade mode attestation url arg](https://github.com/nymtech/nym/pull/6202): This includes bringing over changes introduced in [\#6174](https://github.com/nymtech/nym/pull/6174) + +- [Remove debug feature from `http-macro` spec in gateway probe](https://github.com/nymtech/nym/pull/6195) + +- [DNS reliability and troubleshooting ](https://github.com/nymtech/nym/pull/6179) + +- [Distinguish authenticator errors by credential spent](https://github.com/nymtech/nym/pull/6176): This PR introduces a distinction between authenticator client errors that are happening before a credential was taken out of storage, and after + +- [Disconnect mixnet client if registration fails](https://github.com/nymtech/nym/pull/6158): Currently the registration client does its job and then handles everything to the `vpn-client`, who then takes care on the mixnet client. However, if the registration fails, nothing is disconnecting the mixnet client, which can lead to errors of the kind of `There is already an open connection to this client`. This PR addresses that + + ## `v2025.20-leerdammer` - [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.20-leerdammer) diff --git a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx index 69afc1e41d..d791e7f946 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/configuration.mdx @@ -3,34 +3,11 @@ import { Tabs } from 'nextra/components'; import { VarInfo } from 'components/variable-info.tsx'; import { Steps } from 'nextra/components'; import { AccordionTemplate } from 'components/accordion-template.tsx'; -import ExitPolicyInstallOutput from 'components/operators/snippets/wg-exit-policy-install-output.mdx'; -import ExitPolicyStatusOutput from 'components/operators/snippets/wg-exit-policy-status-output.mdx'; -import ExitPolicyTestOutput from 'components/operators/snippets/wg-exit-policy-test-output.mdx'; -import ExitPolicyTestServer from 'components/operators/snippets/wg-exit-policy-testing-from-server.mdx'; -import ExitPolicyTestOutside from 'components/operators/snippets/wg-exit-policy-testing-from-outside.mdx'; +import WGExitPolicyConf from 'components/operators/snippets/wg-exit-policy-conf.mdx'; +import WGExitPolicyTest from 'components/operators/snippets/wg-exit-policy-test.mdx'; +import RoutingConf from 'components/operators/snippets/routing-conf.mdx'; import QuicDeploymentSteps from 'components/operators/snippets/quic-bridge-deployment-script-setup.mdx'; - -export const ManagerIPOutput = () => ( -
- Correct ./network_tunnel_manager.sh fetch_and_display_ipv6 output -
-); - -export const ManagerTablesOutput = () => ( -
- Correct ./network_tunnel_manager.sh check_nymtun_iptables output -
-); - -export const ShowTun = () => ( -
- Correct ip addr show nymtun0 output -
-); - - - # Nym Node Configuration @@ -222,9 +199,11 @@ This lets your operating system know it's ok to reload the service configuration
-## Connectivity Test and Configuration +## Routing Configuration -During our ongoing testing events we found out, that after introducing IP Packet Router (IPR) and [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) on embedded Network Requester (NR) by default, only a fragment of Gateways routes correctly through IPv4 and IPv6. We built a useful monitor to check out your Gateway (`nym-node --mode exit-gateway`) at [harbourmaster.nymtech.net](https://harbourmaster.nymtech.net/). + + +### Quick IPv6 Check IPv6 routing is not only a case for gateways. Imagine a rare occasion when you run a `mixnode` without IPv6 enabled and a client will sent IPv6 packets through the Mixnet through such route: ```ascii @@ -232,19 +211,6 @@ IPv6 routing is not only a case for gateways. Imagine a rare occasion when you r ``` In this (unusual) case your `mixnode` will not be able to route the packets. The node will drop the packets and its performance would go down. For that reason it's beneficial to have IPv6 enabled when running a `mixnode` functionality. - -We recommend operators to configure their `nym-node` with the full routing configuration. - -However, most of the time the packets sent through the Mixnet are IPv4 based. The IPv6 packets are still pretty rare and therefore it's not mandatory from operational point of view to have this configuration implemented if you running only `mixnode` mode. - -If you preparing to run a `nym-node` with all modes enabled in the future, this setup is required. - - - -For everyone participating in Delegation Program or Service Grant program, this setup is a requirement! - - -### Quick IPv6 Check You can always check IPv6 address and connectivity by using some of these methods:
@@ -273,267 +239,13 @@ telnet -6 ipv6.telnetmyip.com Make sure to keep your IPv4 address enabled while setting up IPv6, as the majority of routing goes through that one!
-### Routing Configuration - -While we're working on Rust implementation to have these settings as a part of the binary build, to solve these connectivity requirements in the meantime we wrote a script [`network_tunnel_manager.sh`](https://github.com/nymtech/nym/blob/develop/scripts/network_tunnel_manager.sh) to support operators to configure their servers and address all the connectivity requirements. - -Networking configuration across different ISPs and various operation systems does not have a generic solution. If the provided configuration setup doesn't solve your problem check out [IPv6 troubleshooting](../../troubleshooting/vps-isp.mdx#ipv6-troubleshooting) page. Be aware that you may have to do more research, customised adjustments or contact your ISP to change settings for your VPS. - -The `nymtun0` interface is dynamically managed by the `exit-gateway` service. When the service is stopped, `nymtun0` disappears, and when started, `nymtun0` is recreated. - -The `nymwg` interface is used for creating a secure wireguard tunnel as part of the Nym Network configuration. Similar to `nymtun0`, the script manages iptables rules specific to `nymwg` to ensure proper routing and forwarding through the wireguard tunnel. The `nymwg` interface needs to be correctly configured and active for the related commands to function properly. This includes applying or removing iptables rules and running connectivity tests through the `nymwg` tunnel. - -The script should be used in a context where `nym-node` is running to fully utilise its capabilities, particularly for fetching IPv6 addresses or applying network rules that depend on the `nymtun0` and `nymwg` interfaces and to establish a WireGuard tunnel. - -**Before starting with the following configuration, make sure you have the [latest `nym-node` binary](https://github.com/nymtech/nym/releases) installed and your [VPS setup](../preliminary-steps/vps-setup.mdx) finished properly!** - - - -###### 1. Download `network_tunnel_manager.sh`, make executable and run: - -```sh -curl -L https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/network_tunnel_manager.sh -o network_tunnel_manager.sh && \ -chmod +x network_tunnel_manager.sh && \ -./network_tunnel_manager.sh -``` - -###### 2. Make sure your `nym-node` service is up and running and bond - -- **If you setting up a new node and not upgrading an existing one, keep it running and [bond](bonding.mdx) your node now**. Then come back here and follow the rest of the configuration. - - -**Run the following steps as root or with `sudo` prefix!** - - - -###### 3. Setup IP tables rules - -- Delete IP tables rules for IPv4 and IPv6 and apply new ones: -```sh -./network_tunnel_manager.sh remove_duplicate_rules nymtun0 - -./network_tunnel_manager.sh apply_iptables_rules -``` - -- The process may prompt you if you want to save current IPv4 and IPv6 rules, choose yes. - -![](/images/operators/ip_table_prompt.png) - -- At this point you should see a `global ipv6` address. -```sh -./network_tunnel_manager.sh fetch_and_display_ipv6 -``` -
-}> -```sh -iptables-persistent is already installed. -Using IPv6 address: 2001:db8:a160::1/112 #the address will be different for you -operation fetch_ipv6_address_nym_tun completed successfully. -``` - - -###### 4. Check Nymtun IP tables: - -```sh -./network_tunnel_manager.sh check_nymtun_iptables -``` - -- If there's no process running it wouldn't return anything. -- In case you see `nymtun0` but not active, this is probably because you are setting up a new (never bonded) node and not upgrading an existing one. - -
-}> -```sh -iptables-persistent is already installed. -network Device: eth0 ---------------------------------------- - -inspecting IPv4 firewall rules... -Chain FORWARD (policy DROP 0 packets, 0 bytes) - 0 0 ufw-reject-forward all -- * * 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- nymtun0 eth0 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- eth0 nymtun0 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - 0 0 ACCEPT all -- nymtun0 eth0 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- eth0 nymtun0 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED - 0 0 ACCEPT all -- nymtun0 eth0 0.0.0.0/0 0.0.0.0/0 - 0 0 ACCEPT all -- eth0 nymtun0 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED ---------------------------------------- - -inspecting IPv6 firewall rules... -Chain FORWARD (policy DROP 0 packets, 0 bytes) - 0 0 ufw6-reject-forward all * * ::/0 ::/0 - 0 0 ACCEPT all eth0 nymtun0 ::/0 ::/0 state RELATED,ESTABLISHED - 0 0 ACCEPT all nymtun0 eth0 ::/0 ::/0 - 0 0 ACCEPT all eth0 nymtun0 ::/0 ::/0 state RELATED,ESTABLISHED - 0 0 ACCEPT all nymtun0 eth0 ::/0 ::/0 - 0 0 ACCEPT all eth0 nymtun0 ::/0 ::/0 state RELATED,ESTABLISHED - 0 0 ACCEPT all nymtun0 eth0 ::/0 ::/0 -operation check_nymtun_iptables completed successfully. -``` - - -###### 5. Remove old and apply new rules for wireguad routing - -```sh -/network_tunnel_manager.sh remove_duplicate_rules nymwg - -./network_tunnel_manager.sh apply_iptables_rules_wg -``` - -###### 6. Apply rules to configure DNS routing and allow ICMP piung test for node probing (network testing) - -```sh -./network_tunnel_manager.sh configure_dns_and_icmp_wg -``` -###### 7. Adjust and validate IP forwarding - -```sh -./network_tunnel_manager.sh adjust_ip_forwarding - -./network_tunnel_manager.sh check_ipv6_ipv4_forwarding -``` - -###### 8. Check `nymtun0` interface and test routing configuration - -```sh -ip addr show nymtun0 -``` - -
-}> -```sh -# your addresses will be different -8: nymtun0: mtu 1420 qdisc fq_codel state UNKNOWN group default qlen 500 - link/none - inet 10.0.0.1/16 scope global nymtun0 - valid_lft forever preferred_lft forever - inet6 fc00::1/112 scope global - valid_lft forever preferred_lft forever - inet6 fe80::ad08:d167:5700:8c7c/64 scope link stable-privacy - valid_lft forever preferred_lft forever` -``` - - -- Validate your IPv6 and IPv4 networking by running a joke test via Mixnet: -```sh -./network_tunnel_manager.sh joke_through_the_mixnet -``` - -- Validate your tunneling by running a joke test via WG: -```sh -./network_tunnel_manager.sh joke_through_wg_tunnel -``` - -- **Note:** WireGuard will return only IPv4 joke, not IPv6. WG IPv6 is under development. Running IPR joke through the mixnet with `./network_tunnel_manager.sh joke_through_the_mixnet` should work with both IPv4 and IPv6! - - -###### 9. Enable wireguard - -Now you can run your node with the `--wireguard-enabled true` flag or add it to your [systemd service config](#systemd). Restart your `nym-node` or [systemd](#2-following-steps-for-nym-nodes-running-as-systemd-service) service (recommended): - -```sh -systemctl daemon-reload && service nym-node restart -``` -- Optionally, you can check if the node is running correctly by monitoring the service logs: - -```sh -journalctl -u nym-node.service -f -n 100 -``` -
- -Make sure that you get the validation of all connectivity. If there are still any problems, please refer to [troubleshooting section](../../troubleshooting/vps-isp.mdx#incorrect-gateway-network-check). - ## Wireguard Exit Policy Configuration -Nym Node running as Exit Gateway has contains multiple modules, one of them is Nym Network Requester(NR), routing TCP traffic to the internet. To make sure that the node is not just an open proxy, NR checks agains [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) following these conditions (in this exact order): - -1. Do we explicitly block those IP addresses regardless of ports? -2. Do we allow those specific ports regardless of IPs? -3. Do block EVERYTHING else! - -The exit policy is same for all NRs, the content is shaped by an offchain governance of Nym Node operators on our [forum](https://forum.nym.com/t/poll-a-new-nym-exit-policy-for-exit-gateways-and-the-nym-mixnet-is-inbound/464). - -There is a caveat though. NR is only routing TCP streams and therefore any other type of routing is *not* filtered thorugh the exit policy. To ensure that Nym Nodes follow the same exit policy when routing IP packets through wireguard and don't act as open proxies, the operators have to set up these rules via IP tables rules. - -**Follow these steps, using a [setup script](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh) and [testing scripts](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh) written by Nym quality assurance team, to setup exit policy for wireguard:** - - - -###### 1. Download the scripts and make executable - -- SSH to your node -- Create a folder `~/nym-binaries` and navigate there -```sh -mkdir $HOME/nym-binaries -cd $HOME/nym-binaries -``` -- Download the scripts -```sh -wget https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh - -wget https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh -``` -- Make executable -```sh -chmod +x wireguard-exit-policy-manager.sh exit-policy-tests.sh -``` - -###### 2. Install `wireguard-exit-policy-manager.sh` -```sh -./wireguard-exit-policy-manager.sh install -``` -- The output should look like this: - - - - - -###### 3. Run `wireguard-exit-policy-manager.sh` -```sh -./wireguard-exit-policy-manager.sh status -``` - -- The output should look like this: - - - - -###### 4. Test with `exit-policy-tests.sh` - -```sh -./exit-policy-tests.sh -``` - -- The output should look like this: - - - - -###### 5. In case of problems, you can clear the exit policy rule -```sh -./wireguard-exit-policy-manager.sh clear - -./wireguard-exit-policy-manager.sh status -``` - - -Now your wireguart routing should have same rotuing permissions like [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) used on 5-hop (Mixnet) mode of NymVPN. + ### Testing Wireguard Exit Policy -You can validate the application of the IP tables routes on your `nym-node` by checking it from the server side as well as from the outside. - -
- From the server, - From the outside - using NymVPN - ]} defaultIndex={0}> - - - -
- -Your node has successfully implemented wireguard exit policy with the same routing permissions like [Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt) used on 5-hop (Mixnet). + ## QUIC Transport Bridge Deployment diff --git a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx index de584513ec..36e415e78a 100644 --- a/documentation/docs/pages/operators/nodes/nym-node/setup.mdx +++ b/documentation/docs/pages/operators/nodes/nym-node/setup.mdx @@ -21,10 +21,10 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](. ```sh nym-node Binary Name: nym-node -Build Timestamp: 2025-11-12T08:19:33.288341371Z -Build Version: 1.21.0 -Commit SHA: babf113fe5d396fa8a84fa939ad4b1b5b4d38b83 -Commit Date: 2025-11-12T08:39:48.000000000+01:00 +Build Timestamp: 2025-11-25T14:26:29.627763948Z +Build Version: 1.22.0 +Commit SHA: 22793bc45ea21561671d6670497ff42bc36b9d76 +Commit Date: 2025-11-25T15:16:42.000000000+01:00 Commit Branch: HEAD rustc Version: 1.88.0 rustc Channel: stable diff --git a/documentation/docs/pages/operators/tools.mdx b/documentation/docs/pages/operators/tools.mdx index 8a801e526a..c29ecd6038 100644 --- a/documentation/docs/pages/operators/tools.mdx +++ b/documentation/docs/pages/operators/tools.mdx @@ -5,7 +5,13 @@ import { RunTabs } from 'components/operators/nodes/node-run-command-tabs'; import { VarInfo } from 'components/variable-info.tsx'; import { AccordionTemplate } from 'components/accordion-template.tsx'; import { Steps } from 'nextra/components'; +import NymNodeCliInstallHelp from 'components/outputs/command-outputs/nym-node-cli-install-help.md'; +export const NymNodeCliCommand = () => ( +
+ Arguments and options: ./nym-node-cli.py install --help +
+); # Tools @@ -21,9 +27,11 @@ Nym Network stats can be humanly read on some of the explorers and dashboards. - **[SpectreDAO Explorer](https://explorer.nym.spectredao.net/):** By operators for operators - currently the most used Nym explorer -- **[Nymesis](https://nymesis.vercel.app/):** A slick dashboard by operator community +- **[Nym Node Status Observatory](https://harbourmaster.nymtech.net/):** An explorer with a list of Nym Nodes, their properformance and per node data -- **[Nym Harbourmaster](https://harbourmaster.nymtech.net/):** A dashboard showing results of Gateway probes and more (*in development*) +- **[Nym Node Status UI](https://node-status.nym.com/):** A dashboard displaying results of Gateway probes and much more in one table + +- **[Nymesis](https://nymesis.vercel.app/):** A slick dashboard by operator community ## Nym Node CLI @@ -45,10 +53,50 @@ chmod +x ./nym-node-cli.py ``` ###### 3. Run the program + +There are two ways how to run the program. fully interactive or with provided arguments. The former has a shorter initial command but then operators must fill the values on the go, the latter requires more complex command, but then there is a minimum prompts during the process. + +1. Fully interactive mode - just run: +```sh +./nym-node-cli.py install ``` -./nym-node-cli.py + +2. Use arguments - run this command to see all options: +```sh +./nym-node-cli.py install --help ``` +}> + + +- An example can look like this: +```sh +# substitute with your real values: +./nym-node-cli.py install + --hostname node-install.devrel.nymte.ch + --moniker MainnetGW-DE + --description "This node is installed with nym-node-cli v1.2.0" + --wireguard-enabled true + --location DE + --mode exit-gateway + --email kawa_hesinkar@example.ku +``` + ###### 4. Read and follow the prompts + +There ware a few required confirmations and in case of running the program without the arguments, you will be prompted for values neccessary to setup a `nym-node` and configure your server. + +**Read these prompts carefully!** + +###### 5. Setup finished + +Congratulation, your node is installed. + +This version of the program does not prompt an operator for the local node moniker (`--id `) therefore it asigns your node an automatic one `default-nym-node`. + +All configuration and data of your node can be found in this location: +```sh +ls $HOME/.nym/nym-nodes/default-nym-node/ +``` ## CMD Reward Tracker diff --git a/documentation/docs/pages/operators/troubleshooting/nodes.mdx b/documentation/docs/pages/operators/troubleshooting/nodes.mdx index 64c09e4a6c..0011a9a6b4 100644 --- a/documentation/docs/pages/operators/troubleshooting/nodes.mdx +++ b/documentation/docs/pages/operators/troubleshooting/nodes.mdx @@ -113,7 +113,7 @@ There are a few community explorers as well. Enter your **identity key** to find your node. Check the contents of the `Mixnode stats` and `Routing score` sections. -[Here](https://github.com/cosmos/chain-registry/blob/master/nyx/chain.json#L158-L187) is a dictionary with Nyx chain registry entry regarding all explorers. +[Here](https://github.com/cosmos/chain-registry/blob/master/nyx/chain.json#L183-L225) is a dictionary with Nyx chain registry entry regarding all explorers. If you want more information, or if your node isn't showing up on the explorer of your choice and you want to double-check, here are some examples on how to check if the node is configured properly. diff --git a/documentation/scripts/next-scripts/python-prebuild.sh b/documentation/scripts/next-scripts/python-prebuild.sh index a78bfbf11e..9083b2fd57 100755 --- a/documentation/scripts/next-scripts/python-prebuild.sh +++ b/documentation/scripts/next-scripts/python-prebuild.sh @@ -59,4 +59,14 @@ echo '```sh' > ../../documentation/docs/components/outputs/command-outputs/nym-a ./nym-api --help >> ../../documentation/docs/components/outputs/command-outputs/nym-api-help.md && echo '```' >> ../../documentation/docs/components/outputs/command-outputs/nym-api-help.md && +cd ../../scripts/nym-node-setup + +echo '```sh' > ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md && +python ./nym-node-cli.py install --help >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md && +echo '```' >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md && + +echo '```sh' > ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md && +python ./nym-node-cli.py install --help >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md && +echo '```' >> ../../documentation/docs/components/outputs/command-outputs/nym-node-cli-install-help.md && + echo "prebuild finished" diff --git a/documentation/scripts/post-process/package-lock.json b/documentation/scripts/post-process/package-lock.json index 8fcd8df5e6..34bedd213a 100644 --- a/documentation/scripts/post-process/package-lock.json +++ b/documentation/scripts/post-process/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "dependencies": { "@jsdevtools/rehype-url-inspector": "^2.0.2", - "glob": "^10.3.4", + "glob": "^10.5.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "to-vfile": "^8.0.0", @@ -20,6 +20,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", @@ -48,6 +49,7 @@ "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", "optional": true, "engines": { "node": ">=14" @@ -80,9 +82,10 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -91,9 +94,10 @@ } }, "node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -113,12 +117,14 @@ "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -154,6 +160,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -164,7 +171,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/comma-separated-tokens": { "version": "2.0.3", @@ -211,12 +219,14 @@ "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" }, "node_modules/entities": { "version": "4.5.0", @@ -245,21 +255,20 @@ } }, "node_modules/glob": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", - "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, "bin": { - "glob": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=16 || 14 >=14.17" + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -527,6 +536,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", "engines": { "node": ">=8" } @@ -548,15 +558,13 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/jackspeak": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.3.tgz", - "integrity": "sha512-R2bUw+kVZFS/h1AZqBKrSgDmdmjApzgY0AlCPumopFiAlbUxE2gf+SCuBzQ0cP5hHmUmFYF5yw55T97Th5Kstg==", + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -565,12 +573,10 @@ } }, "node_modules/lru-cache": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", - "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", - "engines": { - "node": "14 || >=16.14" - } + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" }, "node_modules/mdast-util-to-hast": { "version": "13.0.2", @@ -676,9 +682,10 @@ ] }, "node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -690,13 +697,20 @@ } }, "node_modules/minipass": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", - "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parse5": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", @@ -717,15 +731,16 @@ } }, "node_modules/path-scurry": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", - "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -811,6 +826,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -828,6 +844,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -841,6 +858,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -848,12 +866,14 @@ "node_modules/string-width-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" }, "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -875,9 +895,10 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -893,6 +914,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -904,6 +926,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -1220,6 +1243,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -1237,6 +1261,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -1253,6 +1278,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -1261,6 +1287,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1274,12 +1301,14 @@ "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" }, "node_modules/wrap-ansi-cjs/node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -1293,6 +1322,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, diff --git a/documentation/scripts/post-process/package.json b/documentation/scripts/post-process/package.json index 04e3a762a2..1c31b9c78c 100644 --- a/documentation/scripts/post-process/package.json +++ b/documentation/scripts/post-process/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "@jsdevtools/rehype-url-inspector": "^2.0.2", - "glob": "^10.3.4", + "glob": "^10.5.0", "rehype-parse": "^9.0.0", "rehype-stringify": "^10.0.0", "to-vfile": "^8.0.0", diff --git a/envs/canary.env b/envs/canary.env index f494ad3270..fbf97b2643 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -22,3 +22,5 @@ NYXD=https://rpc.canary-validator.performance.nymte.ch NYM_API=https://canary-api.performance.nymte.ch/api/ NYXD_WS=wss://rpc.canary-validator.performance.nymte.ch/websocket NYM_VPN_API=https://nym-vpn-api-git-deploy-canary-nyx-network-staging.vercel.app/api/ + +UPGRADE_MODE_ATTESTER_ED25519_PUBKEY=U1NXToPYUTsh7pYPLcwXCXwcL6pGoLUou7fyAJrNz8b \ No newline at end of file diff --git a/envs/mainnet.env b/envs/mainnet.env index 444307f836..01e5e9092b 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -25,3 +25,5 @@ NYXD=https://rpc.nymtech.net NYM_API=https://validator.nymtech.net/api/ NYXD_WS=wss://rpc.nymtech.net/websocket NYM_VPN_API=https://nymvpn.com/api/ + +UPGRADE_MODE_ATTESTER_ED25519_PUBKEY=3bgffBYcfFkTTXc2npNNn9MkddFZ3H2LrPjXDmnJzrqd \ No newline at end of file diff --git a/envs/sandbox.env b/envs/sandbox.env index e90257f015..96a46881dd 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -23,3 +23,5 @@ NYXD=https://rpc.sandbox.nymtech.net NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket NYM_API=https://sandbox-nym-api1.nymtech.net/api/ NYM_VPN_API=https://nym-vpn-api-git-deploy-sandbox-nyx-network-staging.vercel.app/api/ + +UPGRADE_MODE_ATTESTER_ED25519_PUBKEY=EGwzKXPrqStv8cHF68VT2LbQuEBGDPzhCAixScvybfem \ No newline at end of file diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 1e4a5be417..cf1b8f286b 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -19,7 +19,6 @@ rust-version = "1.85" path = "src/lib.rs" [dependencies] -anyhow = { workspace = true } bincode = { workspace = true } async-trait = { workspace = true } bip39 = { workspace = true } @@ -30,7 +29,6 @@ futures = { workspace = true } ipnetwork = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } -sha2 = { workspace = true } thiserror = { workspace = true } time = { workspace = true } tokio = { workspace = true, features = [ @@ -40,16 +38,14 @@ tokio = { workspace = true, features = [ "fs", "time", ] } -tokio-stream = { workspace = true, features = ["fs"] } +tokio-stream = { workspace = true } tokio-tungstenite = { workspace = true } -tokio-util = { workspace = true, features = ["codec"] } tracing = { workspace = true } url = { workspace = true, features = ["serde"] } zeroize = { workspace = true } # internal -nym-api-requests = { path = "../nym-api/nym-api-requests" } nym-credentials = { path = "../common/credentials" } nym-credentials-interface = { path = "../common/credentials-interface" } nym-credential-verification = { path = "../common/credential-verification" } @@ -58,7 +54,6 @@ nym-gateway-storage = { path = "../common/gateway-storage" } nym-gateway-stats-storage = { path = "../common/gateway-stats-storage" } nym-gateway-requests = { path = "../common/gateway-requests" } nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } -nym-mixnode-common = { path = "../common/mixnode-common" } nym-network-defaults = { path = "../common/network-defaults" } nym-network-requester = { path = "../service-providers/network-requester" } nym-sdk = { path = "../sdk/rust/nym-sdk" } @@ -66,10 +61,10 @@ nym-sphinx = { path = "../common/nymsphinx" } nym-statistics-common = { path = "../common/statistics" } nym-task = { path = "../common/task" } nym-topology = { path = "../common/topology" } -nym-types = { path = "../common/types" } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } nym-node-metrics = { path = "../nym-node/nym-node-metrics" } +nym-upgrade-mode-check = { path = "../common/upgrade-mode-check" } nym-wireguard = { path = "../common/wireguard" } nym-wireguard-private-metadata-server = { path = "../common/wireguard-private-metadata/server" } @@ -80,7 +75,6 @@ nym-client-core = { path = "../common/client-core", features = ["cli"] } nym-id = { path = "../common/nym-id" } nym-service-provider-requests-common = { path = "../common/service-provider-requests-common" } - defguard_wireguard_rs = { workspace = true } [dev-dependencies] @@ -88,3 +82,6 @@ nym-gateway-storage = { path = "../common/gateway-storage", features = ["mock"] nym-wireguard = { path = "../common/wireguard", features = ["mock"] } mock_instant = "0.6.0" time = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/gateway/src/config.rs b/gateway/src/config.rs index 363ddbb88e..8df528674b 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -13,6 +13,8 @@ pub struct Config { pub ip_packet_router: IpPacketRouter, + pub upgrade_mode_watcher: UpgradeModeWatcher, + pub debug: Debug, } @@ -21,12 +23,14 @@ impl Config { gateway: impl Into, network_requester: impl Into, ip_packet_router: impl Into, + upgrade_mode_watcher: impl Into, debug: impl Into, ) -> Self { Config { gateway: gateway.into(), network_requester: network_requester.into(), ip_packet_router: ip_packet_router.into(), + upgrade_mode_watcher: upgrade_mode_watcher.into(), debug: debug.into(), } } @@ -57,6 +61,28 @@ pub struct Gateway { pub nyxd_urls: Vec, } +#[derive(Debug)] +pub struct UpgradeModeWatcher { + /// Specifies whether this gateway watches for upgrade mode changes + /// via the published attestation file. + pub enabled: bool, + + /// Endpoint to query to retrieve current upgrade mode attestation. + /// If not provided, it implicitly disables the watcher and upgrade-mode features + pub attestation_url: Url, + + pub debug: UpgradeModeWatcherDebug, +} + +#[derive(Debug)] +pub struct UpgradeModeWatcherDebug { + /// Default polling interval + pub regular_polling_interval: Duration, + + /// Expedited polling interval for once upgrade mode is detected + pub expedited_poll_interval: Duration, +} + #[derive(Debug, PartialEq)] pub struct NetworkRequester { /// Specifies whether network requester service is enabled in this process. @@ -104,6 +130,9 @@ pub struct Debug { /// Defines the timestamp skew of a signed authentication request before it's deemed too excessive to process. pub max_request_timestamp_skew: Duration, + + /// The minimum duration since the last explicit check for the upgrade mode to allow creation of new requests. + pub upgrade_mode_min_staleness_recheck: Duration, } #[derive(Debug, Clone)] diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index a215affdf9..8f577ba13f 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -147,7 +147,7 @@ impl ActiveClientsStore { handle: MixMessageSender, is_active_request_sender: IsActiveRequestSender, session_request_timestamp: OffsetDateTime, - ) { + ) -> bool { let entry = ActiveClient::Remote(RemoteClientData { session_request_timestamp, channels: ClientIncomingChannels { @@ -156,11 +156,16 @@ impl ActiveClientsStore { }, }); if self.inner.insert(client, entry).is_some() { - panic!("inserted a duplicate remote client") + // this should be impossible under normal circumstances, + // but in some rare edge cases of clients performing very careful timing attacks, + // this branch could be potentially triggered + return false; } + true } /// Inserts a handle to the embedded client + #[allow(clippy::panic)] pub fn insert_embedded(&self, local_client_handle: LocalEmbeddedClientHandle) { let key = local_client_handle.client_destination(); let entry = ActiveClient::Embedded(Box::new(local_client_handle)); diff --git a/gateway/src/node/client_handling/websocket/common_state.rs b/gateway/src/node/client_handling/websocket/common_state.rs index 7f4d66c828..f3e9f711fa 100644 --- a/gateway/src/node/client_handling/websocket/common_state.rs +++ b/gateway/src/node/client_handling/websocket/common_state.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node::ActiveClientsStore; +use nym_credential_verification::upgrade_mode::UpgradeModeDetails; use nym_credential_verification::{ecash::EcashManager, BandwidthFlushingBehaviourConfig}; use nym_crypto::asymmetric::ed25519; use nym_gateway_storage::GatewayStorage; @@ -11,7 +12,7 @@ use nym_node_metrics::NymNodeMetrics; use std::sync::Arc; use std::time::Duration; -#[derive(Clone)] +#[derive(Clone, Copy)] pub(crate) struct Config { pub(crate) enforce_zk_nym: bool, pub(crate) max_request_timestamp_skew: Duration, @@ -29,6 +30,7 @@ pub(crate) struct CommonHandlerState { pub(crate) metrics_sender: MetricEventsSender, pub(crate) outbound_mix_sender: MixForwardingSender, pub(crate) active_clients_store: ActiveClientsStore, + pub(crate) upgrade_mode: UpgradeModeDetails, } impl CommonHandlerState { diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index 36b8029332..b40642930a 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -14,14 +14,16 @@ use futures::{ future::{FusedFuture, OptionFuture}, FutureExt, StreamExt, }; +use nym_credential_verification::upgrade_mode::UpgradeModeEnableError; use nym_credential_verification::CredentialVerifier; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, ClientBandwidth, }; +use nym_credentials_interface::DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD; use nym_gateway_requests::{ types::{BinaryRequest, ServerResponse}, - ClientControlRequest, ClientRequest, GatewayRequestsError, SensitiveServerResponse, - SimpleGatewayRequestsError, + BandwidthResponse, ClientControlRequest, ClientRequest, GatewayRequestsError, SendResponse, + SensitiveServerResponse, SimpleGatewayRequestsError, }; use nym_gateway_storage::error::GatewayStorageError; use nym_gateway_storage::traits::BandwidthGatewayStorage; @@ -31,6 +33,7 @@ use nym_sphinx::forwarding::packet::MixPacket; use nym_statistics_common::{gateways::GatewaySessionEvent, types::SessionType}; use nym_validator_client::coconut::EcashApiError; use rand::{random, CryptoRng, Rng}; +use std::cmp::max; use std::{process, time::Duration}; use thiserror::Error; use tokio::io::{AsyncRead, AsyncWrite}; @@ -92,8 +95,11 @@ pub enum RequestHandlingError { #[error("failed to recover bandwidth value: {0}")] BandwidthRecoveryFailure(#[from] BandwidthError), - #[error("{0}")] + #[error(transparent)] CredentialVerification(#[from] nym_credential_verification::Error), + + #[error(transparent)] + UpgradeModeEnable(#[from] UpgradeModeEnableError), } impl RequestHandlingError { @@ -161,6 +167,10 @@ impl AuthenticatedHandler { &self.inner } + fn upgrade_mode_enabled(&self) -> bool { + self.inner.upgrade_mode_enabled() + } + /// Upgrades `FreshHandler` into the Authenticated variant implying the client is now authenticated /// and thus allowed to perform more actions with the gateway, such as redeeming bandwidth or /// sending sphinx packets. @@ -271,7 +281,50 @@ impl AuthenticatedHandler { .inspect_err(|verification_failure| debug!("{verification_failure}"))?; trace!("available total bandwidth: {available_total}"); - Ok(ServerResponse::Bandwidth { available_total }) + Ok(ServerResponse::Bandwidth(BandwidthResponse { + available_total, + upgrade_mode: self.upgrade_mode_enabled(), + })) + } + + async fn upgrade_mode_bandwidth(&self) -> i64 { + // if we're undergoing upgrade mode, we don't meter bandwidth, + // we simply return MAX of clients current bandwidth and minimum bandwidth before default + // client would have attempted to send new ticket + // the latter is to support older clients that will ignore `upgrade_mode` field in the response + // as they're not aware of its existence + let available_bandwidth = self.bandwidth_storage_manager.available_bandwidth().await; + max( + DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD + 1, + available_bandwidth, + ) + } + + /// Tries to handle the received JWT token request by checking its correctness and + /// internally enables upgrade mode if it hasn't been set before. + /// Furthermore, clients bandwidth metering is getting disabled. + async fn handle_upgrade_mode_jwt( + &self, + token: String, + ) -> Result { + // if we're already in the upgrade mode, don't bother validating the token + if self.upgrade_mode_enabled() { + return Ok(ServerResponse::Bandwidth(BandwidthResponse { + available_total: self.upgrade_mode_bandwidth().await, + upgrade_mode: true, + })); + } + + self.inner + .shared_state + .upgrade_mode + .try_enable_via_received_jwt(token) + .await?; + + Ok(ServerResponse::Bandwidth(BandwidthResponse { + available_total: self.upgrade_mode_bandwidth().await, + upgrade_mode: true, + })) } /// Tries to handle request to forward sphinx packet into the network. The request can only succeed @@ -289,15 +342,22 @@ impl AuthenticatedHandler { ) -> Result { let required_bandwidth = mix_packet.packet().len() as i64; - let remaining_bandwidth = self - .bandwidth_storage_manager - .try_use_bandwidth(required_bandwidth) - .await?; + let upgrade_mode = self.upgrade_mode_enabled(); + + let remaining_bandwidth = if self.upgrade_mode_enabled() { + self.upgrade_mode_bandwidth().await + } else { + self.bandwidth_storage_manager + .try_use_bandwidth(required_bandwidth) + .await? + }; + self.forward_packet(mix_packet); - Ok(ServerResponse::Send { + Ok(ServerResponse::Send(SendResponse { remaining_bandwidth, - }) + upgrade_mode, + })) } /// Attempts to handle a binary data frame websocket message. @@ -432,6 +492,9 @@ impl AuthenticatedHandler { ClientControlRequest::EcashCredential { enc_credential, iv } => { self.handle_ecash_bandwidth(enc_credential, iv).await } + ClientControlRequest::UpgradeModeJWT { token } => { + self.handle_upgrade_mode_jwt(token).await + } ClientControlRequest::BandwidthCredential { .. } => { Err(RequestHandlingError::IllegalRequest { additional_context: "coconut credential are not longer supported".into(), @@ -446,7 +509,13 @@ impl AuthenticatedHandler { .bandwidth_storage_manager .handle_claim_testnet_bandwidth() .await - .map_err(|e| e.into()), + .map_err(|e| e.into()) + .map(|available_total| { + ServerResponse::Bandwidth(BandwidthResponse { + available_total, + upgrade_mode: self.upgrade_mode_enabled(), + }) + }), ClientControlRequest::SupportedProtocol { .. } => { Ok(self.inner.handle_supported_protocol_request()) } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 54d6e02f6e..ead0fb18f3 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -13,18 +13,19 @@ use futures::{ channel::{mpsc, oneshot}, SinkExt, StreamExt, }; -use nym_credentials_interface::AvailableBandwidth; +use nym_credentials_interface::{AvailableBandwidth, DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD}; use nym_crypto::aes::cipher::crypto_common::rand_core::RngCore; use nym_crypto::asymmetric::ed25519; use nym_gateway_requests::authenticate::AuthenticateRequest; use nym_gateway_requests::authentication::encrypted_address::{ EncryptedAddressBytes, EncryptedAddressConversionError, }; +use nym_gateway_requests::registration::handshake::HandshakeResult; use nym_gateway_requests::{ registration::handshake::{error::HandshakeError, gateway_handshake}, types::{ClientControlRequest, ServerResponse}, - AuthenticationFailure, BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION, - INITIAL_PROTOCOL_VERSION, + AuthenticationFailure, BinaryResponse, GatewayProtocolVersion, GatewayProtocolVersionExt, + SharedGatewayKey, CURRENT_PROTOCOL_VERSION, }; use nym_gateway_storage::error::GatewayStorageError; use nym_gateway_storage::traits::BandwidthGatewayStorage; @@ -34,6 +35,7 @@ use nym_node_metrics::events::MetricsEvent; use nym_sphinx::DestinationAddressBytes; use nym_task::ShutdownToken; use rand::CryptoRng; +use std::cmp::max; use std::net::SocketAddr; use std::time::Duration; use thiserror::Error; @@ -88,9 +90,6 @@ pub(crate) enum InitialAuthenticationError { #[error("Experienced connection error: {0}")] ConnectionError(Box), - #[error("Attempted to negotiate connection with client using incompatible protocol version. Ours is {current} and the client reports {client:?}")] - IncompatibleProtocol { client: Option, current: u8 }, - #[error("failed to send authentication response: {source}")] ResponseSendFailure { #[source] @@ -130,7 +129,7 @@ pub(crate) struct FreshHandler { pub(crate) shutdown: ShutdownToken, // currently unused (but populated) - pub(crate) negotiated_protocol: Option, + pub(crate) negotiated_protocol: Option, } impl FreshHandler { @@ -138,6 +137,10 @@ impl FreshHandler { &self.shared_state } + pub(crate) fn upgrade_mode_enabled(&self) -> bool { + self.shared_state.upgrade_mode.enabled() + } + // for time being we assume handle is always constructed from raw socket. // if we decide we want to change it, that's not too difficult pub(crate) fn new( @@ -189,7 +192,8 @@ impl FreshHandler { async fn perform_registration_handshake( &mut self, init_msg: Vec, - ) -> Result + requested_protocol: Option, + ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, R: CryptoRng + RngCore + Send, @@ -202,15 +206,17 @@ impl FreshHandler { ws_stream, self.shared_state.local_identity.as_ref(), init_msg, + requested_protocol, self.shutdown.clone(), ) .await } - _ => unreachable!(), + _ => Err(HandshakeError::ConnectionInInvalidState), } } /// Attempts to read websocket message from the associated socket. + #[allow(clippy::panic)] pub(crate) async fn read_websocket_message(&mut self) -> Option> where S: AsyncRead + AsyncWrite + Unpin, @@ -226,6 +232,7 @@ impl FreshHandler { /// # Arguments /// /// * `msg`: WebSocket message to write back to the client. + #[allow(clippy::panic)] pub(crate) async fn send_websocket_message( &mut self, msg: impl Into, @@ -269,6 +276,7 @@ impl FreshHandler { /// /// * `shared_keys`: keys derived between the client and gateway. /// * `packets`: unwrapped packets that are to be pushed back to the client. + #[allow(clippy::panic)] pub(crate) async fn push_packets_to_client( &mut self, shared_keys: &SharedGatewayKey, @@ -411,59 +419,6 @@ impl FreshHandler { } } - fn negotiate_client_protocol( - &self, - client_protocol: Option, - ) -> Result { - debug!("client protocol: {client_protocol:?}, ours: {CURRENT_PROTOCOL_VERSION}"); - let Some(client_protocol_version) = client_protocol else { - warn!("the client we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); - // note: in +1.2.0 we will have to return a hard error here - return Ok(INITIAL_PROTOCOL_VERSION); - }; - - // ##### - // On backwards compat: - // Currently it is the case that gateways will understand all previous protocol versions - // and will downgrade accordingly, but this will now always be the case. - // For example, once we remove downgrade on legacy auth, anything below version 4 will be rejected - // ##### - - // a v2 gateway will understand v1 requests, but v1 client will not understand v2 responses - if client_protocol_version == 1 { - return Ok(1); - } - - // a v3 gateway will understand v2 requests (legacy keys) - if client_protocol_version == 2 { - return Ok(2); - } - - // a v4 gateway will understand v3 requests (aes256gcm-siv) - if client_protocol_version == 3 { - return Ok(3); - } - - // a v5 gateway will understand v4 requests (key-rotation) - if client_protocol_version == 4 { - return Ok(4); - } - - // we can't handle clients with higher protocol than ours - // (perhaps we could try to negotiate downgrade on our end? sounds like a nice future improvement) - if client_protocol_version <= CURRENT_PROTOCOL_VERSION { - debug!("the client is using exactly the same (or older) protocol version as we are. We're good to continue!"); - Ok(CURRENT_PROTOCOL_VERSION) - } else { - let err = InitialAuthenticationError::IncompatibleProtocol { - client: client_protocol, - current: CURRENT_PROTOCOL_VERSION, - }; - error!("{err}"); - Err(err) - } - } - async fn handle_duplicate_client( &mut self, address: DestinationAddressBytes, @@ -551,6 +506,58 @@ impl FreshHandler { Ok(available_bandwidth) } + fn negotiate_proposed_protocol( + &self, + client_protocol_version: Option, + ) -> Option { + if client_protocol_version.is_future_version() { + // this should never happen in a non-malicious client as it should use at most whatever version this gateway has announced + warn!("client has announced protocol version greater than one known by this gateway (v{client_protocol_version:?} vs v{}). attempting to downgrade.", GatewayProtocolVersion::CURRENT); + // we just reply with our current version, and it's up to the client to accept it or terminate the connection + Some(GatewayProtocolVersion::CURRENT) + } else { + // ##### + // On backwards compat: + // Currently it is the case that gateways will understand all previous protocol versions + // and will downgrade accordingly, but this will not always be the case. + // For example, once we remove downgrade on legacy auth, anything below version 4 will be rejected + // ##### + debug!( + "using the protocol version proposed by the client: v{client_protocol_version:?}" + ); + client_protocol_version + } + } + + /// Determine the amount of remaining bandwidth the authenticated client should see. + /// This depends on whether the bandwidth stored persistently had already expired (in which case it's set back to 0) + /// and whether the upgrade mode is enabled. In that case the minimum constant amount is returned. + async fn authenticated_bandwidth_bytes( + &self, + client_id: i64, + ) -> Result { + // 1. get the actual registered bandwidth + let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; + + // 2. check if it had already expired + let true_remaining_bandwidth = if available_bandwidth.expired() { + self.shared_state.storage.reset_bandwidth(client_id).await?; + 0 + } else { + available_bandwidth.bytes + }; + + // 3. perform upgrade mode adjustments + if self.upgrade_mode_enabled() { + Ok(max( + true_remaining_bandwidth, + DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD + 1, + )) + } else { + Ok(true_remaining_bandwidth) + } + } + /// Tries to handle the received authentication request by checking correctness of the received data. /// /// # Arguments @@ -565,7 +572,7 @@ impl FreshHandler { )] async fn handle_legacy_authenticate( &mut self, - client_protocol_version: Option, + client_protocol_version: Option, address: String, enc_address: String, raw_nonce: String, @@ -575,9 +582,9 @@ impl FreshHandler { { debug!("handling client authentication (v1)"); - let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?; + let negotiated_protocol = self.negotiate_proposed_protocol(client_protocol_version); // populate the negotiated protocol for future uses - self.negotiated_protocol = Some(negotiated_protocol); + self.negotiated_protocol = negotiated_protocol; let address = DestinationAddressBytes::try_from_base58_string(address) .map_err(|err| InitialAuthenticationError::MalformedClientAddress(err.to_string()))?; @@ -592,7 +599,7 @@ impl FreshHandler { .await? else { // it feels weird to be returning an 'Ok' here, but I didn't want to change the existing behaviour - return Ok(InitialAuthResult::new_failed(Some(negotiated_protocol))); + return Ok(InitialAuthResult::new_legacy_failed(negotiated_protocol)); }; // in v1 we don't have explicit data so we have to use current timestamp @@ -617,14 +624,7 @@ impl FreshHandler { .await?; // check the bandwidth - let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; - - let bandwidth_remaining = if available_bandwidth.expired() { - self.shared_state.storage.reset_bandwidth(client_id).await?; - 0 - } else { - available_bandwidth.bytes - }; + let bandwidth_remaining = self.authenticated_bandwidth_bytes(client_id).await?; Ok(InitialAuthResult::new( Some(ClientDetails::new( @@ -634,9 +634,10 @@ impl FreshHandler { session_request_start, )), ServerResponse::Authenticate { - protocol_version: Some(negotiated_protocol), + protocol_version: negotiated_protocol, status: true, bandwidth_remaining, + upgrade_mode: self.upgrade_mode_enabled(), }, )) } @@ -651,9 +652,9 @@ impl FreshHandler { debug!("handling client authentication (v2)"); let negotiated_protocol = - self.negotiate_client_protocol(Some(request.content.protocol_version))?; + self.negotiate_proposed_protocol(Some(request.content.protocol_version)); // populate the negotiated protocol for future uses - self.negotiated_protocol = Some(negotiated_protocol); + self.negotiated_protocol = negotiated_protocol; let address = request.content.client_identity.derive_destination_address(); @@ -703,14 +704,7 @@ impl FreshHandler { .await?; // finally check and retrieve client's bandwidth - let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; - - let bandwidth_remaining = if available_bandwidth.expired() { - self.shared_state.storage.reset_bandwidth(client_id).await?; - 0 - } else { - available_bandwidth.bytes - }; + let bandwidth_remaining = self.authenticated_bandwidth_bytes(client_id).await?; Ok(InitialAuthResult::new( Some(ClientDetails::new( @@ -720,9 +714,10 @@ impl FreshHandler { session_request_start, )), ServerResponse::Authenticate { - protocol_version: Some(negotiated_protocol), + protocol_version: negotiated_protocol, status: true, bandwidth_remaining, + upgrade_mode: self.upgrade_mode_enabled(), }, )) } @@ -782,17 +777,13 @@ impl FreshHandler { /// * `init_data`: init payload of the registration handshake. async fn handle_register( &mut self, - client_protocol_version: Option, + client_protocol_version: Option, init_data: Vec, ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, R: CryptoRng + RngCore + Send, { - let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?; - // populate the negotiated protocol for future uses - self.negotiated_protocol = Some(negotiated_protocol); - let remote_identity = Self::extract_remote_identity_from_register_init(&init_data)?; let remote_address = remote_identity.derive_destination_address(); @@ -806,11 +797,20 @@ impl FreshHandler { return Err(InitialAuthenticationError::DuplicateConnection); } - let shared_keys = self.perform_registration_handshake(init_data).await?; + let handshake_result = self + .perform_registration_handshake(init_data, client_protocol_version) + .await?; + let shared_keys = handshake_result.derived_key; + + // populate the negotiated protocol for future uses + self.negotiated_protocol = Some(handshake_result.negotiated_protocol); + let client_id = self.register_client(remote_address, &shared_keys).await?; debug!(client_id = %client_id, "managed to finalize client registration"); + let upgrade_mode = self.upgrade_mode_enabled(); + let client_details = ClientDetails::new( client_id, remote_address, @@ -821,8 +821,9 @@ impl FreshHandler { Ok(InitialAuthResult::new( Some(client_details), ServerResponse::Register { - protocol_version: Some(negotiated_protocol), + protocol_version: self.negotiated_protocol, status: true, + upgrade_mode, }, )) } @@ -946,12 +947,15 @@ impl FreshHandler { let (mix_sender, mix_receiver) = mpsc::unbounded(); // Channel for handlers to ask other handlers if they are still active. let (is_active_request_sender, is_active_request_receiver) = mpsc::unbounded(); - self.shared_state.active_clients_store.insert_remote( + if !self.shared_state.active_clients_store.insert_remote( registration_details.address, mix_sender, is_active_request_sender, registration_details.session_request_timestamp, - ); + ) { + error!("failed to insert remote client handle as it already existed!"); + return None; + } return AuthenticatedHandler::upgrade( self, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index 2fd97600a7..b8ecf12ec8 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -79,13 +79,16 @@ impl InitialAuthResult { } } - fn new_failed(protocol_version: Option) -> Self { + fn new_legacy_failed(protocol_version: Option) -> Self { InitialAuthResult { client_details: None, server_response: ServerResponse::Authenticate { protocol_version, status: false, bandwidth_remaining: 0, + // given this response is given only to legacy clients, + // we use the default value as clients wouldn't deserialise it anyway + upgrade_mode: false, }, } } diff --git a/gateway/src/node/internal_service_providers/authenticator/error.rs b/gateway/src/node/internal_service_providers/authenticator/error.rs index ac9b4fd261..5bdde15919 100644 --- a/gateway/src/node/internal_service_providers/authenticator/error.rs +++ b/gateway/src/node/internal_service_providers/authenticator/error.rs @@ -3,7 +3,9 @@ use ipnetwork::IpNetworkError; use nym_client_core::error::ClientCoreError; +use nym_credential_verification::upgrade_mode::UpgradeModeEnableError; use nym_id::NymIdError; +use nym_service_provider_requests_common::ProtocolError; #[derive(thiserror::Error, Debug)] pub enum AuthenticatorError { @@ -17,9 +19,6 @@ pub enum AuthenticatorError { #[error("{0}")] CredentialVerificationError(#[from] nym_credential_verification::Error), - #[error("invalid credential type")] - InvalidCredentialType, - #[error("the entity wrapping the network requester has disconnected")] DisconnectedParent, @@ -50,6 +49,12 @@ pub enum AuthenticatorError { #[error("internal error: {0}")] InternalError(String), + #[error(transparent)] + InvalidPacketHeader { + #[from] + source: ProtocolError, + }, + #[error("received packet has an invalid type: {0}")] InvalidPacketType(u8), @@ -100,4 +105,15 @@ pub enum AuthenticatorError { #[error("no credential received")] NoCredentialReceived, + + #[error(transparent)] + UpgradeModeEnable(#[from] UpgradeModeEnableError), +} + +impl AuthenticatorError { + pub fn response_serialisation(source: impl Into>) -> Self { + AuthenticatorError::FailedToSerializeResponsePacket { + source: source.into(), + } + } } diff --git a/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs index af476bfba7..c05d9d8cd6 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mixnet_listener.rs @@ -1,12 +1,6 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::{ - net::IpAddr, - sync::Arc, - time::{Duration, SystemTime}, -}; - use crate::node::internal_service_providers::authenticator::{ config::Config, error::AuthenticatorError, peer_manager::PeerManager, seen_credential_cache::SeenCredentialCache, @@ -14,47 +8,58 @@ use crate::node::internal_service_providers::authenticator::{ use defguard_wireguard_rs::net::IpAddrMask; use defguard_wireguard_rs::{host::Peer, key::Key}; use futures::StreamExt; -use nym_authenticator_requests::{ - latest::registration::RegistrationData, v4::registration::IpPair, -}; +use nym_authenticator_requests::models::BandwidthClaim; +use nym_authenticator_requests::traits::UpgradeModeMessage; +use nym_authenticator_requests::{latest, v4::registration::IpPair}; use nym_authenticator_requests::{ latest::registration::{GatewayClient, PendingRegistrations, PrivateIPs}, request::AuthenticatorRequest, traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage}, - v1, v2, v3, v4, v5, AuthenticatorVersion, CURRENT_VERSION, + v1, v2, v3, v4, v5, v6, AuthenticatorVersion, CURRENT_VERSION, }; use nym_credential_verification::ecash::traits::EcashManager; +use nym_credential_verification::upgrade_mode::UpgradeModeDetails; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, BandwidthFlushingBehaviourConfig, ClientBandwidth, CredentialVerifier, }; -use nym_credentials_interface::{CredentialSpendingData, TicketType}; +use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData}; use nym_crypto::asymmetric::x25519::KeyPair; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_storage::models::PersistedBandwidth; use nym_sdk::mixnet::{ AnonymousSenderTag, InputMessage, MixnetMessageSender, Recipient, TransmissionLane, }; -use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt}; use nym_sphinx::receiver::ReconstructedMessage; use nym_task::ShutdownToken; use nym_wireguard::WireguardGatewayData; use nym_wireguard_types::PeerPublicKey; use rand::{prelude::IteratorRandom, thread_rng}; +use std::cmp::max; +use std::{ + net::IpAddr, + sync::Arc, + time::{Duration, SystemTime}, +}; use tokio::sync::RwLock; use tokio_stream::wrappers::IntervalStream; type AuthenticatorHandleResult = Result<(Vec, Option), AuthenticatorError>; const DEFAULT_REGISTRATION_TIMEOUT_CHECK: Duration = Duration::from_secs(60); // 1 minute -pub(crate) struct RegistredAndFree { +// we need to be above MINIMUM_REMAINING_BANDWIDTH (500MB) plus we also have to trick the client +// its depletion is low enough to not require sending new tickets +const DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD: i64 = 1024 * 1024 * 1024; + +pub(crate) struct RegisteredAndFree { registration_in_progres: PendingRegistrations, free_private_network_ips: PrivateIPs, } -impl RegistredAndFree { +impl RegisteredAndFree { pub(crate) fn new(free_private_network_ips: PrivateIPs) -> Self { - RegistredAndFree { + RegisteredAndFree { registration_in_progres: Default::default(), free_private_network_ips, } @@ -69,10 +74,12 @@ pub(crate) struct MixnetListener { pub(crate) mixnet_client: nym_sdk::mixnet::MixnetClient, // Registrations awaiting confirmation - pub(crate) registred_and_free: RwLock, + pub(crate) registered_and_free: RwLock, pub(crate) peer_manager: PeerManager, + pub(crate) upgrade_mode: UpgradeModeDetails, + pub(crate) ecash_verifier: Arc, pub(crate) timeout_check_interval: IntervalStream, @@ -86,6 +93,7 @@ impl MixnetListener { free_private_network_ips: PrivateIPs, wireguard_gateway_data: WireguardGatewayData, mixnet_client: nym_sdk::mixnet::MixnetClient, + upgrade_mode: UpgradeModeDetails, ecash_verifier: Arc, ) -> Self { let timeout_check_interval = @@ -93,27 +101,45 @@ impl MixnetListener { MixnetListener { config, mixnet_client, - registred_and_free: RwLock::new(RegistredAndFree::new(free_private_network_ips)), + registered_and_free: RwLock::new(RegisteredAndFree::new(free_private_network_ips)), peer_manager: PeerManager::new(wireguard_gateway_data), + upgrade_mode, ecash_verifier, timeout_check_interval, seen_credential_cache: SeenCredentialCache::new(), } } + fn upgrade_mode_enabled(&self) -> bool { + self.upgrade_mode.enabled() + } + fn keypair(&self) -> &Arc { self.peer_manager.wireguard_gateway_data.keypair() } + async fn upgrade_mode_bandwidth(&self, peer: PeerPublicKey) -> Result { + // if we're undergoing upgrade mode, we don't meter bandwidth, + // we simply return MAX of clients current bandwidth and minimum bandwidth before default + // client would have attempted to send new ticket (hopefully) + // the latter is to support older clients that will ignore `upgrade_mode` field in the response + // as they're not aware of its existence + let available_bandwidth = self.peer_manager.query_bandwidth(peer).await?; + Ok(max( + DEFAULT_WG_CLIENT_BANDWIDTH_THRESHOLD, + available_bandwidth, + )) + } + async fn remove_stale_registrations(&self) -> Result<(), AuthenticatorError> { - let mut registred_and_free = self.registred_and_free.write().await; - let registred_values: Vec<_> = registred_and_free + let mut registered_and_free = self.registered_and_free.write().await; + let registered_values: Vec<_> = registered_and_free .registration_in_progres .values() .cloned() .collect(); - for reg in registred_values { - let ip = registred_and_free + for reg in registered_values { + let ip = registered_and_free .free_private_network_ips .get_mut(®.gateway_data.private_ips) .ok_or(AuthenticatorError::InternalDataCorruption(format!( @@ -122,7 +148,7 @@ impl MixnetListener { )))?; let Some(timestamp) = ip else { - registred_and_free + registered_and_free .registration_in_progres .remove(®.gateway_data.pub_key()); tracing::debug!( @@ -138,7 +164,7 @@ impl MixnetListener { })?; if duration > DEFAULT_REGISTRATION_TIMEOUT_CHECK { *ip = None; - registred_and_free + registered_and_free .registration_in_progres .remove(®.gateway_data.pub_key()); tracing::debug!( @@ -159,8 +185,8 @@ impl MixnetListener { ) -> AuthenticatorHandleResult { let remote_public = init_message.pub_key(); let nonce: u64 = fastrand::u64(..); - let mut registred_and_free = self.registred_and_free.write().await; - if let Some(registration_data) = registred_and_free + let mut registered_and_free = self.registered_and_free.write().await; + if let Some(registration_data) = registered_and_free .registration_in_progres .get(&remote_public) { @@ -181,9 +207,7 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V2 => { v2::response::AuthenticatorResponse::new_pending_registration_success( @@ -201,9 +225,7 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V3 => { v3::response::AuthenticatorResponse::new_pending_registration_success( @@ -221,38 +243,49 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_pending_registration_success( v4::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data.clone().into(), + // convert current to v5 and then v5 to v4 (current as of 28.08.25) + gateway_data: v5::registration::GatewayClient::from( + registration_data.gateway_data.clone(), + ) + .into(), wg_port: registration_data.wg_port, }, request_id, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V5 => { v5::response::AuthenticatorResponse::new_pending_registration_success( v5::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data.clone(), + gateway_data: registration_data.gateway_data.clone().into(), wg_port: registration_data.wg_port, }, request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::new_pending_registration_success( + v6::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data.clone(), + wg_port: registration_data.wg_port, + }, + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; @@ -265,7 +298,7 @@ impl MixnetListener { .allowed_ips .iter() .find_map(|ip_mask| match ip_mask.ip { - std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr), + IpAddr::V4(ipv4_addr) => Some(ipv4_addr), _ => None, }) .ok_or(AuthenticatorError::InternalError( @@ -275,14 +308,14 @@ impl MixnetListener { .allowed_ips .iter() .find_map(|ip_mask| match ip_mask.ip { - std::net::IpAddr::V6(ipv6_addr) => Some(ipv6_addr), + IpAddr::V6(ipv6_addr) => Some(ipv6_addr), _ => None, }) .unwrap_or(IpPair::from(IpAddr::from(allowed_ipv4)).ipv6); let bytes = match AuthenticatorVersion::from(protocol) { AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered( - v1::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v1::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.tunnel_announced_port, }, @@ -290,12 +323,10 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::new_registered( - v2::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v2::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.tunnel_announced_port, }, @@ -303,12 +334,10 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_registered( - v3::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v3::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ip: allowed_ipv4.into(), wg_port: self.config.authenticator.tunnel_announced_port, }, @@ -316,12 +345,10 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered( - v4::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v4::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ips: (allowed_ipv4, allowed_ipv6).into(), wg_port: self.config.authenticator.tunnel_announced_port, }, @@ -329,27 +356,34 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( - v5::registration::RegistredData { - pub_key: PeerPublicKey::new(self.keypair().public_key().to_bytes().into()), + v5::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), private_ips: (allowed_ipv4, allowed_ipv6).into(), wg_port: self.config.authenticator.tunnel_announced_port, }, request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })?, + .map_err(AuthenticatorError::response_serialisation)?, + AuthenticatorVersion::V6 => v6::response::AuthenticatorResponse::new_registered( + v6::registration::RegisteredData { + pub_key: self.keypair().public_key().into(), + private_ips: (allowed_ipv4, allowed_ipv6).into(), + wg_port: self.config.authenticator.tunnel_announced_port, + }, + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; return Ok((bytes, reply_to)); } - let private_ip_ref = registred_and_free + let private_ip_ref = registered_and_free .free_private_network_ips .iter_mut() .filter(|r| r.1.is_none()) @@ -364,12 +398,12 @@ impl MixnetListener { *private_ip_ref.0, nonce, ); - let registration_data = RegistrationData { + let registration_data = latest::registration::RegistrationData { nonce, gateway_data: gateway_data.clone(), wg_port: self.config.authenticator.tunnel_announced_port, }; - registred_and_free + registered_and_free .registration_in_progres .insert(remote_public, registration_data.clone()); let bytes = match AuthenticatorVersion::from(protocol) { @@ -389,9 +423,7 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V2 => { v2::response::AuthenticatorResponse::new_pending_registration_success( @@ -409,9 +441,7 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V3 => { v3::response::AuthenticatorResponse::new_pending_registration_success( @@ -429,38 +459,49 @@ impl MixnetListener { reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_pending_registration_success( v4::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data.into(), + // convert current to v5 and then v5 to v4 (current as of 28.08.25) + gateway_data: v5::registration::GatewayClient::from( + registration_data.gateway_data.clone(), + ) + .into(), wg_port: registration_data.wg_port, }, request_id, reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V5 => { v5::response::AuthenticatorResponse::new_pending_registration_success( v5::registration::RegistrationData { nonce: registration_data.nonce, - gateway_data: registration_data.gateway_data, + gateway_data: registration_data.gateway_data.into(), wg_port: registration_data.wg_port, }, request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::new_pending_registration_success( + v6::registration::RegistrationData { + nonce: registration_data.nonce, + gateway_data: registration_data.gateway_data, + wg_port: registration_data.wg_port, + }, + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; @@ -468,6 +509,29 @@ impl MixnetListener { Ok((bytes, reply_to)) } + async fn handle_final_credential_claim( + &self, + claim: BandwidthClaim, + client_id: i64, + ) -> Result<(), AuthenticatorError> { + match claim.credential { + BandwidthCredential::ZkNym(zk_nym) => { + // if we got zk-nym, we just try to verify it + credential_verification(self.ecash_verifier.clone(), *zk_nym, client_id).await?; + Ok(()) + } + BandwidthCredential::UpgradeModeJWT { token } => { + // if we're already in the upgrade mode, don't bother validating the token + if self.upgrade_mode_enabled() { + return Ok(()); + } + + self.upgrade_mode.try_enable_via_received_jwt(token).await?; + Ok(()) + } + } + } + async fn on_final_request( &mut self, final_message: Box, @@ -475,8 +539,8 @@ impl MixnetListener { request_id: u64, reply_to: Option, ) -> AuthenticatorHandleResult { - let mut registred_and_free = self.registred_and_free.write().await; - let registration_data = registred_and_free + let mut registered_and_free = self.registered_and_free.write().await; + let registration_data = registered_and_free .registration_in_progres .get(&final_message.gateway_client_pub_key()) .ok_or(AuthenticatorError::RegistrationNotInProgress)? @@ -497,28 +561,31 @@ impl MixnetListener { 128, )); + // ideally credential wouldn't have been required in upgrade mode, + // however, we need some basic information to insert valid wg peer let Some(credential) = final_message.credential() else { return Err(AuthenticatorError::NoCredentialReceived); }; + + let typ = credential.kind; + let client_id = self .ecash_verifier .storage() - .insert_wireguard_peer( - &peer, - TicketType::try_from_encoded(credential.payment.t_type) - .map_err(|_| AuthenticatorError::InvalidCredentialType)? - .into(), - ) + .insert_wireguard_peer(&peer, typ.into()) .await?; - if let Err(e) = - credential_verification(self.ecash_verifier.clone(), credential, client_id).await + + if let Err(err) = self + .handle_final_credential_claim(credential, client_id) + .await { self.ecash_verifier .storage() .remove_wireguard_peer(&peer.public_key.to_string()) .await?; - return Err(e); + return Err(err); } + let public_key = peer.public_key.to_string(); if let Err(e) = self.peer_manager.add_peer(peer).await { self.ecash_verifier @@ -528,13 +595,13 @@ impl MixnetListener { return Err(e); } - registred_and_free + registered_and_free .registration_in_progres .remove(&final_message.gateway_client_pub_key()); let bytes = match AuthenticatorVersion::from(protocol) { AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered( - v1::registration::RegistredData { + v1::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, @@ -543,9 +610,9 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::new_registered( - v2::registration::RegistredData { + v2::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, @@ -554,9 +621,9 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_registered( - v3::registration::RegistredData { + v3::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ip: registration_data.gateway_data.private_ips.ipv4.into(), wg_port: registration_data.wg_port, @@ -565,28 +632,43 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_registered( - v4::registration::RegistredData { + v4::registration::RegisteredData { + pub_key: registration_data.gateway_data.pub_key, + // convert current to v5 and then v5 to v4 (current as of 28.08.25) + private_ips: v5::registration::IpPair::from( + registration_data.gateway_data.private_ips, + ) + .into(), + wg_port: registration_data.wg_port, + }, + reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, + request_id, + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)?, + AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( + v5::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ips: registration_data.gateway_data.private_ips.into(), wg_port: registration_data.wg_port, }, - reply_to.ok_or(AuthenticatorError::MissingReplyToForOldClient)?, request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, - AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_registered( - v5::registration::RegistredData { + .map_err(AuthenticatorError::response_serialisation)?, + AuthenticatorVersion::V6 => v6::response::AuthenticatorResponse::new_registered( + v6::registration::RegisteredData { pub_key: registration_data.gateway_data.pub_key, private_ips: registration_data.gateway_data.private_ips, wg_port: registration_data.wg_port, }, request_id, + self.upgrade_mode_enabled(), ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; Ok((bytes, reply_to)) @@ -599,7 +681,12 @@ impl MixnetListener { request_id: u64, reply_to: Option, ) -> AuthenticatorHandleResult { - let available_bandwidth = self.peer_manager.query_bandwidth(msg.pub_key()).await?; + let available_bandwidth = if self.upgrade_mode_enabled() { + self.upgrade_mode_bandwidth(msg.pub_key()).await? + } else { + self.peer_manager.query_bandwidth(msg.pub_key()).await? + }; + let bytes = match AuthenticatorVersion::from(protocol) { AuthenticatorVersion::V1 => { v1::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -611,9 +698,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V2 => { v2::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -624,9 +709,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V3 => { v3::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -637,9 +720,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V4 => { v4::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -650,9 +731,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::V5 => { v5::response::AuthenticatorResponse::new_remaining_bandwidth( @@ -662,15 +741,25 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| { - AuthenticatorError::FailedToSerializeResponsePacket { source: err } - })? + .map_err(AuthenticatorError::response_serialisation)? + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::new_remaining_bandwidth( + Some(v6::registration::RemainingBandwidthData { + available_bandwidth, + }), + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)? } AuthenticatorVersion::UNKNOWN => return Err(AuthenticatorError::UnknownVersion), }; Ok((bytes, reply_to)) } + // if we received a topup request, don't do anything with the upgrade mode async fn on_topup_bandwidth_request( &mut self, msg: Box, @@ -693,6 +782,15 @@ impl MixnetListener { }; let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::V6 => v6::response::AuthenticatorResponse::new_topup_bandwidth( + v6::registration::RemainingBandwidthData { + available_bandwidth, + }, + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::new_topup_bandwidth( v5::registration::RemainingBandwidthData { available_bandwidth, @@ -700,7 +798,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::new_topup_bandwidth( v4::registration::RemainingBandwidthData { available_bandwidth, @@ -709,7 +807,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::new_topup_bandwidth( v3::registration::RemainingBandwidthData { available_bandwidth, @@ -718,7 +816,7 @@ impl MixnetListener { request_id, ) .to_bytes() - .map_err(|err| AuthenticatorError::FailedToSerializeResponsePacket { source: err })?, + .map_err(AuthenticatorError::response_serialisation)?, AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => { return Err(AuthenticatorError::UnknownVersion) } @@ -727,6 +825,46 @@ impl MixnetListener { Ok((bytes, reply_to)) } + async fn on_upgrade_mode_check( + &mut self, + msg: Box, + protocol: Protocol, + request_id: u64, + ) -> AuthenticatorHandleResult { + // if upgrade mode is already enabled, we don't need to perform any additional checks + if !self.upgrade_mode_enabled() { + // currently upgrade mode JWT is the only type of emergency credentials supported + if let Some(upgrade_mode_jwt) = msg.upgrade_mode_global_attestation_jwt() { + self.upgrade_mode + .try_enable_via_received_jwt(upgrade_mode_jwt) + .await?; + } + } + + let bytes = match AuthenticatorVersion::from(protocol) { + AuthenticatorVersion::UNKNOWN + | AuthenticatorVersion::V1 + | AuthenticatorVersion::V2 + | AuthenticatorVersion::V3 + | AuthenticatorVersion::V4 + | AuthenticatorVersion::V5 => { + // pre v6 this message hasn't existed + return Err(AuthenticatorError::UnknownVersion); + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::new_upgrade_mode_check( + request_id, + self.upgrade_mode_enabled(), + ) + .to_bytes() + .map_err(AuthenticatorError::response_serialisation)? + } + }; + + // no need to support reply_to, as this is never set in v6 and older versions do not include this message + Ok((bytes, None)) + } + fn received_retry(&self, msg: &(dyn TopUpMessage + Send + Sync + 'static)) -> bool { if let Some(peer_pub_key) = self .seen_credential_cache @@ -787,6 +925,11 @@ impl MixnetListener { self.on_topup_bandwidth_request(msg, protocol, request_id, reply_to) .await } + AuthenticatorRequest::CheckUpgradeMode { + msg, + protocol, + request_id, + } => self.on_upgrade_mode_check(msg, protocol, request_id).await, } } @@ -893,64 +1036,68 @@ async fn credential_verification( fn deserialize_request( reconstructed: &ReconstructedMessage, ) -> Result { - let request_version = *reconstructed + let header = reconstructed .message .first_chunk::<2>() .ok_or(AuthenticatorError::ShortPacket)?; - // Check version of the request and convert to the latest version if necessary - match request_version { - [1, _] => v1::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + let version = header[0]; + + // special case for v1 request where service provider information hasn't been exposed in the header + if version == v1::VERSION { + return v1::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) - .map(Into::into), - [2, request_type] => { - if request_type == ServiceProviderType::Authenticator as u8 { - v2::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) - .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { - source: err, - }) - .map(Into::::into) - .map(Into::into) - } else { - Err(AuthenticatorError::InvalidPacketType(request_type)) - } + .map(Into::into); + } + + let protocol = Protocol::try_from(header)?; + + if !protocol.service_provider_type.is_authenticator() { + return Err(AuthenticatorError::InvalidPacketType( + protocol.service_provider_type as u8, + )); + } + + let version = AuthenticatorVersion::from(protocol.version); + + // Check version of the request and convert to the latest version if necessary + match version { + AuthenticatorVersion::V1 => { + // this branch should be unreachable as v1 has already been handled independently + Err(AuthenticatorError::UnknownVersion) } - [3, request_type] => { - if request_type == ServiceProviderType::Authenticator as u8 { - v3::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) - .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { - source: err, - }) - .map(Into::into) - } else { - Err(AuthenticatorError::InvalidPacketType(request_type)) - } + AuthenticatorVersion::V2 => { + v2::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::::into) + .map(Into::into) } - [4, request_type] => { - if request_type == ServiceProviderType::Authenticator as u8 { - v4::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) - .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { - source: err, - }) - .map(Into::into) - } else { - Err(AuthenticatorError::InvalidPacketType(request_type)) - } + AuthenticatorVersion::V3 => { + v3::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::into) } - [5, request_type] => { - if request_type == ServiceProviderType::Authenticator as u8 { - v5::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) - .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { - source: err, - }) - .map(Into::into) - } else { - Err(AuthenticatorError::InvalidPacketType(request_type)) - } + AuthenticatorVersion::V4 => { + v4::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::into) } - [version, _] => { - tracing::info!("Received packet with invalid version: v{version}"); - Err(AuthenticatorError::InvalidPacketVersion(version)) + AuthenticatorVersion::V5 => { + v5::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::into) + } + AuthenticatorVersion::V6 => { + v6::request::AuthenticatorRequest::from_reconstructed_message(reconstructed) + .map_err(|err| AuthenticatorError::FailedToDeserializeTaggedPacket { source: err }) + .map(Into::into) + } + AuthenticatorVersion::UNKNOWN => { + tracing::info!( + "Received packet with invalid version: v{}", + protocol.version + ); + Err(AuthenticatorError::InvalidPacketVersion(protocol.version)) } } } diff --git a/gateway/src/node/internal_service_providers/authenticator/mod.rs b/gateway/src/node/internal_service_providers/authenticator/mod.rs index 358bc5cb30..f63a86fcc2 100644 --- a/gateway/src/node/internal_service_providers/authenticator/mod.rs +++ b/gateway/src/node/internal_service_providers/authenticator/mod.rs @@ -11,6 +11,9 @@ use nym_task::ShutdownTracker; use nym_wireguard::WireguardGatewayData; use std::{net::IpAddr, path::Path, sync::Arc, time::SystemTime}; +pub use config::Config; +use nym_credential_verification::upgrade_mode::UpgradeModeDetails; + pub mod config; pub mod error; pub mod mixnet_client; @@ -18,8 +21,6 @@ pub mod mixnet_listener; mod peer_manager; mod seen_credential_cache; -pub use config::Config; - pub struct OnStartData { // to add more fields as required pub address: Recipient, @@ -33,7 +34,8 @@ impl OnStartData { pub struct Authenticator { #[allow(unused)] - config: crate::node::internal_service_providers::authenticator::Config, + config: Config, + upgrade_mode_state: UpgradeModeDetails, wait_for_gateway: bool, custom_topology_provider: Option>, custom_gateway_transceiver: Option>, @@ -46,7 +48,8 @@ pub struct Authenticator { impl Authenticator { pub fn new( - config: crate::node::internal_service_providers::authenticator::Config, + config: Config, + upgrade_mode_state: UpgradeModeDetails, wireguard_gateway_data: WireguardGatewayData, used_private_network_ips: Vec, ecash_verifier: Arc, @@ -54,6 +57,7 @@ impl Authenticator { ) -> Self { Self { config, + upgrade_mode_state, wait_for_gateway: false, custom_topology_provider: None, custom_gateway_transceiver: None, @@ -152,6 +156,7 @@ impl Authenticator { free_private_network_ips, self.wireguard_gateway_data, mixnet_client, + self.upgrade_mode_state, self.ecash_verifier, ); diff --git a/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs b/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs index fce8cf7174..c057dfa57c 100644 --- a/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs +++ b/gateway/src/node/internal_service_providers/authenticator/peer_manager.rs @@ -19,7 +19,7 @@ impl PeerManager { wireguard_gateway_data, } } - pub async fn add_peer(&mut self, peer: Peer) -> Result<(), AuthenticatorError> { + pub async fn add_peer(&self, peer: Peer) -> Result<(), AuthenticatorError> { let (response_tx, response_rx) = oneshot::channel(); let msg = PeerControlRequest::AddPeer { peer, response_tx }; self.wireguard_gateway_data @@ -38,7 +38,7 @@ impl PeerManager { }) } - pub async fn _remove_peer(&mut self, pub_key: PeerPublicKey) -> Result<(), AuthenticatorError> { + pub async fn _remove_peer(&self, pub_key: PeerPublicKey) -> Result<(), AuthenticatorError> { let key = Key::new(pub_key.to_bytes()); let (response_tx, response_rx) = oneshot::channel(); let msg = PeerControlRequest::RemovePeer { key, response_tx }; @@ -61,7 +61,7 @@ impl PeerManager { } pub async fn query_peer( - &mut self, + &self, public_key: PeerPublicKey, ) -> Result, AuthenticatorError> { let key = Key::new(public_key.to_bytes()); @@ -86,7 +86,7 @@ impl PeerManager { } pub async fn query_bandwidth( - &mut self, + &self, public_key: PeerPublicKey, ) -> Result { let client_bandwidth = self.query_client_bandwidth(public_key).await?; @@ -94,7 +94,7 @@ impl PeerManager { } pub async fn query_client_bandwidth( - &mut self, + &self, key: PeerPublicKey, ) -> Result { let key = Key::new(key.to_bytes()); @@ -121,7 +121,7 @@ impl PeerManager { } pub async fn query_verifier_by_key( - &mut self, + &self, key: PeerPublicKey, credential: CredentialSpendingData, ) -> Result, AuthenticatorError> { @@ -243,7 +243,7 @@ mod tests { Authenticator::default().into(), Arc::new(KeyPair::new(&mut OsRng)), ); - let mut peer_manager = PeerManager::new(wireguard_data); + let peer_manager = PeerManager::new(wireguard_data); let (storage, task_manager) = start_controller( peer_manager.wireguard_gateway_data.peer_tx().clone(), request_rx, diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index e4542b0dd6..ba891bd716 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -1,8 +1,10 @@ // Copyright 2020-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::config::Config; use crate::error::GatewayError; use crate::node::client_handling::websocket; +use crate::node::internal_service_providers::authenticator::Authenticator; use crate::node::internal_service_providers::{ authenticator, ExitServiceProviders, ServiceProviderBeingBuilt, SpMessageRouterBuilder, }; @@ -11,6 +13,9 @@ use futures::channel::oneshot; use nym_credential_verification::ecash::{ credential_sender::CredentialHandlerConfig, EcashManager, }; +use nym_credential_verification::upgrade_mode::{ + UpgradeModeCheckConfig, UpgradeModeDetails, UpgradeModeState, +}; use nym_crypto::asymmetric::ed25519; use nym_ip_packet_router::IpPacketRouter; use nym_mixnet_client::forwarder::MixForwardingSender; @@ -30,13 +35,9 @@ use std::sync::Arc; use tracing::*; use zeroize::Zeroizing; -pub(crate) mod client_handling; -pub(crate) mod internal_service_providers; -mod stale_data_cleaner; - -use crate::config::Config; -use crate::node::internal_service_providers::authenticator::Authenticator; +pub use crate::node::upgrade_mode::watcher::UpgradeModeWatcher; pub use client_handling::active_clients::ActiveClientsStore; +pub use nym_credential_verification::upgrade_mode::UpgradeModeCheckRequestSender; pub use nym_gateway_stats_storage::PersistentStatsStorage; pub use nym_gateway_storage::{ error::GatewayStorageError, @@ -45,6 +46,11 @@ pub use nym_gateway_storage::{ }; pub use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent}; +pub(crate) mod client_handling; +pub(crate) mod internal_service_providers; +mod stale_data_cleaner; +pub mod upgrade_mode; + #[derive(Debug, Clone)] pub struct LocalNetworkRequesterOpts { pub config: nym_network_requester::Config, @@ -78,6 +84,8 @@ pub struct GatewayTasksBuilder { // TODO: combine with authenticator, since you have to start both wireguard_data: Option, + user_agent: UserAgent, + /// ed25519 keypair used to assert one's identity. identity_keypair: Arc, @@ -89,6 +97,8 @@ pub struct GatewayTasksBuilder { metrics: NymNodeMetrics, + upgrade_mode_state: UpgradeModeState, + mnemonic: Arc>, shutdown_tracker: ShutdownTracker, @@ -111,6 +121,8 @@ impl GatewayTasksBuilder { metrics_sender: MetricEventsSender, metrics: NymNodeMetrics, mnemonic: Arc>, + user_agent: UserAgent, + upgrade_mode_state: UpgradeModeState, shutdown_tracker: ShutdownTracker, ) -> GatewayTasksBuilder { GatewayTasksBuilder { @@ -119,11 +131,13 @@ impl GatewayTasksBuilder { ip_packet_router_opts: None, authenticator_opts: None, wireguard_data: None, + user_agent, identity_keypair: identity, storage, mix_packet_sender, metrics_sender, metrics, + upgrade_mode_state, mnemonic, shutdown_tracker, ecash_manager: None, @@ -247,6 +261,7 @@ impl GatewayTasksBuilder { pub async fn build_websocket_listener( &mut self, active_clients_store: ActiveClientsStore, + upgrade_mode_common_state: UpgradeModeDetails, ) -> Result { let shared_state = websocket::CommonHandlerState { cfg: websocket::Config { @@ -261,6 +276,7 @@ impl GatewayTasksBuilder { metrics_sender: self.metrics_sender.clone(), outbound_mix_sender: self.mix_packet_sender.clone(), active_clients_store: active_clients_store.clone(), + upgrade_mode: upgrade_mode_common_state, }; Ok(websocket::Listener::new( @@ -407,6 +423,7 @@ impl GatewayTasksBuilder { pub async fn build_wireguard_authenticator( &mut self, + upgrade_mode_common: UpgradeModeDetails, topology_provider: Box, ) -> Result, GatewayError> { let ecash_manager = self.ecash_manager().await?; @@ -431,6 +448,7 @@ impl GatewayTasksBuilder { let mut authenticator_server = Authenticator::new( opts.config.clone(), + upgrade_mode_common, wireguard_data.inner.clone(), used_private_network_ips, ecash_manager, @@ -462,9 +480,47 @@ impl GatewayTasksBuilder { ) } + pub fn build_upgrade_mode_common_state( + &self, + request_checker: UpgradeModeCheckRequestSender, + ) -> UpgradeModeDetails { + UpgradeModeDetails::new( + UpgradeModeCheckConfig { + min_staleness_recheck: self.config.debug.upgrade_mode_min_staleness_recheck, + }, + request_checker, + self.upgrade_mode_state.clone(), + ) + } + + pub fn try_build_upgrade_mode_watcher(&self) -> Option { + if !self.config.upgrade_mode_watcher.enabled { + warn!("upgrade mode watcher is disabled"); + return None; + } + + Some(UpgradeModeWatcher::new( + self.config + .upgrade_mode_watcher + .debug + .regular_polling_interval, + self.config + .upgrade_mode_watcher + .debug + .expedited_poll_interval, + self.config.debug.upgrade_mode_min_staleness_recheck, + self.config.upgrade_mode_watcher.attestation_url.clone(), + self.upgrade_mode_state.clone(), + self.user_agent.clone(), + self.shutdown_tracker.clone_shutdown_token(), + )) + } + #[cfg(not(target_os = "linux"))] + #[allow(clippy::unimplemented)] pub async fn try_start_wireguard( &mut self, + _upgrade_mode_details: UpgradeModeDetails, ) -> Result, Box> { let _ = self.metrics.clone(); let _ = self.shutdown_tracker.clone(); @@ -474,6 +530,7 @@ impl GatewayTasksBuilder { #[cfg(target_os = "linux")] pub async fn try_start_wireguard( &mut self, + upgrade_mode_details: UpgradeModeDetails, ) -> Result< nym_wireguard_private_metadata_server::ShutdownHandles, Box, @@ -497,6 +554,7 @@ impl GatewayTasksBuilder { nym_wireguard_private_metadata_server::PeerControllerTransceiver::new( wireguard_data.inner.peer_tx().clone(), ), + upgrade_mode_details, )); let bind_address = std::net::SocketAddr::new( @@ -508,6 +566,7 @@ impl GatewayTasksBuilder { ecash_manager, self.metrics.clone(), all_peers, + self.upgrade_mode_state.upgrade_mode_status(), self.shutdown_tracker.clone_shutdown_token(), wireguard_data, ) diff --git a/gateway/src/node/upgrade_mode/mod.rs b/gateway/src/node/upgrade_mode/mod.rs new file mode 100644 index 0000000000..c8df59133b --- /dev/null +++ b/gateway/src/node/upgrade_mode/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(crate) mod watcher; diff --git a/gateway/src/node/upgrade_mode/watcher.rs b/gateway/src/node/upgrade_mode/watcher.rs new file mode 100644 index 0000000000..509e89c5f5 --- /dev/null +++ b/gateway/src/node/upgrade_mode/watcher.rs @@ -0,0 +1,174 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::UserAgent; +use futures::channel::mpsc::unbounded; +use futures::StreamExt; +use nym_credential_verification::upgrade_mode::{ + CheckRequest, UpgradeModeCheckRequestReceiver, UpgradeModeCheckRequestSender, UpgradeModeState, +}; +use nym_task::ShutdownToken; +use nym_upgrade_mode_check::attempt_retrieve_attestation; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::task::JoinHandle; +use tokio::time::Instant; +use tracing::{debug, error, info, trace}; +use url::Url; + +/// Specifies the threshold for retrieval failures that will trigger disabling upgrade mode. +/// This assumes the file has been removed incorrectly and has been replaced by some placeholder 404 +/// page that does not deserialise correctly +const FAILURE_THRESHOLD: usize = 5; + +pub struct UpgradeModeWatcher { + // default polling interval + regular_polling_interval: Duration, + + // expedited polling interval once upgrade mode is detected + expedited_poll_interval: Duration, + + min_staleness_recheck: Duration, + + attestation_url: Url, + + check_request_sender: UpgradeModeCheckRequestSender, + + check_request_receiver: UpgradeModeCheckRequestReceiver, + + upgrade_mode_state: UpgradeModeState, + + user_agent: UserAgent, + + shutdown_token: ShutdownToken, + + consecutive_retrieval_failures: usize, +} + +impl UpgradeModeWatcher { + pub(crate) fn new( + regular_polling_interval: Duration, + expedited_poll_interval: Duration, + min_staleness_recheck: Duration, + attestation_url: Url, + upgrade_mode_state: UpgradeModeState, + user_agent: UserAgent, + shutdown_token: ShutdownToken, + ) -> Self { + let (tx, rx) = unbounded(); + UpgradeModeWatcher { + regular_polling_interval, + expedited_poll_interval, + min_staleness_recheck, + attestation_url, + check_request_sender: UpgradeModeCheckRequestSender::new(tx), + check_request_receiver: rx, + upgrade_mode_state, + user_agent, + shutdown_token, + consecutive_retrieval_failures: 0, + } + } + + pub fn request_sender(&self) -> UpgradeModeCheckRequestSender { + self.check_request_sender.clone() + } + + async fn try_update_state(&mut self) { + match attempt_retrieve_attestation( + self.attestation_url.as_str(), + Some(self.user_agent.clone()), + ) + .await + { + Err(err) => { + self.consecutive_retrieval_failures += 1; + info!("upgrade mode attestation is not available at this time"); + debug!("retrieval error: {err}"); + + if self.upgrade_mode_state.upgrade_mode_enabled() + && self.consecutive_retrieval_failures > FAILURE_THRESHOLD + { + self.upgrade_mode_state + .try_set_expected_attestation(None) + .await + } else { + self.upgrade_mode_state + .update_last_queried(OffsetDateTime::now_utc()); + } + } + Ok(attestation) => { + self.consecutive_retrieval_failures = 0; + if attestation.is_some() { + info!("retrieved valid attestation: attempting to begin upgrade mode") + } else { + info!("attempting to disable upgrade mode") + } + self.upgrade_mode_state + .try_set_expected_attestation(attestation) + .await + } + } + } + + fn timer_reset_deadline(&self) -> Instant { + if self.upgrade_mode_state.upgrade_mode_enabled() { + Instant::now() + self.expedited_poll_interval + } else { + Instant::now() + self.regular_polling_interval + } + } + + async fn handle_check_request(&mut self, polled_request: CheckRequest) { + let mut requests = vec![polled_request]; + while let Ok(Some(queued_up)) = self.check_request_receiver.try_next() { + requests.push(queued_up); + } + + if self.upgrade_mode_state.since_last_query() > self.min_staleness_recheck { + self.try_update_state().await; + } + + for request in requests { + request.finalize() + } + } + + async fn run(&mut self) { + info!("starting the update mode watcher"); + + // make sure the first check happens immediately + let check_wait = tokio::time::sleep(Duration::new(0, 0)); + tokio::pin!(check_wait); + + loop { + tokio::select! { + biased; + _ = self.shutdown_token.cancelled() => { + trace!("UpdateModeWatcher: received shutdown"); + break; + } + polled_request = self.check_request_receiver.next() => { + let Some(request) = polled_request else { + // this should NEVER happen as `UpgradeModeWatcher` itself holds one sender instance + // but just in case, don't blow up + error!("UpgradeModeCheckRequestReceiver is finished even though we still hold one of the senders!"); + break; + }; + self.handle_check_request(request).await + } + + _ = &mut check_wait => { + self.try_update_state().await; + check_wait.as_mut().reset(self.timer_reset_deadline()); + } + } + } + + debug!("UpdateModeWatcher: Exiting"); + } + + pub fn start(mut self) -> JoinHandle<()> { + tokio::spawn(async move { self.run().await }) + } +} diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 362c78f731..c7037d32f9 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.70" +version = "1.1.71" authors.workspace = true edition = "2021" rust-version.workspace = true diff --git a/nym-authenticator-client/src/error.rs b/nym-authenticator-client/src/error.rs index 43598acdd8..abf18e47ca 100644 --- a/nym-authenticator-client/src/error.rs +++ b/nym-authenticator-client/src/error.rs @@ -40,14 +40,23 @@ pub enum AuthenticationClientError { source: nym_bandwidth_controller::error::BandwidthControllerError, }, + #[error("failed to retrieve upgrade mode token")] + UpgradeModeToken { + #[source] + source: nym_bandwidth_controller::error::BandwidthControllerError, + }, + #[error("unknown authenticator version number")] UnsupportedAuthenticatorVersion, + + #[error("encountered an internal error")] + InternalError, } #[derive(thiserror::Error, Debug)] pub enum RegistrationError { #[error(transparent)] - NoCredentialSent(AuthenticationClientError), // This intentionnally doesn't use `from` to avoid random ? operator to land here when they shouldn't + NoCredentialSent(AuthenticationClientError), // This intentionally doesn't use `from` to avoid random ? operator to land here when they shouldn't #[error("an error occured after a credential was sent : {source}")] CredentialSent { diff --git a/nym-authenticator-client/src/lib.rs b/nym-authenticator-client/src/lib.rs index ab811ef79d..c09a766696 100644 --- a/nym-authenticator-client/src/lib.rs +++ b/nym-authenticator-client/src/lib.rs @@ -8,22 +8,26 @@ use nym_registration_common::GatewayData; use std::net::{IpAddr, SocketAddr}; use std::sync::Arc; use std::time::Duration; -use tracing::{debug, error, trace}; +use tracing::{debug, error, trace, warn}; use crate::error::Result; use crate::mixnet_listener::{MixnetMessageBroadcastReceiver, MixnetMessageInputSender}; +use crate::types::{AvailableBandwidthClientResponse, TopUpClientResponse}; +use nym_authenticator_requests::models::BandwidthClaim; +use nym_authenticator_requests::traits::UpgradeModeStatus; use nym_authenticator_requests::{ AuthenticatorVersion, client_message::ClientMessage, response::AuthenticatorResponse, - traits::Id, v2, v3, v4, v5, + traits::Id, v2, v3, v4, v5, v6, }; -use nym_credentials_interface::{CredentialSpendingData, TicketType}; -use nym_sdk::mixnet::{IncludedSurbs, Recipient}; +use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData, TicketType}; +use nym_sdk::mixnet::{IncludedSurbs, Recipient, ReconstructedMessage}; use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt}; use nym_wireguard_types::PeerPublicKey; mod error; mod helpers; mod mixnet_listener; +pub mod types; pub use crate::error::{AuthenticationClientError, RegistrationError}; pub use crate::mixnet_listener::{AuthClientMixnetListener, AuthClientMixnetListenerHandle}; @@ -61,6 +65,10 @@ impl AuthenticatorClient { } } + fn peer_public_key(&self) -> PeerPublicKey { + PeerPublicKey::from(self.keypair.public_key().inner()) + } + pub async fn send_and_wait_for_response( &mut self, message: &ClientMessage, @@ -72,7 +80,9 @@ impl AuthenticatorClient { } async fn send_request(&self, message: &ClientMessage) -> Result { - let (data, request_id) = message.bytes(self.our_nym_address)?; + let serialised = message.bytes(self.our_nym_address)?; + let data = serialised.bytes; + let request_id = serialised.request_id; // We use 20 surbs for the connect request because typically the // authenticator mixnet client on the nym-node is configured to have a min @@ -96,6 +106,81 @@ impl AuthenticatorClient { Ok(request_id) } + fn handle_response( + &self, + msg: Arc, + request_id: u64, + ) -> Option> { + let Some(header) = msg.message.first_chunk::<2>() else { + debug!( + "received too short message that couldn't have been from the authenticator while waiting for connect response" + ); + return None; + }; + + let Ok(protocol) = Protocol::try_from(header) else { + debug!( + "received a message not meant to any service provider while waiting for connect response" + ); + return None; + }; + + if !protocol.service_provider_type.is_authenticator() { + debug!("Received non-authenticator message while waiting for connect response"); + return None; + } + // Confirm that the version is correct + let version = AuthenticatorVersion::from(protocol.version); + + // Then we deserialize the message + debug!( + "AuthClient: got message while waiting for connect response with version {version:?}" + ); + let ret: Result = match version { + AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { + return Some(Err( + AuthenticationClientError::UnsupportedAuthenticatorVersion, + )); + } + AuthenticatorVersion::V2 => { + v2::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + AuthenticatorVersion::V3 => { + v3::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + AuthenticatorVersion::V4 => { + v4::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + AuthenticatorVersion::V5 => { + v5::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + AuthenticatorVersion::V6 => { + v6::response::AuthenticatorResponse::from_reconstructed_message(&msg) + .map(Into::into) + .map_err(Into::into) + } + }; + let Ok(response) = ret else { + // This is ok, it's likely just one of our self-pings + debug!("Failed to deserialize reconstructed message"); + return None; + }; + + if response.id() == request_id { + debug!("Got response with matching id"); + return Some(Ok(response)); + } + None + } + async fn listen_for_response(&mut self, request_id: u64) -> Result { let timeout = tokio::time::sleep(Duration::from_secs(10)); tokio::pin!(timeout); @@ -111,42 +196,9 @@ impl AuthenticatorClient { return Err(AuthenticationClientError::NoMixnetMessagesReceived); } Ok(msg) => { - let Some(header) = msg.message.first_chunk::<2>() else { - debug!("received too short message that couldn't have been from the authenticator while waiting for connect response"); - continue; - }; - - let Ok(protocol) = Protocol::try_from(header) else { - debug!("received a message not meant to any service provider while waiting for connect response"); - continue; - }; - - if !protocol.service_provider_type.is_authenticator() { - debug!("Received non-authenticator message while waiting for connect response"); - continue; - } - // Confirm that the version is correct - let version = AuthenticatorVersion::from(protocol.version); - - // Then we deserialize the message - debug!("AuthClient: got message while waiting for connect response with version {version:?}"); - let ret: Result = match version { - AuthenticatorVersion::V1 => Err(AuthenticationClientError::UnsupportedVersion), - AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), - AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), - AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), - AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into), - AuthenticatorVersion::UNKNOWN => Err(AuthenticationClientError::UnknownVersion), - }; - let Ok(response) = ret else { - // This is ok, it's likely just one of our self-pings - debug!("Failed to deserialize reconstructed message"); - continue; - }; - - if response.id() == request_id { - debug!("Got response with matching id"); - return Ok(response); + match self.handle_response(msg, request_id) { + None => continue, + Some(res) => return res, } } } @@ -154,12 +206,65 @@ impl AuthenticatorClient { } } + async fn produce_bandwidth_claim( + &self, + controller: &dyn BandwidthTicketProvider, + upgrade_mode_enabled: bool, + ticketbook_type: TicketType, + ) -> Result { + if upgrade_mode_enabled { + match controller + .get_upgrade_mode_token() + .await + .map_err(|source| AuthenticationClientError::UpgradeModeToken { source })? + { + None => warn!( + "the wireguard node is in the upgrade mode, whilst we do not have an upgrade mode token - we will have to use normal ZK nym instead" + ), + + Some(upgrade_mode_token) => { + return Ok(BandwidthClaim { + credential: BandwidthCredential::UpgradeModeJWT { + token: upgrade_mode_token, + }, + kind: ticketbook_type, + }); + } + } + } + + let credential = controller + .get_ecash_ticket( + ticketbook_type, + self.auth_recipient.gateway(), + DEFAULT_TICKETS_TO_SPEND, + ) + .await + .map_err(|source| AuthenticationClientError::GetTicket { + ticketbook_type, + source, + })? + .data; + + let credential = credential + .try_into() + .inspect_err(|err| { + error!( + "failed to convert {ticketbook_type} ticket to a valid BandwidthClaim: {err}" + ) + }) + .map_err(|_| AuthenticationClientError::InternalError)?; + Ok(credential) + } + pub async fn register_wireguard( &mut self, controller: &dyn BandwidthTicketProvider, ticketbook_type: TicketType, ) -> std::result::Result { debug!("Registering with the wg gateway..."); + let pub_key = self.peer_public_key(); + let init_message = match self.auth_version { AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { return Err(RegistrationError::NoCredentialSent( @@ -167,24 +272,19 @@ impl AuthenticatorClient { )); } AuthenticatorVersion::V2 => { - ClientMessage::Initial(Box::new(v2::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) + ClientMessage::Initial(Box::new(v2::registration::InitMessage { pub_key })) } AuthenticatorVersion::V3 => { - ClientMessage::Initial(Box::new(v3::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) + ClientMessage::Initial(Box::new(v3::registration::InitMessage { pub_key })) } AuthenticatorVersion::V4 => { - ClientMessage::Initial(Box::new(v4::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) + ClientMessage::Initial(Box::new(v4::registration::InitMessage { pub_key })) } AuthenticatorVersion::V5 => { - ClientMessage::Initial(Box::new(v5::registration::InitMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - })) + ClientMessage::Initial(Box::new(v5::registration::InitMessage { pub_key })) + } + AuthenticatorVersion::V6 => { + ClientMessage::Initial(Box::new(v6::registration::InitMessage { pub_key })) } }; trace!("sending init msg to {}: {:?}", &self.ip_addr, &init_message); @@ -207,82 +307,25 @@ impl AuthenticatorClient { &self.ip_addr, &pending_registration_response ); - // This call takes care of updating the credential count in storage, so failure of this must be counted as credential waste - let credential = Some( - controller - .get_ecash_ticket( - ticketbook_type, - self.auth_recipient.gateway(), - DEFAULT_TICKETS_TO_SPEND, - ) - .await - .map_err(|source| RegistrationError::CredentialSent { - source: AuthenticationClientError::GetTicket { - ticketbook_type, - source, - }, - })? - .data, - ); + // if the node reports upgrade mode, we can use the corresponding token for registration + // instead of spending zk-nym ticket + let upgrade_mode_enabled = pending_registration_response + .upgrade_mode_status() + .is_enabled(); - let finalized_message = match self.auth_version { - AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { - return Err(RegistrationError::CredentialSent { - source: AuthenticationClientError::UnsupportedAuthenticatorVersion, - }); - } - AuthenticatorVersion::V2 => { - ClientMessage::Final(Box::new(v2::registration::FinalMessage { - gateway_client: v2::registration::GatewayClient::new( - self.keypair.private_key(), - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().ipv4.into(), - pending_registration_response.nonce(), - ), - credential, - })) - } - AuthenticatorVersion::V3 => { - ClientMessage::Final(Box::new(v3::registration::FinalMessage { - gateway_client: v3::registration::GatewayClient::new( - self.keypair.private_key(), - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().ipv4.into(), - pending_registration_response.nonce(), - ), - credential, - })) - } - AuthenticatorVersion::V4 => { - ClientMessage::Final(Box::new(v4::registration::FinalMessage { - gateway_client: v4::registration::GatewayClient::new( - self.keypair.private_key(), - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().into(), - pending_registration_response.nonce(), - ), - credential, - })) - } - AuthenticatorVersion::V5 => { - ClientMessage::Final(Box::new(v5::registration::FinalMessage { - gateway_client: v5::registration::GatewayClient::new( - self.keypair.private_key(), - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips(), - pending_registration_response.nonce(), - ), - credential, - })) - } - }; - trace!( - "sending final msg to {}: {:?}", - &self.ip_addr, &finalized_message - ); + let bandwidth_claim = self + .produce_bandwidth_claim(controller, upgrade_mode_enabled, ticketbook_type) + .await + .map_err(|source| RegistrationError::CredentialSent { source })?; + + let finalized_message = pending_registration_response + .finalise_registration(self.keypair.private_key(), Some(bandwidth_claim)); + let client_message = ClientMessage::Final(finalized_message); + + trace!("sending final msg to {}: {client_message:?}", &self.ip_addr); let response = self - .send_and_wait_for_response(&finalized_message) + .send_and_wait_for_response(&client_message) .await .map_err(|source| RegistrationError::CredentialSent { source })?; let AuthenticatorResponse::Registered(registered_response) = response else { @@ -316,32 +359,24 @@ impl AuthenticatorClient { } // This is up to the caller to know nothing is ever spent there - pub async fn query_bandwidth(&mut self) -> Result> { + pub async fn query_bandwidth(&mut self) -> Result { + let pub_key = self.peer_public_key(); + let version = self.auth_version; + let query_message = match self.auth_version { - AuthenticatorVersion::V1 => { + AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { return Err(AuthenticationClientError::UnsupportedAuthenticatorVersion); } - AuthenticatorVersion::V2 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - version: AuthenticatorVersion::V2, - })), - AuthenticatorVersion::V3 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - version: AuthenticatorVersion::V3, - })), - AuthenticatorVersion::V4 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - version: AuthenticatorVersion::V4, - })), - AuthenticatorVersion::V5 => ClientMessage::Query(Box::new(QueryMessageImpl { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), - version: AuthenticatorVersion::V5, - })), - AuthenticatorVersion::UNKNOWN => { - return Err(AuthenticationClientError::UnsupportedAuthenticatorVersion); + AuthenticatorVersion::V2 + | AuthenticatorVersion::V3 + | AuthenticatorVersion::V4 + | AuthenticatorVersion::V5 + | AuthenticatorVersion::V6 => { + ClientMessage::Query(Box::new(QueryMessageImpl { pub_key, version })) } }; let response = self.send_and_wait_for_response(&query_message).await?; + let current_upgrade_mode_status = response.upgrade_mode_status(); let available_bandwidth = match response { AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => { @@ -350,7 +385,10 @@ impl AuthenticatorClient { { available_bandwidth } else { - return Ok(None); + return Ok(AvailableBandwidthClientResponse { + available_bandwidth_bytes: None, + current_upgrade_mode_status, + }); } } _ => return Err(AuthenticationClientError::InvalidGatewayAuthResponse), @@ -371,24 +409,35 @@ impl AuthenticatorClient { "Remaining bandwidth is under 1 MB. The wireguard mode will get suspended after that until tomorrow, UTC time. The client might shutdown with timeout soon" ); } - Ok(Some(available_bandwidth)) + Ok(AvailableBandwidthClientResponse { + available_bandwidth_bytes: Some(available_bandwidth), + current_upgrade_mode_status, + }) } // Since the caller provides the credential, it knows it is spent - pub async fn top_up(&mut self, credential: CredentialSpendingData) -> Result { + pub async fn top_up( + &mut self, + credential: CredentialSpendingData, + ) -> Result { + let pub_key = self.peer_public_key(); let top_up_message = match self.auth_version { AuthenticatorVersion::V3 => ClientMessage::TopUp(Box::new(v3::topup::TopUpMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + pub_key, credential, })), // NOTE: looks like a bug here using v3. But we're leaving it as is since it's working // and V4 is deprecated in favour of V5 AuthenticatorVersion::V4 => ClientMessage::TopUp(Box::new(v4::topup::TopUpMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + pub_key, credential, })), AuthenticatorVersion::V5 => ClientMessage::TopUp(Box::new(v5::topup::TopUpMessage { - pub_key: PeerPublicKey::new(self.keypair.public_key().to_bytes().into()), + pub_key, + credential, + })), + AuthenticatorVersion::V6 => ClientMessage::TopUp(Box::new(v6::topup::TopUpMessage { + pub_key, credential, })), AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => { @@ -396,14 +445,46 @@ impl AuthenticatorClient { } }; let response = self.send_and_wait_for_response(&top_up_message).await?; + let current_upgrade_mode_status = response.upgrade_mode_status(); - let remaining_bandwidth = match response { + let remaining_bandwidth_bytes = match response { AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => { top_up_bandwidth_response.available_bandwidth() } _ => return Err(AuthenticationClientError::InvalidGatewayAuthResponse), }; - Ok(remaining_bandwidth) + Ok(TopUpClientResponse { + remaining_bandwidth_bytes, + current_upgrade_mode_status, + }) + } + + pub async fn check_upgrade_mode(&mut self, upgrade_mode_jwt: String) -> Result { + let check_um_message = match self.auth_version { + AuthenticatorVersion::V1 + | AuthenticatorVersion::V2 + | AuthenticatorVersion::V3 + | AuthenticatorVersion::V4 + | AuthenticatorVersion::V5 + | AuthenticatorVersion::UNKNOWN => { + return Err(AuthenticationClientError::UnsupportedAuthenticatorVersion); + } + + AuthenticatorVersion::V6 => ClientMessage::UpgradeModeCheck(Box::new( + v6::upgrade_mode_check::UpgradeModeCheckRequest::UpgradeModeJwt { + token: upgrade_mode_jwt, + }, + )), + }; + + let response = self.send_and_wait_for_response(&check_um_message).await?; + let AuthenticatorResponse::UpgradeMode(upgrade_mode_check_response) = response else { + return Err(AuthenticationClientError::InvalidGatewayAuthResponse); + }; + + Ok(upgrade_mode_check_response + .upgrade_mode_status() + .is_enabled()) } } diff --git a/nym-authenticator-client/src/types.rs b/nym-authenticator-client/src/types.rs new file mode 100644 index 0000000000..6ed3188fd0 --- /dev/null +++ b/nym-authenticator-client/src/types.rs @@ -0,0 +1,16 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use nym_authenticator_requests::models::CurrentUpgradeModeStatus; + +#[derive(Debug, Clone, Copy)] +pub struct TopUpClientResponse { + pub remaining_bandwidth_bytes: i64, + pub current_upgrade_mode_status: CurrentUpgradeModeStatus, +} + +#[derive(Debug, Clone, Copy)] +pub struct AvailableBandwidthClientResponse { + pub available_bandwidth_bytes: Option, + pub current_upgrade_mode_status: CurrentUpgradeModeStatus, +} diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs index 6ff65c9fda..54ba8126b5 100644 --- a/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs @@ -5,8 +5,6 @@ pub mod api; pub mod client; mod helpers; -pub const CREDENTIAL_PROXY_JWT_ISSUER: &str = "nym-credential-proxy"; - macro_rules! absolute_route { ( $name:ident, $parent:expr, $suffix:expr ) => { pub fn $name() -> String { diff --git a/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs b/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs index e0ff97ac05..06931852fc 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs @@ -12,6 +12,11 @@ use tokio_util::sync::CancellationToken; use tracing::{debug, info}; use url::Url; +/// Specifies the threshold for retrieval failures that will trigger disabling upgrade mode. +/// This assumes the file has been removed incorrectly and has been replaced by some placeholder 404 +/// page that does not deserialise correctly +const FAILURE_THRESHOLD: usize = 5; + pub struct AttestationWatcher { // default polling interval regular_polling_interval: Duration, @@ -21,17 +26,22 @@ pub struct AttestationWatcher { attestation_url: Url, + expected_attester_public_key: ed25519::PublicKey, + jwt_signing_keys: ed25519::KeyPair, jwt_validity: Duration, upgrade_mode_state: UpgradeModeState, + + consecutive_retrieval_failures: usize, } impl AttestationWatcher { pub(crate) fn new( regular_polling_interval: Duration, expedited_poll_interval: Duration, + expected_attester_public_key: ed25519::PublicKey, attestation_url: Url, jwt_signing_keys: ed25519::KeyPair, jwt_validity: Duration, @@ -40,11 +50,13 @@ impl AttestationWatcher { regular_polling_interval, expedited_poll_interval, attestation_url, + expected_attester_public_key, jwt_signing_keys, jwt_validity, upgrade_mode_state: UpgradeModeState { inner: Arc::new(Default::default()), }, + consecutive_retrieval_failures: 0, } } @@ -52,7 +64,7 @@ impl AttestationWatcher { self.upgrade_mode_state.clone() } - async fn try_update_state(&self) { + async fn try_update_state(&mut self) { match attempt_retrieve_attestation( self.attestation_url.as_str(), Some(generate_user_agent!()), @@ -60,21 +72,43 @@ impl AttestationWatcher { .await { Err(err) => { + self.consecutive_retrieval_failures += 1; info!("upgrade mode attestation is not available at this time"); - debug!("retrieval error: {err}") + debug!("retrieval error: {err}"); + + if self.upgrade_mode_state.has_attestation().await + && self.consecutive_retrieval_failures > FAILURE_THRESHOLD + { + self.upgrade_mode_state + .update( + None, + self.expected_attester_public_key, + &self.jwt_signing_keys, + self.jwt_validity, + ) + .await + } } Ok(attestation) => { + self.consecutive_retrieval_failures = 0; + self.upgrade_mode_state - .update(attestation, &self.jwt_signing_keys, self.jwt_validity) + .update( + attestation, + self.expected_attester_public_key, + &self.jwt_signing_keys, + self.jwt_validity, + ) .await } } } - pub async fn run_forever(self, cancellation_token: CancellationToken) { + pub async fn run_forever(mut self, cancellation_token: CancellationToken) { info!("starting the attestation watcher task"); - let check_wait = tokio::time::sleep(self.regular_polling_interval); + // make sure the first check happens immediately + let check_wait = tokio::time::sleep(Duration::new(0, 0)); tokio::pin!(check_wait); loop { diff --git a/nym-credential-proxy/nym-credential-proxy/src/cli.rs b/nym-credential-proxy/nym-credential-proxy/src/cli.rs index d5c27a7730..5ab32daf32 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/cli.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/cli.rs @@ -159,6 +159,10 @@ pub struct UpgradeModeConfig { #[clap(long, env = "NYM_CREDENTIAL_PROXY_ATTESTATION_CHECK_URL")] pub(crate) attestation_check_url: Option, + /// Base58-encoded expected upgrade mode attestation ed25519 public key. + #[clap(long, env = "NYM_CREDENTIAL_PROXY_ATTESTER_PUBKEY")] + pub(crate) attester_pubkey: Option, + /// Default polling interval of the upgrade mode endpoint. #[clap( long, diff --git a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs index cd061d8962..7044112d79 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs @@ -79,6 +79,29 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { } }; + let attester_pubkey = match cli.upgrade_mode.attester_pubkey { + Some(pubkey) => pubkey, + None => { + // argument hasn't been provided and env is not configured + if std::env::var(CONFIGURED).is_err() { + return Err(CredentialProxyError::AttesterPublicKeyNotSet); + } + // argument hasn't been provided and the relevant env value hasn't been set + // (technically this shouldn't be possible) + let Ok(env_key) = std::env::var(var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY) + else { + return Err(CredentialProxyError::AttesterPublicKeyNotSet); + }; + + match env_key.parse() { + Ok(key) => key, + Err(err) => { + return Err(CredentialProxyError::MalformedAttesterPublicKey { source: err }); + } + } + } + }; + let ticketbook_manager = TicketbookManager::new( build_sha_short(), cli.quorum_check_interval, @@ -94,6 +117,7 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { cli.upgrade_mode.attestation_check_regular_polling_interval, cli.upgrade_mode .attestation_check_expedited_polling_interval, + attester_pubkey, upgrade_mode_attestation_check_url, jwt_signing_keys, cli.upgrade_mode.upgrade_mode_jwt_validity, diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/nyx_upgrade_mode.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/nyx_upgrade_mode.rs index 80c94a055f..eed655d695 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/state/nyx_upgrade_mode.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/nyx_upgrade_mode.rs @@ -1,14 +1,15 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_credential_proxy_requests::CREDENTIAL_PROXY_JWT_ISSUER; -use nym_credential_proxy_requests::api::v1::ticketbook::models::UpgradeModeAttestation; use nym_crypto::asymmetric::ed25519; -use nym_upgrade_mode_check::generate_jwt_for_upgrade_mode_attestation; +use nym_upgrade_mode_check::{ + CREDENTIAL_PROXY_JWT_ISSUER, UpgradeModeAttestation, generate_jwt_for_upgrade_mode_attestation, +}; use std::sync::Arc; use std::time::Duration; use time::OffsetDateTime; use tokio::sync::RwLock; +use tracing::error; #[derive(Debug, Clone)] pub(crate) struct UpgradeModeState { @@ -23,6 +24,7 @@ impl UpgradeModeState { pub(crate) async fn update( &self, retrieved_attestation: Option, + expected_attester_public_key: ed25519::PublicKey, jwt_signing_keys: &ed25519::KeyPair, jwt_validity: Duration, ) { @@ -32,6 +34,14 @@ impl UpgradeModeState { return; }; + if attestation.content.attester_public_key != expected_attester_public_key { + error!( + "the retrieved attestation has been signed with an unexpected key! expected pubkey: {} actual: {}", + expected_attester_public_key, attestation.content.attester_public_key + ); + return; + } + match guard.as_mut() { None => { // no existing state - it's the first time we're going into upgrade mode, diff --git a/nym-gateway-probe/netstack_ping/go.mod b/nym-gateway-probe/netstack_ping/go.mod index ac45bca5eb..1845deeb3a 100644 --- a/nym-gateway-probe/netstack_ping/go.mod +++ b/nym-gateway-probe/netstack_ping/go.mod @@ -4,15 +4,15 @@ go 1.24.4 require ( github.com/amnezia-vpn/amneziawg-go v0.2.13 - golang.org/x/net v0.41.0 + golang.org/x/net v0.47.0 ) require ( github.com/google/btree v1.1.3 // indirect github.com/tevino/abool v1.2.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/sys v0.33.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/sys v0.38.0 // indirect golang.org/x/time v0.9.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect gvisor.dev/gvisor v0.0.0-20250611222258-0fe9a4bf489c // indirect diff --git a/nym-gateway-probe/netstack_ping/go.sum b/nym-gateway-probe/netstack_ping/go.sum index f43212422f..c9a61712c7 100644 --- a/nym-gateway-probe/netstack_ping/go.sum +++ b/nym-gateway-probe/netstack_ping/go.sum @@ -12,12 +12,12 @@ github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA= github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= diff --git a/nym-gateway-probe/src/lib.rs b/nym-gateway-probe/src/lib.rs index cac1225a8c..5f7827dff0 100644 --- a/nym-gateway-probe/src/lib.rs +++ b/nym-gateway-probe/src/lib.rs @@ -16,7 +16,7 @@ use futures::StreamExt; use nym_authenticator_client::{AuthClientMixnetListener, AuthenticatorClient}; use nym_authenticator_requests::{ AuthenticatorVersion, client_message::ClientMessage, response::AuthenticatorResponse, v2, v3, - v4, v5, + v4, v5, v6, }; use nym_client_core::config::ForgetMe; use nym_config::defaults::{ @@ -130,12 +130,20 @@ pub enum TestedNode { SameAsEntry, Custom { identity: NodeIdentity, + shares_entry: bool, }, } impl TestedNode { pub fn is_same_as_entry(&self) -> bool { - matches!(self, TestedNode::SameAsEntry) + matches!( + self, + TestedNode::SameAsEntry + | TestedNode::Custom { + shares_entry: true, + .. + } + ) } } @@ -309,7 +317,20 @@ impl Probe { let entry_gateway = directory.entry_gateway(&self.entrypoint)?; let node_info: TestedNodeDetails = match self.tested_node { - TestedNode::Custom { identity } => { + TestedNode::Custom { + identity: _, + shares_entry: true, + } => { + debug!( + "testing node {} as both entry and exit", + entry_gateway.identity() + ); + entry_gateway.to_testable_node()? + } + TestedNode::Custom { + identity, + shares_entry: false, + } => { let node = directory.get_nym_node(identity)?; info!( "testing node {} (via entry {})", @@ -421,12 +442,17 @@ impl Probe { storage.credential_store().clone(), client, ); - let credential = bw_controller - .prepare_ecash_ticket( + let (wg_ticket_type, credential_provider) = if tested_entry { + ( TicketType::V1WireguardEntry, nym_address.gateway().to_bytes(), - 1, ) + } else { + (TicketType::V1WireguardExit, node_info.identity.to_bytes()) + }; + + let credential = bw_controller + .prepare_ecash_ticket(wg_ticket_type, credential_provider, 1) .await? .data; @@ -467,6 +493,7 @@ async fn wg_probe( auth_version: AuthenticatorVersion, awg_args: String, netstack_args: NetstackArgs, + // TODO: update type credential: CredentialSpendingData, ) -> anyhow::Result { info!("attempting to use authenticator version {auth_version:?}"); @@ -493,6 +520,9 @@ async fn wg_probe( AuthenticatorVersion::V5 => ClientMessage::Initial(Box::new( v5::registration::InitMessage::new(authenticator_pub_key), )), + AuthenticatorVersion::V6 => ClientMessage::Initial(Box::new( + v6::registration::InitMessage::new(authenticator_pub_key), + )), AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => bail!("unknown version number"), }; @@ -512,57 +542,17 @@ async fn wg_probe( debug!("Verifying data"); pending_registration_response.verify(&private_key)?; - let finalized_message = match auth_version { - AuthenticatorVersion::V2 => { - ClientMessage::Final(Box::new(v2::registration::FinalMessage { - gateway_client: v2::registration::GatewayClient::new( - &private_key, - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().ipv4.into(), - pending_registration_response.nonce(), - ), - credential: Some(credential), - })) - } - AuthenticatorVersion::V3 => { - ClientMessage::Final(Box::new(v3::registration::FinalMessage { - gateway_client: v3::registration::GatewayClient::new( - &private_key, - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().ipv4.into(), - pending_registration_response.nonce(), - ), - credential: Some(credential), - })) - } - AuthenticatorVersion::V4 => { - ClientMessage::Final(Box::new(v4::registration::FinalMessage { - gateway_client: v4::registration::GatewayClient::new( - &private_key, - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips().into(), - pending_registration_response.nonce(), - ), - credential: Some(credential), - })) - } - AuthenticatorVersion::V5 => { - ClientMessage::Final(Box::new(v5::registration::FinalMessage { - gateway_client: v5::registration::GatewayClient::new( - &private_key, - pending_registration_response.pub_key().inner(), - pending_registration_response.private_ips(), - pending_registration_response.nonce(), - ), - credential: Some(credential), - })) - } - AuthenticatorVersion::V1 | AuthenticatorVersion::UNKNOWN => { - bail!("Unknown version number") - } - }; + let credential = credential + .try_into() + .inspect_err(|err| error!("invalid zk-nym data: {err}")) + .ok(); + + let finalized_message = + pending_registration_response.finalise_registration(&private_key, credential); + let client_message = ClientMessage::Final(finalized_message); + let response = auth_client - .send_and_wait_for_response(&finalized_message) + .send_and_wait_for_response(&client_message) .await?; let AuthenticatorResponse::Registered(registered_response) = response else { bail!("Unexpected response"); @@ -747,11 +737,7 @@ async fn do_ping_entry( } info!("Successfully mixnet pinged ourselves"); - if tested_entry { - Entry::success() - } else { - Entry::NotTested - } + Entry::success() } async fn connect_exit( diff --git a/nym-gateway-probe/src/nodes.rs b/nym-gateway-probe/src/nodes.rs index 0726bb3b7d..773470859f 100644 --- a/nym-gateway-probe/src/nodes.rs +++ b/nym-gateway-probe/src/nodes.rs @@ -199,6 +199,16 @@ impl NymApiDirectory { .map(|(id, _)| *id) } + pub fn random_entry_gateway(&self) -> anyhow::Result { + info!("Selecting random entry gateway"); + self.nodes + .iter() + .filter(|(_, n)| n.described.description.declared_role.entry) + .choose(&mut rand::thread_rng()) + .ok_or(anyhow!("no entry gateways available")) + .map(|(id, _)| *id) + } + pub fn get_nym_node(&self, identity: NodeIdentity) -> anyhow::Result { self.nodes .get(&identity) diff --git a/nym-gateway-probe/src/run.rs b/nym-gateway-probe/src/run.rs index 9487b10905..9c715bcdda 100644 --- a/nym-gateway-probe/src/run.rs +++ b/nym-gateway-probe/src/run.rs @@ -124,16 +124,31 @@ pub(crate) async fn run() -> anyhow::Result { let directory = NymApiDirectory::new(api_url).await?; - let entry = if let Some(gateway) = &args.entry_gateway { - NodeIdentity::from_base58_string(gateway)? + let node_override = args.node; + let entry_override = if let Some(gateway) = &args.entry_gateway { + Some(NodeIdentity::from_base58_string(gateway)?) } else { - directory.random_exit_with_ipr()? + None }; - let test_point = if let Some(node) = args.node { - TestedNode::Custom { identity: node } + let entry = if let Some(entry) = entry_override { + entry + } else if let Some(node) = node_override { + node } else { - TestedNode::SameAsEntry + directory.random_entry_gateway()? + }; + + let test_point = match (node_override, entry_override) { + (Some(node), Some(_)) => TestedNode::Custom { + identity: node, + shares_entry: false, + }, + (Some(node), None) => TestedNode::Custom { + identity: node, + shares_entry: true, + }, + (None, _) => TestedNode::SameAsEntry, }; let mut trial = diff --git a/nym-node-status-api/nym-node-status-api/Cargo.toml b/nym-node-status-api/nym-node-status-api/Cargo.toml index c92ebb4dd9..df6d5c12cf 100644 --- a/nym-node-status-api/nym-node-status-api/Cargo.toml +++ b/nym-node-status-api/nym-node-status-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node-status-api" -version = "4.0.11-rc1" +version = "4.0.12" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/nym-node-status-api/nym-node-status-api/src/http/models.rs b/nym-node-status-api/nym-node-status-api/src/http/models.rs index 2721115068..2f0fe09183 100644 --- a/nym-node-status-api/nym-node-status-api/src/http/models.rs +++ b/nym-node-status-api/nym-node-status-api/src/http/models.rs @@ -372,6 +372,21 @@ impl DVpnGateway { } } +struct NodeScore { + download_speed_score: f64, + ping_ips_score: f64, + mixnet_performance: f64, +} + +impl NodeScore { + // Weighted scoring: mixnet performance (40%), download speed (30%), ping performance (30%) + fn calculate_weighted_score(&self) -> f64 { + (self.mixnet_performance * 0.4) + + (self.download_speed_score * 0.3) + + (self.ping_ips_score * 0.3) + } +} + /// calculates the gateway probe score for mixnet mode fn calculate_mixnet_score(gateway: &Gateway) -> ScoreValue { let mixnet_performance = gateway.performance as f64 / 100.0; @@ -387,13 +402,14 @@ fn calculate_mixnet_score(gateway: &Gateway) -> ScoreValue { } } -/// calculates a visual score for the gateway +/// calculates a visual score for the gateway using weighted metrics fn calculate_score(gateway: &Gateway, probe_outcome: &LastProbeResult) -> ScoreValue { let mixnet_performance = gateway.performance as f64 / 100.0; - let ping_ips_performance = probe_outcome + + let node_score = probe_outcome .outcome .wg - .clone() + .as_ref() .map(|p| { let ping_ips_performance = p.ping_ips_performance_v4 as f64; @@ -419,18 +435,25 @@ fn calculate_score(gateway: &Gateway, probe_outcome: &LastProbeResult) -> ScoreV 0.1 }; - // combine the scores - file_download_score * ping_ips_performance + NodeScore { + download_speed_score: file_download_score, + ping_ips_score: ping_ips_performance, + mixnet_performance, + } }) - .unwrap_or(0f64); + .unwrap_or(NodeScore { + download_speed_score: 0.0, + ping_ips_score: 0.0, + mixnet_performance, + }); - let score = mixnet_performance * ping_ips_performance; + let weighted_score = node_score.calculate_weighted_score(); - if score > 0.75 { + if weighted_score > 0.75 { ScoreValue::High - } else if score > 0.5 { + } else if weighted_score > 0.5 { ScoreValue::Medium - } else if score > 0.1 { + } else if weighted_score > 0.1 { ScoreValue::Low } else { ScoreValue::Offline @@ -733,6 +756,130 @@ mod test { assert!(service.mixnet_websockets.is_none()); assert!(service.last_successful_ping_utc.is_none()); } + + #[test] + fn test_weighted_score_calculation() { + use crate::http::models::directory_gw_probe_outcome::EntryTestResult; + use crate::http::models::wg_outcome_versions::ProbeOutcomeV1; + + // Helper function to create a test gateway + fn create_test_gateway(performance: u8) -> Gateway { + Gateway { + gateway_identity_key: "test_key".to_string(), + bonded: true, + performance, + self_described: None, + explorer_pretty_bond: None, + description: nym_node_requests::api::v1::node::models::NodeDescription { + moniker: "test".to_string(), + details: "test".to_string(), + security_contact: "test@example.com".to_string(), + website: "https://example.com".to_string(), + }, + last_probe_result: None, + last_probe_log: None, + last_testrun_utc: None, + last_updated_utc: "2025-10-10T00:00:00Z".to_string(), + routing_score: 0.0, + config_score: 0, + bridges: None, + } + } + + // Helper function to create a test probe outcome + fn create_test_probe_outcome( + download_speed_mbps: f64, + ping_ips_performance: f32, + ) -> LastProbeResult { + let duration_sec = 1.0; + let file_size_mb = download_speed_mbps; + + LastProbeResult { + node: "test_node".to_string(), + used_entry: "test_entry".to_string(), + outcome: ProbeOutcome { + as_entry: directory_gw_probe_outcome::Entry::Tested(EntryTestResult { + can_connect: true, + can_route: true, + }), + as_exit: None, + wg: Some(ProbeOutcomeV1 { + can_register: true, + can_handshake: Some(true), + can_resolve_dns: Some(true), + ping_hosts_performance: Some(ping_ips_performance), + ping_ips_performance: Some(ping_ips_performance), + can_query_metadata_v4: Some(true), + can_handshake_v4: true, + can_resolve_dns_v4: true, + ping_hosts_performance_v4: ping_ips_performance, + ping_ips_performance_v4: ping_ips_performance, + can_handshake_v6: true, + can_resolve_dns_v6: true, + ping_hosts_performance_v6: ping_ips_performance, + ping_ips_performance_v6: ping_ips_performance, + download_duration_sec_v4: (duration_sec * 1000.0) as u64, + download_duration_milliseconds_v4: Some((duration_sec * 1000.0) as u64), + downloaded_file_size_bytes_v4: Some( + (file_size_mb * 1024.0 * 1024.0) as u64, + ), + downloaded_file_v4: "test".to_string(), + download_error_v4: "".to_string(), + download_duration_sec_v6: 0, + download_duration_milliseconds_v6: Some(0), + downloaded_file_size_bytes_v6: Some(0), + downloaded_file_v6: "".to_string(), + download_error_v6: "".to_string(), + }), + }, + } + } + + // Test case 1: Excellent node (should be High) + let gateway = create_test_gateway(90); // 90% mixnet performance + let probe = create_test_probe_outcome(6.0, 1.0); // 6 Mbps, 100% ping + let score = calculate_score(&gateway, &probe); + assert_eq!(score, ScoreValue::High, "Excellent node should be High"); + + // Test case 2: Good node (should be High with weighted scoring) + let gateway = create_test_gateway(90); // 90% mixnet performance + let probe = create_test_probe_outcome(3.0, 0.9); // 3 Mbps (0.75 score), 90% ping + let score = calculate_score(&gateway, &probe); + assert_eq!( + score, + ScoreValue::High, + "Good node should be High with weighted scoring" + ); + + // Test case 3: Medium node + let gateway = create_test_gateway(80); // 80% mixnet performance + let probe = create_test_probe_outcome(1.5, 0.8); // 1.5 Mbps (0.5 score), 80% ping + let score = calculate_score(&gateway, &probe); + assert_eq!(score, ScoreValue::Medium, "Medium node should be Medium"); + + // Test case 4: Poor node + let gateway = create_test_gateway(60); // 60% mixnet performance + let probe = create_test_probe_outcome(0.3, 0.3); // 0.3 Mbps (0.1 score), 30% ping + let score = calculate_score(&gateway, &probe); + assert_eq!(score, ScoreValue::Low, "Poor node should be Low"); + + // Test case 5: Failed node + let gateway = create_test_gateway(10); // 10% mixnet performance + let probe = create_test_probe_outcome(0.1, 0.0); // 0.1 Mbps (0.1 score), 0% ping + let score = calculate_score(&gateway, &probe); + assert_eq!(score, ScoreValue::Offline, "Failed node should be Offline"); + + // Test case 6: Edge case - just above threshold + let gateway = create_test_gateway(76); // 76% mixnet performance + let probe = create_test_probe_outcome(2.1, 0.75); // 2.1 Mbps (0.75 score), 75% ping + let score = calculate_score(&gateway, &probe); + // Weighted: (0.76 * 0.4) + (0.75 * 0.3) + (0.75 * 0.3) = 0.304 + 0.225 + 0.225 = 0.754 + assert_eq!( + score, + ScoreValue::High, + "Edge case just above 0.75 threshold should be High" + ); + } } #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] diff --git a/nym-node-status-api/nym-node-status-ui/package.json b/nym-node-status-api/nym-node-status-ui/package.json index 610a288446..32eba23c5a 100644 --- a/nym-node-status-api/nym-node-status-ui/package.json +++ b/nym-node-status-api/nym-node-status-ui/package.json @@ -19,9 +19,10 @@ "@mui/x-charts": "^8.8.0", "@mui/x-date-pickers": "^7.29.4", "@tanstack/react-query": "^5.83.0", + "d3-array": "^3.2.4", "dayjs": "^1.11.13", "material-react-table": "^3.2.1", - "next": "^15.4.1", + "next": "^15.4.7", "openapi-typescript": "^7.5.2", "react": "^19.0.0", "react-country-flag": "^3.1.0", @@ -30,6 +31,7 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@tanstack/react-query-devtools": "^5.83.0", + "@types/d3-array": "^3.2.2", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", diff --git a/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml b/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml index ab4a48acac..53e52c8b68 100644 --- a/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml +++ b/nym-node-status-api/nym-node-status-ui/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: version: 7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/material-nextjs': specifier: ^7.2.0 - version: 7.2.0(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(next@15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + version: 7.2.0(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(next@15.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) '@mui/x-charts': specifier: ^8.8.0 version: 8.8.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@mui/material@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@7.2.0(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -32,6 +32,9 @@ importers: '@tanstack/react-query': specifier: ^5.83.0 version: 5.83.0(react@19.0.0) + d3-array: + specifier: ^3.2.4 + version: 3.2.4 dayjs: specifier: ^1.11.13 version: 1.11.13 @@ -39,8 +42,8 @@ importers: specifier: ^3.2.1 version: 3.2.1(e4be24f02074e47c0e16001efb50991b) next: - specifier: ^15.4.1 - version: 15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + specifier: ^15.4.7 + version: 15.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) openapi-typescript: specifier: ^7.5.2 version: 7.5.2(typescript@5.7.3) @@ -60,6 +63,9 @@ importers: '@tanstack/react-query-devtools': specifier: ^5.83.0 version: 5.83.0(@tanstack/react-query@5.83.0(react@19.0.0))(react@19.0.0) + '@types/d3-array': + specifier: ^3.2.2 + version: 3.2.2 '@types/node': specifier: ^20 version: 20.17.14 @@ -173,8 +179,8 @@ packages: cpu: [x64] os: [win32] - '@emnapi/runtime@1.4.4': - resolution: {integrity: sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==} + '@emnapi/runtime@1.7.0': + resolution: {integrity: sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==} '@emotion/babel-plugin@11.13.5': resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} @@ -230,124 +236,139 @@ packages: '@emotion/weak-memoize@0.4.0': resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} - '@img/sharp-darwin-arm64@0.34.3': - resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==} + '@img/colour@1.0.0': + resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.3': - resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==} + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.0': - resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==} + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.0': - resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==} + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.0': - resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==} + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linux-arm@1.2.0': - resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==} + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] - '@img/sharp-libvips-linux-ppc64@1.2.0': - resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==} + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] - '@img/sharp-libvips-linux-s390x@1.2.0': - resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==} + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] - '@img/sharp-libvips-linux-x64@1.2.0': - resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==} + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': - resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==} + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] - '@img/sharp-libvips-linuxmusl-x64@1.2.0': - resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==} + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] - '@img/sharp-linux-arm64@0.34.3': - resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==} + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linux-arm@0.34.3': - resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==} + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - '@img/sharp-linux-ppc64@0.34.3': - resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==} + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] - '@img/sharp-linux-s390x@0.34.3': - resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==} + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - '@img/sharp-linux-x64@0.34.3': - resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==} + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-linuxmusl-arm64@0.34.3': - resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==} + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - '@img/sharp-linuxmusl-x64@0.34.3': - resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==} + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - '@img/sharp-wasm32@0.34.3': - resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==} + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.3': - resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==} + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.3': - resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==} + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.3': - resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==} + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -551,53 +572,53 @@ packages: '@mui/system': ^5.15.14 || ^6.0.0 || ^7.0.0 react: ^17.0.0 || ^18.0.0 || ^19.0.0 - '@next/env@15.4.1': - resolution: {integrity: sha512-DXQwFGAE2VH+f2TJsKepRXpODPU+scf5fDbKOME8MMyeyswe4XwgRdiiIYmBfkXU+2ssliLYznajTrOQdnLR5A==} + '@next/env@15.4.7': + resolution: {integrity: sha512-PrBIpO8oljZGTOe9HH0miix1w5MUiGJ/q83Jge03mHEE0E3pyqzAy2+l5G6aJDbXoobmxPJTVhbCuwlLtjSHwg==} - '@next/swc-darwin-arm64@15.4.1': - resolution: {integrity: sha512-L+81yMsiHq82VRXS2RVq6OgDwjvA4kDksGU8hfiDHEXP+ncKIUhUsadAVB+MRIp2FErs/5hpXR0u2eluWPAhig==} + '@next/swc-darwin-arm64@15.4.7': + resolution: {integrity: sha512-2Dkb+VUTp9kHHkSqtws4fDl2Oxms29HcZBwFIda1X7Ztudzy7M6XF9HDS2dq85TmdN47VpuhjE+i6wgnIboVzQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@15.4.1': - resolution: {integrity: sha512-jfz1RXu6SzL14lFl05/MNkcN35lTLMJWPbqt7Xaj35+ZWAX342aePIJrN6xBdGeKl6jPXJm0Yqo3Xvh3Gpo3Uw==} + '@next/swc-darwin-x64@15.4.7': + resolution: {integrity: sha512-qaMnEozKdWezlmh1OGDVFueFv2z9lWTcLvt7e39QA3YOvZHNpN2rLs/IQLwZaUiw2jSvxW07LxMCWtOqsWFNQg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@15.4.1': - resolution: {integrity: sha512-k0tOFn3dsnkaGfs6iQz8Ms6f1CyQe4GacXF979sL8PNQxjYS1swx9VsOyUQYaPoGV8nAZ7OX8cYaeiXGq9ahPQ==} + '@next/swc-linux-arm64-gnu@15.4.7': + resolution: {integrity: sha512-ny7lODPE7a15Qms8LZiN9wjNWIeI+iAZOFDOnv2pcHStncUr7cr9lD5XF81mdhrBXLUP9yT9RzlmSWKIazWoDw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-arm64-musl@15.4.1': - resolution: {integrity: sha512-4ogGQ/3qDzbbK3IwV88ltihHFbQVq6Qr+uEapzXHXBH1KsVBZOB50sn6BWHPcFjwSoMX2Tj9eH/fZvQnSIgc3g==} + '@next/swc-linux-arm64-musl@15.4.7': + resolution: {integrity: sha512-4SaCjlFR/2hGJqZLLWycccy1t+wBrE/vyJWnYaZJhUVHccpGLG5q0C+Xkw4iRzUIkE+/dr90MJRUym3s1+vO8A==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@next/swc-linux-x64-gnu@15.4.1': - resolution: {integrity: sha512-Jj0Rfw3wIgp+eahMz/tOGwlcYYEFjlBPKU7NqoOkTX0LY45i5W0WcDpgiDWSLrN8KFQq/LW7fZq46gxGCiOYlQ==} + '@next/swc-linux-x64-gnu@15.4.7': + resolution: {integrity: sha512-2uNXjxvONyRidg00VwvlTYDwC9EgCGNzPAPYbttIATZRxmOZ3hllk/YYESzHZb65eyZfBR5g9xgCZjRAl9YYGg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-linux-x64-musl@15.4.1': - resolution: {integrity: sha512-9WlEZfnw1vFqkWsTMzZDgNL7AUI1aiBHi0S2m8jvycPyCq/fbZjtE/nDkhJRYbSjXbtRHYLDBlmP95kpjEmJbw==} + '@next/swc-linux-x64-musl@15.4.7': + resolution: {integrity: sha512-ceNbPjsFgLscYNGKSu4I6LYaadq2B8tcK116nVuInpHHdAWLWSwVK6CHNvCi0wVS9+TTArIFKJGsEyVD1H+4Kg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@next/swc-win32-arm64-msvc@15.4.1': - resolution: {integrity: sha512-WodRbZ9g6CQLRZsG3gtrA9w7Qfa9BwDzhFVdlI6sV0OCPq9JrOrJSp9/ioLsezbV8w9RCJ8v55uzJuJ5RgWLZg==} + '@next/swc-win32-arm64-msvc@15.4.7': + resolution: {integrity: sha512-pZyxmY1iHlZJ04LUL7Css8bNvsYAMYOY9JRwFA3HZgpaNKsJSowD09Vg2R9734GxAcLJc2KDQHSCR91uD6/AAw==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@15.4.1': - resolution: {integrity: sha512-y+wTBxelk2xiNofmDOVU7O5WxTHcvOoL3srOM0kxTzKDjQ57kPU0tpnPJ/BWrRnsOwXEv0+3QSbGR7hY4n9LkQ==} + '@next/swc-win32-x64-msvc@15.4.7': + resolution: {integrity: sha512-HjuwPJ7BeRzgl3KrjKqD2iDng0eQIpIReyhpF5r4yeAHFwWRuAhfW92rWv/r3qeQHEwHsLRzFDvMqRjyM5DI6A==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -659,6 +680,9 @@ packages: '@tanstack/virtual-core@3.11.2': resolution: {integrity: sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + '@types/d3-color@3.1.3': resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} @@ -733,8 +757,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001695: - resolution: {integrity: sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==} + caniuse-lite@1.0.30001754: + resolution: {integrity: sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==} change-case@5.4.4: resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==} @@ -746,20 +770,6 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - color-string@1.9.1: - resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} - - color@4.2.3: - resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} - engines: {node: '>=12.5.0'} - colorette@1.4.0: resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==} @@ -832,8 +842,8 @@ packages: delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} - detect-libc@2.0.4: - resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} dom-helpers@5.2.1: @@ -889,9 +899,6 @@ packages: is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - is-arrayish@0.3.2: - resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-core-module@2.16.1: resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} engines: {node: '>= 0.4'} @@ -944,13 +951,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - next@15.4.1: - resolution: {integrity: sha512-eNKB1q8C7o9zXF8+jgJs2CzSLIU3T6bQtX6DcTnCq1sIR1CJ0GlSyRs1BubQi3/JgCnr9Vr+rS5mOMI38FFyQw==} + next@15.4.7: + resolution: {integrity: sha512-OcqRugwF7n7mC8OSYjvsZhhG1AYSvulor1EIUsIkbbEbf1qoE5EbH36Swj8WhF4cHqmDgkiam3z1c1W0J1Wifg==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -1077,18 +1084,15 @@ packages: scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} - semver@7.7.2: - resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} engines: {node: '>=10'} hasBin: true - sharp@0.34.3: - resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - simple-swizzle@0.2.2: - resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1259,7 +1263,7 @@ snapshots: '@biomejs/cli-win32-x64@1.9.4': optional: true - '@emnapi/runtime@1.4.4': + '@emnapi/runtime@1.7.0': dependencies: tslib: 2.8.1 optional: true @@ -1347,90 +1351,101 @@ snapshots: '@emotion/weak-memoize@0.4.0': {} - '@img/sharp-darwin-arm64@0.34.3': + '@img/colour@1.0.0': + optional: true + + '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.0 + '@img/sharp-libvips-darwin-arm64': 1.2.4 optional: true - '@img/sharp-darwin-x64@0.34.3': + '@img/sharp-darwin-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.0 + '@img/sharp-libvips-darwin-x64': 1.2.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.0': + '@img/sharp-libvips-darwin-arm64@1.2.4': optional: true - '@img/sharp-libvips-darwin-x64@1.2.0': + '@img/sharp-libvips-darwin-x64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm64@1.2.0': + '@img/sharp-libvips-linux-arm64@1.2.4': optional: true - '@img/sharp-libvips-linux-arm@1.2.0': + '@img/sharp-libvips-linux-arm@1.2.4': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.0': + '@img/sharp-libvips-linux-ppc64@1.2.4': optional: true - '@img/sharp-libvips-linux-s390x@1.2.0': + '@img/sharp-libvips-linux-riscv64@1.2.4': optional: true - '@img/sharp-libvips-linux-x64@1.2.0': + '@img/sharp-libvips-linux-s390x@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.0': + '@img/sharp-libvips-linux-x64@1.2.4': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.0': + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': optional: true - '@img/sharp-linux-arm64@0.34.3': + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.0 + '@img/sharp-libvips-linux-arm64': 1.2.4 optional: true - '@img/sharp-linux-arm@0.34.3': + '@img/sharp-linux-arm@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.0 + '@img/sharp-libvips-linux-arm': 1.2.4 optional: true - '@img/sharp-linux-ppc64@0.34.3': + '@img/sharp-linux-ppc64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.0 + '@img/sharp-libvips-linux-ppc64': 1.2.4 optional: true - '@img/sharp-linux-s390x@0.34.3': + '@img/sharp-linux-riscv64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.0 + '@img/sharp-libvips-linux-riscv64': 1.2.4 optional: true - '@img/sharp-linux-x64@0.34.3': + '@img/sharp-linux-s390x@0.34.5': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.0 + '@img/sharp-libvips-linux-s390x': 1.2.4 optional: true - '@img/sharp-linuxmusl-arm64@0.34.3': + '@img/sharp-linux-x64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 + '@img/sharp-libvips-linux-x64': 1.2.4 optional: true - '@img/sharp-linuxmusl-x64@0.34.3': + '@img/sharp-linuxmusl-arm64@0.34.5': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 optional: true - '@img/sharp-wasm32@0.34.3': + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': dependencies: - '@emnapi/runtime': 1.4.4 + '@emnapi/runtime': 1.7.0 optional: true - '@img/sharp-win32-arm64@0.34.3': + '@img/sharp-win32-arm64@0.34.5': optional: true - '@img/sharp-win32-ia32@0.34.3': + '@img/sharp-win32-ia32@0.34.5': optional: true - '@img/sharp-win32-x64@0.34.3': + '@img/sharp-win32-x64@0.34.5': optional: true '@jridgewell/gen-mapping@0.3.8': @@ -1460,11 +1475,11 @@ snapshots: optionalDependencies: '@types/react': 19.0.7 - '@mui/material-nextjs@7.2.0(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(next@15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + '@mui/material-nextjs@7.2.0(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.0.7)(react@19.0.0))(@types/react@19.0.7)(next@15.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': dependencies: '@babel/runtime': 7.27.6 '@emotion/react': 11.14.0(@types/react@19.0.7)(react@19.0.0) - next: 15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 optionalDependencies: '@emotion/cache': 11.14.0 @@ -1631,30 +1646,30 @@ snapshots: transitivePeerDependencies: - '@types/react' - '@next/env@15.4.1': {} + '@next/env@15.4.7': {} - '@next/swc-darwin-arm64@15.4.1': + '@next/swc-darwin-arm64@15.4.7': optional: true - '@next/swc-darwin-x64@15.4.1': + '@next/swc-darwin-x64@15.4.7': optional: true - '@next/swc-linux-arm64-gnu@15.4.1': + '@next/swc-linux-arm64-gnu@15.4.7': optional: true - '@next/swc-linux-arm64-musl@15.4.1': + '@next/swc-linux-arm64-musl@15.4.7': optional: true - '@next/swc-linux-x64-gnu@15.4.1': + '@next/swc-linux-x64-gnu@15.4.7': optional: true - '@next/swc-linux-x64-musl@15.4.1': + '@next/swc-linux-x64-musl@15.4.7': optional: true - '@next/swc-win32-arm64-msvc@15.4.1': + '@next/swc-win32-arm64-msvc@15.4.7': optional: true - '@next/swc-win32-x64-msvc@15.4.1': + '@next/swc-win32-x64-msvc@15.4.7': optional: true '@popperjs/core@2.11.8': {} @@ -1723,6 +1738,8 @@ snapshots: '@tanstack/virtual-core@3.11.2': {} + '@types/d3-array@3.2.2': {} + '@types/d3-color@3.1.3': {} '@types/d3-delaunay@6.0.4': {} @@ -1787,7 +1804,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001695: {} + caniuse-lite@1.0.30001754: {} change-case@5.4.4: {} @@ -1795,26 +1812,6 @@ snapshots: clsx@2.1.1: {} - color-convert@2.0.1: - dependencies: - color-name: 1.1.4 - optional: true - - color-name@1.1.4: - optional: true - - color-string@1.9.1: - dependencies: - color-name: 1.1.4 - simple-swizzle: 0.2.2 - optional: true - - color@4.2.3: - dependencies: - color-convert: 2.0.1 - color-string: 1.9.1 - optional: true - colorette@1.4.0: {} convert-source-map@1.9.0: {} @@ -1881,7 +1878,7 @@ snapshots: dependencies: robust-predicates: 3.0.2 - detect-libc@2.0.4: + detect-libc@2.1.2: optional: true dom-helpers@5.2.1: @@ -1931,9 +1928,6 @@ snapshots: is-arrayish@0.2.1: {} - is-arrayish@0.3.2: - optional: true - is-core-module@2.16.1: dependencies: hasown: 2.0.2 @@ -1978,27 +1972,27 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.8: {} + nanoid@3.3.11: {} - next@15.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.4.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: - '@next/env': 15.4.1 + '@next/env': 15.4.7 '@swc/helpers': 0.5.15 - caniuse-lite: 1.0.30001695 + caniuse-lite: 1.0.30001754 postcss: 8.4.31 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) styled-jsx: 5.1.6(react@19.0.0) optionalDependencies: - '@next/swc-darwin-arm64': 15.4.1 - '@next/swc-darwin-x64': 15.4.1 - '@next/swc-linux-arm64-gnu': 15.4.1 - '@next/swc-linux-arm64-musl': 15.4.1 - '@next/swc-linux-x64-gnu': 15.4.1 - '@next/swc-linux-x64-musl': 15.4.1 - '@next/swc-win32-arm64-msvc': 15.4.1 - '@next/swc-win32-x64-msvc': 15.4.1 - sharp: 0.34.3 + '@next/swc-darwin-arm64': 15.4.7 + '@next/swc-darwin-x64': 15.4.7 + '@next/swc-linux-arm64-gnu': 15.4.7 + '@next/swc-linux-arm64-musl': 15.4.7 + '@next/swc-linux-x64-gnu': 15.4.7 + '@next/swc-linux-x64-musl': 15.4.7 + '@next/swc-win32-arm64-msvc': 15.4.7 + '@next/swc-win32-x64-msvc': 15.4.7 + sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -2048,7 +2042,7 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.11 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -2102,42 +2096,39 @@ snapshots: scheduler@0.25.0: {} - semver@7.7.2: + semver@7.7.3: optional: true - sharp@0.34.3: + sharp@0.34.5: dependencies: - color: 4.2.3 - detect-libc: 2.0.4 - semver: 7.7.2 + '@img/colour': 1.0.0 + detect-libc: 2.1.2 + semver: 7.7.3 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.3 - '@img/sharp-darwin-x64': 0.34.3 - '@img/sharp-libvips-darwin-arm64': 1.2.0 - '@img/sharp-libvips-darwin-x64': 1.2.0 - '@img/sharp-libvips-linux-arm': 1.2.0 - '@img/sharp-libvips-linux-arm64': 1.2.0 - '@img/sharp-libvips-linux-ppc64': 1.2.0 - '@img/sharp-libvips-linux-s390x': 1.2.0 - '@img/sharp-libvips-linux-x64': 1.2.0 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.0 - '@img/sharp-libvips-linuxmusl-x64': 1.2.0 - '@img/sharp-linux-arm': 0.34.3 - '@img/sharp-linux-arm64': 0.34.3 - '@img/sharp-linux-ppc64': 0.34.3 - '@img/sharp-linux-s390x': 0.34.3 - '@img/sharp-linux-x64': 0.34.3 - '@img/sharp-linuxmusl-arm64': 0.34.3 - '@img/sharp-linuxmusl-x64': 0.34.3 - '@img/sharp-wasm32': 0.34.3 - '@img/sharp-win32-arm64': 0.34.3 - '@img/sharp-win32-ia32': 0.34.3 - '@img/sharp-win32-x64': 0.34.3 - optional: true - - simple-swizzle@0.2.2: - dependencies: - is-arrayish: 0.3.2 + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 optional: true source-map-js@1.2.1: {} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/dvpn/GatewaysTable.tsx b/nym-node-status-api/nym-node-status-ui/src/app/dvpn/GatewaysTable.tsx new file mode 100644 index 0000000000..30a389cd80 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/dvpn/GatewaysTable.tsx @@ -0,0 +1,379 @@ +import type { DVpnGateway } from "@/client"; +import { ReverseScoreIcon, ScoreIcon } from "@/components/ScoreIcon"; +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { + IconButton, + Table, + TableBody, + TableCell, + TableHead, + TableRow, + Tooltip, +} from "@mui/material"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { + type MRT_ColumnDef, + MaterialReactTable, + useMaterialReactTable, +} from "material-react-table"; +import { useMemo } from "react"; +import ReactCountryFlag from "react-country-flag"; + +dayjs.extend(duration); +dayjs.extend(relativeTime); + +const regionNamesInEnglish = new Intl.DisplayNames(["en"], { type: "region" }); + +const staleGatewayBinWidthMinutes = 15; + +interface StaleGatewayStats { + bins: number[]; + average: number; + sum: number; + count: number; +} + +export default function GatewaysTable() { + const { data, isError, isRefetching, isLoading, refetch } = + useDVpnGatewaysTransformed().query; + + const staleGateways = useMemo( + () => + (data || []).reduce( + (acc, g) => { + const last_updated_utc = g.last_probe + ? dayjs(g.last_probe.last_updated_utc) + : null; + if (!last_updated_utc) return acc; + const diff = dayjs().diff(last_updated_utc, "minutes"); + const bin = Math.floor(diff / staleGatewayBinWidthMinutes); + if (!acc.bins[bin]) { + acc.bins[bin] = 0; + } + acc.bins[bin] += 1; + acc.sum += diff; + acc.count += 1; + acc.average = acc.sum / acc.count; + return acc; + }, + { + bins: [], + average: 0, + sum: 0, + count: 0, + } as StaleGatewayStats, + ), + [data], + ); + + const columns = useMemo[]>( + //column definitions... + () => [ + { + accessorKey: "name", + header: "Name", + }, + { + accessorKey: "identity_key", + header: "Identity Key", + Cell: ({ cell }) => ( + {cell.getValue()?.slice(0, 8)}... + ), + }, + { + accessorKey: "location.two_letter_iso_country_code", + header: "Country", + Cell: ({ cell }) => { + const value = cell.getValue(); + return ( + <> + {value} + + {regionNamesInEnglish.of(value)} + + + ); + }, + }, + { + accessorKey: "location.region", + header: "City / Region", + Cell: ({ row }) => { + return ( + <> + + {(row.original.location as any).city}/ + {(row.original.location as any).region} + + + ); + }, + }, + { + accessorKey: "performance_v2.score", + width: 20, + header: "Score", + Cell: ({ cell }) => { + const value = cell.getValue(); + return ( + <> + + + {value || "-"} + + + ); + }, + }, + { + accessorKey: "performance_v2.load", + width: 20, + header: "Load", + Cell: ({ cell }) => { + const value = cell.getValue(); + return ( + <> + + + {value || "-"} + + + ); + }, + }, + { + accessorKey: "extra.downloadSpeedMBPerSec", + header: "Download Speed ipv4 (MB/sec)", + Cell: ({ renderedCellValue, cell }) => { + if (!cell.getValue()) { + return null; + } + return ( + + {renderedCellValue} MB/sec + + ); + }, + }, + { + accessorKey: "extra.downloadSpeedIpv6MBPerSec", + header: "Download Speed ipv6 (MB/sec)", + Cell: ({ renderedCellValue, cell }) => { + if (!cell.getValue()) { + return null; + } + return ( + + {renderedCellValue} MB/sec + + ); + }, + }, + { + accessorKey: "last_probe.outcome.wg.ping_ips_performance_v4", + header: "Probe pings (IPV4)", + Cell: ({ cell }) => { + const value = Math.floor( + Number.parseFloat(cell.getValue() || "0") * 100, + ); + return ( + <> + + {value}% + + + ); + }, + }, + { + accessorKey: "performance_v2.uptime_percentage_last_24_hours", + width: 20, + header: "Uptime", + Cell: ({ cell, row }) => { + const value: number = + ((row.original as any).performance_v2 + ?.uptime_percentage_last_24_hours || 0) * 100; + // const value = Math.floor(Number.parseFloat(cell.getValue()) * 100); + return ( + <> + + {value}% + + + ); + }, + }, + { + accessorKey: "last_probe.outcome.wg.can_query_metadata_v4", + header: "Can query metadata?", + Cell: ({ cell }) => { + const wg = cell.row.original.last_probe?.outcome.wg as any; + const can_query_metadata_v4 = wg?.can_query_metadata_v4; + return ( + <> + + {can_query_metadata_v4 === null || + (can_query_metadata_v4 === undefined && -)} + {can_query_metadata_v4 === true && } + {can_query_metadata_v4 === false && } + + + ); + }, + }, + { + accessorKey: "last_probe.last_updated_utc", + header: "Last Probed At", + Cell: ({ cell }) => { + const parsed = dayjs(cell.getValue()); + return ( + +
+ {parsed.format()} +
+
+ ({parsed.fromNow()}) +
+
+ ); + }, + }, + { + id: "last_probe_age", + accessorKey: "last_probe.last_updated_utc", + header: "Last Probed Age", + Cell: ({ cell, row }) => { + const value = row.original.last_probe?.last_updated_utc; + if (!value) { + return "-"; + } + const parsed = dayjs(value); + const age = dayjs().diff(parsed, "minutes"); + return <>{age} minutes; + }, + }, + { + accessorKey: "build_information.build_version", + header: "Version", + }, + ], + [], + //end + ); + + const table = useMaterialReactTable({ + columns, + data: data || [], + initialState: { + showColumnFilters: true, + density: "compact", + pagination: { + pageIndex: 0, + pageSize: 100, + }, + }, + muiToolbarAlertBannerProps: isError + ? { + color: "error", + children: "Error loading data", + } + : undefined, + renderTopToolbarCustomActions: () => ( + + refetch()}> + + + + ), + rowCount: data?.length || 0, + state: { + isLoading, + showAlertBanner: isError, + showProgressBars: isRefetching, + }, + }); + + return ( + <> + +

Gateway probe age

+ + Average age is {Math.round(staleGateways.average * 10) / 10} minutes old + + + + + + Age + Gateways + + + + {staleGateways.bins.map((r, i) => ( + + + {(i + 1) * staleGatewayBinWidthMinutes} mins old + + {r} + + ))} + +
+
+ + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/dvpn/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/dvpn/page.tsx new file mode 100644 index 0000000000..e5b5b8fbcf --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/dvpn/page.tsx @@ -0,0 +1,15 @@ +"use client"; + +import GatewaysTable from "@/app/dvpn/GatewaysTable"; +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; +import Box from "@mui/material/Box"; + +export default function Page() { + return ( + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/favicon.ico b/nym-node-status-api/nym-node-status-ui/src/app/favicon.ico new file mode 100644 index 0000000000..718d6fea48 Binary files /dev/null and b/nym-node-status-api/nym-node-status-ui/src/app/favicon.ico differ diff --git a/nym-node-status-api/nym-node-status-ui/src/app/layout.tsx b/nym-node-status-api/nym-node-status-ui/src/app/layout.tsx index 83d23fa86d..7eaf4f838a 100644 --- a/nym-node-status-api/nym-node-status-ui/src/app/layout.tsx +++ b/nym-node-status-api/nym-node-status-ui/src/app/layout.tsx @@ -1,10 +1,30 @@ "use client"; +import { QueryContextProvider } from "@/context/queryContext"; +import LayoutWithNav from "@/layouts/LayoutWithNav"; +import AppTheme from "@/theme"; +import { AppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter"; +import CssBaseline from "@mui/material/CssBaseline"; +import InitColorSchemeScript from "@mui/material/InitColorSchemeScript"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider"; + export default function RootLayout(props: { children: React.ReactNode }) { return ( - {props.children} + + + + {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */} + + + + {props.children} + + + + ); diff --git a/nym-node-status-api/nym-node-status-ui/src/app/nodes/NodesTable.tsx b/nym-node-status-api/nym-node-status-ui/src/app/nodes/NodesTable.tsx new file mode 100644 index 0000000000..6ee28b2bcf --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/nodes/NodesTable.tsx @@ -0,0 +1,124 @@ +import { useAllNymNodes } from "@/hooks/useAllNymNodes"; +import type { NymNode } from "@/hooks/useNymNodes"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { IconButton, Tooltip } from "@mui/material"; +import { + type MRT_ColumnDef, + MaterialReactTable, + useMaterialReactTable, +} from "material-react-table"; +import { useMemo } from "react"; + +export default function NodesTable() { + const { data, isError, isRefetching, isLoading, refetch } = + useAllNymNodes().query; + + const columns = useMemo[]>( + //column definitions... + () => [ + { + accessorKey: "node_id", + header: "Node Id", + size: 25, + }, + { + accessorKey: "description.moniker", + header: "Moniker", + }, + { + accessorKey: "identity_key", + header: "Identity Key", + Cell: ({ cell }) => ( + {cell.getValue()?.slice(0, 8)}... + ), + }, + { + accessorKey: "node_type", + header: "Node Type", + }, + { + accessorKey: "bonded", + header: "Bonded", + Cell: ({ cell }) => (cell.getValue() ? "✅" : "⛔️"), + }, + { + accessorKey: "geoip.country", + header: "Country", + }, + { + accessorKey: "geoip.city", + header: "City", + }, + { + accessorKey: "self_description.build_information.build_version", + header: "Version", + }, + { + accessorKey: "self_description.declared_role.entry", + header: "Entry gateway", + Cell: ({ cell }) => (cell.getValue() ? "✅" : "-"), + }, + { + accessorKey: "self_description.declared_role.exit", + header: "Exit gateway", + Cell: ({ cell }) => (cell.getValue() ? "✅" : "-"), + }, + { + accessorKey: "self_description.declared_role.mixnode", + header: "Mixnode", + Cell: ({ cell }) => (cell.getValue() ? "✅" : "-"), + }, + { + accessorKey: "self_description.declared_role.exit_ipr", + header: "Runs IPR", + Cell: ({ cell }) => (cell.getValue() ? "✅" : "-"), + }, + { + accessorKey: "self_description.declared_role.exit_nr", + header: "Runs SOCKS5 NR", + Cell: ({ cell }) => (cell.getValue() ? "✅" : "-"), + }, + { + accessorKey: "self_description.host_information.ip_address", + header: "IP Address", + }, + { + accessorKey: "uptime", + header: "Uptime", + }, + ], + [], + //end + ); + + const table = useMaterialReactTable({ + columns, + data: data || [], + initialState: { + showColumnFilters: true, + density: "compact", + pagination: { pageIndex: 0, pageSize: 100 }, + }, + muiToolbarAlertBannerProps: isError + ? { + color: "error", + children: "Error loading data", + } + : undefined, + renderTopToolbarCustomActions: () => ( + + refetch()}> + + + + ), + rowCount: data?.length ?? 0, + state: { + isLoading, + showAlertBanner: isError, + showProgressBars: isRefetching, + }, + }); + + return ; +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/nodes/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/nodes/page.tsx new file mode 100644 index 0000000000..608f32869c --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/nodes/page.tsx @@ -0,0 +1,15 @@ +"use client"; + +import NodesTable from "@/app/nodes/NodesTable"; +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; +import Box from "@mui/material/Box"; + +export default function Page() { + return ( + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/GatewaysTable.tsx b/nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/GatewaysTable.tsx new file mode 100644 index 0000000000..6c1de054d0 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/GatewaysTable.tsx @@ -0,0 +1,115 @@ +import type { DVpnGateway } from "@/client"; +import { useDVpnGateways } from "@/hooks/useGateways"; +import RefreshIcon from "@mui/icons-material/Refresh"; +import { IconButton, Tooltip } from "@mui/material"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import relativeTime from "dayjs/plugin/relativeTime"; +import { + type MRT_ColumnDef, + MaterialReactTable, + useMaterialReactTable, +} from "material-react-table"; +import { useMemo } from "react"; +import ReactCountryFlag from "react-country-flag"; + +dayjs.extend(duration); +dayjs.extend(relativeTime); + +const regionNamesInEnglish = new Intl.DisplayNames(["en"], { type: "region" }); + +export default function GatewaysTable() { + const { data, isError, isRefetching, isLoading, refetch } = + useDVpnGateways().query; + + const columns = useMemo[]>( + //column definitions... + () => [ + { + accessorKey: "name", + header: "Name", + }, + { + accessorKey: "identity_key", + header: "Identity Key", + Cell: ({ cell }) => ( + {cell.getValue()?.slice(0, 8)}... + ), + }, + { + accessorKey: "location.two_letter_iso_country_code", + header: "Country", + Cell: ({ cell }) => { + const value = cell.getValue(); + return ( + <> + {value} + + {regionNamesInEnglish.of(value)} + + + ); + }, + }, + { + accessorKey: "last_probe.last_updated_utc", + header: "Last Probed At", + Cell: ({ cell }) => { + const parsed = dayjs(cell.getValue()); + return ( + +
+ {parsed.format()} +
+
+ ({parsed.fromNow()}) +
+
+ ); + }, + }, + { + accessorKey: "build_information.build_version", + header: "Version", + }, + ], + [], + //end + ); + + const table = useMaterialReactTable({ + columns, + data: data || [], + initialState: { + showColumnFilters: true, + density: "compact", + pagination: { + pageIndex: 0, + pageSize: 100, + }, + }, + muiToolbarAlertBannerProps: isError + ? { + color: "error", + children: "Error loading data", + } + : undefined, + renderTopToolbarCustomActions: () => ( + + refetch()}> + + + + ), + rowCount: data?.length || 0, + state: { + isLoading, + showAlertBanner: isError, + showProgressBars: isRefetching, + }, + }); + + return ; +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/page.tsx new file mode 100644 index 0000000000..6f55cf7dba --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/nym-vpn/page.tsx @@ -0,0 +1,15 @@ +"use client"; + +import GatewaysTable from "@/app/dvpn/GatewaysTable"; +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; +import Box from "@mui/material/Box"; + +export default function Page() { + return ( + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/page.tsx index b77b535926..f3157b0289 100644 --- a/nym-node-status-api/nym-node-status-ui/src/app/page.tsx +++ b/nym-node-status-api/nym-node-status-ui/src/app/page.tsx @@ -1,7 +1,61 @@ "use client"; +import GraphCard from "@/components/GraphCard"; +import { GatewayCanQueryMetadataTopup } from "@/components/graphs/GatewayCanQueryMetadataTopup"; +import { GatewayDownloadSpeeds } from "@/components/graphs/GatewayDownloadSpeeds"; +import { GatewayLoads } from "@/components/graphs/GatewayLoads"; +import { GatewayPingPercentage } from "@/components/graphs/GatewayPingPercentage"; +import { GatewayScores } from "@/components/graphs/GatewayScores"; +import { GatewayUptimePercentage } from "@/components/graphs/GatewayUptimePercentage"; +import { GatewayVersions } from "@/components/graphs/GatewayVersions"; +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; +import Grid from "@mui/material/Grid"; + export default function Home() { return ( -
TODO
+ + theme.spacing(2) }} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/nym-node-status-api/nym-node-status-ui/src/app/socks5/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/socks5/page.tsx new file mode 100644 index 0000000000..7cd5172647 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/socks5/page.tsx @@ -0,0 +1,11 @@ +"use client"; + +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; + +export default function Page() { + return ( + +
SOCKS5
+
+ ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/validators/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/validators/page.tsx new file mode 100644 index 0000000000..6ae3f8edda --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/validators/page.tsx @@ -0,0 +1,11 @@ +"use client"; + +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; + +export default function Page() { + return ( + +
Validators
+
+ ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/app/zk-nym-signers/page.tsx b/nym-node-status-api/nym-node-status-ui/src/app/zk-nym-signers/page.tsx new file mode 100644 index 0000000000..089eca1d97 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/app/zk-nym-signers/page.tsx @@ -0,0 +1,11 @@ +"use client"; + +import NestedLayoutWithHeader from "@/layouts/NestedLayoutWithHeader"; + +export default function Page() { + return ( + +
zk-nym signers
+
+ ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/client/@tanstack/react-query.gen.ts b/nym-node-status-api/nym-node-status-ui/src/client/@tanstack/react-query.gen.ts new file mode 100644 index 0000000000..5e672b9bb7 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/@tanstack/react-query.gen.ts @@ -0,0 +1,936 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { + type DefaultError, + type InfiniteData, + infiniteQueryOptions, + queryOptions, +} from "@tanstack/react-query"; +import { client as _heyApiClient } from "../client.gen"; +import { + type Options, + buildInformation, + gateways, + gatewaysSkinny, + getAllSessions, + getEntryGatewayCountries, + getEntryGateways, + getEntryGatewaysByCountry, + getExitGatewayCountries, + getExitGateways, + getExitGatewaysByCountry, + getGateway, + getGatewayCountries, + getGateways, + getGatewaysByCountry, + getMixnodes, + getStats, + health, + mixnodes, + mixnodes2, + nodeDelegations, + nymNodes, + summary, + summaryHistory, +} from "../sdk.gen"; +import type { + BuildInformationData, + GatewaysData, + GatewaysResponse, + GatewaysSkinnyData, + GatewaysSkinnyResponse, + GetAllSessionsData, + GetAllSessionsResponse, + GetEntryGatewayCountriesData, + GetEntryGatewaysByCountryData, + GetEntryGatewaysData, + GetExitGatewayCountriesData, + GetExitGatewaysByCountryData, + GetExitGatewaysData, + GetGatewayCountriesData, + GetGatewayData, + GetGatewaysByCountryData, + GetGatewaysData, + GetMixnodesData, + GetStatsData, + GetStatsResponse, + HealthData, + Mixnodes2Data, + Mixnodes2Response, + MixnodesData, + MixnodesResponse, + NodeDelegationsData, + NymNodesData, + NymNodesResponse, + SummaryData, + SummaryHistoryData, +} from "../types.gen"; + +export type QueryKey = [ + Pick & { + _id: string; + _infinite?: boolean; + }, +]; + +const createQueryKey = ( + id: string, + options?: TOptions, + infinite?: boolean, +): [QueryKey[0]] => { + const params: QueryKey[0] = { + _id: id, + baseUrl: (options?.client ?? _heyApiClient).getConfig().baseUrl, + } as QueryKey[0]; + if (infinite) { + params._infinite = infinite; + } + if (options?.body) { + params.body = options.body; + } + if (options?.headers) { + params.headers = options.headers; + } + if (options?.path) { + params.path = options.path; + } + if (options?.query) { + params.query = options.query; + } + return [params]; +}; + +export const getGatewaysQueryKey = (options?: Options) => + createQueryKey("getGateways", options); + +/** + * Gets available entry and exit gateways from the Nym network directory + */ +export const getGatewaysOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getGateways({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getGatewaysQueryKey(options), + }); +}; + +export const getGatewayCountriesQueryKey = ( + options?: Options, +) => createQueryKey("getGatewayCountries", options); + +/** + * Gets available exit gateway countries as two-letter ISO country codes from the Nym network directory + */ +export const getGatewayCountriesOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getGatewayCountries({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getGatewayCountriesQueryKey(options), + }); +}; + +export const getGatewaysByCountryQueryKey = ( + options: Options, +) => createQueryKey("getGatewaysByCountry", options); + +/** + * Gets available gateways from the Nym network directory by country + */ +export const getGatewaysByCountryOptions = ( + options: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getGatewaysByCountry({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getGatewaysByCountryQueryKey(options), + }); +}; + +export const getEntryGatewaysQueryKey = ( + options?: Options, +) => createQueryKey("getEntryGateways", options); + +/** + * Gets available entry gateways from the Nym network directory + */ +export const getEntryGatewaysOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEntryGateways({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getEntryGatewaysQueryKey(options), + }); +}; + +export const getEntryGatewayCountriesQueryKey = ( + options?: Options, +) => createQueryKey("getEntryGatewayCountries", options); + +/** + * Gets available entry gateway countries as two-letter ISO country codes from the Nym network directory + */ +export const getEntryGatewayCountriesOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEntryGatewayCountries({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getEntryGatewayCountriesQueryKey(options), + }); +}; + +export const getEntryGatewaysByCountryQueryKey = ( + options: Options, +) => createQueryKey("getEntryGatewaysByCountry", options); + +/** + * Gets available entry gateways from the Nym network directory by country + */ +export const getEntryGatewaysByCountryOptions = ( + options: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getEntryGatewaysByCountry({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getEntryGatewaysByCountryQueryKey(options), + }); +}; + +export const getExitGatewaysQueryKey = ( + options?: Options, +) => createQueryKey("getExitGateways", options); + +/** + * Gets available exit gateways from the Nym network directory + */ +export const getExitGatewaysOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getExitGateways({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getExitGatewaysQueryKey(options), + }); +}; + +export const getExitGatewayCountriesQueryKey = ( + options?: Options, +) => createQueryKey("getExitGatewayCountries", options); + +/** + * Gets available exit gateway countries as two-letter ISO country codes from the Nym network directory + */ +export const getExitGatewayCountriesOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getExitGatewayCountries({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getExitGatewayCountriesQueryKey(options), + }); +}; + +export const getExitGatewaysByCountryQueryKey = ( + options: Options, +) => createQueryKey("getExitGatewaysByCountry", options); + +/** + * Gets available exit gateways from the Nym network directory by country + */ +export const getExitGatewaysByCountryOptions = ( + options: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getExitGatewaysByCountry({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getExitGatewaysByCountryQueryKey(options), + }); +}; + +export const nymNodesQueryKey = (options?: Options) => + createQueryKey("nymNodes", options); + +export const nymNodesOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await nymNodes({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: nymNodesQueryKey(options), + }); +}; + +const createInfiniteParams = < + K extends Pick[0], "body" | "headers" | "path" | "query">, +>( + queryKey: QueryKey, + page: K, +) => { + const params = { + ...queryKey[0], + }; + if (page.body) { + params.body = { + ...(queryKey[0].body as any), + ...(page.body as any), + }; + } + if (page.headers) { + params.headers = { + ...queryKey[0].headers, + ...page.headers, + }; + } + if (page.path) { + params.path = { + ...(queryKey[0].path as any), + ...(page.path as any), + }; + } + if (page.query) { + params.query = { + ...(queryKey[0].query as any), + ...(page.query as any), + }; + } + return params as unknown as typeof page; +}; + +export const nymNodesInfiniteQueryKey = ( + options?: Options, +): QueryKey> => createQueryKey("nymNodes", options, true); + +export const nymNodesInfiniteOptions = (options?: Options) => { + return infiniteQueryOptions< + NymNodesResponse, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + page: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await nymNodes({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: nymNodesInfiniteQueryKey(options), + }, + ); +}; + +export const nodeDelegationsQueryKey = ( + options: Options, +) => createQueryKey("nodeDelegations", options); + +export const nodeDelegationsOptions = ( + options: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await nodeDelegations({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: nodeDelegationsQueryKey(options), + }); +}; + +export const gatewaysQueryKey = (options?: Options) => + createQueryKey("gateways", options); + +export const gatewaysOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await gateways({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: gatewaysQueryKey(options), + }); +}; + +export const gatewaysInfiniteQueryKey = ( + options?: Options, +): QueryKey> => createQueryKey("gateways", options, true); + +export const gatewaysInfiniteOptions = (options?: Options) => { + return infiniteQueryOptions< + GatewaysResponse, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + page: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await gateways({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: gatewaysInfiniteQueryKey(options), + }, + ); +}; + +export const gatewaysSkinnyQueryKey = (options?: Options) => + createQueryKey("gatewaysSkinny", options); + +export const gatewaysSkinnyOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await gatewaysSkinny({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: gatewaysSkinnyQueryKey(options), + }); +}; + +export const gatewaysSkinnyInfiniteQueryKey = ( + options?: Options, +): QueryKey> => + createQueryKey("gatewaysSkinny", options, true); + +export const gatewaysSkinnyInfiniteOptions = ( + options?: Options, +) => { + return infiniteQueryOptions< + GatewaysSkinnyResponse, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + page: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await gatewaysSkinny({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: gatewaysSkinnyInfiniteQueryKey(options), + }, + ); +}; + +export const getGatewayQueryKey = (options: Options) => + createQueryKey("getGateway", options); + +export const getGatewayOptions = (options: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getGateway({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getGatewayQueryKey(options), + }); +}; + +export const getAllSessionsQueryKey = (options?: Options) => + createQueryKey("getAllSessions", options); + +export const getAllSessionsOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getAllSessions({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getAllSessionsQueryKey(options), + }); +}; + +export const getAllSessionsInfiniteQueryKey = ( + options?: Options, +): QueryKey> => + createQueryKey("getAllSessions", options, true); + +export const getAllSessionsInfiniteOptions = ( + options?: Options, +) => { + return infiniteQueryOptions< + GetAllSessionsResponse, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + page: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await getAllSessions({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getAllSessionsInfiniteQueryKey(options), + }, + ); +}; + +export const mixnodesQueryKey = (options?: Options) => + createQueryKey("mixnodes", options); + +export const mixnodesOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await mixnodes({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: mixnodesQueryKey(options), + }); +}; + +export const mixnodesInfiniteQueryKey = ( + options?: Options, +): QueryKey> => createQueryKey("mixnodes", options, true); + +export const mixnodesInfiniteOptions = (options?: Options) => { + return infiniteQueryOptions< + MixnodesResponse, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + page: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await mixnodes({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: mixnodesInfiniteQueryKey(options), + }, + ); +}; + +export const getStatsQueryKey = (options?: Options) => + createQueryKey("getStats", options); + +export const getStatsOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getStats({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getStatsQueryKey(options), + }); +}; + +export const getStatsInfiniteQueryKey = ( + options?: Options, +): QueryKey> => createQueryKey("getStats", options, true); + +export const getStatsInfiniteOptions = (options?: Options) => { + return infiniteQueryOptions< + GetStatsResponse, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + offset: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await getStats({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getStatsInfiniteQueryKey(options), + }, + ); +}; + +export const getMixnodesQueryKey = (options: Options) => + createQueryKey("getMixnodes", options); + +export const getMixnodesOptions = (options: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await getMixnodes({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: getMixnodesQueryKey(options), + }); +}; + +export const mixnodes2QueryKey = (options?: Options) => + createQueryKey("mixnodes2", options); + +export const mixnodes2Options = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await mixnodes2({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: mixnodes2QueryKey(options), + }); +}; + +export const mixnodes2InfiniteQueryKey = ( + options?: Options, +): QueryKey> => + createQueryKey("mixnodes2", options, true); + +export const mixnodes2InfiniteOptions = (options?: Options) => { + return infiniteQueryOptions< + Mixnodes2Response, + DefaultError, + InfiniteData, + QueryKey>, + | number + | Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > + >( + // @ts-ignore + { + queryFn: async ({ pageParam, queryKey, signal }) => { + // @ts-ignore + const page: Pick< + QueryKey>[0], + "body" | "headers" | "path" | "query" + > = + typeof pageParam === "object" + ? pageParam + : { + query: { + page: pageParam, + }, + }; + const params = createInfiniteParams(queryKey, page); + const { data } = await mixnodes2({ + ...options, + ...params, + signal, + throwOnError: true, + }); + return data; + }, + queryKey: mixnodes2InfiniteQueryKey(options), + }, + ); +}; + +export const buildInformationQueryKey = ( + options?: Options, +) => createQueryKey("buildInformation", options); + +export const buildInformationOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await buildInformation({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: buildInformationQueryKey(options), + }); +}; + +export const healthQueryKey = (options?: Options) => + createQueryKey("health", options); + +export const healthOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await health({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: healthQueryKey(options), + }); +}; + +export const summaryQueryKey = (options?: Options) => + createQueryKey("summary", options); + +export const summaryOptions = (options?: Options) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await summary({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: summaryQueryKey(options), + }); +}; + +export const summaryHistoryQueryKey = (options?: Options) => + createQueryKey("summaryHistory", options); + +export const summaryHistoryOptions = ( + options?: Options, +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await summaryHistory({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }); + return data; + }, + queryKey: summaryHistoryQueryKey(options), + }); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/client.gen.ts b/nym-node-status-api/nym-node-status-ui/src/client/client.gen.ts new file mode 100644 index 0000000000..d4af1d498e --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/client.gen.ts @@ -0,0 +1,28 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { + type Config, + type ClientOptions as DefaultClientOptions, + createClient, + createConfig, +} from "./client"; +import type { ClientOptions } from "./types.gen"; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = + ( + override?: Config, + ) => Config & T>; + +export const client = createClient( + createConfig({ + baseUrl: "https://mainnet-node-status-api.nymtech.cc", + }), +); diff --git a/nym-node-status-api/nym-node-status-ui/src/client/client/client.ts b/nym-node-status-api/nym-node-status-ui/src/client/client/client.ts new file mode 100644 index 0000000000..748558f584 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/client/client.ts @@ -0,0 +1,195 @@ +import type { Client, Config, RequestOptions } from "./types"; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from "./utils"; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + RequestOptions + >(); + + const request: Client["request"] = async (options) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body && opts.bodySerializer) { + opts.body = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.body === undefined || opts.body === "") { + opts.headers.delete("Content-Type"); + } + + const url = buildUrl(opts); + const requestInit: ReqInit = { + redirect: "follow", + ...opts, + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request._fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response = await _fetch(request); + + for (const fn of interceptors.response._fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + if ( + response.status === 204 || + response.headers.get("Content-Length") === "0" + ) { + return opts.responseStyle === "data" + ? {} + : { + data: {}, + ...result, + }; + } + + const parseAs = + (opts.parseAs === "auto" + ? getParseAs(response.headers.get("Content-Type")) + : opts.parseAs) ?? "json"; + + let data: any; + switch (parseAs) { + case "arrayBuffer": + case "blob": + case "formData": + case "json": + case "text": + data = await response[parseAs](); + break; + case "stream": + return opts.responseStyle === "data" + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === "json") { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === "data" + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error._fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === "data" + ? undefined + : { + error: finalError, + ...result, + }; + }; + + return { + buildUrl, + connect: (options) => request({ ...options, method: "CONNECT" }), + delete: (options) => request({ ...options, method: "DELETE" }), + get: (options) => request({ ...options, method: "GET" }), + getConfig, + head: (options) => request({ ...options, method: "HEAD" }), + interceptors, + options: (options) => request({ ...options, method: "OPTIONS" }), + patch: (options) => request({ ...options, method: "PATCH" }), + post: (options) => request({ ...options, method: "POST" }), + put: (options) => request({ ...options, method: "PUT" }), + request, + setConfig, + trace: (options) => request({ ...options, method: "TRACE" }), + }; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/client/index.ts b/nym-node-status-api/nym-node-status-ui/src/client/client/index.ts new file mode 100644 index 0000000000..7b4add4180 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/client/index.ts @@ -0,0 +1,22 @@ +export type { Auth } from "../core/auth"; +export type { QuerySerializerOptions } from "../core/bodySerializer"; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from "../core/bodySerializer"; +export { buildClientParams } from "../core/params"; +export { createClient } from "./client"; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + OptionsLegacyParser, + RequestOptions, + RequestResult, + ResponseStyle, + TDataShape, +} from "./types"; +export { createConfig, mergeHeaders } from "./utils"; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/client/types.ts b/nym-node-status-api/nym-node-status-ui/src/client/client/types.ts new file mode 100644 index 0000000000..5344ff3d2a --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/client/types.ts @@ -0,0 +1,219 @@ +import type { Auth } from "../core/auth"; +import type { Client as CoreClient, Config as CoreConfig } from "../core/types"; +import type { Middleware } from "./utils"; + +export type ResponseStyle = "data" | "fields"; + +export interface Config + extends Omit, + CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T["baseUrl"]; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: (request: Request) => ReturnType; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: + | "arrayBuffer" + | "auto" + | "blob" + | "formData" + | "json" + | "stream" + | "text"; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T["throwOnError"]; +} + +export interface RequestOptions< + TResponseStyle extends ResponseStyle = "fields", + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }> { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = "fields", +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends "data" + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends "data" + ? + | (TData extends Record + ? TData[keyof TData] + : TData) + | undefined + : ( + | { + data: TData extends Record + ? TData[keyof TData] + : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record + ? TError[keyof TError] + : TError; + } + ) & { + request: Request; + response: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = "fields", +>( + options: Omit, "method">, +) => RequestResult; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = "fields", +>( + options: Omit, "method"> & + Pick>, "method">, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: Pick & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = "fields", +> = OmitKeys< + RequestOptions, + "body" | "path" | "query" | "url" +> & + Omit; + +export type OptionsLegacyParser< + TData = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = "fields", +> = TData extends { body?: any } + ? TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + "body" | "headers" | "url" + > & + TData + : OmitKeys, "body" | "url"> & + TData & + Pick, "headers"> + : TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + "headers" | "url" + > & + TData & + Pick, "body"> + : OmitKeys, "url"> & TData; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/client/utils.ts b/nym-node-status-api/nym-node-status-ui/src/client/client/utils.ts new file mode 100644 index 0000000000..ff8112bd56 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/client/utils.ts @@ -0,0 +1,417 @@ +import { getAuthToken } from "../core/auth"; +import type { + QuerySerializer, + QuerySerializerOptions, +} from "../core/bodySerializer"; +import { jsonBodySerializer } from "../core/bodySerializer"; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from "../core/pathSerializer"; +import type { Client, ClientOptions, Config, RequestOptions } from "./types"; + +interface PathSerializer { + path: Record; + url: string; +} + +const PATH_PARAM_RE = /\{[^{}]+\}/g; + +type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"; +type MatrixStyle = "label" | "matrix" | "simple"; +type ArraySeparatorStyle = ArrayStyle | MatrixStyle; + +const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = "simple"; + + if (name.endsWith("*")) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith(".")) { + name = name.substring(1); + style = "label"; + } else if (name.startsWith(";")) { + name = name.substring(1); + style = "matrix"; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === "object") { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === "matrix") { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === "label" ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const createQuerySerializer = ({ + allowReserved, + array, + object, +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === "object") { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved, + explode: true, + name, + style: "form", + value, + ...array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === "object") { + const serializedObject = serializeObjectParam({ + allowReserved, + explode: true, + name, + style: "deepObject", + value: value as Record, + ...object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join("&"); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + contentType: string | null, +): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return "stream"; + } + + const cleanContent = contentType.split(";")[0]?.trim(); + + if (!cleanContent) { + return; + } + + if ( + cleanContent.startsWith("application/json") || + cleanContent.endsWith("+json") + ) { + return "json"; + } + + if (cleanContent === "multipart/form-data") { + return "formData"; + } + + if ( + ["application/", "audio/", "image/", "video/"].some((type) => + cleanContent.startsWith(type), + ) + ) { + return "blob"; + } + + if (cleanContent.startsWith("text/")) { + return "text"; + } + + return; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, "security"> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? "Authorization"; + + switch (auth.in) { + case "query": + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case "cookie": + options.headers.append("Cookie", `${name}=${token}`); + break; + case "header": + default: + options.headers.set(name, token); + break; + } + + return; + } +}; + +export const buildUrl: Client["buildUrl"] = (options) => { + const url = getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === "function" + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith("/") ? _url : `/${_url}`; + let url = (baseUrl ?? "") + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ""; + if (search.startsWith("?")) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith("/")) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +export const mergeHeaders = ( + ...headers: Array["headers"] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header || typeof header !== "object") { + continue; + } + + const iterator = + header instanceof Headers ? header.entries() : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === "object" ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + _fns: (Interceptor | null)[]; + + constructor() { + this._fns = []; + } + + clear() { + this._fns = []; + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === "number") { + return this._fns[id] ? id : -1; + } else { + return this._fns.indexOf(id); + } + } + exists(id: number | Interceptor) { + const index = this.getInterceptorIndex(id); + return !!this._fns[index]; + } + + eject(id: number | Interceptor) { + const index = this.getInterceptorIndex(id); + if (this._fns[index]) { + this._fns[index] = null; + } + } + + update(id: number | Interceptor, fn: Interceptor) { + const index = this.getInterceptorIndex(id); + if (this._fns[index]) { + this._fns[index] = fn; + return id; + } else { + return false; + } + } + + use(fn: Interceptor) { + this._fns = [...this._fns, fn]; + return this._fns.length - 1; + } +} + +// `createInterceptors()` response, meant for external use as it does not +// expose internals +export interface Middleware { + error: Pick< + Interceptors>, + "eject" | "use" + >; + request: Pick>, "eject" | "use">; + response: Pick< + Interceptors>, + "eject" | "use" + >; +} + +// do not add `Middleware` as return type so we can use _fns internally +export const createInterceptors = () => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: "form", + }, + object: { + explode: true, + style: "deepObject", + }, +}); + +const defaultHeaders = { + "Content-Type": "application/json", +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: "auto", + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/nym-node-status-api/nym-node-status-ui/src/client/core/auth.ts b/nym-node-status-api/nym-node-status-ui/src/client/core/auth.ts new file mode 100644 index 0000000000..70e9d5dba5 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/core/auth.ts @@ -0,0 +1,40 @@ +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: "header" | "query" | "cookie"; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: "basic" | "bearer"; + type: "apiKey" | "http"; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === "function" ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === "bearer") { + return `Bearer ${token}`; + } + + if (auth.scheme === "basic") { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/core/bodySerializer.ts b/nym-node-status-api/nym-node-status-ui/src/client/core/bodySerializer.ts new file mode 100644 index 0000000000..35e2a17e1a --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/core/bodySerializer.ts @@ -0,0 +1,88 @@ +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from "./pathSerializer"; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +export interface QuerySerializerOptions { + allowReserved?: boolean; + array?: SerializerOptions; + object?: SerializerOptions; +} + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === "string" || value instanceof Blob) { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === "string") { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === "bigint" ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/core/params.ts b/nym-node-status-api/nym-node-status-ui/src/client/core/params.ts new file mode 100644 index 0000000000..0771542b14 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/core/params.ts @@ -0,0 +1,141 @@ +type Slot = "body" | "headers" | "path" | "query"; + +export type Field = + | { + in: Exclude; + key: string; + map?: string; + } + | { + in: Extract; + key?: string; + map?: string; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: "body", + $headers_: "headers", + $path_: "path", + $query_: "query", +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + { + in: Slot; + map?: string; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ("in" in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === "object" && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ("in" in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + (params[field.in] as Record)[name] = arg; + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else { + for (const [slot, allowed] of Object.entries( + config.allowExtra ?? {}, + )) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/core/pathSerializer.ts b/nym-node-status-api/nym-node-status-ui/src/client/core/pathSerializer.ts new file mode 100644 index 0000000000..4052ad1279 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/core/pathSerializer.ts @@ -0,0 +1,179 @@ +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = "form" | "spaceDelimited" | "pipeDelimited"; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = "label" | "matrix" | "simple"; +export type ObjectStyle = "form" | "deepObject"; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case "label": + return "."; + case "matrix": + return ";"; + case "simple": + return ","; + default: + return "&"; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case "form": + return ","; + case "pipeDelimited": + return "|"; + case "spaceDelimited": + return "%20"; + default: + return ","; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case "label": + return "."; + case "matrix": + return ";"; + case "simple": + return ","; + default: + return "&"; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case "label": + return `.${joinedValues}`; + case "matrix": + return `;${name}=${joinedValues}`; + case "simple": + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === "label" || style === "simple") { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === "label" || style === "matrix" + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ""; + } + + if (typeof value === "object") { + throw new Error( + "Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.", + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== "deepObject" && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(","); + switch (style) { + case "form": + return `${name}=${joinedValues}`; + case "label": + return `.${joinedValues}`; + case "matrix": + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === "deepObject" ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === "label" || style === "matrix" + ? separator + joinedValues + : joinedValues; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/core/types.ts b/nym-node-status-api/nym-node-status-ui/src/client/core/types.ts new file mode 100644 index 0000000000..940f22a77f --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/core/types.ts @@ -0,0 +1,104 @@ +import type { Auth, AuthToken } from "./auth"; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from "./bodySerializer"; + +export interface Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, +> { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + connect: MethodFn; + delete: MethodFn; + get: MethodFn; + getConfig: () => Config; + head: MethodFn; + options: MethodFn; + patch: MethodFn; + post: MethodFn; + put: MethodFn; + request: RequestFn; + setConfig: (config: Config) => Config; + trace: MethodFn; +} + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit["headers"] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: + | "CONNECT" + | "DELETE" + | "GET" + | "HEAD" + | "OPTIONS" + | "PATCH" + | "POST" + | "PUT" + | "TRACE"; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} diff --git a/nym-node-status-api/nym-node-status-ui/src/client/index.ts b/nym-node-status-api/nym-node-status-ui/src/client/index.ts new file mode 100644 index 0000000000..da87079367 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/index.ts @@ -0,0 +1,3 @@ +// This file is auto-generated by @hey-api/openapi-ts +export * from "./types.gen"; +export * from "./sdk.gen"; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/sdk.gen.ts b/nym-node-status-api/nym-node-status-ui/src/client/sdk.gen.ts new file mode 100644 index 0000000000..b571278744 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/sdk.gen.ts @@ -0,0 +1,395 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Client, Options as ClientOptions, TDataShape } from "./client"; +import { client as _heyApiClient } from "./client.gen"; +import type { + BuildInformationData, + BuildInformationResponses, + GatewaysData, + GatewaysResponses, + GatewaysSkinnyData, + GatewaysSkinnyResponses, + GetAllSessionsData, + GetAllSessionsResponses, + GetEntryGatewayCountriesData, + GetEntryGatewayCountriesResponses, + GetEntryGatewaysByCountryData, + GetEntryGatewaysByCountryResponses, + GetEntryGatewaysData, + GetEntryGatewaysResponses, + GetExitGatewayCountriesData, + GetExitGatewayCountriesResponses, + GetExitGatewaysByCountryData, + GetExitGatewaysByCountryResponses, + GetExitGatewaysData, + GetExitGatewaysResponses, + GetGatewayCountriesData, + GetGatewayCountriesResponses, + GetGatewayData, + GetGatewayResponses, + GetGatewaysByCountryData, + GetGatewaysByCountryResponses, + GetGatewaysData, + GetGatewaysResponses, + GetMixnodesData, + GetMixnodesResponses, + GetStatsData, + GetStatsResponses, + HealthData, + HealthResponses, + Mixnodes2Data, + Mixnodes2Responses, + MixnodesData, + MixnodesResponses, + NodeDelegationsData, + NodeDelegationsResponses, + NymNodesData, + NymNodesResponses, + SummaryData, + SummaryHistoryData, + SummaryHistoryResponses, + SummaryResponses, +} from "./types.gen"; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, +> = ClientOptions & { + /** + * You can provide a client instance returned by `createClient()` instead of + * individual options. This might be also useful if you want to implement a + * custom client. + */ + client?: Client; + /** + * You can pass arbitrary values through the `meta` object. This can be + * used to access values that aren't defined as part of the SDK function. + */ + meta?: Record; +}; + +/** + * Gets available entry and exit gateways from the Nym network directory + */ +export const getGateways = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetGatewaysResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways", + ...options, + }); +}; + +/** + * Gets available exit gateway countries as two-letter ISO country codes from the Nym network directory + */ +export const getGatewayCountries = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetGatewayCountriesResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/countries", + ...options, + }); +}; + +/** + * Gets available gateways from the Nym network directory by country + */ +export const getGatewaysByCountry = ( + options: Options, +) => { + return (options.client ?? _heyApiClient).get< + GetGatewaysByCountryResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/country/{two_letter_country_code}", + ...options, + }); +}; + +/** + * Gets available entry gateways from the Nym network directory + */ +export const getEntryGateways = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetEntryGatewaysResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/entry", + ...options, + }); +}; + +/** + * Gets available entry gateway countries as two-letter ISO country codes from the Nym network directory + */ +export const getEntryGatewayCountries = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetEntryGatewayCountriesResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/entry/countries", + ...options, + }); +}; + +/** + * Gets available entry gateways from the Nym network directory by country + */ +export const getEntryGatewaysByCountry = ( + options: Options, +) => { + return (options.client ?? _heyApiClient).get< + GetEntryGatewaysByCountryResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/entry/country/{two_letter_country_code}", + ...options, + }); +}; + +/** + * Gets available exit gateways from the Nym network directory + */ +export const getExitGateways = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetExitGatewaysResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/exit", + ...options, + }); +}; + +/** + * Gets available exit gateway countries as two-letter ISO country codes from the Nym network directory + */ +export const getExitGatewayCountries = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetExitGatewayCountriesResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/exit/countries", + ...options, + }); +}; + +/** + * Gets available exit gateways from the Nym network directory by country + */ +export const getExitGatewaysByCountry = ( + options: Options, +) => { + return (options.client ?? _heyApiClient).get< + GetExitGatewaysByCountryResponses, + unknown, + ThrowOnError + >({ + url: "/dvpn/v1/directory/gateways/exit/country/{two_letter_country_code}", + ...options, + }); +}; + +export const nymNodes = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + NymNodesResponses, + unknown, + ThrowOnError + >({ + url: "/explorer/v3/nym-nodes", + ...options, + }); +}; + +export const nodeDelegations = ( + options: Options, +) => { + return (options.client ?? _heyApiClient).get< + NodeDelegationsResponses, + unknown, + ThrowOnError + >({ + url: "/explorer/v3/nym-nodes/{node_id}/delegations", + ...options, + }); +}; + +export const gateways = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GatewaysResponses, + unknown, + ThrowOnError + >({ + url: "/v2/gateways", + ...options, + }); +}; + +export const gatewaysSkinny = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GatewaysSkinnyResponses, + unknown, + ThrowOnError + >({ + url: "/v2/gateways/skinny", + ...options, + }); +}; + +export const getGateway = ( + options: Options, +) => { + return (options.client ?? _heyApiClient).get< + GetGatewayResponses, + unknown, + ThrowOnError + >({ + url: "/v2/gateways/{identity_key}", + ...options, + }); +}; + +export const getAllSessions = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetAllSessionsResponses, + unknown, + ThrowOnError + >({ + url: "/v2/metrics/sessions", + ...options, + }); +}; + +export const mixnodes = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + MixnodesResponses, + unknown, + ThrowOnError + >({ + url: "/v2/mixnodes", + ...options, + }); +}; + +export const getStats = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + GetStatsResponses, + unknown, + ThrowOnError + >({ + url: "/v2/mixnodes/stats", + ...options, + }); +}; + +export const getMixnodes = ( + options: Options, +) => { + return (options.client ?? _heyApiClient).get< + GetMixnodesResponses, + unknown, + ThrowOnError + >({ + url: "/v2/mixnodes/{mix_id}", + ...options, + }); +}; + +export const mixnodes2 = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + Mixnodes2Responses, + unknown, + ThrowOnError + >({ + url: "/v2/services", + ...options, + }); +}; + +export const buildInformation = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + BuildInformationResponses, + unknown, + ThrowOnError + >({ + url: "/v2/status/build_information", + ...options, + }); +}; + +export const health = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + HealthResponses, + unknown, + ThrowOnError + >({ + url: "/v2/status/health", + ...options, + }); +}; + +export const summary = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + SummaryResponses, + unknown, + ThrowOnError + >({ + url: "/v2/summary", + ...options, + }); +}; + +export const summaryHistory = ( + options?: Options, +) => { + return (options?.client ?? _heyApiClient).get< + SummaryHistoryResponses, + unknown, + ThrowOnError + >({ + url: "/v2/summary/history", + ...options, + }); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/client/types.gen.ts b/nym-node-status-api/nym-node-status-ui/src/client/types.gen.ts new file mode 100644 index 0000000000..bb6b7f5839 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/client/types.gen.ts @@ -0,0 +1,940 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AnnouncePorts = { + mix_port?: number | null; + verloc_port?: number | null; +}; + +export type AuthenticatorDetails = { + /** + * address of the embedded authenticator + */ + address: string; +}; + +/** + * Auxiliary details of the associated Nym Node. + */ +export type AuxiliaryDetails = { + /** + * Specifies whether this node operator has agreed to the terms and conditions + * as defined at + */ + accepted_operator_terms_and_conditions?: boolean; + announce_ports?: AnnouncePorts; + /** + * Optional ISO 3166 alpha-2 two-letter country code of the node's **physical** location + */ + location?: string | null; +}; + +export type BasicEntryInformation = { + hostname?: string | null; + ws_port: number; + wss_port?: number | null; +}; + +export type BinaryBuildInformationOwned = { + /** + * Provides the name of the binary, i.e. the content of `CARGO_PKG_NAME` environmental variable. + */ + binary_name: string; + /** + * Provides the build timestamp, for example `2021-02-23T20:14:46.558472672+00:00`. + */ + build_timestamp: string; + /** + * Provides the build version, for example `0.1.0-9-g46f83e1`. + */ + build_version: string; + /** + * Provides the cargo debug mode that was used for the build. + */ + cargo_profile: string; + /** + * Provides the cargo target triple that was used for the build. + */ + cargo_triple?: string; + /** + * Provides the name of the git branch that was used for the build, for example `master`. + */ + commit_branch: string; + /** + * Provides the hash of the commit that was used for the build, for example `46f83e112520533338245862d366f6a02cef07d4`. + */ + commit_sha: string; + /** + * Provides the timestamp of the commit that was used for the build, for example `2021-02-23T08:08:02-05:00`. + */ + commit_timestamp: string; + /** + * Provides the rustc channel that was used for the build, for example `nightly`. + */ + rustc_channel: string; + /** + * Provides the rustc version that was used for the build, for example `1.52.0-nightly`. + */ + rustc_version: string; +}; + +export type BuildInformation = { + build_version: string; + commit_branch: string; + commit_sha: string; +}; + +/** + * Coin + */ +export type CoinSchema = { + amount: string; + denom: string; +}; + +export type DVpnGateway = { + authenticator?: null | AuthenticatorDetails; + build_information: BinaryBuildInformationOwned; + entry?: null | BasicEntryInformation; + identity_key: string; + ip_addresses: Array; + ip_packet_router?: null | IpPacketRouterDetails; + last_probe?: null | DirectoryGwProbe; + location: Location; + mix_port: number; + name: string; + performance: string; + role: NodeRole; +}; + +export type DailyStats = { + date_utc: string; + total_packets_dropped: number; + total_packets_received: number; + total_packets_sent: number; + total_stake: number; +}; + +export type DeclaredRoles = { + entry: boolean; + exit_ipr: boolean; + exit_nr: boolean; + mixnode: boolean; +}; + +export type DescribedNodeType = + | "legacy_mixnode" + | "legacy_gateway" + | "nym_node"; + +export type DirectoryGwProbe = { + last_updated_utc: string; + outcome: ProbeOutcome; +}; + +export type Entry = EntryTestResult | null; + +export type EntryTestResult = { + can_connect: boolean; + can_route: boolean; +}; + +export type Exit = { + can_connect: boolean; + can_route_ip_external_v4: boolean; + can_route_ip_external_v6: boolean; + can_route_ip_v4: boolean; + can_route_ip_v6: boolean; +}; + +export type ExtendedNymNode = { + accepted_tnc: boolean; + bonded: boolean; + bonding_address?: string | null; + description: NodeDescription; + geoip?: null | NodeGeoData; + identity_key: string; + ip_address: string; + node_id: U32; + node_type: DescribedNodeType; + original_pledge: number; + rewarding_details?: null | NodeRewarding; + self_description: NymNodeData; + total_stake: string; + uptime: number; +}; + +export type Gateway = { + bonded: boolean; + config_score: number; + description: NodeDescription; + explorer_pretty_bond?: unknown; + gateway_identity_key: string; + last_probe_log?: string | null; + last_probe_result?: unknown; + last_testrun_utc?: string | null; + last_updated_utc: string; + performance: number; + routing_score: number; + self_described?: unknown; +}; + +export type GatewaySkinny = { + config_score: number; + explorer_pretty_bond?: unknown; + gateway_identity_key: string; + last_probe_result?: unknown; + last_testrun_utc?: string | null; + last_updated_utc: string; + performance: number; + routing_score: number; + self_described?: unknown; +}; + +export type GatewaySummary = { + bonded: GatewaySummaryBonded; + historical: GatewaySummaryHistorical; +}; + +export type GatewaySummaryBonded = { + count: number; + entry: number; + exit: number; + last_updated_utc: string; +}; + +export type GatewaySummaryHistorical = { + count: number; + last_updated_utc: string; +}; + +export type HealthInfo = { + uptime: number; +}; + +export type HostInformation = { + hostname?: string | null; + ip_address: Array; + keys: HostKeys; +}; + +export type HostKeys = { + ed25519: string; + x25519: string; + x25519_noise?: string; +}; + +export type IpPacketRouterDetails = { + /** + * address of the embedded ip packet router + */ + address: string; +}; + +/** + * based on + * https://github.com/nymtech/nym-vpn-client/blob/nym-vpn-core-v1.10.0/nym-vpn-core/crates/nym-gateway-probe/src/types.rs + * TODO: long term types should be moved into this repo because nym-vpn-client + * could pull it as a dependency and we'd have a single source of truth + */ +export type LastProbeResult = { + node: string; + outcome: ProbeOutcome; + used_entry: string; +}; + +export type Location = { + latitude: number; + longitude: number; + two_letter_iso_country_code: string; +}; + +export type MixingNodesSummary = { + count: number; + last_updated_utc: string; + legacy: number; + self_described: number; +}; + +export type Mixnode = { + bonded: boolean; + description: NodeDescription; + full_details?: unknown; + is_dp_delegatee: boolean; + last_updated_utc: string; + mix_id: number; + self_described?: unknown; + total_stake: number; +}; + +export type MixnodeSummary = { + bonded: MixingNodesSummary; + historical: MixnodeSummaryHistorical; +}; + +export type MixnodeSummaryHistorical = { + count: number; + last_updated_utc: string; +}; + +export type NetworkRequesterDetails = { + /** + * address of the embedded network requester + */ + address: string; + /** + * flag indicating whether this network requester uses the exit policy rather than the deprecated allow list + */ + uses_exit_policy: boolean; +}; + +export type NetworkSummary = { + gateways: GatewaySummary; + mixnodes: MixnodeSummary; + total_nodes: number; +}; + +/** + * The cost parameters, or the cost function, defined for the particular mixnode that influences + * how the rewards should be split between the node operator and its delegators. + */ +export type NodeCostParams = { + /** + * Operating cost of the associated node per the entire interval. + */ + interval_operating_cost: CoinSchema; + /** + * The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator. + */ + profit_margin_percent: string; +}; + +export type NodeDelegation = { + amount: CoinSchema; + block_height: number; + cumulative_reward_ratio: string; + owner: string; + proxy?: string | null; +}; + +export type NodeDescription = { + /** + * details define other optional details. + */ + details: string; + /** + * moniker defines a human-readable name for the node. + */ + moniker: string; + /** + * security contact defines an optional email for security contact. + */ + security_contact: string; + /** + * website defines an optional website link. + */ + website: string; +}; + +export type NodeGeoData = { + city: string; + country: string; + ip_address: string; + latitude: string; + longitude: string; + org: string; + postal: string; + region: string; + timezone: string; +}; + +export type NodeRewarding = { + /** + * Information provided by the operator that influence the cost function. + */ + cost_params: NodeCostParams; + /** + * Total delegation and compounded reward earned by all node delegators. + */ + delegates: string; + /** + * Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt + * to reward it multiple times in the same epoch. + */ + last_rewarded_epoch: U32; + /** + * Total pledge and compounded reward earned by the node operator. + */ + operator: string; + /** + * Cumulative reward earned by the "unit delegation" since the block 0. + */ + total_unit_reward: string; + unique_delegations: number; + /** + * Value of the theoretical "unit delegation" that has delegated to this node at block 0. + */ + unit_delegation: string; +}; + +export type NodeRole = + | { + Mixnode: { + layer: number; + }; + } + | "EntryGateway" + | "ExitGateway" + | "Standby" + | "Inactive"; + +export type NymNodeData = { + authenticator?: null | AuthenticatorDetails; + auxiliary_details?: AuxiliaryDetails; + build_information: BinaryBuildInformationOwned; + declared_role?: DeclaredRoles; + host_information: HostInformation; + ip_packet_router?: null | IpPacketRouterDetails; + last_polled?: OffsetDateTimeJsonSchemaWrapper; + mixnet_websockets: WebSockets; + network_requester?: null | NetworkRequesterDetails; + wireguard?: null | WireguardDetails; +}; + +export type OffsetDateTimeJsonSchemaWrapper = string; + +export type PagedResultExtendedNymNode = { + items: Array<{ + accepted_tnc: boolean; + bonded: boolean; + bonding_address?: string | null; + description: NodeDescription; + geoip?: null | NodeGeoData; + identity_key: string; + ip_address: string; + node_id: U32; + node_type: DescribedNodeType; + original_pledge: number; + rewarding_details?: null | NodeRewarding; + self_description: NymNodeData; + total_stake: string; + uptime: number; + }>; + page: number; + size: number; + total: number; +}; + +export type PagedResultGateway = { + items: Array<{ + bonded: boolean; + config_score: number; + description: NodeDescription; + explorer_pretty_bond?: unknown; + gateway_identity_key: string; + last_probe_log?: string | null; + last_probe_result?: unknown; + last_testrun_utc?: string | null; + last_updated_utc: string; + performance: number; + routing_score: number; + self_described?: unknown; + }>; + page: number; + size: number; + total: number; +}; + +export type PagedResultGatewaySkinny = { + items: Array<{ + config_score: number; + explorer_pretty_bond?: unknown; + gateway_identity_key: string; + last_probe_result?: unknown; + last_testrun_utc?: string | null; + last_updated_utc: string; + performance: number; + routing_score: number; + self_described?: unknown; + }>; + page: number; + size: number; + total: number; +}; + +export type PagedResultMixnode = { + items: Array<{ + bonded: boolean; + description: NodeDescription; + full_details?: unknown; + is_dp_delegatee: boolean; + last_updated_utc: string; + mix_id: number; + self_described?: unknown; + total_stake: number; + }>; + page: number; + size: number; + total: number; +}; + +export type PagedResultService = { + items: Array<{ + gateway_identity_key: string; + hostname?: string | null; + ip_address?: string | null; + last_successful_ping_utc?: string | null; + last_updated_utc: string; + mixnet_websockets?: unknown; + routing_score: number; + service_provider_client_id?: string | null; + }>; + page: number; + size: number; + total: number; +}; + +export type PagedResultSessionStats = { + items: Array<{ + day: string; + gateway_identity_key: string; + mixnet_sessions?: unknown; + node_id: number; + session_started: number; + unique_active_clients: number; + unknown_sessions?: unknown; + users_hashes?: unknown; + vpn_sessions?: unknown; + }>; + page: number; + size: number; + total: number; +}; + +export type ProbeOutcome = { + as_entry: Entry; + as_exit?: null | Exit; + wg?: null | ProbeOutcomeV1; +}; + +export type ProbeOutcomeV1 = { + can_handshake_v4: boolean; + can_handshake_v6: boolean; + can_register: boolean; + can_resolve_dns_v4: boolean; + can_resolve_dns_v6: boolean; + download_duration_sec_v4: number; + download_duration_sec_v6: number; + download_error_v4: string; + download_error_v6: string; + downloaded_file_v4: string; + downloaded_file_v6: string; + ping_hosts_performance_v4: number; + ping_hosts_performance_v6: number; + ping_ips_performance_v4: number; + ping_ips_performance_v6: number; +}; + +export type Service = { + gateway_identity_key: string; + hostname?: string | null; + ip_address?: string | null; + last_successful_ping_utc?: string | null; + last_updated_utc: string; + mixnet_websockets?: unknown; + routing_score: number; + service_provider_client_id?: string | null; +}; + +export type SessionStats = { + day: string; + gateway_identity_key: string; + mixnet_sessions?: unknown; + node_id: number; + session_started: number; + unique_active_clients: number; + unknown_sessions?: unknown; + users_hashes?: unknown; + vpn_sessions?: unknown; +}; + +export type SummaryHistory = { + date: string; + timestamp_utc: string; + value_json: unknown; +}; + +export type TestRun = { + id: number; + identity_key: string; + log: string; + status: string; +}; + +export type WebSockets = { + ws_port: number; + wss_port?: number | null; +}; + +export type WireguardDetails = { + port: number; + public_key: string; +}; + +export type U32 = number; + +export type GetGatewaysData = { + body?: never; + path?: never; + query?: { + min_node_version?: string; + }; + url: "/dvpn/v1/directory/gateways"; +}; + +export type GetGatewaysResponses = { + 200: Array; +}; + +export type GetGatewaysResponse = + GetGatewaysResponses[keyof GetGatewaysResponses]; + +export type GetGatewayCountriesData = { + body?: never; + path?: never; + query?: never; + url: "/dvpn/v1/directory/gateways/countries"; +}; + +export type GetGatewayCountriesResponses = { + 200: Array; +}; + +export type GetGatewayCountriesResponse = + GetGatewayCountriesResponses[keyof GetGatewayCountriesResponses]; + +export type GetGatewaysByCountryData = { + body?: never; + path: { + two_letter_country_code: string; + }; + query?: never; + url: "/dvpn/v1/directory/gateways/country/{two_letter_country_code}"; +}; + +export type GetGatewaysByCountryResponses = { + 200: Array; +}; + +export type GetGatewaysByCountryResponse = + GetGatewaysByCountryResponses[keyof GetGatewaysByCountryResponses]; + +export type GetEntryGatewaysData = { + body?: never; + path?: never; + query?: never; + url: "/dvpn/v1/directory/gateways/entry"; +}; + +export type GetEntryGatewaysResponses = { + 200: Array; +}; + +export type GetEntryGatewaysResponse = + GetEntryGatewaysResponses[keyof GetEntryGatewaysResponses]; + +export type GetEntryGatewayCountriesData = { + body?: never; + path?: never; + query?: never; + url: "/dvpn/v1/directory/gateways/entry/countries"; +}; + +export type GetEntryGatewayCountriesResponses = { + 200: Array; +}; + +export type GetEntryGatewayCountriesResponse = + GetEntryGatewayCountriesResponses[keyof GetEntryGatewayCountriesResponses]; + +export type GetEntryGatewaysByCountryData = { + body?: never; + path: { + two_letter_country_code: string; + }; + query?: never; + url: "/dvpn/v1/directory/gateways/entry/country/{two_letter_country_code}"; +}; + +export type GetEntryGatewaysByCountryResponses = { + 200: Array; +}; + +export type GetEntryGatewaysByCountryResponse = + GetEntryGatewaysByCountryResponses[keyof GetEntryGatewaysByCountryResponses]; + +export type GetExitGatewaysData = { + body?: never; + path?: never; + query?: never; + url: "/dvpn/v1/directory/gateways/exit"; +}; + +export type GetExitGatewaysResponses = { + 200: Array; +}; + +export type GetExitGatewaysResponse = + GetExitGatewaysResponses[keyof GetExitGatewaysResponses]; + +export type GetExitGatewayCountriesData = { + body?: never; + path?: never; + query?: never; + url: "/dvpn/v1/directory/gateways/exit/countries"; +}; + +export type GetExitGatewayCountriesResponses = { + 200: Array; +}; + +export type GetExitGatewayCountriesResponse = + GetExitGatewayCountriesResponses[keyof GetExitGatewayCountriesResponses]; + +export type GetExitGatewaysByCountryData = { + body?: never; + path: { + two_letter_country_code: string; + }; + query?: never; + url: "/dvpn/v1/directory/gateways/exit/country/{two_letter_country_code}"; +}; + +export type GetExitGatewaysByCountryResponses = { + 200: Array; +}; + +export type GetExitGatewaysByCountryResponse = + GetExitGatewaysByCountryResponses[keyof GetExitGatewaysByCountryResponses]; + +export type NymNodesData = { + body?: never; + path?: never; + query?: { + size?: number; + page?: number; + }; + url: "/explorer/v3/nym-nodes"; +}; + +export type NymNodesResponses = { + 200: PagedResultExtendedNymNode; +}; + +export type NymNodesResponse = NymNodesResponses[keyof NymNodesResponses]; + +export type NodeDelegationsData = { + body?: never; + path: { + node_id: U32; + }; + query?: never; + url: "/explorer/v3/nym-nodes/{node_id}/delegations"; +}; + +export type NodeDelegationsResponses = { + 200: NodeDelegation; +}; + +export type NodeDelegationsResponse = + NodeDelegationsResponses[keyof NodeDelegationsResponses]; + +export type GatewaysData = { + body?: never; + path?: never; + query?: { + size?: number; + page?: number; + }; + url: "/v2/gateways"; +}; + +export type GatewaysResponses = { + 200: PagedResultGateway; +}; + +export type GatewaysResponse = GatewaysResponses[keyof GatewaysResponses]; + +export type GatewaysSkinnyData = { + body?: never; + path?: never; + query?: { + size?: number; + page?: number; + }; + url: "/v2/gateways/skinny"; +}; + +export type GatewaysSkinnyResponses = { + 200: PagedResultGatewaySkinny; +}; + +export type GatewaysSkinnyResponse = + GatewaysSkinnyResponses[keyof GatewaysSkinnyResponses]; + +export type GetGatewayData = { + body?: never; + path: { + identity_key: string; + }; + query?: never; + url: "/v2/gateways/{identity_key}"; +}; + +export type GetGatewayResponses = { + 200: Gateway; +}; + +export type GetGatewayResponse = GetGatewayResponses[keyof GetGatewayResponses]; + +export type GetAllSessionsData = { + body?: never; + path?: never; + query?: { + size?: number; + page?: number; + node_id?: string; + day?: string; + }; + url: "/v2/metrics/sessions"; +}; + +export type GetAllSessionsResponses = { + 200: PagedResultSessionStats; +}; + +export type GetAllSessionsResponse = + GetAllSessionsResponses[keyof GetAllSessionsResponses]; + +export type MixnodesData = { + body?: never; + path?: never; + query?: { + size?: number; + page?: number; + }; + url: "/v2/mixnodes"; +}; + +export type MixnodesResponses = { + 200: PagedResultMixnode; +}; + +export type MixnodesResponse = MixnodesResponses[keyof MixnodesResponses]; + +export type GetStatsData = { + body?: never; + path?: never; + query?: { + offset?: number; + }; + url: "/v2/mixnodes/stats"; +}; + +export type GetStatsResponses = { + 200: Array; +}; + +export type GetStatsResponse = GetStatsResponses[keyof GetStatsResponses]; + +export type GetMixnodesData = { + body?: never; + path: { + mix_id: string; + }; + query?: never; + url: "/v2/mixnodes/{mix_id}"; +}; + +export type GetMixnodesResponses = { + 200: Mixnode; +}; + +export type GetMixnodesResponse = + GetMixnodesResponses[keyof GetMixnodesResponses]; + +export type Mixnodes2Data = { + body?: never; + path?: never; + query?: { + size?: number; + page?: number; + wss?: boolean; + hostname?: boolean; + entry?: boolean; + }; + url: "/v2/services"; +}; + +export type Mixnodes2Responses = { + 200: PagedResultService; +}; + +export type Mixnodes2Response = Mixnodes2Responses[keyof Mixnodes2Responses]; + +export type BuildInformationData = { + body?: never; + path?: never; + query?: never; + url: "/v2/status/build_information"; +}; + +export type BuildInformationResponses = { + 200: BinaryBuildInformationOwned; +}; + +export type BuildInformationResponse = + BuildInformationResponses[keyof BuildInformationResponses]; + +export type HealthData = { + body?: never; + path?: never; + query?: never; + url: "/v2/status/health"; +}; + +export type HealthResponses = { + 200: HealthInfo; +}; + +export type HealthResponse = HealthResponses[keyof HealthResponses]; + +export type SummaryData = { + body?: never; + path?: never; + query?: never; + url: "/v2/summary"; +}; + +export type SummaryResponses = { + 200: NetworkSummary; +}; + +export type SummaryResponse = SummaryResponses[keyof SummaryResponses]; + +export type SummaryHistoryData = { + body?: never; + path?: never; + query?: never; + url: "/v2/summary/history"; +}; + +export type SummaryHistoryResponses = { + 200: Array; +}; + +export type SummaryHistoryResponse = + SummaryHistoryResponses[keyof SummaryHistoryResponses]; + +export type ClientOptions = { + baseUrl: "https://mainnet-node-status-api.nymtech.cc" | (string & {}); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/CardAlert.tsx b/nym-node-status-api/nym-node-status-ui/src/components/CardAlert.tsx new file mode 100644 index 0000000000..9a7a13ac0b --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/CardAlert.tsx @@ -0,0 +1,25 @@ +"use client"; +import AutoAwesomeRoundedIcon from "@mui/icons-material/AutoAwesomeRounded"; +import Button from "@mui/material/Button"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; + +export default function CardAlert() { + return ( + + + + + Plan about to expire + + + Enjoy 10% off when renewing your plan today. + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/Copyright.tsx b/nym-node-status-api/nym-node-status-ui/src/components/Copyright.tsx new file mode 100644 index 0000000000..e0167a578a --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/Copyright.tsx @@ -0,0 +1,24 @@ +import Link from "@mui/material/Link"; +import Typography from "@mui/material/Typography"; + +export default function Copyright(props: any) { + return ( + + {"Copyright © "} + + Nym Technologies SA + {" "} + {new Date().getFullYear()} + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/GraphCard.tsx b/nym-node-status-api/nym-node-status-ui/src/components/GraphCard.tsx new file mode 100644 index 0000000000..ee5ea54091 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/GraphCard.tsx @@ -0,0 +1,23 @@ +"use client"; + +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; + +export type GraphProps = { + title: string; + children?: React.ReactNode; +}; + +export default function GraphCard({ title, children }: GraphProps) { + return ( + + + + {title} + + {children} + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/HighlightedCard.tsx b/nym-node-status-api/nym-node-status-ui/src/components/HighlightedCard.tsx new file mode 100644 index 0000000000..f551b85e3d --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/HighlightedCard.tsx @@ -0,0 +1,42 @@ +"use client"; +import ChevronRightRoundedIcon from "@mui/icons-material/ChevronRightRounded"; +import InsightsRoundedIcon from "@mui/icons-material/InsightsRounded"; +import Button from "@mui/material/Button"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; + +export default function HighlightedCard() { + const theme = useTheme(); + const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm")); + + return ( + + + + + Explore your data + + + Uncover performance and visitor insights with our data wizardry. + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/ScoreIcon.tsx b/nym-node-status-api/nym-node-status-ui/src/components/ScoreIcon.tsx new file mode 100644 index 0000000000..d1c47c549f --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/ScoreIcon.tsx @@ -0,0 +1,36 @@ +import SignalCellularAltIcon from "@mui/icons-material/SignalCellularAlt"; +import SignalCellularAlt1Bar from "@mui/icons-material/SignalCellularAlt1Bar"; +import SignalCellularAlt2Bar from "@mui/icons-material/SignalCellularAlt2Bar"; +import SignalCellularConnectedNoInternet0BarIcon from "@mui/icons-material/SignalCellularConnectedNoInternet0Bar"; + +export const ScoreIcon = ({ score }: { score?: string }) => { + if (!score) { + return ; + } + if (score.toLowerCase() === "offline") { + return ; + } + if (score.toLowerCase() === "high") { + return ; + } + if (score.toLowerCase() === "medium") { + return ; + } + return ; +}; + +export const ReverseScoreIcon = ({ score }: { score?: string }) => { + if (!score) { + return ; + } + if (score.toLowerCase() === "offline") { + return ; + } + if (score.toLowerCase() === "low") { + return ; + } + if (score.toLowerCase() === "medium") { + return ; + } + return ; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/StatCard.tsx b/nym-node-status-api/nym-node-status-ui/src/components/StatCard.tsx new file mode 100644 index 0000000000..0ecf2514a0 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/StatCard.tsx @@ -0,0 +1,130 @@ +"use client"; + +import Box from "@mui/material/Box"; +import Card from "@mui/material/Card"; +import CardContent from "@mui/material/CardContent"; +import Chip from "@mui/material/Chip"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { areaElementClasses } from "@mui/x-charts/LineChart"; +import { SparkLineChart } from "@mui/x-charts/SparkLineChart"; + +export type StatCardProps = { + title: string; + value: string; + interval: string; + trend: "up" | "down" | "neutral"; + data: number[]; +}; + +function getDaysInMonth(month: number, year: number) { + const date = new Date(year, month, 0); + const monthName = date.toLocaleDateString("en-US", { + month: "short", + }); + const daysInMonth = date.getDate(); + const days = []; + let i = 1; + while (days.length < daysInMonth) { + days.push(`${monthName} ${i}`); + i += 1; + } + return days; +} + +function AreaGradient({ color, id }: { color: string; id: string }) { + return ( + + + + + + + ); +} + +export default function StatCard({ + title, + value, + interval, + trend, + data, +}: StatCardProps) { + const theme = useTheme(); + const daysInWeek = getDaysInMonth(4, 2024); + + const trendColors = { + up: + theme.palette.mode === "light" + ? theme.palette.success.main + : theme.palette.success.dark, + down: + theme.palette.mode === "light" + ? theme.palette.error.main + : theme.palette.error.dark, + neutral: + theme.palette.mode === "light" + ? theme.palette.grey[400] + : theme.palette.grey[700], + }; + + const labelColors = { + up: "success" as const, + down: "error" as const, + neutral: "default" as const, + }; + + const color = labelColors[trend]; + const chartColor = trendColors[trend]; + const trendValues = { up: "+25%", down: "-25%", neutral: "+5%" }; + + return ( + + + + {title} + + + + + + {value} + + + + + {interval} + + + + + + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayCanQueryMetadataTopup.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayCanQueryMetadataTopup.tsx new file mode 100644 index 0000000000..30f066e937 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayCanQueryMetadataTopup.tsx @@ -0,0 +1,58 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import { rollup } from "d3-array"; +import React from "react"; + +export const GatewayCanQueryMetadataTopup = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const results = data.map((g) => { + const r = (g.last_probe?.outcome.wg as any)?.can_query_metadata_v4; + if (r === undefined) { + return "-"; + } + if (r === true) { + return "yes"; + } + return "no"; + }); + // count occurrences of each result + const resultCounts = rollup( + results, + (v) => v.length, + (v) => v, // group by result string + ); + + const chartData = Array.from(resultCounts, ([result, count]) => ({ + result, + count, + })); + + const labels = chartData.map((d) => d.result); + const values = chartData.map((d) => d.count); + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayDownloadSpeeds.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayDownloadSpeeds.tsx new file mode 100644 index 0000000000..17ad7c128f --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayDownloadSpeeds.tsx @@ -0,0 +1,43 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import { bin } from "d3-array"; +import React from "react"; + +export const GatewayDownloadSpeeds = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const binner = bin().thresholds(10); // Number of bins + const bins = binner( + data + .map((g) => g.extra.downloadSpeedMBPerSec) + .filter((g) => Boolean(g)) as number[], + ); + + const labels = bins.map((b) => `${b.x0}-${b.x1} MB/sec`); + const values = bins.map((b) => b.length); // count per bin + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayLoads.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayLoads.tsx new file mode 100644 index 0000000000..f3c201dce9 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayLoads.tsx @@ -0,0 +1,45 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import React from "react"; + +export const GatewayLoads = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const binned = data.reduce( + (acc, g) => { + const score: "low" | "medium" | "high" | "offline" = + (g as any).performance_v2?.load || "offline"; + acc[score] += 1; + return acc; + }, + { offline: 0, low: 0, medium: 0, high: 0 }, + ); + + const labels = ["offline", "low", "medium", "high"]; + const values = Object.values(binned); + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayPingPercentage.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayPingPercentage.tsx new file mode 100644 index 0000000000..38bca5f5c5 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayPingPercentage.tsx @@ -0,0 +1,43 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import { bin } from "d3-array"; +import React from "react"; + +export const GatewayPingPercentage = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const binner = bin().domain([0, 1]).thresholds(10); // Number of bins + const bins = binner( + data.map((g) => g.last_probe?.outcome.wg?.ping_ips_performance_v4 || 0), + ); + + const labels = bins.map( + (b) => `${(b.x0 || 0) * 100}-${(b.x1 || 0) * 100}%`, + ); + const values = bins.map((b) => b.length); // count per bin + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayScores.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayScores.tsx new file mode 100644 index 0000000000..3daa8f4a67 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayScores.tsx @@ -0,0 +1,45 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import React from "react"; + +export const GatewayScores = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const binned = data.reduce( + (acc, g) => { + const score: "low" | "medium" | "high" | "offline" = + (g as any).performance_v2?.score || "offline"; + acc[score] += 1; + return acc; + }, + { offline: 0, low: 0, medium: 0, high: 0 }, + ); + + const labels = ["offline", "low", "medium", "high"]; + const values = Object.values(binned); + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayUptimePercentage.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayUptimePercentage.tsx new file mode 100644 index 0000000000..d84f707316 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayUptimePercentage.tsx @@ -0,0 +1,41 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import { bin } from "d3-array"; +import React from "react"; + +export const GatewayUptimePercentage = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const binner = bin().domain([0, 1]).thresholds(20); // Number of bins + const bins = binner(data.map((g) => Number.parseFloat(g.performance))); + + const labels = bins.map( + (b) => `${(b.x0 || 0) * 100}-${(b.x1 || 0) * 100}%`, + ); + const values = bins.map((b) => b.length); // count per bin + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayVersions.tsx b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayVersions.tsx new file mode 100644 index 0000000000..a6e9d0f3c7 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/graphs/GatewayVersions.tsx @@ -0,0 +1,49 @@ +import { useDVpnGatewaysTransformed } from "@/hooks/useGatewaysTransformed"; +import Box from "@mui/material/Box"; +import { BarChart } from "@mui/x-charts/BarChart"; +import { rollup } from "d3-array"; +import React from "react"; + +export const GatewayVersions = () => { + const { + query: { isSuccess, isError, data }, + } = useDVpnGatewaysTransformed(); + const binnedData = React.useMemo(() => { + if (!isSuccess || data === undefined) { + return undefined; + } + const versions = data.map((g) => g.build_information.build_version); + // count occurrences of each version + const versionCounts = rollup( + versions, + (v) => v.length, + (v) => v, // group by version string + ); + + const chartData = Array.from(versionCounts, ([version, count]) => ({ + version, + count, + })); + + const labels = chartData.map((d) => d.version); + const values = chartData.map((d) => d.count); + + return { labels, values }; + }, [data, isSuccess]); + + if (isError || !binnedData) { + return null; + } + + const { labels, values } = binnedData; + + return ( + + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/AppNavbar.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/AppNavbar.tsx new file mode 100644 index 0000000000..5f978551f4 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/AppNavbar.tsx @@ -0,0 +1,77 @@ +"use client"; + +import ColorModeIconDropdown from "@/theme/ColorModeIconDropdown"; +import MenuRoundedIcon from "@mui/icons-material/MenuRounded"; +import AppBar from "@mui/material/AppBar"; +import Stack from "@mui/material/Stack"; +import { tabsClasses } from "@mui/material/Tabs"; +import MuiToolbar from "@mui/material/Toolbar"; +import { styled } from "@mui/material/styles"; +import * as React from "react"; +import MenuButton from "./MenuButton"; +import SideMenuMobile from "./SideMenuMobile"; +import { SiteLogo } from "./SiteLogo"; + +const Toolbar = styled(MuiToolbar)({ + width: "100%", + padding: "12px", + display: "flex", + flexDirection: "column", + alignItems: "start", + justifyContent: "center", + gap: "12px", + flexShrink: 0, + [`& ${tabsClasses.flexContainer}`]: { + gap: "8px", + p: "8px", + pb: 0, + }, +}); + +export default function AppNavbar() { + const [open, setOpen] = React.useState(false); + + const toggleDrawer = (newOpen: boolean) => () => { + setOpen(newOpen); + }; + + return ( + + + + + + + + + + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeIconDropdown.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeIconDropdown.tsx new file mode 100644 index 0000000000..dd1ebef7d4 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeIconDropdown.tsx @@ -0,0 +1,91 @@ +"use client"; + +import DarkModeIcon from "@mui/icons-material/DarkModeRounded"; +import LightModeIcon from "@mui/icons-material/LightModeRounded"; +import Box from "@mui/material/Box"; +import IconButton, { type IconButtonOwnProps } from "@mui/material/IconButton"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import { useColorScheme } from "@mui/material/styles"; +import * as React from "react"; + +export default function ColorModeIconDropdown(props: IconButtonOwnProps) { + const { mode, systemMode, setMode } = useColorScheme(); + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + const handleMode = (targetMode: "system" | "light" | "dark") => () => { + setMode(targetMode); + handleClose(); + }; + if (!mode) { + return ( + ({ + verticalAlign: "bottom", + display: "inline-flex", + width: "2.25rem", + height: "2.25rem", + borderRadius: theme.shape.borderRadius, + border: "1px solid", + borderColor: theme.palette.divider, + })} + /> + ); + } + const resolvedMode = (systemMode || mode) as "light" | "dark"; + const icon = { + light: , + dark: , + }[resolvedMode]; + return ( + + + {icon} + + + + System + + + Light + + + Dark + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeSelect.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeSelect.tsx new file mode 100644 index 0000000000..839b3b6dc3 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/ColorModeSelect.tsx @@ -0,0 +1,29 @@ +"use client"; + +import MenuItem from "@mui/material/MenuItem"; +import Select, { type SelectProps } from "@mui/material/Select"; +import { useColorScheme } from "@mui/material/styles"; + +export default function ColorModeSelect(props: SelectProps) { + const { mode, setMode } = useColorScheme(); + if (!mode) { + return null; + } + return ( + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/Header.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/Header.tsx new file mode 100644 index 0000000000..5d7ebe12c2 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/Header.tsx @@ -0,0 +1,33 @@ +"use client"; + +import ColorModeIconDropdown from "@/theme/ColorModeIconDropdown"; +import Stack from "@mui/material/Stack"; +import NavbarBreadcrumbs from "./NavbarBreadcrumbs"; + +import type React from "react"; +import Search from "./Search"; + +const showSearch = false; + +export default function Header({ title }: { title?: React.ReactNode }) { + return ( + + + + {showSearch && } + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/MenuButton.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/MenuButton.tsx new file mode 100644 index 0000000000..c628996e25 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/MenuButton.tsx @@ -0,0 +1,23 @@ +"use client"; +import Badge, { badgeClasses } from "@mui/material/Badge"; +import IconButton, { type IconButtonProps } from "@mui/material/IconButton"; + +export interface MenuButtonProps extends IconButtonProps { + showBadge?: boolean; +} + +export default function MenuButton({ + showBadge = false, + ...props +}: MenuButtonProps) { + return ( + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/MenuContent.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/MenuContent.tsx new file mode 100644 index 0000000000..a38b662c88 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/MenuContent.tsx @@ -0,0 +1,60 @@ +"use client"; + +import List from "@mui/material/List"; +import ListItem from "@mui/material/ListItem"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Stack from "@mui/material/Stack"; +import NextLink from "next/link"; +import { usePathname } from "next/navigation"; + +import DoorSlidingOutlinedIcon from "@mui/icons-material/DoorSlidingOutlined"; +import HubIcon from "@mui/icons-material/Hub"; +import SettingsInputAntennaIcon from "@mui/icons-material/SettingsInputAntenna"; +import ViewModuleIcon from "@mui/icons-material/ViewModule"; +import WorkspacePremiumIcon from "@mui/icons-material/WorkspacePremium"; + +const mainListItemsAll = [ + { text: "Network Nodes", icon: , url: "/nodes" }, + { text: "dVPN Gateways", icon: , url: "/dvpn" }, + { text: "SOCKS5 NRs", icon: , url: "/socks5" }, + { + text: "zk-nym Signers", + icon: , + url: "/zk-nym-signers", + }, + { + text: "Nyx Chain Validators", + icon: , + url: "/validators", + }, +]; + +const mainListItems = [mainListItemsAll[0], mainListItemsAll[1]]; + +export default function MenuContent() { + const path = usePathname(); + return ( + + + {mainListItems.map((item, index) => ( + + + {item.icon} + + + + ))} + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/NavbarBreadcrumbs.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/NavbarBreadcrumbs.tsx new file mode 100644 index 0000000000..b74145778e --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/NavbarBreadcrumbs.tsx @@ -0,0 +1,37 @@ +"use client"; + +import NavigateNextRoundedIcon from "@mui/icons-material/NavigateNextRounded"; +import Breadcrumbs, { breadcrumbsClasses } from "@mui/material/Breadcrumbs"; +import Typography from "@mui/material/Typography"; +import { styled } from "@mui/material/styles"; +import type React from "react"; + +const StyledBreadcrumbs = styled(Breadcrumbs)(({ theme }) => ({ + margin: theme.spacing(1, 0), + [`& .${breadcrumbsClasses.separator}`]: { + color: theme.palette.action.disabled, + margin: 1, + }, + [`& .${breadcrumbsClasses.ol}`]: { + alignItems: "center", + }, +})); + +export default function NavbarBreadcrumbs({ + title, +}: { title?: React.ReactNode }) { + return ( + } + > + Dashboard + + {title || "Home"} + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/OptionsMenu.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/OptionsMenu.tsx new file mode 100644 index 0000000000..940ad44148 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/OptionsMenu.tsx @@ -0,0 +1,81 @@ +"use client"; + +import LogoutRoundedIcon from "@mui/icons-material/LogoutRounded"; +import MoreVertRoundedIcon from "@mui/icons-material/MoreVertRounded"; +import Divider, { dividerClasses } from "@mui/material/Divider"; +import { listClasses } from "@mui/material/List"; +import ListItemIcon, { listItemIconClasses } from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import Menu from "@mui/material/Menu"; +import MuiMenuItem from "@mui/material/MenuItem"; +import { paperClasses } from "@mui/material/Paper"; +import { styled } from "@mui/material/styles"; +import * as React from "react"; +import MenuButton from "./MenuButton"; + +const MenuItem = styled(MuiMenuItem)({ + margin: "2px 0", +}); + +export default function OptionsMenu() { + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + return ( + + + + + + Profile + My account + + Add another account + Settings + + + Logout + + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/Search.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/Search.tsx new file mode 100644 index 0000000000..78d95c6bdb --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/Search.tsx @@ -0,0 +1,27 @@ +"use client"; + +import SearchRoundedIcon from "@mui/icons-material/SearchRounded"; +import FormControl from "@mui/material/FormControl"; +import InputAdornment from "@mui/material/InputAdornment"; +import OutlinedInput from "@mui/material/OutlinedInput"; + +export default function Search() { + return ( + + + + + } + inputProps={{ + "aria-label": "search", + }} + /> + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/SelectContent.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/SelectContent.tsx new file mode 100644 index 0000000000..3b0d2c2ed2 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/SelectContent.tsx @@ -0,0 +1,107 @@ +"use client"; + +import AddRoundedIcon from "@mui/icons-material/AddRounded"; +import ConstructionRoundedIcon from "@mui/icons-material/ConstructionRounded"; +import DevicesRoundedIcon from "@mui/icons-material/DevicesRounded"; +import SmartphoneRoundedIcon from "@mui/icons-material/SmartphoneRounded"; +import MuiAvatar from "@mui/material/Avatar"; +import Divider from "@mui/material/Divider"; +import MuiListItemAvatar from "@mui/material/ListItemAvatar"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import ListItemText from "@mui/material/ListItemText"; +import ListSubheader from "@mui/material/ListSubheader"; +import MenuItem from "@mui/material/MenuItem"; +import Select, { + type SelectChangeEvent, + selectClasses, +} from "@mui/material/Select"; +import { styled } from "@mui/material/styles"; +import * as React from "react"; + +const Avatar = styled(MuiAvatar)(({ theme }) => ({ + width: 28, + height: 28, + backgroundColor: theme.palette.background.paper, + color: theme.palette.text.secondary, + border: `1px solid ${theme.palette.divider}`, +})); + +const ListItemAvatar = styled(MuiListItemAvatar)({ + minWidth: 0, + marginRight: 12, +}); + +export default function SelectContent() { + const [company, setCompany] = React.useState(""); + + const handleChange = (event: SelectChangeEvent) => { + setCompany(event.target.value as string); + }; + + return ( + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenu.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenu.tsx new file mode 100644 index 0000000000..047e2b696d --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenu.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { SiteLogo } from "@/components/nav/SiteLogo"; +import Box from "@mui/material/Box"; +import Divider from "@mui/material/Divider"; +import MuiDrawer, { drawerClasses } from "@mui/material/Drawer"; +import { styled } from "@mui/material/styles"; +import MenuContent from "./MenuContent"; + +const drawerWidth = 240; + +const Drawer = styled(MuiDrawer)({ + width: drawerWidth, + flexShrink: 0, + boxSizing: "border-box", + mt: 10, + [`& .${drawerClasses.paper}`]: { + width: drawerWidth, + boxSizing: "border-box", + }, +}); + +export default function SideMenu() { + return ( + + + + + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenuMobile.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenuMobile.tsx new file mode 100644 index 0000000000..f6e866af5a --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/SideMenuMobile.tsx @@ -0,0 +1,43 @@ +"use client"; + +import Divider from "@mui/material/Divider"; +import Drawer, { drawerClasses } from "@mui/material/Drawer"; +import Stack from "@mui/material/Stack"; +import MenuContent from "./MenuContent"; + +interface SideMenuMobileProps { + open: boolean | undefined; + toggleDrawer: (newOpen: boolean) => () => void; +} + +export default function SideMenuMobile({ + open, + toggleDrawer, +}: SideMenuMobileProps) { + return ( + theme.zIndex.drawer + 1, + [`& .${drawerClasses.paper}`]: { + backgroundImage: "none", + backgroundColor: "background.paper", + }, + }} + > + + + + + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/components/nav/SiteLogo.tsx b/nym-node-status-api/nym-node-status-ui/src/components/nav/SiteLogo.tsx new file mode 100644 index 0000000000..336ee3795c --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/components/nav/SiteLogo.tsx @@ -0,0 +1,15 @@ +import Link from "@mui/material/Link"; +import NextLink from "next/link"; + +export function SiteLogo() { + return ( + + Nym Node Status + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/context/queryContext.tsx b/nym-node-status-api/nym-node-status-ui/src/context/queryContext.tsx new file mode 100644 index 0000000000..43578ed17f --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/context/queryContext.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { type Client, createClient } from "@/client/client"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import type React from "react"; +import { createContext, useContext, useRef } from "react"; + +interface State { + client?: Client; +} + +export const QueryContext = createContext({ + client: undefined, +}); + +export const useQueryContext = (): React.ContextType => + useContext(QueryContext); + +export const QueryContextProvider = ({ + children, +}: { + children: React.ReactNode | React.ReactNode[]; +}) => { + const openApiClient = useRef( + createClient({ baseUrl: "https://mainnet-node-status-api.nymtech.cc" }), + ); + const queryClient = useRef(new QueryClient()); + + const state: State = { + client: openApiClient.current, + }; + + return ( + + + {children} + {/* Add devtools in development */} + {process.env.NODE_ENV === "development" && ( + + )} + + + ); +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/hooks/types.ts b/nym-node-status-api/nym-node-status-ui/src/hooks/types.ts new file mode 100644 index 0000000000..852d109cc7 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/hooks/types.ts @@ -0,0 +1,4 @@ +export interface Pagination { + pageIndex?: number; + pageSize?: number; +} diff --git a/nym-node-status-api/nym-node-status-ui/src/hooks/useAllNymNodes.ts b/nym-node-status-api/nym-node-status-ui/src/hooks/useAllNymNodes.ts new file mode 100644 index 0000000000..05bfa0a5cf --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/hooks/useAllNymNodes.ts @@ -0,0 +1,39 @@ +import { nymNodes } from "@/client/sdk.gen"; +import { useQueryContext } from "@/context/queryContext"; +import type { NymNode } from "@/hooks/useNymNodes"; +import { useQuery } from "@tanstack/react-query"; +import React from "react"; + +export const useAllNymNodes = () => { + const { client } = useQueryContext(); + const key = "nym-nodes-all"; + + const queryFn = React.useCallback(async (): Promise => { + const size = 100; + let busy = true; + let page = 0; + const allData = []; + do { + const { data, error } = await nymNodes({ client, query: { page, size } }); + if (error) throw error; + + if (data?.items) { + allData.push(...data.items); + } + + // keep querying until data is less than a page + if ((data?.items.length || 0) < size) { + busy = false; + } + page += 1; + } while (busy); + return allData; + }, [client]); + + const query = useQuery({ queryKey: [key], queryFn }); + + return { + key, + query, + }; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/hooks/useGateways.ts b/nym-node-status-api/nym-node-status-ui/src/hooks/useGateways.ts new file mode 100644 index 0000000000..d1508ce7da --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/hooks/useGateways.ts @@ -0,0 +1,20 @@ +import { getGatewaysOptions } from "@/client/@tanstack/react-query.gen"; +import { useQueryContext } from "@/context/queryContext"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; + +export const useDVpnGateways = () => { + const { client } = useQueryContext(); + const key = "gateways"; + + const query = useQuery({ + ...getGatewaysOptions({ + client, + }), + placeholderData: keepPreviousData, + }); + + return { + key, + query, + }; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/hooks/useGatewaysTransformed.ts b/nym-node-status-api/nym-node-status-ui/src/hooks/useGatewaysTransformed.ts new file mode 100644 index 0000000000..02c87b46af --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/hooks/useGatewaysTransformed.ts @@ -0,0 +1,47 @@ +import { getGateways } from "@/client/sdk.gen"; +import { useQueryContext } from "@/context/queryContext"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; +import React from "react"; + +export const useDVpnGatewaysTransformed = () => { + const { client } = useQueryContext(); + const key = "gateways"; + + const queryFn = React.useCallback(async () => { + const { data, error } = await getGateways({ client }); + if (error) throw error; + return (data || []).map((g) => { + const wg = g.last_probe?.outcome.wg as any; + const downloadSpeedMBPerSec = wg + ? Math.round( + (10 * ((wg?.downloaded_file_size_bytes_v4 || 0) / 1024 / 1024)) / + ((wg?.download_duration_milliseconds_v4 || 1) / 1000), + ) / 10 + : undefined; + const downloadSpeedIpv6MBPerSec = wg + ? Math.round( + (10 * ((wg?.downloaded_file_size_bytes_v6 || 0) / 1024 / 1024)) / + ((wg?.download_duration_milliseconds_v6 || 1) / 1000), + ) / 10 + : undefined; + return { + ...g, + extra: { + downloadSpeedMBPerSec, + downloadSpeedIpv6MBPerSec, + }, + }; + }); + }, [client]); + + const query = useQuery({ + queryKey: [key], + queryFn, + placeholderData: keepPreviousData, + }); + + return { + key, + query, + }; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/hooks/useNymNodes.ts b/nym-node-status-api/nym-node-status-ui/src/hooks/useNymNodes.ts new file mode 100644 index 0000000000..fa17795d7d --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/hooks/useNymNodes.ts @@ -0,0 +1,52 @@ +import type { + DescribedNodeType, + NodeDescription, + NodeGeoData, + NodeRewarding, + NymNodeData, + U32, +} from "@/client"; +import { nymNodesOptions } from "@/client/@tanstack/react-query.gen"; +import { useQueryContext } from "@/context/queryContext"; +import type { Pagination } from "@/hooks/types"; +import { keepPreviousData, useQuery } from "@tanstack/react-query"; + +// TODO: how to re-use autogenerated subtype +export type NymNode = { + accepted_tnc: boolean; + bonded: boolean; + bonding_address?: string | null; + description: NodeDescription; + geoip?: null | NodeGeoData; + identity_key: string; + ip_address: string; + node_id: U32; + node_type: DescribedNodeType; + original_pledge: number; + rewarding_details?: null | NodeRewarding; + self_description: NymNodeData; + total_stake: string; + uptime: number; +}; + +export const useNymNodes = (props?: Pagination) => { + const { client } = useQueryContext(); + const { pageIndex = 0, pageSize = 10 } = props || {}; + const key = "nym-nodes"; + + const query = useQuery({ + ...nymNodesOptions({ + client, + query: { + page: pageIndex, + size: pageSize, + }, + }), + placeholderData: keepPreviousData, + }); + + return { + key, + query, + }; +}; diff --git a/nym-node-status-api/nym-node-status-ui/src/layouts/LayoutWithNav.tsx b/nym-node-status-api/nym-node-status-ui/src/layouts/LayoutWithNav.tsx new file mode 100644 index 0000000000..e82ba5336c --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/layouts/LayoutWithNav.tsx @@ -0,0 +1,29 @@ +import Copyright from "@/components/Copyright"; +import AppNavbar from "@/components/nav/AppNavbar"; +import SideMenu from "@/components/nav/SideMenu"; +import Box from "@mui/material/Box"; +import { alpha } from "@mui/material/styles"; +import type React from "react"; + +export default function LayoutWithNav({ + children, +}: { children?: React.ReactNode }) { + return ( + + + + {/* Main content */} + ({ + flexGrow: 1, + backgroundColor: alpha(theme.palette.background.default, 1), + overflow: "auto", + })} + > + {children} + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/layouts/NestedLayoutWithHeader.tsx b/nym-node-status-api/nym-node-status-ui/src/layouts/NestedLayoutWithHeader.tsx new file mode 100644 index 0000000000..9b2e50bc45 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/layouts/NestedLayoutWithHeader.tsx @@ -0,0 +1,23 @@ +import Header from "@/components/nav/Header"; +import Stack from "@mui/material/Stack"; +import type React from "react"; + +export default function NestedLayoutWithHeader({ + children, + header, +}: { children?: React.ReactNode; header?: React.ReactNode }) { + return ( + +
+ {children} + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/theme/ColorModeIconDropdown.tsx b/nym-node-status-api/nym-node-status-ui/src/theme/ColorModeIconDropdown.tsx new file mode 100644 index 0000000000..dd1ebef7d4 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/theme/ColorModeIconDropdown.tsx @@ -0,0 +1,91 @@ +"use client"; + +import DarkModeIcon from "@mui/icons-material/DarkModeRounded"; +import LightModeIcon from "@mui/icons-material/LightModeRounded"; +import Box from "@mui/material/Box"; +import IconButton, { type IconButtonOwnProps } from "@mui/material/IconButton"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import { useColorScheme } from "@mui/material/styles"; +import * as React from "react"; + +export default function ColorModeIconDropdown(props: IconButtonOwnProps) { + const { mode, systemMode, setMode } = useColorScheme(); + const [anchorEl, setAnchorEl] = React.useState(null); + const open = Boolean(anchorEl); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + const handleMode = (targetMode: "system" | "light" | "dark") => () => { + setMode(targetMode); + handleClose(); + }; + if (!mode) { + return ( + ({ + verticalAlign: "bottom", + display: "inline-flex", + width: "2.25rem", + height: "2.25rem", + borderRadius: theme.shape.borderRadius, + border: "1px solid", + borderColor: theme.palette.divider, + })} + /> + ); + } + const resolvedMode = (systemMode || mode) as "light" | "dark"; + const icon = { + light: , + dark: , + }[resolvedMode]; + return ( + + + {icon} + + + + System + + + Light + + + Dark + + + + ); +} diff --git a/nym-node-status-api/nym-node-status-ui/src/theme/index.tsx b/nym-node-status-api/nym-node-status-ui/src/theme/index.tsx new file mode 100644 index 0000000000..4773499620 --- /dev/null +++ b/nym-node-status-api/nym-node-status-ui/src/theme/index.tsx @@ -0,0 +1,27 @@ +import { ThemeProvider, createTheme } from "@mui/material/styles"; +import * as React from "react"; + +interface AppThemeProps { + children: React.ReactNode; + // themeComponents?: ThemeOptions["components"]; +} + +export default function AppTheme(props: AppThemeProps) { + const { children } = props; + const theme = React.useMemo(() => { + return createTheme({ + colorSchemes: { + dark: true, + light: true, + }, + typography: { + fontFamily: "system-ui, sans-serif", + }, + }); + }, []); + return ( + + {children} + + ); +} diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index d02fcc06f4..f8467cdd3c 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.22.0" +version = "1.23.0" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -57,6 +57,7 @@ nym-client-core-config-types = { path = "../common/client-core/config-types", fe "disk-persistence", ] } nym-config = { path = "../common/config" } +nym-credential-verification = { path = "../common/credential-verification" } nym-crypto = { path = "../common/crypto", features = ["asymmetric", "rand"] } nym-nonexhaustive-delayqueue = { path = "../common/nonexhaustive-delayqueue" } nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } diff --git a/nym-node/nym-node-requests/Cargo.toml b/nym-node/nym-node-requests/Cargo.toml index 82119f47c8..ad1ed3013a 100644 --- a/nym-node/nym-node-requests/Cargo.toml +++ b/nym-node/nym-node-requests/Cargo.toml @@ -12,7 +12,6 @@ license.workspace = true [dependencies] celes = { workspace = true } # country codes -humantime = { workspace = true } humantime-serde = { workspace = true } schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } @@ -21,6 +20,7 @@ strum = { workspace = true, features = ["derive"] } strum_macros = { workspace = true } time = { workspace = true, features = ["serde", "formatting", "parsing"] } thiserror = { workspace = true } +url = { workspace = true, features = ["serde"] } nym-crypto = { path = "../../common/crypto", features = [ "asymmetric", @@ -29,6 +29,7 @@ nym-crypto = { path = "../../common/crypto", features = [ nym-exit-policy = { path = "../../common/exit-policy" } nym-noise-keys = { path = "../../common/nymnoise/keys" } nym-wireguard-types = { path = "../../common/wireguard-types", default-features = false } +nym-upgrade-mode-check = { path = "../../common/upgrade-mode-check", features = ["openapi"] } # feature-specific dependencies: diff --git a/nym-node/nym-node-requests/src/api/v1/mod.rs b/nym-node/nym-node-requests/src/api/v1/mod.rs index 544633944f..e19b611f1b 100644 --- a/nym-node/nym-node-requests/src/api/v1/mod.rs +++ b/nym-node/nym-node-requests/src/api/v1/mod.rs @@ -7,6 +7,7 @@ pub mod health; pub mod ip_packet_router; pub mod metrics; pub mod mixnode; +pub mod network; pub mod network_requester; pub mod node; pub mod node_load; diff --git a/nym-node/nym-node-requests/src/api/v1/network.rs b/nym-node/nym-node-requests/src/api/v1/network.rs new file mode 100644 index 0000000000..13811deb67 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/network.rs @@ -0,0 +1,4 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod models; diff --git a/nym-node/nym-node-requests/src/api/v1/network/models.rs b/nym-node/nym-node-requests/src/api/v1/network/models.rs new file mode 100644 index 0000000000..be5d01b198 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/network/models.rs @@ -0,0 +1,28 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_crypto::asymmetric::ed25519; +use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; +use nym_upgrade_mode_check::UpgradeModeAttestation; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use url::Url; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct UpgradeModeStatus { + pub enabled: bool, + + #[serde(with = "time::serde::rfc3339")] + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub last_queried: OffsetDateTime, + + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub attestation_provider: Url, + + #[serde(with = "bs58_ed25519_pubkey")] + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub attester_pubkey: ed25519::PublicKey, + + pub published_attestation: Option, +} diff --git a/nym-node/nym-node-requests/src/lib.rs b/nym-node/nym-node-requests/src/lib.rs index 2b539f831e..aaf63baa25 100644 --- a/nym-node/nym-node-requests/src/lib.rs +++ b/nym-node/nym-node-requests/src/lib.rs @@ -36,6 +36,7 @@ pub mod routes { pub const AUXILIARY: &str = "/auxiliary-details"; pub const HEALTH: &str = "/health"; pub const LOAD: &str = "/load"; + pub const NETWORK: &str = "/network"; pub const SWAGGER: &str = "/swagger"; pub const GATEWAY: &str = "/gateway"; @@ -49,6 +50,7 @@ pub mod routes { // define helper functions to get absolute routes absolute_route!(health_absolute, v1_absolute(), HEALTH); absolute_route!(load_absolute, v1_absolute(), LOAD); + absolute_route!(network_absolute, v1_absolute(), NETWORK); absolute_route!(roles_absolute, v1_absolute(), ROLES); absolute_route!(build_info_absolute, v1_absolute(), BUILD_INFO); absolute_route!(host_info_absolute, v1_absolute(), HOST_INFO); @@ -131,6 +133,17 @@ pub mod routes { pub mod ip_packet_router { // use super::*; } + + pub mod network { + use super::*; + pub const UPGRADE_MODE_STATUS: &str = "/upgrade-mode-status"; + + absolute_route!( + upgrade_mode_status_absolute, + network_absolute(), + UPGRADE_MODE_STATUS + ); + } } } } diff --git a/nym-node/src/cli/commands/run/args.rs b/nym-node/src/cli/commands/run/args.rs index 36117d0ba6..8c214f5e5e 100644 --- a/nym-node/src/cli/commands/run/args.rs +++ b/nym-node/src/cli/commands/run/args.rs @@ -159,7 +159,7 @@ impl Args { name: "id".to_string(), })?; - let config = ConfigBuilder::new(id, config_path.clone(), data_dir.clone()) + ConfigBuilder::new(id, config_path.clone(), data_dir.clone()) // the old default behaviour of running in mixnode mode if nothing is explicitly set .with_modes( self.custom_modes() @@ -172,11 +172,9 @@ impl Args { .with_storage_paths(NymNodePaths::new(&data_dir)) .with_verloc(self.verloc.build_config_section()) .with_metrics(self.metrics.build_config_section()) - .with_gateway_tasks(self.entry_gateway.build_config_section(&data_dir)) + .with_gateway_tasks(self.entry_gateway.build_config_section(&data_dir)?) .with_service_providers(self.exit_gateway.build_config_section(&data_dir)) - .build(); - - Ok(config) + .build() } pub(crate) fn override_config(self, mut config: Config) -> Config { diff --git a/nym-node/src/cli/commands/run/mod.rs b/nym-node/src/cli/commands/run/mod.rs index 36f77c7b29..450a88a761 100644 --- a/nym-node/src/cli/commands/run/mod.rs +++ b/nym-node/src/cli/commands/run/mod.rs @@ -29,7 +29,7 @@ fn check_public_ips(ips: &[IpAddr], local: bool) -> Result<(), NymNodeError> { warn!("\n##### WARNING #####"); for ip in suspicious_ip { warn!( - "The 'public' IP address you're trying to announce: {ip} may not be accessible to other clients.\ + "The 'public' IP address you're trying to announce: {ip} may not be accessible to other clients. \ Please make sure this is what you intended to announce.\ You can ignore this warning if you're running setup on a local network " ) diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 5211696485..08ccef0857 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -5,9 +5,11 @@ use super::DEFAULT_NYMNODE_ID; use crate::config; use crate::config::default_config_filepath; use crate::env::vars::*; +use crate::error::NymNodeError; use celes::Country; use clap::Args; use clap::builder::ArgPredicate; +use nym_crypto::asymmetric::ed25519; use std::net::{IpAddr, SocketAddr}; use std::path::{Path, PathBuf}; use url::Url; @@ -426,6 +428,24 @@ pub(crate) struct EntryGatewayArgs { env = NYMNODE_MNEMONIC_ARG )] pub(crate) mnemonic: Option, + + /// Endpoint to query to retrieve current upgrade mode attestation. + /// This argument should never be set outside testnets and local networks. + #[clap( + long, + env = NYMNODE_UPGRADE_MODE_ATTESTATION_URL_ARG + )] + #[zeroize(skip)] + pub(crate) upgrade_mode_attestation_url: Option, + + /// Expected public key of the entity signing the published attestation. + /// This argument should never be set outside testnets and local networks. + #[clap( + long, + env = NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY_ARG + )] + #[zeroize(skip)] + pub(crate) upgrade_mode_attester_public_key: Option, } impl EntryGatewayArgs { @@ -433,12 +453,12 @@ impl EntryGatewayArgs { pub(crate) fn build_config_section>( self, data_dir: P, - ) -> config::GatewayTasksConfig { - self.override_config_section(config::GatewayTasksConfig::new_default(data_dir)) + ) -> Result { + Ok(self.override_config_section(config::GatewayTasksConfig::new(data_dir)?)) } pub(crate) fn override_config_section( - self, + mut self, mut section: config::GatewayTasksConfig, ) -> config::GatewayTasksConfig { if let Some(bind_address) = self.entry_bind_address { @@ -453,6 +473,12 @@ impl EntryGatewayArgs { if let Some(enforce_zk_nyms) = self.enforce_zk_nyms { section.enforce_zk_nyms = enforce_zk_nyms } + if let Some(upgrade_mode_attestation_url) = self.upgrade_mode_attestation_url.take() { + section.upgrade_mode.attestation_url = upgrade_mode_attestation_url + } + if let Some(upgrade_mode_attester_public_key) = self.upgrade_mode_attester_public_key { + section.upgrade_mode.attester_public_key = upgrade_mode_attester_public_key + } section } diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index 0fcf54d9dd..ca47c15538 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -1,14 +1,22 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::config::helpers::log_error_and_return; use crate::config::persistence::GatewayTasksPaths; -use nym_config::defaults::{DEFAULT_CLIENT_LISTENING_PORT, TICKETBOOK_VALIDITY_DAYS}; +use crate::error::NymNodeError; +use nym_config::defaults::{ + DEFAULT_CLIENT_LISTENING_PORT, TICKETBOOK_VALIDITY_DAYS, mainnet, var_names, +}; use nym_config::helpers::in6addr_any_init; use nym_config::serde_helpers::de_maybe_port; +use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; use serde::{Deserialize, Serialize}; +use std::env; use std::net::SocketAddr; use std::path::Path; use std::time::Duration; +use tracing::info; +use url::Url; pub const DEFAULT_WS_PORT: u16 = DEFAULT_CLIENT_LISTENING_PORT; @@ -36,6 +44,8 @@ pub struct GatewayTasksConfig { #[serde(deserialize_with = "de_maybe_port")] pub announce_wss_port: Option, + pub upgrade_mode: UpgradeModeWatcher, + #[serde(default)] pub debug: Debug, } @@ -63,6 +73,10 @@ pub struct Debug { pub client_bandwidth: ClientBandwidthDebug, pub zk_nym_tickets: ZkNymTicketHandlerDebug, + + /// The minimum duration since the last explicit check for the upgrade mode to allow creation of new requests. + #[serde(with = "humantime_serde")] + pub upgrade_mode_min_staleness_recheck: Duration, } impl Debug { @@ -70,6 +84,7 @@ impl Debug { pub const DEFAULT_MINIMUM_MIX_PERFORMANCE: u8 = 50; pub const DEFAULT_MAXIMUM_AUTH_REQUEST_TIMESTAMP_SKEW: Duration = Duration::from_secs(120); pub const DEFAULT_MAXIMUM_OPEN_CONNECTIONS: usize = 8192; + pub const DEFAULT_UPGRADE_MODE_MIN_STALENESS_RECHECK: Duration = Duration::from_secs(30); } impl Default for Debug { @@ -82,6 +97,7 @@ impl Default for Debug { stale_messages: Default::default(), client_bandwidth: Default::default(), zk_nym_tickets: Default::default(), + upgrade_mode_min_staleness_recheck: Self::DEFAULT_UPGRADE_MODE_MIN_STALENESS_RECHECK, } } } @@ -201,14 +217,150 @@ impl Default for StaleMessageDebug { } impl GatewayTasksConfig { - pub fn new_default>(data_dir: P) -> Self { - GatewayTasksConfig { + pub fn new>(data_dir: P) -> Result { + Ok(GatewayTasksConfig { storage_paths: GatewayTasksPaths::new(data_dir), enforce_zk_nyms: false, ws_bind_address: SocketAddr::new(in6addr_any_init(), DEFAULT_WS_PORT), announce_ws_port: None, announce_wss_port: None, + upgrade_mode: UpgradeModeWatcher::new()?, debug: Default::default(), + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpgradeModeWatcher { + /// Specifies whether this gateway watches for upgrade mode changes + /// via the published attestation file. + pub enabled: bool, + + /// Endpoint to query to retrieve current upgrade mode attestation. + pub attestation_url: Url, + + /// Expected public key of the attester providing the upgrade mode attestation + /// on the specified endpoint + #[serde(with = "bs58_ed25519_pubkey")] + pub attester_public_key: ed25519::PublicKey, + + #[serde(default)] + pub debug: UpgradeModeWatcherDebug, +} + +impl From for nym_gateway::config::UpgradeModeWatcher { + fn from(config: UpgradeModeWatcher) -> Self { + nym_gateway::config::UpgradeModeWatcher { + enabled: config.enabled, + attestation_url: config.attestation_url, + debug: nym_gateway::config::UpgradeModeWatcherDebug { + regular_polling_interval: config.debug.regular_polling_interval, + expedited_poll_interval: config.debug.expedited_poll_interval, + }, + } + } +} + +impl UpgradeModeWatcher { + pub fn new_mainnet() -> UpgradeModeWatcher { + info!("using mainnet configuration for the upgrade mode:"); + info!("\t- url: {}", mainnet::UPGRADE_MODE_ATTESTATION_URL); + info!( + "\t- attester public key: {}", + mainnet::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY + ); + + // SAFETY: + // our hardcoded values should always be valid + #[allow(clippy::expect_used)] + let attestation_url = mainnet::UPGRADE_MODE_ATTESTATION_URL + .parse() + .expect("invalid default upgrade mode attestation URL"); + + #[allow(clippy::expect_used)] + let attester_public_key = mainnet::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY + .parse() + .expect("invalid default upgrade mode attester public key"); + + UpgradeModeWatcher { + enabled: true, + attestation_url, + attester_public_key, + debug: UpgradeModeWatcherDebug::default(), + } + } + + pub fn new() -> Result { + // if env is configured, extract relevant values from there, otherwise fallback to mainnet + if env::var(var_names::CONFIGURED).is_err() { + return Ok(Self::new_mainnet()); + } + + // if env is configured, the relevant values should be set + let Ok(env_attestation_url) = env::var(var_names::UPGRADE_MODE_ATTESTATION_URL) else { + return log_error_and_return(format!( + "'{}' is not set whilst the env is set to be configured", + var_names::UPGRADE_MODE_ATTESTATION_URL + )); + }; + + let Ok(env_attester_pubkey) = + env::var(var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY) + else { + return log_error_and_return(format!( + "'{}' is not set whilst the env is set to be configured", + var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY + )); + }; + + let attestation_url = match env_attestation_url.parse() { + Ok(url) => url, + Err(err) => { + return log_error_and_return(format!( + "provided attestation url {env_attestation_url} is invalid: {err}!" + )); + } + }; + + let attester_public_key = match env_attester_pubkey.parse() { + Ok(public_key) => public_key, + Err(err) => { + return log_error_and_return(format!( + "provided attester public key {env_attester_pubkey} is invalid: {err}!" + )); + } + }; + + Ok(UpgradeModeWatcher { + enabled: true, + attestation_url, + attester_public_key, + debug: UpgradeModeWatcherDebug::default(), + }) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct UpgradeModeWatcherDebug { + /// Default polling interval + #[serde(with = "humantime_serde")] + pub regular_polling_interval: Duration, + + /// Expedited polling interval for once upgrade mode is detected + #[serde(with = "humantime_serde")] + pub expedited_poll_interval: Duration, +} + +impl UpgradeModeWatcherDebug { + const DEFAULT_REGULAR_POLLING_INTERVAL: Duration = Duration::from_secs(15 * 60); + const DEFAULT_EXPEDITED_POLL_INTERVAL: Duration = Duration::from_secs(2 * 60); +} + +impl Default for UpgradeModeWatcherDebug { + fn default() -> Self { + UpgradeModeWatcherDebug { + regular_polling_interval: Self::DEFAULT_REGULAR_POLLING_INTERVAL, + expedited_poll_interval: Self::DEFAULT_EXPEDITED_POLL_INTERVAL, } } } diff --git a/nym-node/src/config/helpers.rs b/nym-node/src/config/helpers.rs index cdbffb0f25..9605302aa2 100644 --- a/nym-node/src/config/helpers.rs +++ b/nym-node/src/config/helpers.rs @@ -3,11 +3,13 @@ use super::LocalWireguardOpts; use crate::config::Config; +use crate::error::NymNodeError; use clap::crate_version; use nym_gateway::node::{ LocalAuthenticatorOpts, LocalIpPacketRouterOpts, LocalNetworkRequesterOpts, }; use nym_gateway::nym_authenticator; +use tracing::error; // a temporary solution until further refactoring is made fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { @@ -24,6 +26,7 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { nym_gateway::config::IpPacketRouter { enabled: config.service_providers.network_requester.debug.enabled, }, + config.gateway_tasks.upgrade_mode.clone(), nym_gateway::config::Debug { client_bandwidth_max_flushing_rate: config .gateway_tasks @@ -62,6 +65,10 @@ fn ephemeral_gateway_config(config: &Config) -> nym_gateway::config::Config { .maximum_time_between_redemption, }, max_request_timestamp_skew: config.gateway_tasks.debug.max_request_timestamp_skew, + upgrade_mode_min_staleness_recheck: config + .gateway_tasks + .debug + .upgrade_mode_min_staleness_recheck, }, ) } @@ -218,3 +225,9 @@ pub fn gateway_tasks_config(config: &Config) -> GatewayTasksConfig { wg_opts, } } + +pub(crate) fn log_error_and_return(msg: impl Into) -> Result { + let msg = msg.into(); + error!("{msg}"); + Err(NymNodeError::config_validation_failure(msg)) +} diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 3a20022a15..08b578760e 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -262,8 +262,13 @@ impl ConfigBuilder { self } - pub fn build(self) -> Config { - Config { + pub fn build(self) -> Result { + let gateway_tasks = match self.gateway_tasks { + Some(gateway_tasks) => gateway_tasks, + None => GatewayTasksConfig::new(&self.data_dir)?, + }; + + Ok(Config { id: self.id, modes: self.modes, host: self.host.unwrap_or_default(), @@ -279,16 +284,14 @@ impl ConfigBuilder { .storage_paths .unwrap_or_else(|| NymNodePaths::new(&self.data_dir)), metrics: self.metrics.unwrap_or_default(), - gateway_tasks: self - .gateway_tasks - .unwrap_or_else(|| GatewayTasksConfig::new_default(&self.data_dir)), + gateway_tasks, service_providers: self .service_providers .unwrap_or_else(|| ServiceProvidersConfig::new_default(&self.data_dir)), logging: self.logging.unwrap_or_default(), save_path: Some(self.config_path), debug: Default::default(), - } + }) } } @@ -692,6 +695,10 @@ impl ReplayProtectionDebug { pub const DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE: usize = 200; pub fn validate(&self) -> Result<(), NymNodeError> { + if self.unsafe_disabled { + return Ok(()); + } + if self.false_positive_rate >= 1.0 || self.false_positive_rate <= 0.0 { return Err(NymNodeError::config_validation_failure( "false positive rate for replay detection can't be larger than (or equal to) 1 or smaller than (or equal to) 0", diff --git a/nym-node/src/config/old_configs/mod.rs b/nym-node/src/config/old_configs/mod.rs index f87c88a43a..c46a72d1a1 100644 --- a/nym-node/src/config/old_configs/mod.rs +++ b/nym-node/src/config/old_configs/mod.rs @@ -3,6 +3,7 @@ mod old_config_v1; mod old_config_v10; +mod old_config_v11; mod old_config_v2; mod old_config_v3; mod old_config_v4; @@ -22,3 +23,4 @@ pub use old_config_v7::try_upgrade_config_v7; pub use old_config_v8::try_upgrade_config_v8; pub use old_config_v9::try_upgrade_config_v9; pub use old_config_v10::try_upgrade_config_v10; +pub use old_config_v11::try_upgrade_config_v11; diff --git a/nym-node/src/config/old_configs/old_config_v10.rs b/nym-node/src/config/old_configs/old_config_v10.rs index 3a395685f5..bda32f2938 100644 --- a/nym-node/src/config/old_configs/old_config_v10.rs +++ b/nym-node/src/config/old_configs/old_config_v10.rs @@ -1,26 +1,11 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; -use crate::config::gateway_tasks::{ - ClientBandwidthDebug, StaleMessageDebug, ZkNymTicketHandlerDebug, -}; -use crate::config::persistence::{ - AuthenticatorPaths, GatewayTasksPaths, IpPacketRouterPaths, KeysPaths, NetworkRequesterPaths, - NymNodePaths, ReplayProtectionPaths, ServiceProvidersPaths, WireguardPaths, -}; -use crate::config::service_providers::{ - IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug, -}; -use crate::config::{ - Config, DEFAULT_HTTP_PORT, GatewayTasksConfig, Host, Http, KeyRotation, KeyRotationDebug, - Mixnet, MixnetDebug, NodeModes, ReplayProtection, ReplayProtectionDebug, - ServiceProvidersConfig, Verloc, VerlocDebug, Wireguard, gateway_tasks, service_providers, -}; +use crate::config::old_configs::old_config_v11::{ConfigV11, WireguardV11}; +use crate::config::{DEFAULT_HTTP_PORT, NodeModes}; use crate::error::NymNodeError; use celes::Country; use clap::ValueEnum; -use nym_bin_common::logging::LoggingSettings; use nym_client_core_config_types::DebugConfig as ClientDebugConfig; use nym_config::defaults::{DEFAULT_VERLOC_LISTENING_PORT, WG_METADATA_PORT}; use nym_config::helpers::{in6addr_any_init, inaddr_any}; @@ -1169,7 +1154,7 @@ impl ConfigV10 { pub async fn try_upgrade_config_v10>( path: P, prev_config: Option, -) -> Result { +) -> Result { debug!("attempting to load v10 config..."); let old_cfg = if let Some(prev_config) = prev_config { @@ -1178,390 +1163,37 @@ pub async fn try_upgrade_config_v10>( ConfigV10::read_from_path(&path)? }; - let cfg = Config { + let cfg = ConfigV11 { save_path: old_cfg.save_path, id: old_cfg.id, - modes: NodeModes { - mixnode: old_cfg.modes.mixnode, - entry: old_cfg.modes.entry, - exit: old_cfg.modes.exit, - }, - host: Host { - public_ips: old_cfg.host.public_ips, - hostname: old_cfg.host.hostname, - location: old_cfg.host.location, - }, - mixnet: Mixnet { - bind_address: old_cfg.mixnet.bind_address, - announce_port: old_cfg.mixnet.announce_port, - nym_api_urls: old_cfg.mixnet.nym_api_urls, - nyxd_urls: old_cfg.mixnet.nyxd_urls, - replay_protection: ReplayProtection { - storage_paths: ReplayProtectionPaths { - current_bloomfilters_directory: old_cfg - .mixnet - .replay_protection - .storage_paths - .current_bloomfilters_directory, - }, - debug: ReplayProtectionDebug { - unsafe_disabled: old_cfg.mixnet.replay_protection.debug.unsafe_disabled, - maximum_replay_detection_deferral: old_cfg - .mixnet - .replay_protection - .debug - .maximum_replay_detection_deferral, - maximum_replay_detection_pending_packets: old_cfg - .mixnet - .replay_protection - .debug - .maximum_replay_detection_pending_packets, - false_positive_rate: old_cfg.mixnet.replay_protection.debug.false_positive_rate, - initial_expected_packets_per_second: old_cfg - .mixnet - .replay_protection - .debug - .initial_expected_packets_per_second, - bloomfilter_minimum_packets_per_second_size: old_cfg - .mixnet - .replay_protection - .debug - .bloomfilter_minimum_packets_per_second_size, - bloomfilter_size_multiplier: old_cfg - .mixnet - .replay_protection - .debug - .bloomfilter_size_multiplier, - bloomfilter_disk_flushing_rate: old_cfg - .mixnet - .replay_protection - .debug - .bloomfilter_disk_flushing_rate, - }, - }, - key_rotation: KeyRotation { - debug: KeyRotationDebug { - rotation_state_poling_interval: old_cfg - .mixnet - .key_rotation - .debug - .rotation_state_poling_interval, - }, - }, - debug: MixnetDebug { - maximum_forward_packet_delay: old_cfg.mixnet.debug.maximum_forward_packet_delay, - packet_forwarding_initial_backoff: old_cfg - .mixnet - .debug - .packet_forwarding_initial_backoff, - packet_forwarding_maximum_backoff: old_cfg - .mixnet - .debug - .packet_forwarding_maximum_backoff, - initial_connection_timeout: old_cfg.mixnet.debug.initial_connection_timeout, - maximum_connection_buffer_size: old_cfg.mixnet.debug.maximum_connection_buffer_size, - unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, - use_legacy_packet_encoding: old_cfg.mixnet.debug.use_legacy_packet_encoding, - }, - }, - storage_paths: NymNodePaths { - keys: KeysPaths { - private_ed25519_identity_key_file: old_cfg - .storage_paths - .keys - .private_ed25519_identity_key_file, - public_ed25519_identity_key_file: old_cfg - .storage_paths - .keys - .public_ed25519_identity_key_file, - primary_x25519_sphinx_key_file: old_cfg - .storage_paths - .keys - .primary_x25519_sphinx_key_file, - private_x25519_noise_key_file: old_cfg - .storage_paths - .keys - .private_x25519_noise_key_file, - public_x25519_noise_key_file: old_cfg - .storage_paths - .keys - .public_x25519_noise_key_file, - secondary_x25519_sphinx_key_file: old_cfg - .storage_paths - .keys - .secondary_x25519_sphinx_key_file, - }, - description: old_cfg.storage_paths.description, - }, - http: Http { - bind_address: old_cfg.http.bind_address, - landing_page_assets_path: old_cfg.http.landing_page_assets_path, - access_token: old_cfg.http.access_token, - expose_system_info: old_cfg.http.expose_system_info, - expose_system_hardware: old_cfg.http.expose_system_hardware, - expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, - node_load_cache_ttl: old_cfg.http.node_load_cache_ttl, - }, - verloc: Verloc { - bind_address: old_cfg.verloc.bind_address, - announce_port: old_cfg.verloc.announce_port, - debug: VerlocDebug { - packets_per_node: old_cfg.verloc.debug.packets_per_node, - connection_timeout: old_cfg.verloc.debug.connection_timeout, - packet_timeout: old_cfg.verloc.debug.packet_timeout, - delay_between_packets: old_cfg.verloc.debug.delay_between_packets, - tested_nodes_batch_size: old_cfg.verloc.debug.tested_nodes_batch_size, - testing_interval: old_cfg.verloc.debug.testing_interval, - retry_timeout: old_cfg.verloc.debug.retry_timeout, - }, - }, - wireguard: Wireguard { + modes: old_cfg.modes, + host: old_cfg.host, + mixnet: old_cfg.mixnet, + storage_paths: old_cfg.storage_paths, + http: old_cfg.http, + verloc: old_cfg.verloc, + wireguard: WireguardV11 { enabled: old_cfg.wireguard.enabled, bind_address: old_cfg.wireguard.bind_address, private_ipv4: old_cfg.wireguard.private_ipv4, private_ipv6: old_cfg.wireguard.private_ipv6, + + // \/ CHANGED announced_tunnel_port: old_cfg.wireguard.announced_port, + // /\ CHANGED + + // \/ ADDED announced_metadata_port: WG_METADATA_PORT, + // /\ ADDED private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4, private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6, - storage_paths: WireguardPaths { - private_diffie_hellman_key_file: old_cfg - .wireguard - .storage_paths - .private_diffie_hellman_key_file, - public_diffie_hellman_key_file: old_cfg - .wireguard - .storage_paths - .public_diffie_hellman_key_file, - }, + storage_paths: old_cfg.wireguard.storage_paths, }, - gateway_tasks: GatewayTasksConfig { - storage_paths: GatewayTasksPaths { - clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage, - stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage, - cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic, - bridge_client_params: old_cfg.gateway_tasks.storage_paths.bridge_client_params, - }, - enforce_zk_nyms: old_cfg.gateway_tasks.enforce_zk_nyms, - ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, - announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, - announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, - debug: gateway_tasks::Debug { - message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, - maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, - minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, - max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, - stale_messages: StaleMessageDebug { - cleaner_run_interval: old_cfg - .gateway_tasks - .debug - .stale_messages - .cleaner_run_interval, - max_age: old_cfg.gateway_tasks.debug.stale_messages.max_age, - }, - client_bandwidth: ClientBandwidthDebug { - max_flushing_rate: old_cfg - .gateway_tasks - .debug - .client_bandwidth - .max_flushing_rate, - max_delta_flushing_amount: old_cfg - .gateway_tasks - .debug - .client_bandwidth - .max_delta_flushing_amount, - }, - zk_nym_tickets: ZkNymTicketHandlerDebug { - revocation_bandwidth_penalty: old_cfg - .gateway_tasks - .debug - .zk_nym_tickets - .revocation_bandwidth_penalty, - pending_poller: old_cfg.gateway_tasks.debug.zk_nym_tickets.pending_poller, - minimum_api_quorum: old_cfg - .gateway_tasks - .debug - .zk_nym_tickets - .minimum_api_quorum, - minimum_redemption_tickets: old_cfg - .gateway_tasks - .debug - .zk_nym_tickets - .minimum_redemption_tickets, - maximum_time_between_redemption: old_cfg - .gateway_tasks - .debug - .zk_nym_tickets - .maximum_time_between_redemption, - }, - }, - }, - service_providers: ServiceProvidersConfig { - storage_paths: ServiceProvidersPaths { - clients_storage: old_cfg.service_providers.storage_paths.clients_storage, - stats_storage: old_cfg.service_providers.storage_paths.stats_storage, - network_requester: NetworkRequesterPaths { - private_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .private_ed25519_identity_key_file, - public_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .public_ed25519_identity_key_file, - private_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .private_x25519_diffie_hellman_key_file, - public_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .public_x25519_diffie_hellman_key_file, - ack_key_file: old_cfg - .service_providers - .storage_paths - .network_requester - .ack_key_file, - reply_surb_database: old_cfg - .service_providers - .storage_paths - .network_requester - .reply_surb_database, - gateway_registrations: old_cfg - .service_providers - .storage_paths - .network_requester - .gateway_registrations, - }, - ip_packet_router: IpPacketRouterPaths { - private_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .private_ed25519_identity_key_file, - public_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .public_ed25519_identity_key_file, - private_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .private_x25519_diffie_hellman_key_file, - public_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .public_x25519_diffie_hellman_key_file, - ack_key_file: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .ack_key_file, - reply_surb_database: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .reply_surb_database, - gateway_registrations: old_cfg - .service_providers - .storage_paths - .ip_packet_router - .gateway_registrations, - }, - authenticator: AuthenticatorPaths { - private_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .private_ed25519_identity_key_file, - public_ed25519_identity_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .public_ed25519_identity_key_file, - private_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .private_x25519_diffie_hellman_key_file, - public_x25519_diffie_hellman_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .public_x25519_diffie_hellman_key_file, - ack_key_file: old_cfg - .service_providers - .storage_paths - .authenticator - .ack_key_file, - reply_surb_database: old_cfg - .service_providers - .storage_paths - .authenticator - .reply_surb_database, - gateway_registrations: old_cfg - .service_providers - .storage_paths - .authenticator - .gateway_registrations, - }, - }, - open_proxy: old_cfg.service_providers.open_proxy, - upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url, - network_requester: NetworkRequester { - debug: NetworkRequesterDebug { - enabled: old_cfg.service_providers.network_requester.debug.enabled, - disable_poisson_rate: old_cfg - .service_providers - .network_requester - .debug - .disable_poisson_rate, - client_debug: old_cfg - .service_providers - .network_requester - .debug - .client_debug, - }, - }, - ip_packet_router: IpPacketRouter { - debug: IpPacketRouterDebug { - enabled: old_cfg.service_providers.ip_packet_router.debug.enabled, - disable_poisson_rate: old_cfg - .service_providers - .ip_packet_router - .debug - .disable_poisson_rate, - client_debug: old_cfg - .service_providers - .ip_packet_router - .debug - .client_debug, - }, - }, - authenticator: Authenticator { - debug: AuthenticatorDebug { - enabled: old_cfg.service_providers.authenticator.debug.enabled, - disable_poisson_rate: old_cfg - .service_providers - .authenticator - .debug - .disable_poisson_rate, - client_debug: old_cfg.service_providers.authenticator.debug.client_debug, - }, - }, - debug: service_providers::Debug { - message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit, - }, - }, - metrics: Default::default(), - logging: LoggingSettings {}, - debug: Default::default(), + gateway_tasks: old_cfg.gateway_tasks, + service_providers: old_cfg.service_providers, + metrics: old_cfg.metrics, + logging: old_cfg.logging, + debug: old_cfg.debug, }; Ok(cfg) } diff --git a/nym-node/src/config/old_configs/old_config_v11.rs b/nym-node/src/config/old_configs/old_config_v11.rs new file mode 100644 index 0000000000..a2ac70a122 --- /dev/null +++ b/nym-node/src/config/old_configs/old_config_v11.rs @@ -0,0 +1,599 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::authenticator::{Authenticator, AuthenticatorDebug}; +use crate::config::gateway_tasks::{ + ClientBandwidthDebug, StaleMessageDebug, UpgradeModeWatcher, ZkNymTicketHandlerDebug, +}; +use crate::config::persistence::{ + AuthenticatorPaths, GatewayTasksPaths, IpPacketRouterPaths, KeysPaths, NetworkRequesterPaths, + NymNodePaths, ReplayProtectionPaths, ServiceProvidersPaths, WireguardPaths, +}; +use crate::config::service_providers::{ + IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug, +}; +use crate::config::{ + Config, GatewayTasksConfig, Host, Http, KeyRotation, KeyRotationDebug, Mixnet, MixnetDebug, + NodeModes, ReplayProtection, ReplayProtectionDebug, ServiceProvidersConfig, Verloc, + VerlocDebug, Wireguard, gateway_tasks, service_providers, +}; +use crate::error::NymNodeError; +use nym_bin_common::logging::LoggingSettings; +use nym_config::read_config_from_toml_file; +use serde::{Deserialize, Serialize}; +use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use tracing::{debug, error, instrument}; + +pub use unchanged_v11_types::*; + +// (while some of those are technically unused, they might be needed in future migrations, +// thus allow them to exist) +#[allow(dead_code)] +pub mod unchanged_v11_types { + use crate::config::old_configs::old_config_v10::{ + AuthenticatorDebugV10, AuthenticatorPathsV10, AuthenticatorV10, ClientBandwidthDebugV10, + DebugV10, GatewayTasksConfigDebugV10, GatewayTasksConfigV10, GatewayTasksPathsV10, HostV10, + HttpV10, IpPacketRouterDebugV10, IpPacketRouterPathsV10, IpPacketRouterV10, + KeyRotationDebugV10, KeyRotationV10, KeysPathsV10, LoggingSettingsV10, MetricsConfigV10, + MetricsDebugV10, MixnetDebugV10, MixnetV10, NetworkRequesterDebugV10, + NetworkRequesterPathsV10, NetworkRequesterV10, NodeModeV10, NodeModesV10, NymNodePathsV10, + ReplayProtectionDebugV10, ReplayProtectionPathsV10, ReplayProtectionV10, + ServiceProvidersConfigDebugV10, ServiceProvidersConfigV10, ServiceProvidersPathsV10, + StaleMessageDebugV10, VerlocDebugV10, VerlocV10, WireguardPathsV10, + ZkNymTicketHandlerDebugV10, + }; + + pub type WireguardPathsV11 = WireguardPathsV10; + pub type NodeModeV11 = NodeModeV10; + pub type NodeModesV11 = NodeModesV10; + pub type HostV11 = HostV10; + pub type KeyRotationDebugV11 = KeyRotationDebugV10; + pub type KeyRotationV11 = KeyRotationV10; + pub type MixnetDebugV11 = MixnetDebugV10; + pub type MixnetV11 = MixnetV10; + pub type ReplayProtectionV11 = ReplayProtectionV10; + pub type ReplayProtectionPathsV11 = ReplayProtectionPathsV10; + pub type ReplayProtectionDebugV11 = ReplayProtectionDebugV10; + pub type KeysPathsV11 = KeysPathsV10; + pub type NymNodePathsV11 = NymNodePathsV10; + pub type HttpV11 = HttpV10; + pub type VerlocDebugV11 = VerlocDebugV10; + pub type VerlocV11 = VerlocV10; + pub type DebugV11 = DebugV10; + pub type ZkNymTicketHandlerDebugV11 = ZkNymTicketHandlerDebugV10; + pub type NetworkRequesterPathsV11 = NetworkRequesterPathsV10; + pub type IpPacketRouterPathsV11 = IpPacketRouterPathsV10; + pub type AuthenticatorPathsV11 = AuthenticatorPathsV10; + pub type AuthenticatorV11 = AuthenticatorV10; + pub type AuthenticatorDebugV11 = AuthenticatorDebugV10; + pub type IpPacketRouterDebugV11 = IpPacketRouterDebugV10; + pub type IpPacketRouterV11 = IpPacketRouterV10; + pub type NetworkRequesterDebugV11 = NetworkRequesterDebugV10; + pub type NetworkRequesterV11 = NetworkRequesterV10; + pub type GatewayTasksPathsV11 = GatewayTasksPathsV10; + pub type StaleMessageDebugV11 = StaleMessageDebugV10; + pub type ClientBandwidthDebugV11 = ClientBandwidthDebugV10; + pub type GatewayTasksConfigDebugV11 = GatewayTasksConfigDebugV10; + pub type GatewayTasksConfigV11 = GatewayTasksConfigV10; + pub type ServiceProvidersPathsV11 = ServiceProvidersPathsV10; + pub type ServiceProvidersConfigDebugV11 = ServiceProvidersConfigDebugV10; + pub type ServiceProvidersConfigV11 = ServiceProvidersConfigV10; + pub type MetricsConfigV11 = MetricsConfigV10; + pub type MetricsDebugV11 = MetricsDebugV10; + pub type LoggingSettingsV11 = LoggingSettingsV10; +} + +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] +#[serde(deny_unknown_fields)] +pub struct WireguardV11 { + /// Specifies whether the wireguard service is enabled on this node. + pub enabled: bool, + + /// Socket address this node will use for binding its wireguard interface. + /// default: `[::]:51822` + pub bind_address: SocketAddr, + + /// Private IPv4 address of the wireguard gateway. + /// default: `10.1.0.1` + pub private_ipv4: Ipv4Addr, + + /// Private IPv6 address of the wireguard gateway. + /// default: `fc01::1` + pub private_ipv6: Ipv6Addr, + + /// Tunnel port announced to external clients wishing to connect to the wireguard interface. + /// Useful in the instances where the node is behind a proxy. + pub announced_tunnel_port: u16, + + /// Metadata port announced to external clients wishing to connect to the metadata endpoint. + /// Useful in the instances where the node is behind a proxy. + pub announced_metadata_port: u16, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv4. + /// The maximum value for IPv4 is 32 + pub private_network_prefix_v4: u8, + + /// The prefix denoting the maximum number of the clients that can be connected via Wireguard using IPv6. + /// The maximum value for IPv6 is 128 + pub private_network_prefix_v6: u8, + + /// Paths for wireguard keys, client registries, etc. + pub storage_paths: WireguardPathsV11, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ConfigV11 { + // additional metadata holding on-disk location of this config file + #[serde(skip)] + pub(crate) save_path: Option, + + /// Human-readable ID of this particular node. + pub id: String, + + /// Current modes of this nym-node. + pub modes: NodeModesV11, + + pub host: HostV11, + + pub mixnet: MixnetV11, + + /// Storage paths to persistent nym-node data, such as its long term keys. + pub storage_paths: NymNodePathsV11, + + #[serde(default)] + pub http: HttpV11, + + #[serde(default)] + pub verloc: VerlocV11, + + pub wireguard: WireguardV11, + + #[serde(alias = "entry_gateway")] + pub gateway_tasks: GatewayTasksConfigV11, + + #[serde(alias = "exit_gateway")] + pub service_providers: ServiceProvidersConfigV11, + + #[serde(default)] + pub metrics: MetricsConfigV11, + + #[serde(default)] + pub logging: LoggingSettingsV11, + + #[serde(default)] + pub debug: DebugV11, +} + +impl ConfigV11 { + // simple wrapper that reads config file and assigns path location + fn read_from_path>(path: P) -> Result { + let path = path.as_ref(); + let mut loaded: ConfigV11 = + read_config_from_toml_file(path).map_err(|source| NymNodeError::ConfigLoadFailure { + path: path.to_path_buf(), + source, + })?; + loaded.save_path = Some(path.to_path_buf()); + debug!("loaded config file from {}", path.display()); + Ok(loaded) + } +} + +#[instrument(skip_all)] +pub async fn try_upgrade_config_v11>( + path: P, + prev_config: Option, +) -> Result { + debug!("attempting to load v11 config..."); + + let old_cfg = if let Some(prev_config) = prev_config { + prev_config + } else { + ConfigV11::read_from_path(&path)? + }; + + // for future reference: when creating v12 migration, + // look at how v10 -> v11 is implemented + // you might be able to create a bunch of type aliases again to save you some headache + let cfg = Config { + save_path: old_cfg.save_path, + id: old_cfg.id, + modes: NodeModes { + mixnode: old_cfg.modes.mixnode, + entry: old_cfg.modes.entry, + exit: old_cfg.modes.exit, + }, + host: Host { + public_ips: old_cfg.host.public_ips, + hostname: old_cfg.host.hostname, + location: old_cfg.host.location, + }, + mixnet: Mixnet { + bind_address: old_cfg.mixnet.bind_address, + announce_port: old_cfg.mixnet.announce_port, + nym_api_urls: old_cfg.mixnet.nym_api_urls, + nyxd_urls: old_cfg.mixnet.nyxd_urls, + replay_protection: ReplayProtection { + storage_paths: ReplayProtectionPaths { + current_bloomfilters_directory: old_cfg + .mixnet + .replay_protection + .storage_paths + .current_bloomfilters_directory, + }, + debug: ReplayProtectionDebug { + unsafe_disabled: old_cfg.mixnet.replay_protection.debug.unsafe_disabled, + maximum_replay_detection_deferral: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_deferral, + maximum_replay_detection_pending_packets: old_cfg + .mixnet + .replay_protection + .debug + .maximum_replay_detection_pending_packets, + false_positive_rate: old_cfg.mixnet.replay_protection.debug.false_positive_rate, + initial_expected_packets_per_second: old_cfg + .mixnet + .replay_protection + .debug + .initial_expected_packets_per_second, + bloomfilter_minimum_packets_per_second_size: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_minimum_packets_per_second_size, + bloomfilter_size_multiplier: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_size_multiplier, + bloomfilter_disk_flushing_rate: old_cfg + .mixnet + .replay_protection + .debug + .bloomfilter_disk_flushing_rate, + }, + }, + key_rotation: KeyRotation { + debug: KeyRotationDebug { + rotation_state_poling_interval: old_cfg + .mixnet + .key_rotation + .debug + .rotation_state_poling_interval, + }, + }, + debug: MixnetDebug { + maximum_forward_packet_delay: old_cfg.mixnet.debug.maximum_forward_packet_delay, + packet_forwarding_initial_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_initial_backoff, + packet_forwarding_maximum_backoff: old_cfg + .mixnet + .debug + .packet_forwarding_maximum_backoff, + initial_connection_timeout: old_cfg.mixnet.debug.initial_connection_timeout, + maximum_connection_buffer_size: old_cfg.mixnet.debug.maximum_connection_buffer_size, + unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise, + use_legacy_packet_encoding: old_cfg.mixnet.debug.use_legacy_packet_encoding, + }, + }, + storage_paths: NymNodePaths { + keys: KeysPaths { + private_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .storage_paths + .keys + .public_ed25519_identity_key_file, + primary_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .primary_x25519_sphinx_key_file, + private_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .private_x25519_noise_key_file, + public_x25519_noise_key_file: old_cfg + .storage_paths + .keys + .public_x25519_noise_key_file, + secondary_x25519_sphinx_key_file: old_cfg + .storage_paths + .keys + .secondary_x25519_sphinx_key_file, + }, + description: old_cfg.storage_paths.description, + }, + http: Http { + bind_address: old_cfg.http.bind_address, + landing_page_assets_path: old_cfg.http.landing_page_assets_path, + access_token: old_cfg.http.access_token, + expose_system_info: old_cfg.http.expose_system_info, + expose_system_hardware: old_cfg.http.expose_system_hardware, + expose_crypto_hardware: old_cfg.http.expose_crypto_hardware, + node_load_cache_ttl: old_cfg.http.node_load_cache_ttl, + }, + verloc: Verloc { + bind_address: old_cfg.verloc.bind_address, + announce_port: old_cfg.verloc.announce_port, + debug: VerlocDebug { + packets_per_node: old_cfg.verloc.debug.packets_per_node, + connection_timeout: old_cfg.verloc.debug.connection_timeout, + packet_timeout: old_cfg.verloc.debug.packet_timeout, + delay_between_packets: old_cfg.verloc.debug.delay_between_packets, + tested_nodes_batch_size: old_cfg.verloc.debug.tested_nodes_batch_size, + testing_interval: old_cfg.verloc.debug.testing_interval, + retry_timeout: old_cfg.verloc.debug.retry_timeout, + }, + }, + wireguard: Wireguard { + enabled: old_cfg.wireguard.enabled, + bind_address: old_cfg.wireguard.bind_address, + private_ipv4: old_cfg.wireguard.private_ipv4, + private_ipv6: old_cfg.wireguard.private_ipv6, + announced_tunnel_port: old_cfg.wireguard.announced_tunnel_port, + announced_metadata_port: old_cfg.wireguard.announced_metadata_port, + private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4, + private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6, + storage_paths: WireguardPaths { + private_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .private_diffie_hellman_key_file, + public_diffie_hellman_key_file: old_cfg + .wireguard + .storage_paths + .public_diffie_hellman_key_file, + }, + }, + gateway_tasks: GatewayTasksConfig { + storage_paths: GatewayTasksPaths { + clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage, + stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage, + cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic, + bridge_client_params: old_cfg.gateway_tasks.storage_paths.bridge_client_params, + }, + enforce_zk_nyms: old_cfg.gateway_tasks.enforce_zk_nyms, + ws_bind_address: old_cfg.gateway_tasks.ws_bind_address, + announce_ws_port: old_cfg.gateway_tasks.announce_ws_port, + announce_wss_port: old_cfg.gateway_tasks.announce_wss_port, + // \/ ADDED + upgrade_mode: UpgradeModeWatcher::new() + .inspect_err(|_| { + error!( + "failed to set custom upgrade mode configuration - falling back to mainnet" + ) + }) + .unwrap_or(UpgradeModeWatcher::new_mainnet()), + // /\ ADDED + debug: gateway_tasks::Debug { + message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit, + maximum_open_connections: old_cfg.gateway_tasks.debug.maximum_open_connections, + minimum_mix_performance: old_cfg.gateway_tasks.debug.minimum_mix_performance, + max_request_timestamp_skew: old_cfg.gateway_tasks.debug.max_request_timestamp_skew, + stale_messages: StaleMessageDebug { + cleaner_run_interval: old_cfg + .gateway_tasks + .debug + .stale_messages + .cleaner_run_interval, + max_age: old_cfg.gateway_tasks.debug.stale_messages.max_age, + }, + client_bandwidth: ClientBandwidthDebug { + max_flushing_rate: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_flushing_rate, + max_delta_flushing_amount: old_cfg + .gateway_tasks + .debug + .client_bandwidth + .max_delta_flushing_amount, + }, + zk_nym_tickets: ZkNymTicketHandlerDebug { + revocation_bandwidth_penalty: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .revocation_bandwidth_penalty, + pending_poller: old_cfg.gateway_tasks.debug.zk_nym_tickets.pending_poller, + minimum_api_quorum: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_api_quorum, + minimum_redemption_tickets: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .minimum_redemption_tickets, + maximum_time_between_redemption: old_cfg + .gateway_tasks + .debug + .zk_nym_tickets + .maximum_time_between_redemption, + }, + // \/ ADDED (be explicit about the value rather than using ..Default::default() + upgrade_mode_min_staleness_recheck: gateway_tasks::Debug::default() + .upgrade_mode_min_staleness_recheck, + // /\ ADDED + }, + }, + service_providers: ServiceProvidersConfig { + storage_paths: ServiceProvidersPaths { + clients_storage: old_cfg.service_providers.storage_paths.clients_storage, + stats_storage: old_cfg.service_providers.storage_paths.stats_storage, + network_requester: NetworkRequesterPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .network_requester + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .network_requester + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .network_requester + .gateway_registrations, + }, + ip_packet_router: IpPacketRouterPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .ip_packet_router + .gateway_registrations, + }, + authenticator: AuthenticatorPaths { + private_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_ed25519_identity_key_file, + public_ed25519_identity_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_ed25519_identity_key_file, + private_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .private_x25519_diffie_hellman_key_file, + public_x25519_diffie_hellman_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .public_x25519_diffie_hellman_key_file, + ack_key_file: old_cfg + .service_providers + .storage_paths + .authenticator + .ack_key_file, + reply_surb_database: old_cfg + .service_providers + .storage_paths + .authenticator + .reply_surb_database, + gateway_registrations: old_cfg + .service_providers + .storage_paths + .authenticator + .gateway_registrations, + }, + }, + open_proxy: old_cfg.service_providers.open_proxy, + upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url, + network_requester: NetworkRequester { + debug: NetworkRequesterDebug { + enabled: old_cfg.service_providers.network_requester.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .network_requester + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .network_requester + .debug + .client_debug, + }, + }, + ip_packet_router: IpPacketRouter { + debug: IpPacketRouterDebug { + enabled: old_cfg.service_providers.ip_packet_router.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .ip_packet_router + .debug + .disable_poisson_rate, + client_debug: old_cfg + .service_providers + .ip_packet_router + .debug + .client_debug, + }, + }, + authenticator: Authenticator { + debug: AuthenticatorDebug { + enabled: old_cfg.service_providers.authenticator.debug.enabled, + disable_poisson_rate: old_cfg + .service_providers + .authenticator + .debug + .disable_poisson_rate, + client_debug: old_cfg.service_providers.authenticator.debug.client_debug, + }, + }, + debug: service_providers::Debug { + message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit, + }, + }, + metrics: Default::default(), + logging: LoggingSettings {}, + debug: Default::default(), + }; + Ok(cfg) +} diff --git a/nym-node/src/config/template.rs b/nym-node/src/config/template.rs index bf8ed72710..8177119780 100644 --- a/nym-node/src/config/template.rs +++ b/nym-node/src/config/template.rs @@ -201,6 +201,18 @@ announce_ws_port = {{#if gateway_tasks.announce_ws_port }} {{ gateway_tasks.anno # (default: 0 - disabled) announce_wss_port = {{#if gateway_tasks.announce_wss_port }} {{ gateway_tasks.announce_wss_port }} {{else}} 0 {{/if}} +[gateway_tasks.upgrade_mode] +# Specifies whether this gateway watches for upgrade mode changes +# via the published attestation file. +enabled = {{ gateway_tasks.upgrade_mode.enabled }} + +# Endpoint to query to retrieve current upgrade mode attestation. +# If not provided, it implicitly disables the watcher and upgrade-mode features +attestation_url = '{{ gateway_tasks.upgrade_mode.attestation_url }}' + +# Expected public key of the attester providing the upgrade mode attestation +# on the specified endpoint +attester_public_key = '{{ gateway_tasks.upgrade_mode.attester_public_key }}' [gateway_tasks.storage_paths] # Path to sqlite database containing all persistent data: messages for offline clients, diff --git a/nym-node/src/config/upgrade_helpers.rs b/nym-node/src/config/upgrade_helpers.rs index 63dda0d6e8..b2b8018231 100644 --- a/nym-node/src/config/upgrade_helpers.rs +++ b/nym-node/src/config/upgrade_helpers.rs @@ -17,7 +17,8 @@ async fn try_upgrade_config(path: &Path) -> Result<(), NymNodeError> { let cfg = try_upgrade_config_v7(path, cfg).await.ok(); let cfg = try_upgrade_config_v8(path, cfg).await.ok(); let cfg = try_upgrade_config_v9(path, cfg).await.ok(); - match try_upgrade_config_v10(path, cfg).await { + let cfg = try_upgrade_config_v10(path, cfg).await.ok(); + match try_upgrade_config_v11(path, cfg).await { Ok(cfg) => cfg.save(), Err(e) => { tracing::error!("Failed to finish upgrade: {e}"); diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index 13aedc7c0a..1564d087a4 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -61,6 +61,10 @@ pub mod vars { pub const NYMNODE_ENTRY_ANNOUNCE_WSS_PORT_ARG: &str = "NYMNODE_ENTRY_ANNOUNCE_WSS_PORT"; pub const NYMNODE_ENFORCE_ZK_NYMS_ARG: &str = "NYMNODE_ENFORCE_ZK_NYMS"; pub const NYMNODE_MNEMONIC_ARG: &str = "NYMNODE_MNEMONIC"; + pub const NYMNODE_UPGRADE_MODE_ATTESTATION_URL_ARG: &str = + "NYMNODE_UPGRADE_MODE_ATTESTATION_URL"; + pub const NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY_ARG: &str = + "NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY"; // exit gateway: pub const NYMNODE_UPSTREAM_EXIT_POLICY_ARG: &str = "NYMNODE_UPSTREAM_EXIT_POLICY"; diff --git a/nym-node/src/node/http/router/api/v1/mod.rs b/nym-node/src/node/http/router/api/v1/mod.rs index d0f2239f1d..b9a74b27bd 100644 --- a/nym-node/src/node/http/router/api/v1/mod.rs +++ b/nym-node/src/node/http/router/api/v1/mod.rs @@ -14,6 +14,7 @@ pub mod ip_packet_router; pub mod load; pub mod metrics; pub mod mixnode; +pub mod network; pub mod network_requester; pub mod node; pub mod openapi; @@ -34,6 +35,7 @@ pub(super) fn routes(config: Config) -> Router { Router::new() .route(v1::HEALTH, get(health::root_health)) .route(v1::LOAD, get(load::root_load)) + .nest(v1::NETWORK, network::routes()) .nest(v1::METRICS, metrics::routes(config.metrics)) .nest(v1::BRIDGES, bridges::routes(config.bridges)) .nest(v1::GATEWAY, gateway::routes(config.gateway)) diff --git a/nym-node/src/node/http/router/api/v1/network/mod.rs b/nym-node/src/node/http/router/api/v1/network/mod.rs new file mode 100644 index 0000000000..2d619eba3c --- /dev/null +++ b/nym-node/src/node/http/router/api/v1/network/mod.rs @@ -0,0 +1,14 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::api::v1::network::upgrade_mode::upgrade_mode_status; +use crate::node::http::state::AppState; +use axum::Router; +use axum::routing::get; +use nym_node_requests::routes::api::v1::network; + +pub mod upgrade_mode; + +pub(crate) fn routes() -> Router { + Router::new().route(network::UPGRADE_MODE_STATUS, get(upgrade_mode_status)) +} diff --git a/nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs b/nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs new file mode 100644 index 0000000000..f487c9a4fe --- /dev/null +++ b/nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs @@ -0,0 +1,39 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::state::AppState; +use axum::extract::{Query, State}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_node_requests::api::v1::network::models::UpgradeModeStatus; + +/// Returns current upgrade mode information as perceived by this node. +#[utoipa::path( + get, + path = "/upgrade-mode-status", + context_path = "/api/v1/network", + tag = "Network", + responses( + (status = 200, content( + (UpgradeModeStatus = "application/json"), + (UpgradeModeStatus = "application/yaml"), + (UpgradeModeStatus = "application/bincode") + )) + ), + params(OutputParams) +)] +pub(crate) async fn upgrade_mode_status( + Query(output): Query, + State(state): State, +) -> UpgradeModeStatusResponse { + let output = output.get_output(); + let um_state = &state.upgrade_mode_state.node_state; + output.to_response(UpgradeModeStatus { + enabled: um_state.upgrade_mode_enabled(), + last_queried: um_state.last_queried(), + attestation_provider: state.upgrade_mode_state.attestation_url.clone(), + attester_pubkey: um_state.attester_pubkey(), + published_attestation: um_state.attestation().await, + }) +} + +pub type UpgradeModeStatusResponse = FormattedResponse; diff --git a/nym-node/src/node/http/router/api/v1/openapi.rs b/nym-node/src/node/http/router/api/v1/openapi.rs index 98835e4c59..ac410aba5f 100644 --- a/nym-node/src/node/http/router/api/v1/openapi.rs +++ b/nym-node/src/node/http/router/api/v1/openapi.rs @@ -1,7 +1,6 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::http::router::api; use axum::Router; use nym_node_requests::api as api_requests; use nym_node_requests::routes::api::{v1, v1_absolute}; @@ -13,25 +12,31 @@ use utoipa_swagger_ui::SwaggerUi; #[openapi( info(title = "NymNode API"), paths( - api::v1::node::build_information::build_information, - api::v1::node::host_information::host_information, - api::v1::node::roles::roles, - api::v1::node::hardware::host_system, - api::v1::node::description::description, - api::v1::node::auxiliary::auxiliary, - api::v1::metrics::legacy_mixing::legacy_mixing_stats, - api::v1::metrics::packets_stats::packets_stats, - api::v1::metrics::verloc::verloc_stats, - api::v1::metrics::prometheus::prometheus_metrics, - api::v1::health::root_health, - api::v1::load::root_load, - api::v1::gateway::root::root_gateway, - api::v1::gateway::client_interfaces::client_interfaces, - api::v1::gateway::client_interfaces::mixnet_websockets, - api::v1::mixnode::root::root_mixnode, - api::v1::network_requester::root::root_network_requester, - api::v1::network_requester::exit_policy::node_exit_policy, - api::v1::ip_packet_router::root::root_ip_packet_router, + crate::node::http::router::api::v1::metrics::verloc::verloc_stats, + crate::node::http::router::api::v1::metrics::legacy_mixing::legacy_mixing_stats, + crate::node::http::router::api::v1::metrics::wireguard::wireguard_stats, + crate::node::http::router::api::v1::metrics::sessions::sessions_stats, + crate::node::http::router::api::v1::metrics::packets_stats::packets_stats, + crate::node::http::router::api::v1::metrics::prometheus::prometheus_metrics, + crate::node::http::router::api::v1::ip_packet_router::root::root_ip_packet_router, + crate::node::http::router::api::v1::health::root_health, + crate::node::http::router::api::v1::network::upgrade_mode::upgrade_mode_status, + crate::node::http::router::api::v1::mixnode::root::root_mixnode, + crate::node::http::router::api::v1::load::root_load, + crate::node::http::router::api::v1::node::host_information::host_information, + crate::node::http::router::api::v1::node::description::description, + crate::node::http::router::api::v1::node::roles::roles, + crate::node::http::router::api::v1::node::auxiliary::auxiliary, + crate::node::http::router::api::v1::node::build_information::build_information, + crate::node::http::router::api::v1::node::hardware::host_system, + crate::node::http::router::api::v1::authenticator::root::root_authenticator, + crate::node::http::router::api::v1::network_requester::exit_policy::node_exit_policy, + crate::node::http::router::api::v1::network_requester::root::root_network_requester, + crate::node::http::router::api::v1::gateway::client_interfaces::client_interfaces, + crate::node::http::router::api::v1::gateway::client_interfaces::mixnet_websockets, + crate::node::http::router::api::v1::gateway::client_interfaces::wireguard_details, + crate::node::http::router::api::v1::gateway::root::root_gateway, + ), components( schemas( diff --git a/nym-node/src/node/http/state/mod.rs b/nym-node/src/node/http/state/mod.rs index 9cd3348cb6..8ed756aa60 100644 --- a/nym-node/src/node/http/state/mod.rs +++ b/nym-node/src/node/http/state/mod.rs @@ -4,6 +4,7 @@ use crate::node::http::state::load::CachedNodeLoad; use crate::node::http::state::metrics::MetricsAppState; use crate::node::key_rotation::active_keys::ActiveSphinxKeys; +use nym_credential_verification::UpgradeModeState; use nym_crypto::asymmetric::ed25519; use nym_node_metrics::NymNodeMetrics; use nym_noise_keys::VersionedNoiseKey; @@ -12,6 +13,7 @@ use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; use tokio::time::Instant; +use url::Url; pub mod load; pub mod metrics; @@ -23,6 +25,12 @@ pub(crate) struct StaticNodeInformation { pub(crate) hostname: Option, } +#[derive(Clone)] +pub(crate) struct UpgradeModeApiState { + pub(crate) node_state: UpgradeModeState, + pub(crate) attestation_url: Url, +} + #[derive(Clone)] pub(crate) struct AppState { pub(crate) startup_time: Instant, @@ -34,15 +42,18 @@ pub(crate) struct AppState { pub(crate) cached_load: CachedNodeLoad, pub(crate) metrics: MetricsAppState, + + pub(crate) upgrade_mode_state: UpgradeModeApiState, } impl AppState { - #[allow(clippy::new_without_default)] pub(crate) fn new( static_information: StaticNodeInformation, x25519_sphinx_keys: ActiveSphinxKeys, metrics: NymNodeMetrics, verloc: SharedVerlocStats, + upgrade_mode_attestation_url: Url, + upgrade_mode_state: UpgradeModeState, load_cache_ttl: Duration, ) -> Self { AppState { @@ -56,6 +67,10 @@ impl AppState { startup_time: Instant::now(), cached_load: CachedNodeLoad::new(load_cache_ttl), metrics: MetricsAppState { metrics, verloc }, + upgrade_mode_state: UpgradeModeApiState { + node_state: upgrade_mode_state, + attestation_url: upgrade_mode_attestation_url, + }, } } } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index 44d35b5d41..8ba968ca29 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -39,8 +39,9 @@ use crate::node::shared_network::{ CachedNetwork, CachedTopologyProvider, LocalGatewayNode, NetworkRefresher, }; use nym_bin_common::bin_info; +use nym_credential_verification::UpgradeModeState; use nym_crypto::asymmetric::{ed25519, x25519}; -use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder}; +use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder, UpgradeModeCheckRequestSender}; use nym_mixnet_client::client::ActiveConnections; use nym_mixnet_client::forwarder::MixForwardingSender; use nym_network_requester::{ @@ -373,6 +374,8 @@ pub(crate) struct NymNode { entry_gateway: GatewayTasksData, + upgrade_mode_state: UpgradeModeState, + #[allow(dead_code)] service_providers: ServiceProvidersData, @@ -461,6 +464,9 @@ impl NymNode { metrics: NymNodeMetrics::new(), verloc_stats: Default::default(), entry_gateway: GatewayTasksData::new(&config.gateway_tasks).await?, + upgrade_mode_state: UpgradeModeState::new( + config.gateway_tasks.upgrade_mode.attester_public_key, + ), service_providers: ServiceProvidersData::new(&config.service_providers)?, wireguard: Some(wireguard_data), config, @@ -624,9 +630,27 @@ impl NymNode { metrics_sender, self.metrics.clone(), self.entry_gateway.mnemonic.clone(), + Self::user_agent(), + self.upgrade_mode_state.clone(), self.shutdown_tracker().clone(), ); + // start task for watching the changes in upgrade mode attestation + let upgrade_check_request_sender = if let Some(upgrade_mode_watcher) = + gateway_tasks_builder.try_build_upgrade_mode_watcher() + { + let req_sender = upgrade_mode_watcher.request_sender(); + upgrade_mode_watcher.start(); + req_sender + } else { + UpgradeModeCheckRequestSender::new_empty() + }; + + // create the common state for subtasks relying on the upgrade mode information + // (i.e. everything that'd require ticket/bandwidth processing) + let upgrade_mode_common_state = + gateway_tasks_builder.build_upgrade_mode_common_state(upgrade_check_request_sender); + // if we're running in entry mode, start the websocket if self.modes().entry { info!( @@ -634,7 +658,10 @@ impl NymNode { self.config.gateway_tasks.ws_bind_address ); let mut websocket = gateway_tasks_builder - .build_websocket_listener(active_clients_store.clone()) + .build_websocket_listener( + active_clients_store.clone(), + upgrade_mode_common_state.clone(), + ) .await?; self.shutdown_tracker() .try_spawn_named(async move { websocket.run().await }, "EntryWebsocket"); @@ -682,7 +709,7 @@ impl NymNode { gateway_tasks_builder.set_wireguard_data(wg_data.into()); let authenticator = gateway_tasks_builder - .build_wireguard_authenticator(topology_provider) + .build_wireguard_authenticator(upgrade_mode_common_state.clone(), topology_provider) .await?; let started_authenticator = authenticator.start_service_provider().await?; active_clients_store.insert_embedded(started_authenticator.handle); @@ -693,7 +720,7 @@ impl NymNode { ); gateway_tasks_builder - .try_start_wireguard() + .try_start_wireguard(upgrade_mode_common_state) .await .map_err(NymNodeError::GatewayTasksStartupFailure)?; } else { @@ -840,6 +867,12 @@ impl NymNode { self.active_sphinx_keys()?.clone(), self.metrics.clone(), self.verloc_stats.clone(), + self.config + .gateway_tasks + .upgrade_mode + .attestation_url + .clone(), + self.upgrade_mode_state.clone(), self.config.http.node_load_cache_ttl, ); @@ -1131,7 +1164,7 @@ impl NymNode { } pub(crate) async fn run_minimal_mixnet_processing(mut self) -> Result<(), NymNodeError> { - let noise_config = nym_noise::config::NoiseConfig::new( + let noise_config = NoiseConfig::new( self.x25519_noise_keys.clone(), NoiseNetworkView::new_empty(), self.config.mixnet.debug.initial_connection_timeout, diff --git a/nym-statistics-api/.sqlx/query-14d75cdd34201313e34ae7f0b931c9df43603232e3be42b0573013cd74226518.json b/nym-statistics-api/.sqlx/query-14d75cdd34201313e34ae7f0b931c9df43603232e3be42b0573013cd74226518.json new file mode 100644 index 0000000000..a16fa1997d --- /dev/null +++ b/nym-statistics-api/.sqlx/query-14d75cdd34201313e34ae7f0b931c9df43603232e3be42b0573013cd74226518.json @@ -0,0 +1,33 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO report_v2(\n received_at,\n source_ip,\n from_mixnet,\n country_code,\n report_version,\n device_id,\n os_type,\n os_version,\n architecture,\n app_version,\n user_agent,\n start_day_utc,\n connection_time_ms,\n tunnel_type,\n retry_attempt,\n session_duration_min,\n disconnection_time_ms,\n exit_id,\n follow_up_id,\n error)\n VALUES ($1::timestamptz, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Text", + "Bool", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Date", + "Int4", + "Text", + "Int4", + "Int4", + "Int4", + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "14d75cdd34201313e34ae7f0b931c9df43603232e3be42b0573013cd74226518" +} diff --git a/nym-statistics-api/.sqlx/query-dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba.json b/nym-statistics-api/.sqlx/query-dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba.json deleted file mode 100644 index b477ea243d..0000000000 --- a/nym-statistics-api/.sqlx/query-dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO connection_stats (\n received_at,\n connection_time_ms,\n two_hop,\n source_ip,\n country_code,\n from_mixnet) VALUES ($1::timestamptz, $2, $3, $4, $5, $6)", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Timestamptz", - "Int4", - "Bool", - "Text", - "Text", - "Bool" - ] - }, - "nullable": [] - }, - "hash": "dce9f3dae7ae0dc5953d1f69e843bea9553fb05a2f656cfff6598f12a21c99ba" -} diff --git a/nym-statistics-api/Cargo.toml b/nym-statistics-api/Cargo.toml index e9e51ac626..769aeacb33 100644 --- a/nym-statistics-api/Cargo.toml +++ b/nym-statistics-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-statistics-api" -version = "0.2.1" +version = "0.3.0" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -20,14 +20,13 @@ axum-client-ip.workspace = true axum-extra = { workspace = true, features = ["typed-header"] } celes.workspace = true clap = { workspace = true, features = ["cargo", "derive", "env", "string"] } -serde = { workspace = true, features = ["derive"] } serde_json.workspace = true sqlx = { workspace = true, features = [ "runtime-tokio-rustls", "postgres", "time", ] } -time.workspace = true +time = { workspace = true, features = ["serde-human-readable"] } tokio = { workspace = true, features = ["rt-multi-thread"] } tokio-util.workspace = true tracing.workspace = true diff --git a/nym-statistics-api/migrations/003_sessions.sql b/nym-statistics-api/migrations/003_sessions.sql new file mode 100644 index 0000000000..ecd65d9173 --- /dev/null +++ b/nym-statistics-api/migrations/003_sessions.sql @@ -0,0 +1,29 @@ +CREATE TABLE report_v2 ( + -- some info about the report, inferred from when/from where we got it + received_at TIMESTAMP WITH TIME ZONE NOT NULL, + source_ip TEXT, + from_mixnet BOOLEAN, + country_code TEXT, + report_version TEXT, + + -- some infos about the device sending the report + device_id TEXT NOT NULL, + os_type TEXT, + os_version TEXT, + architecture TEXT, + app_version TEXT, + user_agent TEXT, + + -- session info + start_day_utc DATE, + connection_time_ms INTEGER, + tunnel_type TEXT, + retry_attempt INTEGER, + session_duration_min INTEGER, + disconnection_time_ms INTEGER, + exit_id TEXT, + follow_up_id TEXT, + error TEXT +); + +CREATE INDEX idx_report_v2_received_at ON report_v2 (received_at); diff --git a/nym-statistics-api/src/http/api/mod.rs b/nym-statistics-api/src/http/api/mod.rs index dd03791238..bbbe5351e2 100644 --- a/nym-statistics-api/src/http/api/mod.rs +++ b/nym-statistics-api/src/http/api/mod.rs @@ -24,7 +24,7 @@ impl RouterBuilder { ) .route( "/", - axum::routing::get(|| async { Redirect::permanent("/swagger") }), // SW let's redirect to a blogpost explaining the stats collection process once it exists + axum::routing::get(|| async { Redirect::permanent("/swagger") }), ) .nest("/v1", Router::new().nest("/stats", stats::routes())); diff --git a/nym-statistics-api/src/http/api/stats.rs b/nym-statistics-api/src/http/api/stats.rs index ab6b69926d..f085a03172 100644 --- a/nym-statistics-api/src/http/api/stats.rs +++ b/nym-statistics-api/src/http/api/stats.rs @@ -1,7 +1,7 @@ use axum::{Json, Router, extract::State}; use axum_client_ip::InsecureClientIp; use axum_extra::{TypedHeader, headers::UserAgent}; -use nym_statistics_common::report::vpn_client::VpnClientStatsReport; +use nym_statistics_common::report::vpn_client::{VpnClientStatsReport, VpnClientStatsReportV2}; use tracing::debug; use crate::{ @@ -9,11 +9,13 @@ use crate::{ error::{HttpError, HttpResult}, state::AppState, }, - storage::models::{ConnectionInfoDto, DailyActiveDeviceDto, StatsReportV1Dto}, + storage::models::{DailyActiveDeviceDto, StatsReportV1Dto, StatsReportV2Dto}, }; pub(crate) fn routes() -> Router { - Router::new().route("/report", axum::routing::post(submit_stats_report)) + Router::new() + .route("/report", axum::routing::post(submit_stats_report)) + .route("/session", axum::routing::post(submit_session_report)) } #[utoipa::path( @@ -44,19 +46,11 @@ async fn submit_stats_report( let maybe_location = gateway_record.unwrap_or_default(); if from_mixnet { - debug!("Received a report from the network"); + debug!("Received a V1 report from the network"); } else { - debug!("Received a report from outside of the network"); + debug!("Received a V1 report from outside of the network"); } - let active_device = DailyActiveDeviceDto::new(now, &report, user_agent.clone(), from_mixnet); - let maybe_connection_info = ConnectionInfoDto::maybe_new( - now, - &report, - insecure_ip_addr.0, - maybe_location, - from_mixnet, - ); let stats_report = StatsReportV1Dto::new( now, @@ -75,7 +69,64 @@ async fn submit_stats_report( state .storage() - .store_legacy_vpn_client_report(active_device, maybe_connection_info) + .store_active_device(active_device) + .await + .map_err(HttpError::internal_with_logging)?; + + Ok(Json(())) +} + +#[utoipa::path( + post, + request_body = VpnClientStatsReportV2, + tag = "Stats", + path = "/session", + context_path = "/v1/stats", + responses( + (status = 200) + ) +)] +#[tracing::instrument(level = "info", skip_all)] +async fn submit_session_report( + State(mut state): State, + TypedHeader(user_agent): TypedHeader, + insecure_ip_addr: InsecureClientIp, // This is the reverse proxy IP for now, but maybe in the future? + Json(report): Json, +) -> HttpResult> { + let now = time::OffsetDateTime::now_utc(); + let gateway_record = state + .network_view() + .get_country_by_id(&report.session_report.exit_id) + .await; + + let from_mixnet = gateway_record.is_some(); + let maybe_location = gateway_record.unwrap_or_default(); + + if from_mixnet { + debug!("Received a V2 report from the network"); + } else { + debug!("Received a V2 report from outside of the network"); + } + let active_device = DailyActiveDeviceDto::new_v2(now, &report, user_agent.clone(), from_mixnet); + + let stats_report = StatsReportV2Dto::new( + now, + &report, + user_agent, + from_mixnet, + insecure_ip_addr.0, + maybe_location, + ); + + state + .storage() + .store_active_device(active_device) + .await + .map_err(HttpError::internal_with_logging)?; + + state + .storage() + .store_vpn_client_report_v2(stats_report) .await .map_err(HttpError::internal_with_logging)?; diff --git a/nym-statistics-api/src/network_view.rs b/nym-statistics-api/src/network_view.rs index db3a153de0..1491a00903 100644 --- a/nym-statistics-api/src/network_view.rs +++ b/nym-statistics-api/src/network_view.rs @@ -20,6 +20,7 @@ use tracing::{error, info, trace, warn}; const NETWORK_CACHE_TTL: Duration = Duration::from_secs(600); type IpToCountryMap = HashMap>; +type IdToCountryMap = HashMap>; // SW this should use a proper NS API client once it exists struct NodesQuerier { @@ -45,19 +46,24 @@ impl NetworkView { fn new_empty() -> Self { NetworkView { inner: Arc::new(RwLock::new(NetworkViewInner { - network_nodes: HashMap::new(), + ip_to_country: HashMap::new(), + id_to_country: HashMap::new(), })), } } pub(crate) async fn get_country_by_ip(&self, ip_addr: &IpAddr) -> Option> { - self.inner.read().await.network_nodes.get(ip_addr).copied() + self.inner.read().await.ip_to_country.get(ip_addr).copied() + } + pub(crate) async fn get_country_by_id(&self, id_key: &str) -> Option> { + self.inner.read().await.id_to_country.get(id_key).copied() } } #[derive(Debug)] struct NetworkViewInner { - network_nodes: IpToCountryMap, + ip_to_country: IpToCountryMap, + id_to_country: IdToCountryMap, } pub struct NetworkRefresher { @@ -112,7 +118,7 @@ impl NetworkRefresher { let nodes = querier.current_nymnodes().await?; // collect all known/allowed nodes information - let known_nodes = nodes + let ip_to_country = nodes .iter() .flat_map(|n| { n.description @@ -124,8 +130,19 @@ impl NetworkRefresher { }) .collect::>(); + let id_to_country = nodes + .iter() + .map(|n| { + ( + n.ed25519_identity_key().to_base58_string(), + n.description.auxiliary_details.location, + ) + }) + .collect::>(); + let mut network_guard = self.network.inner.write().await; - network_guard.network_nodes = known_nodes; + network_guard.ip_to_country = ip_to_country; + network_guard.id_to_country = id_to_country; } Ok(()) diff --git a/nym-statistics-api/src/storage/mod.rs b/nym-statistics-api/src/storage/mod.rs index f6547da55c..3008855013 100644 --- a/nym-statistics-api/src/storage/mod.rs +++ b/nym-statistics-api/src/storage/mod.rs @@ -1,5 +1,4 @@ use anyhow::{Result, anyhow}; -use models::{ConnectionInfoDto, DailyActiveDeviceDto}; use sqlx::{ Executor, migrate::Migrator, @@ -7,7 +6,7 @@ use sqlx::{ }; use std::{path::PathBuf, str::FromStr}; -use crate::storage::models::StatsReportV1Dto; +use crate::storage::models::{DailyActiveDeviceDto, StatsReportV1Dto, StatsReportV2Dto}; pub(crate) mod models; @@ -62,19 +61,10 @@ impl StatisticsStorage { }) } - pub(crate) async fn store_legacy_vpn_client_report( - &mut self, + pub(crate) async fn store_active_device( + &self, active_device: DailyActiveDeviceDto, - connection_info: Option, ) -> Result<()> { - self.store_device(active_device).await?; - if let Some(connection_info) = connection_info { - self.store_connection_stats(connection_info).await?; - } - Ok(()) - } - - async fn store_device(&self, active_device: DailyActiveDeviceDto) -> Result<()> { sqlx::query!( r#"INSERT INTO active_device ( day, @@ -101,27 +91,6 @@ impl StatisticsStorage { Ok(()) } - async fn store_connection_stats(&self, connection_info: ConnectionInfoDto) -> Result<()> { - sqlx::query!( - r#"INSERT INTO connection_stats ( - received_at, - connection_time_ms, - two_hop, - source_ip, - country_code, - from_mixnet) VALUES ($1::timestamptz, $2, $3, $4, $5, $6)"#, - connection_info.received_at as time::OffsetDateTime, - connection_info.connection_time_ms, - connection_info.two_hop, - connection_info.received_from, - connection_info.country_code, - connection_info.from_mixnet - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - pub(crate) async fn store_vpn_client_report( &mut self, report_v1: StatsReportV1Dto, @@ -158,4 +127,57 @@ impl StatisticsStorage { .await?; Ok(()) } + + pub(crate) async fn store_vpn_client_report_v2( + &self, + report_v2: StatsReportV2Dto, + ) -> Result<()> { + sqlx::query!( + r#"INSERT INTO report_v2( + received_at, + source_ip, + from_mixnet, + country_code, + report_version, + device_id, + os_type, + os_version, + architecture, + app_version, + user_agent, + start_day_utc, + connection_time_ms, + tunnel_type, + retry_attempt, + session_duration_min, + disconnection_time_ms, + exit_id, + follow_up_id, + error) + VALUES ($1::timestamptz, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)"#, + report_v2.received_at as time::OffsetDateTime, + report_v2.received_from, + report_v2.from_mixnet, + report_v2.country_code, + report_v2.report_version, + report_v2.stats_id, + report_v2.os_type, + report_v2.os_version, + report_v2.os_arch, + report_v2.app_version, + report_v2.user_agent, + report_v2.start_day_utc, + report_v2.connection_time_ms, + report_v2.tunnel_type, + report_v2.retry_attempt, + report_v2.session_duration_min, + report_v2.disconnection_time_ms, + report_v2.exit_id, + report_v2.follow_up_id, + report_v2.error + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } } diff --git a/nym-statistics-api/src/storage/models.rs b/nym-statistics-api/src/storage/models.rs index 95112ae64a..b7c5ba1b83 100644 --- a/nym-statistics-api/src/storage/models.rs +++ b/nym-statistics-api/src/storage/models.rs @@ -2,7 +2,7 @@ use std::net::IpAddr; use axum_extra::headers::UserAgent; use celes::Country; -use nym_statistics_common::report::vpn_client::VpnClientStatsReport; +use nym_statistics_common::report::vpn_client::{VpnClientStatsReport, VpnClientStatsReportV2}; use time::{Date, OffsetDateTime}; pub type StatsId = String; @@ -37,38 +37,25 @@ impl DailyActiveDeviceDto { from_mixnet, } } -} - -#[derive(Debug, Clone, sqlx::FromRow)] -pub(crate) struct ConnectionInfoDto { - pub(crate) received_at: OffsetDateTime, - pub(crate) received_from: String, - pub(crate) connection_time_ms: Option, - pub(crate) two_hop: bool, - pub(crate) country_code: Option, - pub(crate) from_mixnet: bool, -} - -impl ConnectionInfoDto { - pub(crate) fn maybe_new( + pub(crate) fn new_v2( received_at: OffsetDateTime, - stats_report: &VpnClientStatsReport, - received_from: IpAddr, - maybe_country: Option, + stats_report: &VpnClientStatsReportV2, + user_agent: UserAgent, from_mixnet: bool, - ) -> Option { - stats_report.basic_usage.as_ref().map(|usage_report| Self { - received_at, - received_from: received_from.to_string(), - connection_time_ms: usage_report.connection_time_ms, - two_hop: usage_report.two_hop, - country_code: maybe_country.map(|c| c.alpha2.into()), + ) -> Self { + Self { + day: received_at.date(), + stats_id: stats_report.stats_id.clone(), + os_type: stats_report.static_information.os_type.clone(), + os_version: stats_report.static_information.os_version.clone(), + os_arch: stats_report.static_information.os_arch.clone(), + app_version: stats_report.static_information.app_version.clone(), + user_agent: user_agent.to_string(), from_mixnet, - }) + } } } -// New structure. The two above will be removed when it is confirmed to work #[derive(Debug, Clone, sqlx::FromRow)] pub(crate) struct StatsReportV1Dto { pub(crate) received_at: OffsetDateTime, @@ -116,3 +103,66 @@ impl StatsReportV1Dto { report } } + +#[derive(Debug, Clone, sqlx::FromRow)] +pub(crate) struct StatsReportV2Dto { + // Report metadata + pub(crate) received_at: OffsetDateTime, + pub(crate) received_from: String, + pub(crate) from_mixnet: bool, + pub(crate) country_code: Option, + pub(crate) report_version: String, + + // Device info + pub(crate) stats_id: StatsId, + pub(crate) os_type: String, + pub(crate) os_version: Option, + pub(crate) os_arch: String, + pub(crate) app_version: String, + pub(crate) user_agent: String, + + // session info + pub(crate) start_day_utc: Date, + pub(crate) connection_time_ms: i32, + pub(crate) tunnel_type: String, + pub(crate) retry_attempt: i32, + pub(crate) session_duration_min: i32, + pub(crate) disconnection_time_ms: i32, + pub(crate) exit_id: String, + pub(crate) follow_up_id: Option, + pub(crate) error: Option, +} + +impl StatsReportV2Dto { + pub(crate) fn new( + received_at: OffsetDateTime, + stats_report: &VpnClientStatsReportV2, + user_agent: UserAgent, + from_mixnet: bool, + received_from: IpAddr, + maybe_country: Option, + ) -> Self { + Self { + received_at, + received_from: received_from.to_string(), + from_mixnet, + country_code: maybe_country.map(|c| c.alpha2.into()), + report_version: stats_report.api_version.clone(), + stats_id: stats_report.stats_id.clone(), + os_type: stats_report.static_information.os_type.clone(), + os_version: stats_report.static_information.os_version.clone(), + os_arch: stats_report.static_information.os_arch.clone(), + app_version: stats_report.static_information.app_version.clone(), + user_agent: user_agent.to_string(), + start_day_utc: stats_report.session_report.start_day_utc, + connection_time_ms: stats_report.session_report.connection_time_ms, + tunnel_type: stats_report.session_report.tunnel_type.clone(), + retry_attempt: stats_report.session_report.retry_attempt, + session_duration_min: stats_report.session_report.session_duration_min, + disconnection_time_ms: stats_report.session_report.disconnection_time_ms, + exit_id: stats_report.session_report.exit_id.clone(), + follow_up_id: stats_report.session_report.follow_up_id.clone(), + error: stats_report.session_report.error.clone(), + } + } +} diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 26df1b52e6..19db598a32 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -385,6 +385,18 @@ dependencies = [ "rayon", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-broadcast" version = "0.7.2" @@ -650,6 +662,12 @@ dependencies = [ "serde", ] +[[package]] +name = "binstring" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef" + [[package]] name = "bip32" version = "0.5.3" @@ -721,6 +739,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "blake2b_simd" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -1069,6 +1098,17 @@ dependencies = [ "error-code", ] +[[package]] +name = "coarsetime" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91849686042de1b41cd81490edc83afbcb0abe5a9b6f2c4114f23ce8cca1bcf4" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + [[package]] name = "colorchoice" version = "1.0.3" @@ -1127,6 +1167,12 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "convert_case" version = "0.4.0" @@ -1421,6 +1467,12 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "ct-codecs" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8" + [[package]] name = "ctor" version = "0.2.9" @@ -1624,6 +1676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -1869,6 +1922,16 @@ dependencies = [ "signature", ] +[[package]] +name = "ed25519-compact" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190" +dependencies = [ + "ct-codecs", + "getrandom 0.2.15", +] + [[package]] name = "ed25519-consensus" version = "2.1.0" @@ -1930,6 +1993,8 @@ dependencies = [ "ff", "generic-array", "group", + "hkdf", + "pem-rfc7468", "pkcs8", "rand_core 0.6.4", "sec1", @@ -2886,6 +2951,15 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -2895,6 +2969,30 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac-sha1-compact" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18492c9f6f9a560e0d346369b665ad2bdbc89fa9bceca75796584e79042694c3" + +[[package]] +name = "hmac-sha256" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89e8d20b3799fa526152a5301a771eaaad80857f83e01b23216ceaafb2d9280" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "hostname" version = "0.4.0" @@ -3560,6 +3658,32 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jwt-simple" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "731011e9647a71ff4f8474176ff6ce6e0d2de87a0173f15613af3a84c3e3401a" +dependencies = [ + "anyhow", + "binstring", + "blake2b_simd", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand 0.8.5", + "serde", + "serde_json", + "superboring", + "thiserror 2.0.12", + "zeroize", +] + [[package]] name = "k256" version = "0.13.4" @@ -3603,6 +3727,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libappindicator" @@ -3644,6 +3771,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libredox" version = "0.1.3" @@ -3727,11 +3860,11 @@ dependencies = [ [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -3918,12 +4051,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -3936,6 +4068,23 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -3951,6 +4100,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3958,6 +4118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -4111,6 +4272,7 @@ dependencies = [ "nym-compact-ecash", "nym-ecash-time", "nym-network-defaults", + "nym-upgrade-mode-check", "rand 0.8.5", "serde", "strum", @@ -4127,6 +4289,7 @@ dependencies = [ "base64 0.22.1", "bs58", "ed25519-dalek", + "jwt-simple", "nym-pemstore", "nym-sphinx-types", "rand 0.8.5", @@ -4302,13 +4465,13 @@ version = "0.1.0" dependencies = [ "async-trait", "celes", - "humantime 2.2.0", "humantime-serde", "nym-bin-common", "nym-crypto", "nym-exit-policy", "nym-http-api-client", "nym-noise-keys", + "nym-upgrade-mode-check", "nym-wireguard-types", "schemars", "serde", @@ -4317,6 +4480,7 @@ dependencies = [ "strum_macros", "thiserror 2.0.12", "time", + "url", "utoipa", ] @@ -4429,6 +4593,22 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "nym-upgrade-mode-check" +version = "0.1.0" +dependencies = [ + "jwt-simple", + "nym-crypto", + "nym-http-api-client", + "reqwest 0.12.15", + "serde", + "serde_json", + "thiserror 2.0.12", + "time", + "tracing", + "utoipa", +] + [[package]] name = "nym-validator-client" version = "0.1.0" @@ -4529,9 +4709,7 @@ name = "nym-wireguard-types" version = "0.1.0" dependencies = [ "base64 0.22.1", - "log", - "nym-config", - "nym-network-defaults", + "nym-crypto", "serde", "thiserror 2.0.12", "x25519-dalek", @@ -4882,12 +5060,6 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "p256" version = "0.13.2" @@ -4900,6 +5072,18 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "pairing" version = "0.23.0" @@ -5024,6 +5208,15 @@ dependencies = [ "regex", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -5262,6 +5455,17 @@ dependencies = [ "futures-io", ] +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -5742,17 +5946,8 @@ checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", + "regex-automata", + "regex-syntax", ] [[package]] @@ -5763,15 +5958,9 @@ checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] -[[package]] -name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - [[package]] name = "regex-syntax" version = "0.8.5" @@ -5944,6 +6133,27 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "rsa" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -6619,6 +6829,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.7.3" @@ -6714,6 +6930,19 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" +[[package]] +name = "superboring" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "515cce34a781d7250b8a65706e0f2a5b99236ea605cb235d4baed6685820478f" +dependencies = [ + "getrandom 0.2.15", + "hmac-sha256", + "hmac-sha512", + "rand 0.8.5", + "rsa", +] + [[package]] name = "swift-rs" version = "1.0.7" @@ -7700,14 +7929,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -8079,6 +8308,15 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasix" +version = "0.12.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d" +dependencies = [ + "wasi 0.11.0+wasi-snapshot-preview1", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index 3319363be1..63dc75e884 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -11,7 +11,7 @@ use nym_config::defaults::{NymNetworkDetails, COSMOS_DERIVATION_PATH}; use nym_types::account::{Account, AccountEntry, Balance}; use nym_validator_client::nyxd::CosmWasmClient; use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet; -use nym_validator_client::signing::AccountData; +use nym_validator_client::signing::signer::OfflineSigner; use nym_validator_client::DirectSigningHttpRpcValidatorClient; use nym_wallet_types::network::Network as WalletNetwork; use std::collections::HashMap; @@ -575,10 +575,10 @@ fn derive_address( prefix: &str, ) -> Result { // note: the ephemeral wallet will zeroize the mnemonic on drop - DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic) - .try_derive_accounts()? + DirectSecp256k1HdWallet::checked_from_mnemonic(prefix, mnemonic) + .map_err(|_| BackendError::FailedToDeriveAddress)? + .signer_addresses() .first() - .map(AccountData::address) .cloned() .ok_or(BackendError::FailedToDeriveAddress) } diff --git a/nym-wallet/src-tauri/src/operations/signatures/sign.rs b/nym-wallet/src-tauri/src/operations/signatures/sign.rs index e93edd57f4..16bb74ecef 100644 --- a/nym-wallet/src-tauri/src/operations/signatures/sign.rs +++ b/nym-wallet/src-tauri/src/operations/signatures/sign.rs @@ -24,7 +24,7 @@ pub async fn sign( ) -> Result { let guard = state.read().await; let client = guard.current_client()?; - let derived_accounts = client.nyxd.get_accounts()?; + let derived_accounts = client.nyxd.get_accounts(); let account = derived_accounts.first().ok_or_else(|| { log::error!(">>> Unable to derive account"); BackendError::SignatureError("unable to derive account".to_string()) diff --git a/scripts/network_tunnel_manager.sh b/scripts/network_tunnel_manager.sh deleted file mode 100644 index 58eff34565..0000000000 --- a/scripts/network_tunnel_manager.sh +++ /dev/null @@ -1,290 +0,0 @@ -#!/bin/bash - -network_device=$(ip route show default | awk '/default/ {print $5}') -tunnel_interface="nymtun0" -wg_tunnel_interface="nymwg" - -if ! dpkg -s iptables-persistent >/dev/null 2>&1; then - sudo apt-get update - sudo apt-get install -y iptables-persistent -else - echo "iptables-persistent is already installed." -fi - -fetch_ipv6_address() { - local interface=$1 - ipv6_global_address=$(ip -6 addr show "$interface" scope global | grep inet6 | awk '{print $2}' | head -n 1) - - if [[ -z "$ipv6_global_address" ]]; then - echo "no globally routable IPv6 address found on $interface. Please configure IPv6 or check your network settings." - exit 1 - else - echo "using IPv6 address: $ipv6_global_address" - fi -} - -fetch_and_display_ipv6() { - ipv6_address=$(ip -6 addr show "$network_device" scope global | grep inet6 | awk '{print $2}') - if [[ -z "$ipv6_address" ]]; then - echo "no global IPv6 address found on $network_device." - else - echo "IPv6 address on $network_device: $ipv6_address" - fi -} - -remove_duplicate_rules() { - local interface=$1 - local script_name=$(basename "$0") - - if [[ -z "$interface" ]]; then - echo "error: no interface specified. please enter the interface (nymwg or nymtun0):" - read -r interface - fi - - if [[ "$interface" != "nymwg" && "$interface" != "nymtun0" ]]; then - echo "error: invalid interface '$interface'. allowed values are 'nymwg' or 'nymtun0'." >&2 - exit 1 - fi - - echo "removing duplicate rules for $interface..." - - iptables-save | grep "$interface" | while read -r line; do - sudo iptables -D ${line#-A } || echo "Failed to delete rule: $line" - done - - ip6tables-save | grep "$interface" | while read -r line; do - sudo ip6tables -D ${line#-A } || echo "Failed to delete rule: $line" - done - - echo "duplicates removed for $interface." - echo "!!-important-!! you need to now reapply the iptables rules for $interface." - if [ "$interface" == "nymwg" ]; then - echo "run: ./$script_name apply_iptables_rules_wg" - else - echo "run: ./$script_name apply_iptables_rules" - fi -} - -adjust_ip_forwarding() { - ipv6_forwarding_setting="net.ipv6.conf.all.forwarding=1" - ipv4_forwarding_setting="net.ipv4.ip_forward=1" - - # remove duplicate entries for these settings from the file - sudo sed -i "/^net.ipv6.conf.all.forwarding=/d" /etc/sysctl.conf - sudo sed -i "/^net.ipv4.ip_forward=/d" /etc/sysctl.conf - - echo "$ipv6_forwarding_setting" | sudo tee -a /etc/sysctl.conf - echo "$ipv4_forwarding_setting" | sudo tee -a /etc/sysctl.conf - - sudo sysctl -p /etc/sysctl.conf - -} - -apply_iptables_rules() { - local interface=$1 - echo "applying IPtables rules for $interface..." - sleep 2 - - sudo iptables -t nat -A POSTROUTING -o "$network_device" -j MASQUERADE - sudo iptables -A FORWARD -i "$interface" -o "$network_device" -j ACCEPT - sudo iptables -A FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT - - sudo ip6tables -t nat -A POSTROUTING -o "$network_device" -j MASQUERADE - sudo ip6tables -A FORWARD -i "$interface" -o "$network_device" -j ACCEPT - sudo ip6tables -A FORWARD -i "$network_device" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT - - sudo iptables-save | sudo tee /etc/iptables/rules.v4 - sudo ip6tables-save | sudo tee /etc/iptables/rules.v6 -} - -check_tunnel_iptables() { - local interface=$1 - echo "inspecting IPtables rules for $interface..." - echo "---------------------------------------" - echo "IPv4 rules:" - iptables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw-reject-forward"' - echo "---------------------------------------" - echo "IPv6 rules:" - ip6tables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw6-reject-forward"' -} - -check_ipv6_ipv4_forwarding() { - result_ipv4=$(cat /proc/sys/net/ipv4/ip_forward) - result_ipv6=$(cat /proc/sys/net/ipv6/conf/all/forwarding) - echo "IPv4 forwarding is $([ "$result_ipv4" == "1" ] && echo "enabled" || echo "not enabled")." - echo "IPv6 forwarding is $([ "$result_ipv6" == "1" ] && echo "enabled" || echo "not enabled")." -} - -check_ip_routing() { - echo "IPv4 routing table:" - ip route - echo "---------------------------------------" - echo "IPv6 routing table:" - ip -6 route -} - -perform_pings() { - echo "performing IPv4 ping to google.com..." - ping -c 4 google.com - echo "---------------------------------------" - echo "performing IPv6 ping to google.com..." - ping6 -c 4 google.com -} - -joke_through_tunnel() { - local interface=$1 - local green="\033[0;32m" - local reset="\033[0m" - local red="\033[0;31m" - local yellow="\033[0;33m" - - sleep 1 - echo - echo -e "${yellow}checking tunnel connectivity and fetching a joke for $interface...${reset}" - echo -e "${yellow}if these test succeeds, it confirms your machine can reach the outside world via IPv4 and IPv6.${reset}" - echo -e "${yellow}however, probes and external clients may experience different connectivity to your nym-node.${reset}" - - ipv4_address=$(ip addr show "$interface" | awk '/inet / {print $2}' | cut -d'/' -f1) - ipv6_address=$(ip addr show "$interface" | awk '/inet6 / && $2 !~ /^fe80/ {print $2}' | cut -d'/' -f1) - - if [[ -z "$ipv4_address" && -z "$ipv6_address" ]]; then - echo -e "${red}no IP address found on $interface. unable to fetch a joke.${reset}" - echo -e "${red}please verify your tunnel configuration and ensure the interface is up.${reset}" - return 1 - fi - - if [[ -n "$ipv4_address" ]]; then - echo - echo -e "------------------------------------" - echo -e "detected IPv4 address: $ipv4_address" - echo -e "testing IPv4 connectivity..." - echo - - if ping -c 1 -I "$ipv4_address" google.com >/dev/null 2>&1; then - echo -e "${green}IPv4 connectivity is working. fetching a joke...${reset}" - joke=$(curl -s -H "Accept: application/json" --interface "$ipv4_address" https://icanhazdadjoke.com/ | jq -r .joke) - [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}IPv4 joke: $joke${reset}" || echo -e "failed to fetch a joke via IPv4." - else - echo -e "${red}IPv4 connectivity is not working for $interface. verify your routing and NAT settings.${reset}" - fi - else - echo -e "${red}no IPv4 address found on $interface. unable to fetch a joke via IPv4.${reset}" - fi - - if [[ -n "$ipv6_address" ]]; then - echo - echo -e "------------------------------------" - echo -e "detected IPv6 address: $ipv6_address" - echo -e "testing IPv6 connectivity..." - echo - - if ping6 -c 1 -I "$ipv6_address" google.com >/dev/null 2>&1; then - echo -e "${green}IPv6 connectivity is working. fetching a joke...${reset}" - joke=$(curl -s -H "Accept: application/json" --interface "$ipv6_address" https://icanhazdadjoke.com/ | jq -r .joke) - [[ -n "$joke" && "$joke" != "null" ]] && echo -e "${green}IPv6 joke: $joke${reset}" || echo -e "${red}failed to fetch a joke via IPv6.${reset}" - else - echo -e "${red}IPv6 connectivity is not working for $interface. verify your routing and NAT settings.${reset}" - fi - else - echo -e "${red}no IPv6 address found on $interface. unable to fetch a joke via IPv6.${reset}" - fi - - echo -e "${green}joke fetching processes completed for $interface.${reset}" - echo -e "------------------------------------" - - sleep 3 - echo - echo - echo -e "${yellow}### connectivity testing recommendations ###${reset}" - echo -e "${yellow}- use the following command to test WebSocket connectivity from an external client:${reset}" - echo -e "${yellow} wscat -c wss://:9001 ${reset}" - echo -e "${yellow}- test UDP connectivity on port 51822 (commonly used for nym wireguard) ${reset}" - echo -e "${yellow} from another machine, use tools like nc or socat to send UDP packets ${reset}" - echo -e "${yellow} echo 'test message' | nc -u 51822 ${reset}" - echo -e "${yellow}if connectivity issues persist, ensure port forwarding and firewall rules are correctly configured ${reset}" - echo -} - - -configure_dns_and_icmp_wg() { - echo "allowing icmp (ping)..." - sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT - sudo iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT - - echo "allowing dns over udp (port 53)..." - sudo iptables -A INPUT -p udp --dport 53 -j ACCEPT - - echo "allowing dns over tcp (port 53)..." - sudo iptables -A INPUT -p tcp --dport 53 -j ACCEPT - - echo "saving iptables rules..." - sudo iptables-save >/etc/iptables/rules.v4 - - echo "dns and icmp configuration completed." -} - -case "$1" in -fetch_ipv6_address_nym_tun) - fetch_ipv6_address "$tunnel_interface" - ;; -fetch_and_display_ipv6) - fetch_and_display_ipv6 - ;; -apply_iptables_rules) - apply_iptables_rules "$tunnel_interface" - ;; -apply_iptables_rules_wg) - apply_iptables_rules "$wg_tunnel_interface" - ;; -check_nymtun_iptables) - check_tunnel_iptables "$tunnel_interface" - ;; -check_nym_wg_tun) - check_tunnel_iptables "$wg_tunnel_interface" - ;; -check_ipv6_ipv4_forwarding) - check_ipv6_ipv4_forwarding - ;; -check_ip_routing) - check_ip_routing - ;; -perform_pings) - perform_pings - ;; -joke_through_the_mixnet) - joke_through_tunnel "$tunnel_interface" - ;; -joke_through_wg_tunnel) - joke_through_tunnel "$wg_tunnel_interface" - ;; -configure_dns_and_icmp_wg) - configure_dns_and_icmp_wg - ;; -adjust_ip_forwarding) - adjust_ip_forwarding - ;; -remove_duplicate_rules) - remove_duplicate_rules "$2" - ;; -*) - echo "Usage: $0 [command]" - echo "Commands:" - echo " fetch_ipv6_address_nym_tun - Fetch IPv6 for nymtun0." - echo " fetch_and_display_ipv6 - Show IPv6 on default device." - echo " apply_iptables_rules - Apply IPtables rules for nymtun0." - echo " apply_iptables_rules_wg - Apply IPtables rules for nymwg." - echo " check_nymtun_iptables - Check IPtables for nymtun0." - echo " check_nym_wg_tun - Check IPtables for nymwg." - echo " check_ipv6_ipv4_forwarding - Check IPv4 and IPv6 forwarding." - echo " check_ip_routing - Display IP routing tables." - echo " perform_pings - Test IPv4 and IPv6 connectivity." - echo " joke_through_the_mixnet - Fetch a joke via nymtun0." - echo " joke_through_wg_tunnel - Fetch a joke via nymwg." - echo " configure_dns_and_icmp_wg - Allows icmp ping tests for probes alongside configuring dns" - echo " adjust_ip_forwarding - Enable IPV6 and IPV4 forwarding" - echo " remove_duplicate_rules - Remove duplicate iptables rules. Valid interfaces: nymwg, nymtun0" - exit 1 - ;; -esac - -echo "operation $1 completed successfully." diff --git a/scripts/nym-node-setup/network-tunnel-manager.sh b/scripts/nym-node-setup/network-tunnel-manager.sh new file mode 100755 index 0000000000..ab941e89b5 --- /dev/null +++ b/scripts/nym-node-setup/network-tunnel-manager.sh @@ -0,0 +1,1352 @@ +#!/bin/bash +# nym tunnel and wireguard exit policy manager +# run this script as root + +set -euo pipefail +set +o errtrace + + +############################################################################### +# colors (no emojis) +############################################################################### +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[0;33m' +NC='\033[0m' +CYAN='\033[0;36m' +RESET='\033[0m' + +info() { + printf "%b\n" "${YELLOW}[INFO] $*${NC}" +} + +ok() { + printf "%b\n" "${GREEN}[OK] $*${NC}" +} + +error() { + printf "%b\n" "${RED}[ERROR] $*${NC}" +} + +############################################################################### +# safety: must run as root, jq +############################################################################### +if [ "$(id -u)" -ne 0 ]; then + error "This script must be run as root" + exit 1 +fi + +############################################################################### +# Logging +############################################################################### +LOG_FILE="/var/log/nym/network_tunnel_manager.log" +mkdir -p "$(dirname "$LOG_FILE")" +touch "$LOG_FILE" +chmod 640 "$LOG_FILE" + +# rotate log if >10MB +if [[ -f "$LOG_FILE" && $(stat -c%s "$LOG_FILE") -gt 10485760 ]]; then + mv "$LOG_FILE" "${LOG_FILE}.1" + touch "$LOG_FILE" + chmod 640 "$LOG_FILE" +fi + +echo "----- $(date '+%Y-%m-%d %H:%M:%S') START network-tunnel-manager -----" | tee -a "$LOG_FILE" +echo -e "${CYAN}Logs are being saved locally to:${RESET} $LOG_FILE" +echo -e "${CYAN}These logs never leave your machine.${RESET}" +echo "" | tee -a "$LOG_FILE" + +# safe logger +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" +} + +# global redirection, strip ANSI before writing to log +add_log_redirection() { + exec > >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >> "$LOG_FILE")) + exec 2> >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >> "$LOG_FILE") >&2) +} +add_log_redirection + + +trap 'log "ERROR: exit=$? line=$LINENO cmd=$(printf "%q" "$BASH_COMMAND")"' ERR + + + + +START_TIME=$(date +%s) + +############################################################################### +# basic config +############################################################################### + +NYM_CHAIN="NYM-EXIT" +POLICY_FILE="/etc/nym/exit-policy.txt" +EXIT_POLICY_LOCATION="https://nymtech.net/.wellknown/network-requester/exit-policy.txt" + +TUNNEL_INTERFACE="${TUNNEL_INTERFACE:-nymtun0}" +WG_INTERFACE="${WG_INTERFACE:-nymwg}" + +# Function to detect and validate uplink interface +detect_uplink_interface() { + local cmd="$1" + local dev + + dev="$(eval "$cmd" 2>/dev/null | awk '{print $5}' | head -n1 || true)" + + if [[ -n "$dev" && "$dev" =~ ^[a-zA-Z0-9._-]+$ ]]; then + echo "$dev" + else + echo "" + fi +} + +# uplink device detection, can be overridden +NETWORK_DEVICE="${NETWORK_DEVICE:-}" +if [[ -z "$NETWORK_DEVICE" ]]; then + NETWORK_DEVICE="$(detect_uplink_interface "ip -o route show default")" +fi +if [[ -z "$NETWORK_DEVICE" ]]; then + NETWORK_DEVICE="$(detect_uplink_interface "ip -o route show default table all")" +fi +if [[ -z "$NETWORK_DEVICE" ]]; then + error "cannot determine uplink interface. set NETWORK_DEVICE or UPLINK_DEV" + exit 1 +fi + +############################################################################### +# shared helpers +############################################################################### + +ensure_jq() { + info "checking for jq..." + + if command -v jq >/dev/null 2>&1; then + ok "jq is already installed" + else + info "jq not found, installing..." + apt-get update -y + DEBIAN_FRONTEND=noninteractive apt-get install -y jq + + if command -v jq >/dev/null 2>&1; then + ok "jq installed successfully" + else + error "failed to install jq" + exit 1 + fi + fi +} + +install_iptables_persistent() { + if ! dpkg -s iptables-persistent >/dev/null 2>&1; then + info "installing iptables-persistent" + apt-get update -y + DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent + else + ok "iptables-persistent is already installed" + fi +} + +adjust_ip_forwarding() { + info "configuring ip forwarding via /etc/sysctl.d/99-nym-forwarding.conf" + install -m 0644 /dev/null /etc/sysctl.d/99-nym-forwarding.conf + cat > /etc/sysctl.d/99-nym-forwarding.conf </dev/null || echo 0) + v6=$(cat /proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || echo 0) + + if [[ "$v4" == "1" && "$v6" == "1" ]]; then + ok "ipv4 and ipv6 forwarding enabled" + else + error "warning: ip forwarding not fully enabled (ipv4=$v4 ipv6=$v6)" + fi +} + +save_iptables_rules() { + info "saving iptables rules to /etc/iptables$" + mkdir -p /etc/iptables + iptables-save > /etc/iptables/rules.v4 + ip6tables-save > /etc/iptables/rules.v6 + ok "iptables rules saved" +} + +############################################################################### +# part 1: network tunnel manager (nymtun0 + nymwg base nat/forwarding) +############################################################################### + +fetch_ipv6_address() { + local interface=$1 + local ipv6_global_address + ipv6_global_address=$(ip -6 addr show "$interface" scope global | awk '/inet6/ {print $2}' | head -n 1) + + if [[ -z "$ipv6_global_address" ]]; then + error "no globally routable ipv6 address found on $interface. please configure ipv6 or check your network settings" + exit 1 + else + info "using ipv6 address: $ipv6_global_address" + fi +} + +fetch_and_display_ipv6() { + local ipv6_address + ipv6_address=$(ip -6 addr show "$NETWORK_DEVICE" scope global | awk '/inet6/ {print $2}') + if [[ -z "$ipv6_address" ]]; then + error "no global ipv6 address found on $NETWORK_DEVICE" + else + ok "ipv6 address on $NETWORK_DEVICE: $ipv6_address" + fi +} + +# dedupe / clean-up rules for an interface in FORWARD and NYM-EXIT +# keeps a single copy of each rule + +remove_duplicate_rules() { + local interface="$1" + + if [[ -z "$interface" ]]; then + error "Error: No interface specified. Usage: $0 remove_duplicate_rules " + exit 1 + fi + + info "detecting and removing duplicate rules for $interface in FORWARD and ${NYM_CHAIN}" + + # + # ipv4 + # + local rules_v4 + rules_v4=$(iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep -F -- "$interface" || true) + + if [[ -n "$rules_v4" ]]; then + info "processing ipv4 rules" + + local tmp4 + tmp4=$(mktemp) + printf "%s\n" "$rules_v4" | sort | uniq > "$tmp4" + + local rule count cleaned chain rest match index + while IFS= read -r rule; do + [[ -z "$rule" ]] && continue + + # FIX: protect grep from rule content becoming flags + count=$(printf "%s\n" "$rules_v4" | grep -F -- "$rule" | wc -l) + + if [[ "$count" -gt 1 ]]; then + info "removing $((count - 1)) duplicate(s) of ipv4 rule: $rule" + + for ((i=1; i/dev/null; then + iptables -t filter -D "$chain" "${RULE_ARR[@]}" && continue + fi + + match=$(iptables -S | grep -F -- "$cleaned" | head -n1 || true) + + if [[ -n "$match" ]]; then + chain=$(echo "$match" | awk '{print $2}') + index=$(iptables -L "$chain" --line-numbers | grep -F "$interface" | awk 'NR==1{print $1}') + + if [[ -n "$index" ]]; then + iptables -D "$chain" "$index" 2>/dev/null || \ + error "warning: failed deleting ipv4 duplicate via index ($chain $index)" + else + error "warning: unable to locate ipv4 duplicate index for: $rule" + fi + else + error "warning: could not reliably match ipv4 duplicate rule: $rule" + fi + done + fi + + done < "$tmp4" + + rm -f "$tmp4" + + else + ok "no ipv4 rules found for $interface to deduplicate" + fi + + + + # + # ipv6 + # + local rules_v6 + rules_v6=$(ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep -F -- "$interface" || true) + + if [[ -n "$rules_v6" ]]; then + info "processing ipv6 rules" + + local tmp6 + tmp6=$(mktemp) + printf "%s\n" "$rules_v6" | sort | uniq > "$tmp6" + + local rule count cleaned chain rule_spec match index + while IFS= read -r rule; do + [[ -z "$rule" ]] && continue + + # FIX: protect grep from interpreting rule as flags + count=$(printf "%s\n" "$rules_v6" | grep -F -- "$rule" | wc -l) + + if [[ "$count" -gt 1 ]]; then + info "removing $((count - 1)) duplicate(s) of ipv6 rule: $rule" + + for ((i=1; i/dev/null; then + ip6tables -t filter -D "$chain" "${RULE6_ARR[@]}" && continue + fi + + match=$(ip6tables -S | grep -F -- "$cleaned" | head -n1 || true) + + if [[ -n "$match" ]]; then + chain=$(echo "$match" | awk '{print $2}') + + index=$(ip6tables -L "$chain" --line-numbers | grep -F "$interface" | awk 'NR==1{print $1}') + + if [[ -n "$index" ]]; then + ip6tables -D "$chain" "$index" 2>/dev/null || \ + error "warning: failed deleting ipv6 duplicate via index ($chain $index)" + else + error "warning: unable to locate ipv6 duplicate index for: $rule" + fi + else + error "warning: could not match ipv6 duplicate rule reliably: $rule" + fi + done + fi + + done < "$tmp6" + + rm -f "$tmp6" + + else + ok "no ipv6 rules found for $interface to deduplicate" + fi + + ok "duplicate rule scan completed for $interface" +} + +apply_iptables_rules() { + local interface=$1 + info "applying iptables rules for $interface using uplink $NETWORK_DEVICE" + sleep 1 + + # ipv4 nat and forwarding + iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null || \ + iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE + + iptables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || \ + iptables -I FORWARD 1 -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT + + iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ + iptables -I FORWARD 2 -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT + + # ipv6 nat and forwarding + ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null || \ + ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE + + ip6tables -C FORWARD -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null || \ + ip6tables -I FORWARD 1 -i "$interface" -o "$NETWORK_DEVICE" -j ACCEPT + + ip6tables -C FORWARD -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \ + ip6tables -I FORWARD 2 -i "$NETWORK_DEVICE" -o "$interface" -m state --state RELATED,ESTABLISHED -j ACCEPT + + save_iptables_rules +} + +check_tunnel_iptables() { + local interface=$1 + info "inspecting iptables rules for $interface" + info "ipv4 forward chain:" + iptables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw-reject-forward"' + echo + info "ipv6 forward chain:" + ip6tables -L FORWARD -v -n | awk -v dev="$interface" '/^Chain FORWARD/ || $0 ~ dev || $0 ~ "ufw6-reject-forward"' +} + +check_ipv6_ipv4_forwarding() { + local result_ipv4 result_ipv6 + result_ipv4=$(cat /proc/sys/net/ipv4/ip_forward) + result_ipv6=$(cat /proc/sys/net/ipv6/conf/all/forwarding) + ok "ipv4 forwarding is $([ "$result_ipv4" == "1" ] && ok enabled || error not enabled)" + ok "ipv6 forwarding is $([ "$result_ipv6" == "1" ] && ok enabled || error not enabled)" +} + +check_ip_routing() { + info "ipv4 routing table:" + ip route + info "---------------------------" + info "ipv6 routing table:" + ip -6 route +} + +perform_pings() { + info "performing ipv4 ping to google.com" + ping -4 -c 4 google.com || error "ipv4 ping failed" + echo "---------------------------" + info "performing ipv6 ping to google.com" + ping6 -6 -c 4 google.com || error "ipv6 ping failed" +} + +joke_through_tunnel() { + ensure_jq + local interface=$1 + + sleep 1 + echo + info "checking tunnel connectivity and fetching a joke for $interface" + info "if this test succeeds, it confirms your machine can reach the outside world via ipv4 and ipv6" + info "probes and external clients may still see different connectivity to your nym node" + + local ipv4_address ipv6_address joke + ipv4_address=$(ip addr show "$interface" | awk '/inet / {print $2}' | cut -d'/' -f1) + ipv6_address=$(ip addr show "$interface" | awk '/inet6 / && $2 !~ /^fe80/ {print $2}' | cut -d'/' -f1) + + if [[ -z "$ipv4_address" && -z "$ipv6_address" ]]; then + error "no ip address found on $interface. unable to fetch a joke" + error "please verify your tunnel configuration and ensure the interface is up" + return 1 + fi + + if [[ -n "$ipv4_address" ]]; then + echo + echo "------------------------------------" + info "detected ipv4 address: $ipv4_address" + info "testing ipv4 connectivity" + echo + + if ping -c 1 -I "$ipv4_address" google.com >/dev/null 2>&1; then + ok "ipv4 connectivity is working. fetching a joke" + joke=$(curl -s -H "Accept: application/json" --interface "$ipv4_address" https://icanhazdadjoke.com/ | jq -r .joke) + [[ -n "$joke" && "$joke" != "null" ]] && ok "ipv4 joke: $joke" || echo "failed to fetch a joke via ipv4" + else + error "ipv4 connectivity is not working for $interface. verify your routing and nat settings" + fi + else + error "no ipv4 address found on $interface. unable to fetch a joke via ipv4" + fi + + if [[ -n "$ipv6_address" ]]; then + echo + echo "------------------------------------" + info "detected ipv6 address: $ipv6_address" + info "testing ipv6 connectivity" + echo + + if ping6 -c 1 -I "$ipv6_address" google.com >/dev/null 2>&1; then + ok "ipv6 connectivity is working. fetching a joke" + joke=$(curl -s -H "Accept: application/json" --interface "$ipv6_address" https://icanhazdadjoke.com/ | jq -r .joke) + [[ -n "$joke" && "$joke" != "null" ]] && ok "ipv6 joke: $joke" || error "failed to fetch a joke via ipv6" + else + error "ipv6 connectivity is not working for $interface. verify your routing and nat settings" + fi + else + error "no ipv6 address found on $interface. unable to fetch a joke via ipv6" + fi + + ok "joke fetching processes completed for $interface" + echo "------------------------------------" + + sleep 3 + echo + echo + info "connectivity testing recommendations" + info "- from another machine use wscat to test websocket connectivity on 9001" + info "- test udp connectivity on port 51822 (wireguard)" + info "- example: echo 'test' | nc -u 51822" +} + +configure_dns_and_icmp_wg() { + info "allowing ping (icmp) and dns on this host" + iptables -C INPUT -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null || \ + iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT + iptables -C OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null || \ + iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT + + iptables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null || \ + iptables -A INPUT -p udp --dport 53 -j ACCEPT + iptables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null || \ + iptables -A INPUT -p tcp --dport 53 -j ACCEPT + + save_iptables_rules + ok "dns and icmp configuration completed" +} + +############################################################################### +# part 2: wireguard exit policy manager +############################################################################### + +add_port_rules() { + local cmd="$1" # iptables or ip6tables + local port="$2" + local protocol="${3:-tcp}" + + if [[ "$port" == *"-"* ]]; then + local start_port end_port + start_port=$(echo "$port" | cut -d'-' -f1) + end_port=$(echo "$port" | cut -d'-' -f2) + + if ! $cmd -C "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then + $cmd -A "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT + ok "added $cmd $NYM_CHAIN $protocol port range $start_port:$end_port" + fi + else + if ! $cmd -C "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT 2>/dev/null; then + $cmd -A "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT + ok "added $cmd $NYM_CHAIN $protocol port $port" + fi + fi +} + +exit_policy_install_deps() { + install_iptables_persistent + + for item in iptables ip6tables ip grep sed awk wget curl; do + if ! command -v "$item" >/dev/null 2>&1; then + info "installing dependency: $item" + apt-get install -y "$item" + fi + done +} + +create_nym_chain() { + info "creating nym exit policy chain $NYM_CHAIN" + + if iptables -S "$NYM_CHAIN" >/dev/null 2>&1; then + iptables -F "$NYM_CHAIN" + else + iptables -N "$NYM_CHAIN" + fi + + if ip6tables -S "$NYM_CHAIN" >/dev/null 2>&1; then + ip6tables -F "$NYM_CHAIN" + else + ip6tables -N "$NYM_CHAIN" + fi + + if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + iptables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" + fi + + if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + ip6tables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" + fi +} + +setup_nat_rules() { + info "setting up nat and forwarding rules for $WG_INTERFACE via $NETWORK_DEVICE" + + if ! iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null; then + iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE + fi + if ! ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null; then + ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE + fi + + if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then + iptables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT + fi + if ! iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then + iptables -I FORWARD 2 -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT + fi + + if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then + ip6tables -I FORWARD 1 -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT + fi + if ! ip6tables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then + ip6tables -I FORWARD 2 -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT + fi +} + +configure_exit_dns_and_icmp() { + info "ensuring dns and icmp are allowed inside nym exit chain" + + # Remove any existing DNS/ICMP rules first to avoid duplicates + iptables -D "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null || true + iptables -D "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null || true + iptables -D "$NYM_CHAIN" -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null || true + iptables -D "$NYM_CHAIN" -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null || true + ip6tables -D "$NYM_CHAIN" -p udp --dport 53 -j ACCEPT 2>/dev/null || true + ip6tables -D "$NYM_CHAIN" -p tcp --dport 53 -j ACCEPT 2>/dev/null || true + ip6tables -D "$NYM_CHAIN" -p ipv6-icmp -j ACCEPT 2>/dev/null || true + + # Insert rules at the beginning in correct order: DNS first, then ICMP + iptables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT + iptables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT + iptables -I "$NYM_CHAIN" 3 -p icmp --icmp-type echo-request -j ACCEPT + iptables -I "$NYM_CHAIN" 4 -p icmp --icmp-type echo-reply -j ACCEPT + ip6tables -I "$NYM_CHAIN" 1 -p udp --dport 53 -j ACCEPT + ip6tables -I "$NYM_CHAIN" 2 -p tcp --dport 53 -j ACCEPT + ip6tables -I "$NYM_CHAIN" 3 -p ipv6-icmp -j ACCEPT +} + +apply_port_allowlist() { + echo "applying allowed port list into ${NYM_CHAIN}" + + configure_exit_dns_and_icmp + + declare -A PORT_MAPPINGS=( + ["FTP"]="20-21" + ["SSH"]="22" + ["WHOIS"]="43" + ["DNS"]="53" + ["Finger"]="79" + ["HTTP"]="80-81" + ["Kerberos"]="88" + ["POP3"]="110" + ["NTP"]="123" + ["IMAP"]="143" + ["IMAP3"]="220" + ["LDAP"]="389" + ["HTTPS"]="443" + ["SMBWindowsFileShare"]="445" + ["Kpasswd"]="464" + ["RTSP"]="554" + ["LDAPS"]="636" + ["SILC"]="706" + ["KerberosAdmin"]="749" + ["DNSOverTLS"]="853" + ["Rsync"]="873" + ["VMware"]="902-904" + ["RemoteHTTPS"]="981" + ["FTPOverTLS"]="989-990" + ["NetnewsAdmin"]="991" + ["TelnetOverTLS"]="992" + ["IMAPOverTLS"]="993" + ["POP3OverTLS"]="995" + ["OpenVPN"]="1194" + ["WireGuardPeer"]="51820-51822" + ["QTServerAdmin"]="1220" + ["PKTKRB"]="1293" + ["MSSQL"]="1433" + ["VLSILicenseManager"]="1500" + ["OracleDB"]="1521" + ["Sametime"]="1533" + ["GroupWise"]="1677" + ["PPTP"]="1723" + ["RTSPAlt"]="1755" + ["MSNP"]="1863" + ["NFS"]="2049" + ["CPanel"]="2082-2083" + ["GNUnet"]="2086-2087" + ["NBX"]="2095-2096" + ["Zephyr"]="2102-2104" + ["XboxLive"]="3074" + ["MySQL"]="3306" + ["SVN"]="3690" + ["RWHOIS"]="4321" + ["Virtuozzo"]="4643" + ["RTPVOIP"]="5000-5005" + ["MMCC"]="5050" + ["ICQ"]="5190" + ["XMPP"]="5222-5223" + ["AndroidMarket"]="5228" + ["PostgreSQL"]="5432" + ["MongoDBDefault"]="27017" + ["Electrum"]="8082" + ["SimplifyMedia"]="8087-8088" + ["Zcash"]="8232-8233" + ["Bitcoin"]="8332-8333" + ["HTTPSALT"]="8443" + ["TeamSpeak"]="8767" + ["MQTTS"]="8883" + ["HTTPProxy"]="8888" + ["TorORPort"]="9001" + ["TorDirPort"]="9030" + ["Tari"]="9053" + ["Gaming"]="9339" + ["Git"]="9418" + ["HTTPSALT2"]="9443" + ["Lightning"]="9735" + ["NDMP"]="10000" + ["OpenPGP"]="11371" + ["Monero"]="18080-18081" + ["MoneroRPC"]="18089" + ["GoogleVoice"]="19294" + ["EnsimControlPanel"]="19638" + ["DarkFiTor"]="25551" + ["Minecraft"]="25565" + ["DarkFi"]="26661" + ["Steam"]="27000-27050" + ["ElectrumSSL"]="50002" + ["MOSH"]="60000-61000" + ["Mumble"]="64738" + ["Metadata"]="51830" + ) + + local port + for service in "${!PORT_MAPPINGS[@]}"; do + port="${PORT_MAPPINGS[$service]}" + echo "adding rules for $service (ports $port)" + add_port_rules iptables "$port" "tcp" + add_port_rules ip6tables "$port" "tcp" + add_port_rules iptables "$port" "udp" + add_port_rules ip6tables "$port" "udp" + done +} + +apply_spamhaus_blocklist() { + info "applying spamhaus-like blocklist from $EXIT_POLICY_LOCATION" + + mkdir -p "$(dirname "$POLICY_FILE")" + + if ! wget -q "$EXIT_POLICY_LOCATION" -O "$POLICY_FILE" 2>/dev/null; then + error "failed to download exit policy, using minimal blocklist" + cat >"$POLICY_FILE" < "$tmpfile" + + local total_rules + total_rules=$(wc -l < "$tmpfile") + info "processing $total_rules blocklist rules" + local line ip_range + while IFS= read -r line; do + [[ -z "$line" ]] && continue + + ip_range=$(echo "$line" | sed -E 's/ExitPolicy reject ([^:]+):.*/\1/') + + if [[ -n "$ip_range" ]]; then + + # ipv4 reject + if ! iptables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then + iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT --reject-with icmp-port-unreachable \ + || error "warning: failed adding ipv4 reject for $ip_range" + fi + + # ipv6 reject + if [[ "$ip_range" == *":"* ]]; then + if ! ip6tables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then + ip6tables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT \ + || error "warning: failed adding ipv6 reject for $ip_range" + fi + fi + + fi + done < "$tmpfile" + + rm -f "$tmpfile" +} + + + +add_default_reject_rule() { + info "ensuring default reject rule at end of ${NYM_CHAIN}" + + iptables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true + iptables -D "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable 2>/dev/null || true + ip6tables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true + ip6tables -D "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable 2>/dev/null || true + + iptables -A "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable + ip6tables -A "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable +} + +clear_exit_policy_rules() { + info "clearing nym exit policy rules ..." + + iptables -F "$NYM_CHAIN" 2>/dev/null || true + ip6tables -F "$NYM_CHAIN" 2>/dev/null || true + + iptables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true + ip6tables -D FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null || true + + iptables -X "$NYM_CHAIN" 2>/dev/null || true + ip6tables -X "$NYM_CHAIN" 2>/dev/null || true +} + +show_exit_policy_status() { + info "nym exit policy status" + info "network device: $NETWORK_DEVICE" + info "wireguard interface: $WG_INTERFACE" + echo + + if ! ip link show "$WG_INTERFACE" >/dev/null 2>&1; then + error "warning: wireguard interface $WG_INTERFACE not found" + else + info "interface details:" + ip link show "$WG_INTERFACE" + echo + info "ipv4 addresses:" + ip -4 addr show dev "$WG_INTERFACE" + echo + info "ipv6 addresses:" + ip -6 addr show dev "$WG_INTERFACE" + fi + + echo + info "iptables chains for ${NYM_CHAIN}:" + iptables -L "$NYM_CHAIN" -n -v 2>/dev/null || echo "ipv4 chain not found" + echo + ip6tables -L "$NYM_CHAIN" -n -v 2>/dev/null || echo "ipv6 chain not found" + echo + info "ip forwarding:" + echo "ipv4: $(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null || echo 0)" + echo "ipv6: $(cat /proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || echo 0)" +} + +test_exit_policy_connectivity() { + info "testing connectivity through $WG_INTERFACE" + + local iface_info + iface_info=$(ip link show "$WG_INTERFACE" 2>/dev/null || true) + if [[ -z "$iface_info" ]]; then + error "interface $WG_INTERFACE not found" + return 1 + fi + + ok "interface:" + ok "$iface_info" + + local ipv4_address ipv6_address + ipv4_address=$(ip -4 addr show dev "$WG_INTERFACE" | awk '/inet / {print $2}' | cut -d'/' -f1 | head -n1) + ipv6_address=$(ip -6 addr show dev "$WG_INTERFACE" scope global | awk '/inet6/ {print $2}' | cut -d'/' -f1 | head -n1) + + ok "ipv4 address: ${ipv4_address:-none}" + ok "ipv6 address: ${ipv6_address:-none}" + + if [[ -n "$ipv4_address" ]]; then + echo -e "${NC}testing ipv4 ping to 8.8.8.8 ..." + timeout 5 ping -c 3 -I "$ipv4_address" 8.8.8.8 >/dev/null 2>&1 && \ + ok "ipv4 ping ok" || error "ipv4 ping failed" + + echo -e "${NC}testing ipv4 dns resolution ..." + timeout 5 ping -c 3 -I "$ipv4_address" google.com >/dev/null 2>&1 && \ + ok "ipv4 dns ok" || error "ipv4 dns failed" + fi + + if [[ -n "$ipv6_address" ]]; then + echo -e "${NC}testing ipv6 ping to google dns ..." + timeout 5 ping6 -c 3 -I "$ipv6_address" 2001:4860:4860::8888 >/dev/null 2>&1 && \ + ok "ipv6 ping ok" || error "ipv6 ping failed" + + echo -e "${NC}testing ipv6 dns resolution ..." + timeout 5 ping6 -c 3 -I "$ipv6_address" google.com >/dev/null 2>&1 && \ + ok "ipv6 dns ok" || error "ipv6 dns failed" + fi + + ok "connectivity tests finished" +} + + +############################################################################### +# part 3: check the firewall setup +############################################################################### + +firewall_rule_line() { + local chain=$1 + local rule_idx=$2 + # this is because thefirst rule appears on line 3 + iptables -L "$chain" -n --line-numbers | sed -n "$((rule_idx + 2))p" +} + +check_forward_chain() { + local output + output=$(iptables -L FORWARD -n --line-numbers) + + if ! echo "$output" | grep -q "^1[[:space:]]\+$NYM_CHAIN"; then + error "FORWARD rule 1 is not ${NYM_CHAIN}; re-run network-tunnel-manager.sh exit_policy_install" + return 1 + fi + + if ! echo "$output" | grep -q "ACCEPT.*state RELATED,ESTABLISHED"; then + error "FORWARD chain missing RELATED,ESTABLISHED accepts; re-run network-tunnel-manager.sh apply_iptables_rules_wg" + return 1 + fi + + ok "FORWARD chain ordering looks good" + return 0 +} + +check_nym_exit_chain() { + local errors=0 + local patterns=("udp.*dpt:53" "tcp.*dpt:53" "icmp.*type 8" "icmp.*type 0") + + for idx in "${!patterns[@]}"; do + local line + line=$(firewall_rule_line "$NYM_CHAIN" $((idx + 1))) + if [[ "$line" =~ ${patterns[$idx]} ]]; then + ok "${NYM_CHAIN} rule $((idx + 1)) ok (${patterns[$idx]})" + else + error "${NYM_CHAIN} rule $((idx + 1)) is not ${patterns[$idx]}; re-run network-tunnel-manager.sh exit_policy_install" + errors=1 + fi + done + + local last_rule + last_rule=$(iptables -L "$NYM_CHAIN" -n --line-numbers | awk 'NR>2 {line=$0} END {print line}') + if [[ -z "${last_rule:-}" ]]; then + error "${NYM_CHAIN} chain is empty; re-run network-tunnel-manager.sh exit_policy_install" + errors=1 + elif [[ "$last_rule" =~ REJECT ]] && [[ "$last_rule" =~ 0\.0\.0\.0/0 ]]; then + ok "${NYM_CHAIN} ends with the catch-all REJECT" + else + error "${NYM_CHAIN} final rule is not the catch-all REJECT (got: $last_rule)" + errors=1 + fi + + return $errors +} + +check_iptables_default_policies() { + info "checking base iptables default policies (INPUT/FORWARD)" + + local issues=0 + local input_policy forward_policy output_policy + + input_policy=$(iptables -S INPUT 2>/dev/null | awk 'NR==1 && $1=="-P" {print $3}') + forward_policy=$(iptables -S FORWARD 2>/dev/null | awk 'NR==1 && $1=="-P" {print $3}') + output_policy=$(iptables -S OUTPUT 2>/dev/null | awk 'NR==1 && $1=="-P" {print $3}') + + if [[ -z "${input_policy:-}" ]]; then + error "unable to read INPUT default policy (iptables -S INPUT failed?)" + issues=1 + elif [[ "${input_policy^^}" != "DROP" ]]; then + error "INPUT default policy is ${input_policy^^}; expected DROP so traffic is only allowed by explicit rules." + issues=1 + else + ok "INPUT default policy is DROP" + fi + + if [[ -z "${forward_policy:-}" ]]; then + error "unable to read FORWARD default policy (iptables -S FORWARD failed?)" + issues=1 + elif [[ "${forward_policy^^}" != "DROP" ]]; then + error "FORWARD default policy is ${forward_policy^^}; expected DROP to ensure traffic only flows via NYM-EXIT rules." + issues=1 + else + ok "FORWARD default policy is DROP" + fi + + if [[ -z "${output_policy:-}" ]]; then + error "unable to read OUTPUT default policy (iptables -S OUTPUT failed?)" + issues=1 + elif [[ "${output_policy^^}" != "ACCEPT" ]]; then + error "OUTPUT default policy is ${output_policy^^}; expected ACCEPT" + issues=1 + else + ok "OUTPUT default policy is ACCEPT" + fi + + return $issues +} + +check_firewall_setup() { + info "checking ipv4 firewall ordering…" + local errors=0 + + check_iptables_default_policies || errors=1 + check_forward_chain || errors=1 + check_nym_exit_chain || errors=1 + + if command -v ip6tables >/dev/null 2>&1; then + info "checking ipv6 firewall ordering…" + if ip6tables -L "$NYM_CHAIN" -n --line-numbers >/dev/null 2>&1; then + if ! ip6tables -L "$NYM_CHAIN" -n --line-numbers | sed -n '3p' | grep -q "udp.*dpt:53"; then + error "ip6tables ${NYM_CHAIN} rule 1 is not UDP 53" + errors=1 + fi + fi + fi + + if [[ $errors -ne 0 ]]; then + error "There may be some ordering issues, it is recommended to re-run network-tunnel-manager.sh exit_policy_install after configuring UFW." + return 1 + fi + + ok "It's looking good!" + return 0 +} + + +############################################################################### +# part 4: full exit policy verification tests +############################################################################### + +test_port_range_rules() { + info "testing port range rules in ${NYM_CHAIN}" + + local port_ranges=( + "20-21:tcp:ftp" + "80-81:tcp:http" + "2082-2083:tcp:cpanel" + "5222-5223:tcp:xmpp" + "27000-27050:tcp:steam-sample" + "989-990:tcp:ftp-tls" + "5000-5005:tcp:rtp-voip" + "8087-8088:tcp:simplify-media" + "8232-8233:tcp:zcash" + "8332-8333:tcp:bitcoin" + "18080-18081:tcp:monero" + ) + + local failures=0 + local start end + for entry in "${port_ranges[@]}"; do + IFS=':' read -r range proto name <<<"$entry" + start=$(echo "$range" | cut -d'-' -f1) + end=$(echo "$range" | cut -d'-' -f2) + + if iptables -t filter -C "$NYM_CHAIN" -p "$proto" --dport "$start:$end" -j ACCEPT 2>/dev/null; then + ok "rule ok: $name $proto $range" + else + error "missing rule: $name $proto $range" + ((failures++)) + fi + done + + return "$failures" +} + +test_critical_services() { + info "testing critical service rules in ${NYM_CHAIN}" + + local tcp_ports=(22 53 443 853 1194) + local udp_ports=(53 123 1194) + local failures=0 + + for port in "${tcp_ports[@]}"; do + if iptables -t filter -C "$NYM_CHAIN" -p tcp --dport "$port" -j ACCEPT 2>/dev/null; then + ok "tcp port $port allowed" + else + if iptables-save | grep -E "^-A $NYM_CHAIN.*tcp.*dpts:" | grep -q "$port"; then + ok "tcp port $port allowed by range" + else + error "tcp port $port missing" + ((failures++)) + fi + fi + done + + for port in "${udp_ports[@]}"; do + if iptables -t filter -C "$NYM_CHAIN" -p udp --dport "$port" -j ACCEPT 2>/dev/null; then + ok "udp port $port allowed" + else + if iptables-save | grep -E "^-A $NYM_CHAIN.*udp.*dpts:" | grep -q "$port"; then + ok "udp port $port allowed by range" + else + error "udp port $port missing" + ((failures++)) + fi + fi + done + + return "$failures" +} + +test_forward_chain_hook() { + info "testing forward chain hook direction for ${NYM_CHAIN}" + + local failures=0 + + if iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + ok "ipv4 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN" + else + error "ipv4 forward hook missing or wrong" + ((failures++)) + fi + + if ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j "$NYM_CHAIN" 2>/dev/null; then + ok "ipv6 forward hook ok: -i $WG_INTERFACE -o $NETWORK_DEVICE -> $NYM_CHAIN" + else + error "ipv6 forward hook missing or wrong" + ((failures++)) + fi + + return "$failures" +} + +test_default_reject_rule() { + info "testing default reject rule position in ${NYM_CHAIN}" + + local last_rule_v4 + last_rule_v4=$(iptables -S "$NYM_CHAIN" | awk '/^-A /{rule=$0} END{print rule}') + if [[ "$last_rule_v4" != "-A $NYM_CHAIN -j REJECT --reject-with icmp-port-unreachable" ]]; then + error "default reject missing or not last in ipv4 chain" + return 1 + fi + + local last_rule_v6 + last_rule_v6=$(ip6tables -S "$NYM_CHAIN" | awk '/^-A /{rule=$0} END{print rule}') + if [[ "$last_rule_v6" != "-A $NYM_CHAIN -j REJECT --reject-with icmp6-port-unreachable" ]]; then + error "default reject missing or not last in ipv6 chain" + return 1 + fi + + ok "default reject confirmed at end of ${NYM_CHAIN}" +} + +exit_policy_run_tests() { + local skip_default=0 + while [[ $# -gt 0 ]]; do + case "$1" in + --skip-default-reject) skip_default=1; shift ;; + *) error "unknown test option: $1"; return 1 ;; + esac + done + + local total=0 + local failed=0 + + test_forward_chain_hook || ((failed += 1)) + ((total += 1)) + + test_port_range_rules || ((failed += 1)) + ((total += 1)) + + test_critical_services || ((failed += 1)) + ((total += 1)) + + if [[ $skip_default -eq 0 ]]; then + test_default_reject_rule || ((failed += 1)) + ((total += 1)) + fi + + info "tests run: ${GREEN}$total${YELLOW}, test failed: ${RED}$failed${NC}" + if [[ $failed -eq 0 ]]; then + ok "all exit policy tests passed" + else + error "some exit policy tests failed" + fi + + return "$failed" +} + +############################################################################### +# part 5: high level workflows +############################################################################### + +nym_tunnel_setup() { + # this mirrors your previous chain of calls but inside one script + info "running full tunnel setup for ${TUNNEL_INTERFACE} and ${WG_INTERFACE}" + + check_tunnel_iptables "$TUNNEL_INTERFACE" + remove_duplicate_rules "$TUNNEL_INTERFACE" + remove_duplicate_rules "$WG_INTERFACE" + check_tunnel_iptables "$TUNNEL_INTERFACE" + + adjust_ip_forwarding + + apply_iptables_rules "$TUNNEL_INTERFACE" + check_tunnel_iptables "$TUNNEL_INTERFACE" + + apply_iptables_rules "$WG_INTERFACE" + + configure_dns_and_icmp_wg + adjust_ip_forwarding + check_ipv6_ipv4_forwarding + + joke_through_tunnel "$TUNNEL_INTERFACE" + joke_through_tunnel "$WG_INTERFACE" + + ok "full tunnel setup completed" +} + +exit_policy_install() { + info "installing nym wireguard exit policy for ${WG_INTERFACE} via ${NETWORK_DEVICE}" + exit_policy_install_deps + adjust_ip_forwarding + create_nym_chain + setup_nat_rules + apply_port_allowlist + apply_spamhaus_blocklist + add_default_reject_rule + save_iptables_rules + ok "nym exit policy installed" +} + +complete_networking_configuration() { + info "starting complete networking configuration: tunnels + exit policy" + + nym_tunnel_setup + exit_policy_install + check_firewall_setup || error "firewall order checks reported problems, please review output" + exit_policy_run_tests || error "exit policy tests reported problems, please review output" + + ok "complete networking configuration finished" +} + +############################################################################### +# cli +############################################################################### + +cmd="${1:-help}" +log "COMMAND: $cmd ARGS: $*" + +case "$cmd" in + nym_tunnel_setup) + nym_tunnel_setup + status=$? + ;; + exit_policy_install) + exit_policy_install + status=$? + ;; + complete_networking_configuration) + complete_networking_configuration + status=$? + ;; + + # tunnel manager cmds + fetch_ipv6_address_nym_tun) + fetch_ipv6_address "$TUNNEL_INTERFACE" + status=$? + ;; + fetch_and_display_ipv6) + fetch_and_display_ipv6 + status=$? + ;; + apply_iptables_rules) + apply_iptables_rules "$TUNNEL_INTERFACE" + status=$? + ;; + apply_iptables_rules_wg) + apply_iptables_rules "$WG_INTERFACE" + status=$? + ;; + check_nymtun_iptables) + check_tunnel_iptables "$TUNNEL_INTERFACE" + status=$? + ;; + check_nym_wg_tun) + check_tunnel_iptables "$WG_INTERFACE" + status=$? + ;; + check_ipv6_ipv4_forwarding) + check_ipv6_ipv4_forwarding + status=$? + ;; + check_ip_routing) + check_ip_routing + status=$? + ;; + perform_pings) + perform_pings + status=$? + ;; + joke_through_the_mixnet) + joke_through_tunnel "$TUNNEL_INTERFACE" + status=$? + ;; + joke_through_wg_tunnel) + joke_through_tunnel "$WG_INTERFACE" + status=$? + ;; + configure_dns_and_icmp_wg) + configure_dns_and_icmp_wg + status=$? + ;; + adjust_ip_forwarding) + adjust_ip_forwarding + status=$? + ;; + remove_duplicate_rules) + remove_duplicate_rules "${2:-}" + status=$? + ;; + + # exit policy manager cmds + exit_policy_status) + show_exit_policy_status + status=$? + ;; + check_firewall_setup) + check_firewall_setup + status=$? + ;; + exit_policy_test_connectivity) + test_exit_policy_connectivity + status=$? + ;; + exit_policy_clear) + clear_exit_policy_rules + status=$? + ;; + exit_policy_tests) + shift + exit_policy_run_tests "$@" + status=$? + ;; + + help|--help|-h) + cat < [args] + +high level workflows: + complete_networking_configuration Install tunnel interfaces, setup networking, iptables, wg exit policy & tests + nym_tunnel_setup Install tunnel interfaces & setup networking + exit_policy_install Install and configure wireguard exit policy +tunnel and nat helpers: + adjust_ip_forwarding Enable ipv4/ipv6 forwarding via sysctl.d + apply_iptables_rules Apply nat/forward rules for ${TUNNEL_INTERFACE} + apply_iptables_rules_wg Apply nat/forward rules for ${WG_INTERFACE} + check_ip_routing Show ipv4 and ipv6 routes + check_ipv6_ipv4_forwarding Show ipv4/ipv6 forwarding flags + check_nym_wg_tun Inspect forward chain for ${WG_INTERFACE} + check_nymtun_iptables Inspect forward chain for ${TUNNEL_INTERFACE} + configure_dns_and_icmp_wg Allow ping and dns ports on this host + fetch_and_display_ipv6 Show ipv6 on uplink ${NETWORK_DEVICE} + fetch_ipv6_address_nym_tun Show global ipv6 address on ${TUNNEL_INTERFACE} + joke_through_the_mixnet Test via ${TUNNEL_INTERFACE} with joke + joke_through_wg_tunnel Test via ${WG_INTERFACE} with joke + perform_pings Test ipv4 and ipv6 pings + remove_duplicate_rules Deduplicate FORWARD and ${NYM_CHAIN} rules for (required). + +exit policy manager: + check_firewall_setup Run ordering sanity check (dns/icmp + FORWARD jump) + exit_policy_clear Remove ${NYM_CHAIN} chains and hooks + exit_policy_install Install exit policy (iptables rules and blocklist) + exit_policy_status Show status of exit policy and forwarding + exit_policy_test_connectivity Test connectivity via ${WG_INTERFACE} + exit_policy_tests [--skip-default-reject] + Run verification tests on exit policy (options: --skip-default-reject). + +environment overrides: + NETWORK_DEVICE Auto-detected uplink (e.g., eth0). Set manually if detection fails. + TUNNEL_INTERFACE Default: nymtun0. Requires root privileges (sudo) to manage. + WG_INTERFACE Default: nymwg - Must match your WireGuard interface name. + +EOF + status=0 + ;; + + *) + error "unknown command: $cmd" + info "run with 'help' for usage" + exit 1 + ;; +esac + +if [[ "$cmd" != help && "$cmd" != "--help" && "$cmd" != "-h" && ${status:-1} -eq 0 ]]; then + echo "" + echo "Logs saved locally at: $LOG_FILE" + ok "operation ${cmd} completed" +fi +END_TIME=$(date +%s) +ELAPSED=$((END_TIME - START_TIME)) +echo "----- $(date '+%Y-%m-%d %H:%M:%S') END operation ${cmd} (status $status, duration ${ELAPSED}s) -----" >> "$LOG_FILE" +exit $status diff --git a/scripts/nym-node-setup/nym-node-cli.py b/scripts/nym-node-setup/nym-node-cli.py index ebae150523..9d99fc4ad0 100755 --- a/scripts/nym-node-setup/nym-node-cli.py +++ b/scripts/nym-node-setup/nym-node-cli.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -__version__ = "1.1.0" +__version__ = "1.2.0" __default_branch__ = "develop" import os @@ -22,17 +22,25 @@ class NodeSetupCLI: def __init__(self, args): self.branch = args.dev self.welcome_message = self.print_welcome_message() - self.mode = self.prompt_mode() + self.mode = self._get_or_prompt_mode(args) self.prereqs_install_sh = self.fetch_script("nym-node-prereqs-install.sh") - self.env_vars_install_sh = self.fetch_script("setup-env-vars.sh") self.node_install_sh = self.fetch_script("nym-node-install.sh") self.service_config_sh = self.fetch_script("setup-systemd-service-file.sh") self.start_node_systemd_service_sh = self.fetch_script("start-node-systemd-service.sh") - self.landing_page_html = self._check_gwx_mode() and self.fetch_script("landing-page.html") - self.nginx_proxy_wss_sh = self._check_gwx_mode() and self.fetch_script("nginx_proxy_wss_sh") - self.tunnel_manager_sh = self._check_gwx_mode() and self.fetch_script("network_tunnel_manager.sh") - self.wg_ip_tables_manager_sh = self._check_gwx_mode() and self.fetch_script("wireguard-exit-policy-manager.sh") - self.wg_ip_tables_test_sh = self._check_gwx_mode() and self.fetch_script("exit-policy-tests.sh") + self.is_gwx = self.mode == "exit-gateway" + if self.is_gwx: + self.landing_page_html = self.fetch_script("landing-page.html") + self.nginx_proxy_wss_sh = self.fetch_script("nginx_proxy_wss_sh") + self.tunnel_manager_sh = self.fetch_script("network_tunnel_manager.sh") + self.quic_bridge_deployment_sh = self.fetch_script("quic_bridge_deployment.sh") + else: + self.landing_page_html = None + self.nginx_proxy_wss_sh = None + self.tunnel_manager_sh = None + self.wg_ip_tables_manager_sh = None + self.wg_ip_tables_test_sh = None + self.quic_bridge_deployment_sh = None + def print_welcome_message(self): """Welcome user, warns for needed pre-reqs and asks for confimation""" @@ -45,7 +53,7 @@ class NodeSetupCLI: self.print_character("=", 41) msg = \ "Before you begin, make sure that:\n"\ - "1. You run this setup on Debian based Linux (ie Ubuntu)\n"\ + "1. You run this setup on Debian based Linux (ie Ubuntu 22.04 LTS)\n"\ "2. You run this installation program from a root shell\n"\ "3. You meet minimal requirements: https://nym.com/docs/operators/nodes\n"\ "4. You accept Operators Terms & Conditions: https://nym.com/operators-validators-terms\n"\ @@ -59,43 +67,103 @@ class NodeSetupCLI: else: print("Without confirming the points above, we cannot continue.") exit(1) + + def ensure_env_values(self, args): + """Collect env vars from args or prompt interactively, then save to env.sh.""" + env_file = Path("env.sh") + fields = [ + ("hostname", "HOSTNAME", "Enter hostname (if you don't use a DNS, press enter): "), + ("location", "LOCATION", "Enter node location (country code or name): "), + ("email", "EMAIL", "Enter your email: "), + ("moniker", "MONIKER", "Enter node public moniker (visible in explorer & NymVPN app): "), + ("description", "DESCRIPTION", "Enter short node public description: "), + ] - def prompt_mode(self): - """Ask user to insert node functionality and save it in python and bash envs""" + existing = self._read_env_file(env_file) + updated = {} + + for arg_name, key, prompt in fields: + cli_val = getattr(args, arg_name, None) + value = cli_val.strip() if cli_val else existing.get(key) or input(prompt).strip() + updated[key] = value + os.environ[key] = value + + # autodetect PUBLIC_IP if not already set + if not os.environ.get("PUBLIC_IP"): + try: + ip = subprocess.run(["curl", "-fsS4", "https://ifconfig.me"], + capture_output=True, text=True, timeout=5) + if ip.returncode == 0 and ip.stdout.strip(): + updated["PUBLIC_IP"] = ip.stdout.strip() + os.environ["PUBLIC_IP"] = ip.stdout.strip() + except subprocess.TimeoutExpired: + print("[WARN] Timeout expired while trying to fetch public IP with curl.") + except FileNotFoundError: + print("[WARN] 'curl' command not found. Please install curl or set PUBLIC_IP manually.") + except subprocess.CalledProcessError as e: + print(f"[WARN] Error while running curl to fetch public IP: {e}") + + # write all collected variables to env.sh in one go + self._upsert_env_vars(updated, env_file) + + print(f"[OK] Updated env.sh with {len(updated)} entries.") + + + + + def _upsert_env_vars(self, updates: dict, env_file: Path = Path("env.sh")): + existing = self._read_env_file(env_file) + existing.update(updates) + with env_file.open("w") as f: + for k, v in existing.items(): + f.write(f'export {k}="{v}"\n') + os.environ.update(updates) + + def _read_env_file(self, env_file: Path) -> dict: + env = {} + if env_file.exists(): + for line in env_file.read_text().splitlines(): + if line.startswith("export ") and "=" in line: + k, v = line.replace("export ", "", 1).split("=", 1) + env[k.strip()] = v.strip().strip('"') + return env + + def _get_or_prompt_mode(self, args): + """Resolve MODE from --mode, env.sh, os.environ, or prompt; persist to env.sh.""" + + env_file = Path("env.sh") + + # CLI arg + mode = getattr(args, "mode", None) + if mode: + mode = mode.strip().lower() + self._upsert_env_vars({"MODE": mode}) + print(f"Mode set to '{mode}' from CLI argument.") + return mode + + # env.sh (replaces manual read) + existing = self._read_env_file(env_file) + mode = existing.get("MODE") + if mode: + os.environ["MODE"] = mode + return mode + + # process env + if os.environ.get("MODE"): + return os.environ["MODE"] + + # prompt mode = input( - "\nEnter the mode you want to run nym-node in: " - "\n1. mixnode " - "\n2. entry-gateway " - "\n3. exit-gateway (works as entry-gateway as well) " - "\nPress 1, 2 or 3 and enter:\n" - ).strip() - - if mode in ("1", "mixnode"): - mode = "mixnode" - elif mode in ("2", "entry-gateway"): - mode = "entry-gateway" - elif mode in ("3", "exit-gateway"): - mode = "exit-gateway" - else: - print("Only numbers 1, 2 or 3 are accepted.") + "\nEnter node mode (mixnode / entry-gateway / exit-gateway): " + ).strip().lower() + if mode not in ("mixnode", "entry-gateway", "exit-gateway"): + print("Invalid mode. Must be one of: mixnode, entry-gateway, exit-gateway.") raise SystemExit(1) - # save mode for this Python instance - self.mode = mode - os.environ["MODE"] = mode - - # persist to env.sh so other scripts can source it - env_file = Path("env.sh") - with env_file.open("a") as f: - f.write(f'export MODE="{mode}"\n') - - # source env.sh so future bash subprocesses see it immediately - subprocess.run("source ./env.sh", shell=True, executable="/bin/bash") - + self._upsert_env_vars({"MODE": mode}) print(f"Mode set to '{mode}' — stored in env.sh and sourced for immediate use.") return mode - def fetch_script(self, script_name): """Fetches needed scripts according to a defined mode""" # print header only the first time @@ -119,16 +187,15 @@ class NodeSetupCLI: github_raw_nymtech_nym_scripts_url = f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/{self.branch}/scripts/" scripts_urls = { "nym-node-prereqs-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-prereqs-install.sh", - "setup-env-vars.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-env-vars.sh", "nym-node-install.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/nym-node-install.sh", "setup-systemd-service-file.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-systemd-service-file.sh", "start-node-systemd-service.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/start-node-systemd-service.sh", "nginx_proxy_wss_sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/setup-nginx-proxy-wss.sh", "landing-page.html": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/landing-page.html", - "network_tunnel_manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/network_tunnel_manager.sh", - "wireguard-exit-policy-manager.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh", - "exit-policy-tests.sh": f"https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh", + "network_tunnel_manager.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/network-tunnel-manager.sh", + "quic_bridge_deployment.sh": f"{github_raw_nymtech_nym_scripts_url}nym-node-setup/quic_bridge_deployment.sh" } + return scripts_urls[script_init_name] def run_script( @@ -200,62 +267,61 @@ class NodeSetupCLI: def _check_gwx_mode(self): """Helper: Several fns run only for GWx - this fn checks this condition""" - if self.mode == "exit-gateway": - return True - else: - return False + return self.mode == "exit-gateway" - def check_wg_enabled(self): - """Checks if Wireguard is enabled and if not, prompts user if they want to enable it, stores it to env.sh""" + def check_wg_enabled(self, args=None): + """Determine if WireGuard is enabled; precedence: CLI > env > env.sh > prompt. Persist normalized value.""" + env_file = os.path.join(os.getcwd(), "env.sh") - env_file = os.path.abspath(os.path.join(os.getcwd(), "env.sh")) + def norm(v): + return "true" if str(v).strip().lower() == "true" else "false" - def norm(v): # -> "true" or "false" - return "true" if str(v).strip().lower() in ("1", "true", "yes", "y") else "false" + val = None - # precedence: process env → env.sh → prompt - val = os.environ.get("WIREGUARD") + # CLI argument + if args and getattr(args, "wireguard", None) is not None: + val = norm(getattr(args, "wireguard")) + print(f"[INFO] WireGuard mode provided via CLI: {val}") - if val is None and os.path.isfile(env_file): - try: - with open(env_file, "r", encoding="utf-8") as f: - m = re.search(r'^\s*export\s+WIREGUARD\s*=\s*"?([^"\n]+)"?', f.read(), re.M) - if m: - val = m.group(1) - except Exception: - pass + # Environment variable + val = val or os.environ.get("WIREGUARD") + # env.sh file + if val is None: + envs = self._read_env_file(Path(env_file)) + val = envs.get("WIREGUARD") + + # Prompt if val is None: ans = input( "\nWireGuard is not configured.\n" "Nodes routing WireGuard can be listed as both entry and exit in the app.\n" - "Enable WireGuard support? (y/n): " + "Enable WireGuard support? (Y/n): " ).strip().lower() - val = "true" if ans in ("y", "yes") else "false" + val = "true" if ans in ("", "y", "yes") else "false" val = norm(val) os.environ["WIREGUARD"] = val - # persist to env.sh (replace or append) + # Persist to env.sh try: text = "" if os.path.isfile(env_file): - with open(env_file, "r", encoding="utf-8") as f: + with open(env_file, encoding="utf-8") as f: text = f.read() if re.search(r'^\s*export\s+WIREGUARD\s*=.*$', text, re.M): text = re.sub(r'^\s*export\s+WIREGUARD\s*=.*$', f'export WIREGUARD="{val}"', text, flags=re.M) else: - if text and not text.endswith("\n"): - text += "\n" - text += f'export WIREGUARD="{val}"\n' + text = (text.rstrip("\n") + "\n" if text else "") + f'export WIREGUARD="{val}"\n' with open(env_file, "w", encoding="utf-8") as f: f.write(text) print(f'WIREGUARD={val} saved to {env_file}') - except Exception as e: + except OSError as e: print(f"Warning: could not write {env_file}: {e}") - return (val == "true") + return val == "true" + def run_bash_command(self, command, args=None, *, env=None, cwd=None, check=True): """ @@ -279,25 +345,13 @@ class NodeSetupCLI: """A standalone fn to pass full cmd list needed for correct setup and test network tunneling, using an external script""" print( "\n* * * Setting up network configuration for mixnet IP router and Wireguard tunneling * * *" - "\nMore info: https://nym.com/docs/operators/nodes/nym-node/configuration#1-download-network_tunnel_managersh-make-executable-and-run" + "\nMore info: https://nym.com/docs/operators/nodes/nym-node/configuration#routing-configuration" "\nThis may take a while; follow the steps below and don't kill the process..." ) # each entry is the exact argv to pass to the script steps = [ - ["check_nymtun_iptables"], - ["remove_duplicate_rules", "nymtun0"], - ["remove_duplicate_rules", "nymwg"], - ["check_nymtun_iptables"], - ["adjust_ip_forwarding"], - ["apply_iptables_rules"], - ["check_nymtun_iptables"], - ["apply_iptables_rules_wg"], - ["configure_dns_and_icmp_wg"], - ["adjust_ip_forwarding"], - ["check_ipv6_ipv4_forwarding"], - ["joke_through_the_mixnet"], - ["joke_through_wg_tunnel"], + ["complete_networking_configuration"] ] for argv in steps: @@ -316,10 +370,17 @@ class NodeSetupCLI: "Setting up Wireguard IP tables to match Nym exit policy for mixnet, stored at: https://nymtech.net/.wellknown/network-requester/exit-policy.txt" "\nThis may take a while, follow the steps below and don't kill the process..." ) - self.run_script(self.wg_ip_tables_manager_sh, args=["install"]) - self.run_script(self.wg_ip_tables_manager_sh, args=["status"]) - self.run_script(self.wg_ip_tables_test_sh) + self.run_script(self.tunnel_manager_sh, args=["exit_policy_install"]) + def quic_bridge_deploy(self): + """Setup QUIC bridge and configuration using external script""" + print("\n* * * Installing and configuring QUIC bridges * * *") + answer = input("\nDo you want to install, setup and run QUIC bridge? (Y/n) ").strip().lower() + + if answer in ("", "y", "yes"): + self.run_script(self.quic_bridge_deployment_sh, args=["full_bridge_setup"]) + else: + print("Skipping QUIC bridge setup.") def run_nym_node_as_service(self): """Starts /etc/systemd/system/nym-node.service based on prompt using external script""" @@ -347,8 +408,8 @@ class NodeSetupCLI: if is_active: while True: - ans = input(f"{service} is already running. Restart it now? (y/n):\n").strip().lower() - if ans == "y": + ans = input(f"{service} is already running. Restart it now? (Y/n):\n").strip().lower() + if ans in ("", "Y", "y"): self.run_script(self.start_node_systemd_service_sh, args=["restart-poll"], env=run_env) return elif ans == "n": @@ -358,8 +419,8 @@ class NodeSetupCLI: print("Invalid input. Please press 'y' or 'n' and press enter.") else: while True: - ans = input(f"{service} is not running. Start it now? (y/n):\n").strip().lower() - if ans == "y": + ans = input(f"{service} is not running. Start it now? (Y/n):\n").strip().lower() + if ans in ("", "Y", "y"): self.run_script(self.start_node_systemd_service_sh, args=["start-poll"], env=run_env) return elif ans == "n": @@ -510,8 +571,12 @@ class NodeSetupCLI: def run_node_installation(self,args): """Main function called by argparser command install running full node install flow""" + self.ensure_env_values(args) + # Pass uplink override to all helper scripts if provided + if getattr(args, "uplink_dev", None): + os.environ["UPLINK_DEV"] = args.uplink_dev + os.environ["NETWORK_DEVICE"] = args.uplink_dev self.run_script(self.prereqs_install_sh) - self.run_script(self.env_vars_install_sh) self.run_script(self.node_install_sh) self.run_script(self.service_config_sh) self._check_gwx_mode() and self.run_script(self.nginx_proxy_wss_sh) @@ -521,7 +586,7 @@ class NodeSetupCLI: self.run_tunnel_manager_setup() if self.check_wg_enabled(): self.setup_test_wg_ip_tables() - self.setup_test_wg_ip_tables() + self.quic_bridge_deploy() @@ -537,7 +602,7 @@ class ArgParser: version=f"nym-node-cli {__version__}" ) parent.add_argument("-d", "--dev", metavar="BRANCH", - help="Define github branch", + help="Define github branch (default: develop)", type=str, default=argparse.SUPPRESS) parent.add_argument("-v", "--verbose", action="store_true", @@ -553,11 +618,38 @@ class ArgParser: subparsers = parser.add_subparsers(dest="command", help="subcommands") subparsers.required = True - p_install = subparsers.add_parser( + install_parser = subparsers.add_parser( "install", parents=[parent], help="Starts nym-node installation setup CLI", aliases=["i", "I"], add_help=True ) + install_parser.add_argument( + "--mode", + choices=["mixnode", "entry-gateway", "exit-gateway"], + help="Node mode: 'mixnode', 'entry-gateway', or 'exit-gateway'", + ) + install_parser.add_argument( + "--wireguard-enabled", + choices=["true", "false"], + help="WireGuard functionality switch: true / false" + ) + install_parser.add_argument("--hostname", help="Node domain / hostname") + install_parser.add_argument("--location", help="Node location (country code or name)") + install_parser.add_argument("--email", help="Contact email for the node operator") + install_parser.add_argument("--moniker", help="Public moniker displayed in explorer & NymVPN app") + install_parser.add_argument("--description", help="Short public description of the node") + install_parser.add_argument("--public-ip", help="External IPv4 address (autodetected if omitted)") + install_parser.add_argument("--nym-node-binary", help="URL for nym-node binary (autodetected if omitted)") + install_parser.add_argument("--uplink-dev", help="Override uplink interface used for NAT/FORWARD (e.g., 'eth0'; autodetected if omitted)") + + # generic fallback + install_parser.add_argument( + "--env", + action="append", + metavar="KEY=VALUE", + help="(Optional) Extra ENV VARS, e.g. --env CUSTOM_KEY=value", + ) + args = parser.parse_args() diff --git a/scripts/nym-node-setup/nym-node-install.sh b/scripts/nym-node-setup/nym-node-install.sh index 3ea25687f5..af36c92017 100644 --- a/scripts/nym-node-setup/nym-node-install.sh +++ b/scripts/nym-node-setup/nym-node-install.sh @@ -34,8 +34,9 @@ check_existing_config() { if [[ "${resp}" =~ ^([Rr][Ee][Ss][Ee][Tt])$ ]]; then echo - read -r -p "We are going to remove the existing node with configuration $NODE_CONFIG_DIR and replace it with a fresh one, do you want to back up the old one first? (y/n) " backup_ans - if [[ "${backup_ans}" =~ ^[Yy]$ ]]; then + echo "We are going to remove the existing node with configuration $NODE_CONFIG_DIR and replace it with a fresh one." + read -r -p "back up the old one first? (Y/n) " backup_ans + if [[ -z "${backup_ans}" || "${backup_ans}" =~ ^[Yy]$ ]]; then ts="$(date +%Y%m%d-%H%M%S)" backup_dir="$HOME/.nym/backup/$(basename "$NODE_CONFIG_DIR")-$ts" echo "Backing up to: $backup_dir" @@ -181,24 +182,27 @@ fi NYM_NODE="$HOME/nym-binaries/nym-node" -# if binary already exists, ask to overwrite; if yes, remove first +# if binary already exists, ask to overwrite; if yes, remove first; if no, skip download if [[ -e "${NYM_NODE}" ]]; then echo echo -e "\n* * * A nym-node binary already exists at: ${NYM_NODE}" - read -r -p "Overwrite with the latest release? (y/n): " ow_ans - if [[ "${ow_ans}" =~ ^[Yy]$ ]]; then - echo "Removing existing binary to avoid 'text file busy'..." - rm -f "${NYM_NODE}" - else - echo "Keeping existing binary." - fi -fi + read -r -p "Overwrite with the latest release? (Y/n): " ow_ans -download_nym_node "$LATEST_TAG_URL" "$NYM_NODE" + if [[ -z "${ow_ans}" || "${ow_ans}" =~ ^[Yy]$ ]]; then + echo "Removing existing binary..." + rm -f "${NYM_NODE}" + download_nym_node "$LATEST_TAG_URL" "$NYM_NODE" + else + echo "Keeping existing binary. Skipping download." + fi + +else + # binary does not exist → must download + download_nym_node "$LATEST_TAG_URL" "$NYM_NODE" +fi echo -e "\n * * * Making binary executable * * *" chmod +x "${NYM_NODE}" - echo "---------------------------------------------------" echo "Nym node binary downloaded:" "${NYM_NODE}" --version || true @@ -225,17 +229,18 @@ WIREGUARD="${WIREGUARD:-}" if [[ ( "$MODE" == "entry-gateway" || "$MODE" == "exit-gateway" ) && ( -n "${ASK_WG:-}" || -z "$WIREGUARD" ) ]]; then echo echo "Gateways can also route WireGuard in NymVPN." - echo "Enabling it means your node may be listed as both entry and exit in the app." - # show current default in the prompt if present def_hint="" [[ -n "${WIREGUARD}" ]] && def_hint=" [current: ${WIREGUARD}]" - read -r -p "Enable WireGuard support? (y/n)${def_hint}: " answer || true - case "${answer:-}" in - [Yy]* ) WIREGUARD="true" ;; - [Nn]* ) WIREGUARD="false" ;; - * ) : ;; # keep existing value if user just pressed enter - esac + + read -r -p "Enable WireGuard support? (Y/n)${def_hint}: " answer || true + + if [[ -z "${answer}" || "${answer}" =~ ^[Yy]$ ]]; then + WIREGUARD="true" + elif [[ "${answer}" =~ ^[Nn]$ ]]; then + WIREGUARD="false" + fi fi + # final default only if still empty WIREGUARD="${WIREGUARD:-false}" diff --git a/scripts/nym-node-setup/nym-node-prereqs-install.sh b/scripts/nym-node-setup/nym-node-prereqs-install.sh index 63f3264ff0..e462ecb5c9 100644 --- a/scripts/nym-node-setup/nym-node-prereqs-install.sh +++ b/scripts/nym-node-setup/nym-node-prereqs-install.sh @@ -1,6 +1,11 @@ #!/bin/bash -# update, upgrade & install dependencies +if [[ "$(id -u)" -ne 0 ]]; then + echo "This script must be run as root." + exit 1 +fi + +# update, upgrade and install dependencies echo -e "\n* * * Installing needed prerequisities * * *" apt update -y && apt --fix-broken install @@ -8,11 +13,9 @@ apt upgrade apt install apt ca-certificates jq curl wget ufw jq tmux pkg-config build-essential libssl-dev git ntp ntpdate neovim tree tmux tig nginx -y apt install ufw --fix-missing - - # enable & setup firewall echo -e "\n* * * Setting up firewall using ufw * * * " -echo "Please enable the firewall in the next prompt for node proper routing!" +echo "Please enable the firewall in the next prompt for node proper routing." echo ufw enable ufw allow 22/tcp # SSH - you're in control of these ports @@ -24,6 +27,6 @@ ufw allow 8080/tcp # Nym specific - nym-node-api ufw allow 9000/tcp # Nym Specific - clients port ufw allow 9001/tcp # Nym specific - wss port ufw allow 51822/udp # WireGuard -ufw allow 'Nginx Full' && \ +ufw allow in on nymwg to any port 51830 proto tcp # bandwidth queries/topup - inside the tunnel ufw reload && \ ufw status diff --git a/scripts/nym-node-setup/quic_bridge_deployment.sh b/scripts/nym-node-setup/quic_bridge_deployment.sh index f7dc60e0f9..a1fe29ff3a 100644 --- a/scripts/nym-node-setup/quic_bridge_deployment.sh +++ b/scripts/nym-node-setup/quic_bridge_deployment.sh @@ -7,6 +7,7 @@ # RUN AS ROOT set -euo pipefail +set +o errtrace # Colors RED="\033[0;31m" @@ -17,25 +18,37 @@ BOLD="\033[1m" RESET="\033[0m" # Logging -LOG_FILE="/var/log/nym-bridge-helper.log" +LOG_FILE="/var/log/nym/quic_bridge_deployment.log" mkdir -p "$(dirname "$LOG_FILE")" + +# rotate log if >10MB BEFORE writing START header +if [[ -f "$LOG_FILE" && $(stat -c%s "$LOG_FILE") -gt 10485760 ]]; then + mv "$LOG_FILE" "${LOG_FILE}.1" +fi + touch "$LOG_FILE" chmod 640 "$LOG_FILE" + +echo "----- $(date '+%Y-%m-%d %H:%M:%S') START quic-bridge-manager -----" | tee -a "$LOG_FILE" echo -e "${CYAN}Logs are being saved locally to:${RESET} $LOG_FILE" echo -e "${CYAN}These logs never leave your machine.${RESET}" echo "" | tee -a "$LOG_FILE" -# safe logger +# safe logger function log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" } -# simple redirection that keeps function scope intact +# global redirection, strip ANSI before writing to log add_log_redirection() { - exec > >(tee -a "$LOG_FILE") 2>&1 + exec > >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >> "$LOG_FILE")) + exec 2> >(tee >(sed -u 's/\x1b\[[0-9;]*m//g' >> "$LOG_FILE") >&2) } add_log_redirection +START_TIME=$(date +%s) + + # Constants / Paths REQUIRED_CMDS=(ip jq curl openssl dpkg) BRIDGE_BIN="/usr/local/bin/nym-bridge" @@ -47,7 +60,17 @@ NYM_ETC_BRIDGES="$NYM_ETC_DIR/bridges.toml" NYM_ETC_CLIENT_PARAMS_DEFAULT="$NYM_ETC_DIR/client_bridge_params.json" SERVICE_FILE="/etc/systemd/system/nym-bridge.service" -NET_DEV="$(ip route show default 2>/dev/null | awk '/default/ {print $5}' || true)" +NET_DEV="${UPLINK_DEV:-}" +if [[ -z "$NET_DEV" ]]; then + NET_DEV="$(ip -o route show default 2>/dev/null | awk '{print $5}' | head -n1)" + [[ -z "$NET_DEV" ]] && NET_DEV="$(ip -o route show default table all 2>/dev/null | awk '{print $5}' | head -n1)" +fi +if [[ -z "$NET_DEV" ]]; then + echo -e "${RED}Cannot determine uplink interface. Set UPLINK_DEV.${RESET}" | tee -a "$LOG_FILE" + exit 1 +fi +echo "Using uplink device: $NET_DEV" + WG_IFACE="nymwg" # Root check @@ -57,13 +80,29 @@ if [[ "$(id -u)" -ne 0 ]]; then fi # UI helpers -hr() { echo -e "${YELLOW}----------------------------------------${RESET}"; } +hr() { echo -e "${YELLOW}----------------------------------------${RESET}" ; } title() { echo -e "\n${YELLOW}==========================================${RESET}\n${YELLOW} $1${RESET}\n${YELLOW}==========================================${RESET}\n"; } ok() { echo -e "${GREEN}$1${RESET}"; } warn() { echo -e "${YELLOW}$1${RESET}"; } err() { echo -e "${RED}$1${RESET}"; } info() { echo -e "${CYAN}$1${RESET}"; } -press_enter() { read -r -p "$1"; } +press_enter() { + echo -n "$1" > /dev/tty + read -r _ < /dev/tty +} + + +# Disable pauses and interactive prompts for noninteractive mode +if [[ "${NONINTERACTIVE:-0}" == "1" ]]; then + press_enter() { :; } + echo_prompt() { :; } +else + press_enter() { + echo -n "$1" > /dev/tty + read -r _ < /dev/tty + } + echo_prompt() { echo -n "$1"; } +fi # Helper: detect dpkg dependency failure for libc6>=2.34 deb_depends_libc_too_old() { @@ -176,13 +215,31 @@ verify_bridge_prerequisites() { } adjust_ip_forwarding() { - title "Adjusting IP Forwarding" - sed -i '/^net\.ipv4\.ip_forward=/d' /etc/sysctl.conf - sed -i '/^net\.ipv6\.conf\.all\.forwarding=/d' /etc/sysctl.conf - echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf - echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf - sysctl -p /etc/sysctl.conf - ok "IPv4/IPv6 forwarding enabled." + title "Checking IP forwarding" + local v4 v6 + v4="$(cat /proc/sys/net/ipv4/ip_forward 2>/dev/null || echo 0)" + v6="$(cat /proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || echo 0)" + + if [[ "$v4" == "1" ]]; then + ok "IPv4 forwarding is enabled." + else + warn "IPv4 forwarding is not enabled." + fi + + if [[ "$v6" == "1" ]]; then + ok "IPv6 forwarding is enabled." + else + warn "IPv6 forwarding is not enabled." + fi + + if [[ "$v4" != "1" || "$v6" != "1" ]]; then + echo + echo "To enable forwarding and routing consistently, run the network tunnel manager script as root." + echo "For example:" + echo " ./network-tunnel-manager.sh complete_networking_configuration" + echo "or:" + echo " ./network-tunnel-manager.sh adjust_ip_forwarding" + fi } # Install nym-bridge @@ -377,6 +434,11 @@ User=root ExecStart=$BRIDGE_BIN --config $NYM_ETC_BRIDGES Restart=on-failure RestartSec=5 +LimitNOFILE=65535 +ProtectSystem=full +ProtectHome=yes +PrivateTmp=yes + [Install] WantedBy=multi-user.target @@ -390,21 +452,40 @@ EOF # IPTABLES & helpers apply_bridge_iptables_rules() { - title "Applying iptables rules" - iptables -I INPUT -i "$WG_IFACE" -j ACCEPT || true - ip6tables -I INPUT -i "$WG_IFACE" -j ACCEPT || true - iptables -t nat -A POSTROUTING -o "$NET_DEV" -j MASQUERADE || true - ip6tables -t nat -A POSTROUTING -o "$NET_DEV" -j MASQUERADE || true - iptables-save > /etc/iptables/rules.v4 - ip6tables-save > /etc/iptables/rules.v6 - ok "iptables rules applied." + title "Checking iptables rules for bridge routing" + + echo "Inspecting current iptables state for interface ${WG_IFACE} and uplink ${NET_DEV}." + echo + + echo "IPv4 FORWARD:" + iptables -L FORWARD -n -v 2>/dev/null | sed -n '1,20p' || echo "iptables not available." + echo + echo "IPv4 NAT POSTROUTING:" + iptables -t nat -L POSTROUTING -n -v 2>/dev/null | sed -n '1,20p' || true + echo + echo "IPv6 FORWARD:" + ip6tables -L FORWARD -n -v 2>/dev/null | sed -n '1,20p' || true + echo + echo "IPv6 NAT POSTROUTING:" + ip6tables -t nat -L POSTROUTING -n -v 2>/dev/null | sed -n '1,20p' || true + + echo + echo "This script no longer changes iptables. To configure routing and NAT for nym, use the network tunnel manager script." + echo "For example (run as root):" + echo " ./network-tunnel-manager.sh complete_networking_configuration" } configure_dns_and_icmp() { - title "Allow ICMP and DNS" - iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT || true - ip6tables -A INPUT -p ipv6-icmp -j ACCEPT || true - ok "ICMP and DNS rules applied." + title "Checking ICMP and DNS firewall rules" + + echo "IPv4 INPUT rules related to ICMP and DNS:" + iptables -L INPUT -n -v 2>/dev/null | grep -E 'icmp|dpt:53' || echo "no matching IPv4 rules shown." + echo + echo "IPv6 INPUT rules related to ICMP and DNS:" + ip6tables -L INPUT -n -v 2>/dev/null | grep -E 'icmp|dpt:53' || echo "no matching IPv6 rules shown." + + echo + echo "If ping or DNS are blocked for bridge traffic, adjust your firewall using the network tunnel manager script or your chosen firewall tool." } # Full interactive setup @@ -429,6 +510,8 @@ full_bridge_setup() { echo "" echo "Step 2/6: Installing bridge binary..." install_bridge_binary + echo "[Bridge Install] $(date '+%F %T') $( $BRIDGE_BIN --version 2>/dev/null || echo 'nym-bridge (unknown)')" \ + >> /var/log/nym/nym-bridge-version.log press_enter "Press Enter to continue..." echo "" @@ -447,7 +530,7 @@ full_bridge_setup() { press_enter "Press Enter to continue..." echo "" - echo "Step 6/6: Configuring network rules (optional but recommended)..." + echo "Step 6/6: Checking network rules and forwarding status..." adjust_ip_forwarding apply_bridge_iptables_rules configure_dns_and_icmp @@ -468,10 +551,6 @@ full_bridge_setup() { echo -e "${YELLOW}------------------------------------------${RESET}" echo -e "All done! You can safely close this session." echo -e "${YELLOW}------------------------------------------${RESET}" - echo "" - echo "Logs saved locally at: $LOG_FILE" - echo "Operation 'full_bridge_setup' completed." - echo "" hr echo -e "${CYAN}Next steps and verification:${RESET}" @@ -509,22 +588,26 @@ full_bridge_setup() { graceful_exit() { local exit_code=$? - echo "" - echo -e "${YELLOW}------------------------------------------${RESET}" + END_TIME=$(date +%s) + ELAPSED=$((END_TIME - START_TIME)) + + # Only print success message when there were NO errors if [[ $exit_code -eq 0 ]]; then - echo -e "${GREEN}Setup completed successfully. Exiting cleanly.${RESET}" - else - echo -e "${RED}Script exited with errors (code: $exit_code).${RESET}" - echo "Check the log at: $LOG_FILE" + echo "Operation '${COMMAND}' completed." fi - echo -e "${YELLOW}------------------------------------------${RESET}" - echo "" - exec >&- 2>&- + + # END footer always logged + echo "----- $(date '+%Y-%m-%d %H:%M:%S') END operation ${COMMAND} (status $exit_code, duration ${ELAPSED}s) -----" >> "$LOG_FILE" + exit $exit_code } -trap graceful_exit EXIT # Command menu +COMMAND="${1:-help}" +trap 'log "ERROR: exit=$? line=$LINENO cmd=$(printf "%q" "$BASH_COMMAND")"' ERR + +trap graceful_exit EXIT + case "${1:-}" in full_bridge_setup) full_bridge_setup ;; install_bridge_binary) install_bridge_binary ;; @@ -550,5 +633,3 @@ case "${1:-}" in ;; esac -echo "Operation '${1:-help}' completed." - diff --git a/scripts/nym-node-setup/setup-env-vars.sh b/scripts/nym-node-setup/setup-env-vars.sh index 3eafd93ba5..e119a61fd6 100644 --- a/scripts/nym-node-setup/setup-env-vars.sh +++ b/scripts/nym-node-setup/setup-env-vars.sh @@ -39,16 +39,6 @@ while true; do esac done -# try to get the latest binary URL (non-fatal if missing) -LATEST_BINARY=$( - curl -fsSL https://github.com/nymtech/nym/releases/latest \ - | grep -Eo 'href="/nymtech/nym/releases/download/[^"]+/nym-node"' \ - | head -n1 \ - | cut -d'"' -f2 -) -if [[ -z "${LATEST_BINARY:-}" ]]; then - echo "WARNING: Could not determine latest nym-node binary URL right now. The installer will resolve it later." -fi PUBLIC_IP=$(curl -fsS -4 https://ifconfig.me || true) PUBLIC_IP=${PUBLIC_IP:-""} diff --git a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh index eae4d7165e..cf1378f5b6 100644 --- a/scripts/nym-node-setup/setup-nginx-proxy-wss.sh +++ b/scripts/nym-node-setup/setup-nginx-proxy-wss.sh @@ -1,315 +1,136 @@ #!/usr/bin/env bash set -euo pipefail -# load env (prefer absolute ENV_FILE injected by Python CLI; fallback to ./env.sh) +if [[ "$(id -u)" -ne 0 ]]; then + echo "This script must be run as root." + exit 1 +fi + +# load env if [[ -n "${ENV_FILE:-}" && -f "${ENV_FILE}" ]]; then set -a; . "${ENV_FILE}"; set +a elif [[ -f "./env.sh" ]]; then set -a; . ./env.sh; set +a fi -: "${HOSTNAME:?HOSTNAME not set in env.sh}" -: "${EMAIL:?EMAIL not set in env.sh}" +: "${HOSTNAME:?HOSTNAME not set}" +: "${EMAIL:?EMAIL not set}" -export SYSTEMD_PAGER="" -export SYSTEMD_COLORS="0" -DEBIAN_FRONTEND=noninteractive +export DEBIAN_FRONTEND=noninteractive -# sanity check -if [[ "${HOSTNAME}" == "localhost" || "${HOSTNAME}" == "127.0.0.1" || "${HOSTNAME}" == "ubuntu" ]]; then - echo "ERROR: HOSTNAME cannot be 'localhost'. Use a public FQDN." >&2 - exit 1 -fi - -echo -e "\n* * * Starting nginx configuration for landing page, reverse proxy and WSS * * *" - -# set paths & ports vars WEBROOT="/var/www/${HOSTNAME}" -LE_ACME_DIR="/var/www/letsencrypt" SITES_AVAIL="/etc/nginx/sites-available" SITES_EN="/etc/nginx/sites-enabled" -BASE_HTTP="${SITES_AVAIL}/${HOSTNAME}" # :80 vhost -BASE_HTTPS="${SITES_AVAIL}/${HOSTNAME}-ssl" # :443 vhost (we’ll write it ourselves) -WSS_AVAIL="${SITES_AVAIL}/wss-config-nym" -BACKUP_DIR="/etc/nginx/sites-backups" -NYM_PORT_HTTP="${NYM_PORT_HTTP:-8080}" -NYM_PORT_WSS="${NYM_PORT_WSS:-9000}" -WSS_LISTEN_PORT="${WSS_LISTEN_PORT:-9001}" +HTTP_CONF="${SITES_AVAIL}/${HOSTNAME}" +WSS_CONF="${SITES_AVAIL}/wss-config-nym" -mkdir -p "${WEBROOT}" "${LE_ACME_DIR}" "${BACKUP_DIR}" "${SITES_AVAIL}" "${SITES_EN}" +echo +echo "* * * Starting nginx configuration for landing page, reverse proxy and WSS * * *" -# helpers -neat_backup() { - local file="$1"; [[ -f "$file" ]] || return 0 - local sha_now; sha_now="$(sha256sum "$file" | awk '{print $1}')" || return 0 - local tag; tag="$(basename "$file")" - local latest="${BACKUP_DIR}/${tag}.latest" - if [[ -f "$latest" ]]; then - local sha_prev; sha_prev="$(awk '{print $1}' "$latest")" - [[ "$sha_now" == "$sha_prev" ]] && return 0 - fi - cp -a "$file" "${BACKUP_DIR}/${tag}.bak.$(date +%s)" - echo "$sha_now ${tag}" > "$latest" - ls -1t "${BACKUP_DIR}/${tag}.bak."* 2>/dev/null | tail -n +6 | xargs -r rm -f -} +############################################################################### +# step 1: ensure landing page exists (local fetch -> github -> template) +############################################################################### -ensure_enabled() { - local src="$1"; local name; name="$(basename "$src")" - ln -sf "$src" "${SITES_EN}/${name}" -} +mkdir -p "${WEBROOT}" -cert_ok() { - [[ -s "/etc/letsencrypt/live/${HOSTNAME}/fullchain.pem" && -s "/etc/letsencrypt/live/${HOSTNAME}/privkey.pem" ]] -} +SCRIPT_DIR="$(dirname "${ENV_FILE:-./env.sh}")" +LOCAL_FETCHED_PAGE="${SCRIPT_DIR}/landing-page.html" -fetch_landing_html() { - local url="https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/nym-node-setup/landing-page.html" - mkdir -p "${WEBROOT}" - - if command -v curl >/dev/null 2>&1; then - curl -fsSL "$url" -o "${WEBROOT}/index.html" || true - else - wget -qO "${WEBROOT}/index.html" "$url" || true - fi - - if [[ ! -s "${WEBROOT}/index.html" ]]; then - cat > "${WEBROOT}/index.html" <<'HTML' +if [[ -s "${LOCAL_FETCHED_PAGE}" ]]; then + cp "${LOCAL_FETCHED_PAGE}" "${WEBROOT}/index.html" +elif curl -fsSL \ + https://raw.githubusercontent.com/nymtech/nym/develop/scripts/nym-node-setup/landing-page.html \ + -o "${WEBROOT}/index.html"; then + : +else + cat > "${WEBROOT}/index.html" < - - - - - Nym Exit Gateway - - - -

Nym Exit Gateway

-

This is a Nym Exit Gateway. The operator of this router has no access to any of the data routing through that due to encryption design.

+ +nym node + +

nym exit gateway

+

this is a nym exit gateway.

+

Operator contact: ${EMAIL}

-HTML - fi -} +EOF +fi -inject_email() { - local file="${WEBROOT}/index.html" - [[ -n "${EMAIL:-}" && -s "$file" ]] || return 0 - - # Escape characters that would break sed replacement - local esc_email - esc_email="$(printf '%s' "$EMAIL" | sed -e 's/[&/\]/\\&/g')" - - # try to update existing meta (case-insensitive on the name attr) - if grep -qiE ']+name=["'"'"']contact:email["'"'"']' "$file"; then - sed -i -E \ - "s|(]+name=[\"']contact:email[\"'][^>]*content=\")[^\"]*(\"[^>]*>)|\1${esc_email}\2|I" \ - "$file" || true - return 0 - fi - - # insert before if present (case-insensitive) - if grep -qi '' "$file"; then - awk -v email="$EMAIL" ' - BEGIN{IGNORECASE=1} - /<\/head>/ && !done { - print " " - done=1 - } - { print } - ' "$file" > "${file}.tmp" && mv "${file}.tmp" "$file" - return 0 - fi - - # fallback: append at end - printf '\n\n' "$EMAIL" >> "$file" || true -} - -fetch_logo() { - local logo_url="https://raw.githubusercontent.com/nymtech/websites/refs/heads/main/www/nym.com/public/images/Nym_meta_Image.png?token=GHSAT0AAAAAACEERII7URYRTFACZ4F2OWZ42GMCPBQ" - mkdir -p "${WEBROOT}/images" - if [[ ! -s "${WEBROOT}/images/nym_logo.png" ]]; then - if command -v curl >/dev/null 2>&1; then - curl -fsSL "$logo_url" -o "${WEBROOT}/images/nym_logo.png" || true - else - wget -qO "${WEBROOT}/images/nym_logo.png" "$logo_url" || true - fi - fi -} - -reload_nginx() { nginx -t && systemctl reload nginx; } - -# landing page (idempotent) -fetch_landing_html -inject_email -fetch_logo echo "Landing page at ${WEBROOT}/index.html" -# disable default and stale SSL configs +############################################################################### +# step 2: remove default site and old configs, restart nginx +############################################################################### + +echo "Cleaning existing nginx configuration" + +# remove default nginx site [[ -L "${SITES_EN}/default" ]] && unlink "${SITES_EN}/default" || true -for f in "${SITES_EN}"/*; do - [[ -L "$f" ]] || continue - if grep -q "/etc/letsencrypt/live/localhost" "$f"; then - echo "Disabling vhost referencing localhost cert: $f"; unlink "$f" - fi -done -for f in "${SITES_EN}"/*; do - [[ -L "$f" ]] || continue - if grep -qE 'listen\s+.*443' "$f"; then - cert=$(awk '/ssl_certificate[ \t]+/ {print $2}' "$f" | tr -d ';' | head -n1) - key=$(awk '/ssl_certificate_key[ \t]+/ {print $2}' "$f" | tr -d ';' | head -n1) - if [[ -n "${cert:-}" && ! -s "$cert" ]] || [[ -n "${key:-}" && ! -s "$key" ]]; then - echo "Disabling SSL vhost with missing cert/key: $f"; unlink "$f" - fi - fi -done -# HTTP :80 vhost (ACME-safe, proxy to :8080) -neat_backup "${BASE_HTTP}" -cat > "${BASE_HTTP}" < 8080) +############################################################################### + +cat > "${HTTP_CONF}" </dev/null; then - echo "WARNING: Can't reach Let's Encrypt directory. We'll still keep HTTP up." >&2 -fi -THIS_IP="$(curl -fsS -4 https://ifconfig.me || true)" -DNS_IP="$(getent ahostsv4 "${HOSTNAME}" 2>/dev/null | awk '{print $1; exit}')" -echo "Public IPv4: ${THIS_IP:-unknown} DNS A(${HOSTNAME}): ${DNS_IP:-unresolved}" -if [[ -n "${THIS_IP:-}" && -n "${DNS_IP:-}" && "${THIS_IP}" != "${DNS_IP}" ]]; then - echo "WARNING: DNS for ${HOSTNAME} does not match this server's public IPv4." -fi -timedatectl show -p NTPSynchronized --value 2>/dev/null | grep -qi yes || timedatectl set-ntp true || true +ln -sf "${HTTP_CONF}" "${SITES_EN}/${HOSTNAME}" -# install certbot if missing -if ! command -v certbot >/dev/null 2>&1; then - if command -v snap >/dev/null 2>&1; then - snap install core || true; snap refresh core || true - snap install --classic certbot; ln -sf /snap/bin/certbot /usr/bin/certbot - else - apt-get update -y >/dev/null 2>&1 || true - apt-get install -y certbot >/dev/null 2>&1 || true - fi -fi +nginx -t +systemctl daemon-reload +systemctl restart nginx -# issue/renew via WEBROOT (no nginx auto-edit), non-fatal if it fails -STAGING_FLAG=""; [[ "${CERTBOT_STAGING:-0}" == "1" ]] && STAGING_FLAG="--staging" && echo "Using Let's Encrypt STAGING." -if ! cert_ok; then - certbot certonly --non-interactive --agree-tos -m "${EMAIL}" -d "${HOSTNAME}" \ - --webroot -w "${LE_ACME_DIR}" ${STAGING_FLAG} || true -fi +############################################################################### +# step 4: install certbot and obtain certificate (letsencrypt) +############################################################################### -# create own 443 vhost (only if certs exist) -if cert_ok; then - neat_backup "${BASE_HTTPS}" - cat > "${BASE_HTTPS}" </dev/null 2>&1 || true +apt-get install -y certbot python3-certbot-nginx >/dev/null 2>&1 || true + +echo "Requesting Let's Encrypt certificate for ${HOSTNAME}" + +certbot --nginx --non-interactive --agree-tos --redirect --reuse-key \ + -m "${EMAIL}" -d "${HOSTNAME}" || true + +############################################################################### +# step 5: create WSS 9001 config using certbot-generated certs +############################################################################### + +if [[ -s "/etc/letsencrypt/live/${HOSTNAME}/fullchain.pem" ]]; then + echo "Certificate detected, creating WSS config" + + cat > "${WSS_CONF}" <HTTPS (keeps ACME path in HTTP too via separate small server) - neat_backup "${BASE_HTTP}" - cat > "${BASE_HTTP}" < "${WSS_AVAIL}" </dev/null; then - echo -e "${GREEN}✓ Rule exists: $chain $protocol port range $start_port:$end_port${NC}" - return 0 - else - echo -e "${RED}✗ Rule missing: $chain $protocol port range $start_port:$end_port${NC}" - - echo -e "${YELLOW}Dumping all rules in $chain:${NC}" - iptables -L "$chain" -n | grep "$protocol" - - return 1 - fi -} - -# Test port range rules -test_port_range_rules() { - echo -e "${YELLOW}Testing Port Range Rules...${NC}" - - # Select the essential port ranges for testing - local port_ranges=( - "20-21:tcp:FTP" - "80-81:tcp:HTTP" - "2082-2083:tcp:CPanel" - "5222-5223:tcp:XMPP" - "27000-27050:tcp:Steam (sampling)" - "989-990:tcp:FTP over TLS" - "5000-5005:tcp:RTP/VoIP" - "8087-8088:tcp:Simplify Media" - "8232-8233:tcp:Zcash" - "8332-8333:tcp:Bitcoin" - "18080-18081:tcp:Monero" - ) - - local total_failures=0 - - for range in "${port_ranges[@]}"; do - IFS=':' read -r port_range protocol service <<< "$range" - - # Extract start and end ports - local start_port=$(echo "$port_range" | cut -d'-' -f1) - local end_port=$(echo "$port_range" | cut -d'-' -f2) - - echo -e "${YELLOW}Testing $service $protocol port range $port_range${NC}" - - if iptables -t filter -C "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: $NYM_CHAIN $protocol port range $start_port:$end_port${NC}" - else - echo -e "${RED}✗ Rule missing: $NYM_CHAIN $protocol port range $start_port:$end_port${NC}" - ((total_failures++)) - - echo -e "${YELLOW}Existing rules for protocol $protocol:${NC}" - iptables -L "$NYM_CHAIN" -n | grep "$protocol" - fi - done - - if [ $total_failures -eq 0 ]; then - return 0 - else - return 1 - fi -} - -test_critical_services() { - echo -e "${YELLOW}Testing Critical Service Rules...${NC}" - - local tcp_services=( - 22 # SSH - 53 # DNS - 443 # HTTPS - 853 # DNS over TLS - 1194 # OpenVPN - ) - - local udp_services=( - 53 # DNS - 123 # NTP - 1194 # OpenVPN - ) - - local failures=0 - - # Test TCP services - for port in "${tcp_services[@]}"; do - local rule_found=false - - # First check for exact match - if iptables -t filter -C "$NYM_CHAIN" -p tcp --dport "$port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT tcp port $port${NC}" - rule_found=true - else - # If not found as exact port, search for it in port ranges - # This checks if the port is covered by any range rule - if iptables-save | grep -E "^-A $NYM_CHAIN.*tcp.*dpts:" | grep -qP "dpts:(\d+:)?$port(:|\d+)" || \ - iptables-save | grep -E "^-A $NYM_CHAIN.*tcp.*dpts:" | grep -qP "dpts:$port:"; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT tcp port $port (covered by a range rule)${NC}" - rule_found=true - else - echo -e "${RED}✗ Rule missing: NYM-EXIT tcp port $port${NC}" - ((failures++)) - fi - fi - done - - # Test UDP services - similar approach - for port in "${udp_services[@]}"; do - local rule_found=false - - if iptables -t filter -C "$NYM_CHAIN" -p udp --dport "$port" -j ACCEPT 2>/dev/null; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT udp port $port${NC}" - rule_found=true - else - # If not found as exact port, search for it in port ranges - if iptables-save | grep -E "^-A $NYM_CHAIN.*udp.*dpts:" | grep -qP "dpts:(\d+:)?$port(:|\d+)" || \ - iptables-save | grep -E "^-A $NYM_CHAIN.*udp.*dpts:" | grep -qP "dpts:$port:"; then - echo -e "${GREEN}✓ Rule exists: NYM-EXIT udp port $port (covered by a range rule)${NC}" - rule_found=true - else - echo -e "${RED}✗ Rule missing: NYM-EXIT udp port $port${NC}" - ((failures++)) - fi - fi - done - - echo -e "${YELLOW}Relevant existing rules for HTTP (port 80):${NC}" - iptables-save | grep -E "$NYM_CHAIN.*tcp" | grep -E "(dpt|dpts):.*80" - - return $failures -} - -# Verify default reject rule exists -test_default_reject_rule() { - echo -e "${YELLOW}This test takes some time, do not quit the process${NC}" - echo - echo -e "${YELLOW}Testing Default Reject Rule...${NC}" - - # Try different patterns to detect the reject rule - if iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all.*anywhere.*anywhere.*reject-with"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all -- .*everywhere.*everywhere"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -L "$NYM_CHAIN" | grep -q "REJECT.*all.*0.0.0.0/0.*0.0.0.0/0"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -n -L "$NYM_CHAIN" | grep -qE "REJECT.*all.*0.0.0.0/0.*0.0.0.0/0"; then - echo -e "${GREEN}✓ Default REJECT rule exists${NC}" - return 0 - elif iptables -L "$NYM_CHAIN" | tail -1 | grep -q "REJECT"; then - echo -e "${GREEN}✓ Default REJECT rule exists at the end of chain${NC}" - return 0 - else - echo -e "${RED}✗ Default REJECT rule missing${NC}" - # Display the last 3 rules in the chain for debugging - echo -e "${YELLOW}Last 3 rules in the chain:${NC}" - iptables -L "$NYM_CHAIN" | tail -3 - return 1 - fi -} - -run_all_tests() { - local total_failures=0 - local total_tests=0 - local skip_default_reject=false - - # Parse arguments - while [[ $# -gt 0 ]]; do - case "$1" in - --skip-default-reject) - skip_default_reject=true - shift - ;; - *) - echo -e "${RED}Unknown argument: $1${NC}" - exit 1 - ;; - esac - done - - local test_functions=( - "test_port_range_rules" - "test_critical_services" - ) - - if [ "$skip_default_reject" = false ]; then - test_functions+=("test_default_reject_rule") - fi - - echo -e "${YELLOW}Running Nym Exit Policy Verification Tests...${NC}" - - for test_func in "${test_functions[@]}"; do - ((total_tests++)) - $test_func - if [ $? -ne 0 ]; then - ((total_failures++)) - echo -e "${RED}Test $test_func FAILED${NC}" - else - echo -e "${GREEN}Test $test_func PASSED${NC}" - fi - done - - echo -e "\n${YELLOW}Test Summary:${NC}" - echo -e "Total Tests: $total_tests" - echo -e "Failures: $total_failures" - - if [ $total_failures -eq 0 ]; then - echo -e "${GREEN}All Tests Passed Successfully!${NC}" - exit 0 - else - echo -e "${RED}Some Tests Failed. Please review the iptables configuration.${NC}" - exit 1 - fi -} - -if [[ $EUID -ne 0 ]]; then - echo -e "${RED}This script must be run as root${NC}" - exit 1 -fi - -# Run the tests -run_all_tests "$@" diff --git a/scripts/wireguard-exit-policy/validate-exit-blocking-test.sh b/scripts/wireguard-exit-policy/validate-exit-blocking-test.sh deleted file mode 100644 index e93d28a24f..0000000000 --- a/scripts/wireguard-exit-policy/validate-exit-blocking-test.sh +++ /dev/null @@ -1,40 +0,0 @@ -#!/bin/bash - -validate_exit_policy() { - echo "=== Nym Exit Policy Blocking Validation ===" - - # Check iptables rules - echo "Checking iptables NYM-EXIT chain:" - sudo iptables -L NYM-EXIT -v -n - - # Test IP ranges and individual IPs - test_ips=( - "5.188.10.0/24" # Blocked network range - "31.132.36.50" # Specific blocked IP - "37.9.42.100" # Another blocked IP - ) - - for target in "${test_ips[@]}"; do - echo -e "\n\e[33mTesting blocking for $target\e[0m" - - # Multiple connection test methods - methods=( - "ping -c 4 -W 2" - "curl -m 5 http://$target" - "nc -z -w 5 $target 80" - "telnet $target 80" - ) - - for method in "${methods[@]}"; do - echo -n "Testing with $method: " - if sudo timeout 5 $method >/dev/null 2>&1; then - echo -e "\e[31mFAILED: Connection succeeded (Blocking ineffective)\e[0m" - else - echo -e "\e[32mBLOCKED\e[0m" - fi - done - done -} - -# Run the test -validate_exit_policy \ No newline at end of file diff --git a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh b/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh deleted file mode 100644 index bce3d09711..0000000000 --- a/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh +++ /dev/null @@ -1,679 +0,0 @@ -#!/bin/bash -# -# Nym Wireguard Exit Policy Manager -# Version: 1.0.0 -# -# This script manages iptables rules for Nym Wireguard exit policies -# Features: -# - Implements the Nym exit policy from official documentation -# - Makes rules persistent across reboots -# - Provides commands to inspect and manage rules -# - Groups rules logically for easier management -# - Integrates with existing Nym node configuration -# -# Usage: ./nym-exit-policy.sh [command] - -set -e - -NETWORK_DEVICE=$(ip route show default | awk '/default/ {print $5}') -WG_INTERFACE="nymwg" -NYM_CHAIN="NYM-EXIT" -POLICY_FILE="/etc/nym/exit-policy.txt" -EXIT_POLICY_LOCATION="https://nymtech.net/.wellknown/network-requester/exit-policy.txt" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' - -add_port_rules() { - local chain="$1" - local port="$2" - local protocol="${3:-tcp}" - - # Check if the port contains a range - if [[ "$port" == *"-"* ]]; then - # Port range handling - add as a single rule with a range - local start_port=$(echo "$port" | cut -d'-' -f1) - local end_port=$(echo "$port" | cut -d'-' -f2) - - if ! $chain -C "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT 2>/dev/null; then - $chain -A "$NYM_CHAIN" -p "$protocol" --dport "$start_port:$end_port" -j ACCEPT - echo -e " ${GREEN}Added: $NYM_CHAIN $protocol port range $start_port:$end_port${NC}" - fi - else - # Single port handling - if ! $chain -C "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT 2>/dev/null; then - $chain -A "$NYM_CHAIN" -p "$protocol" --dport "$port" -j ACCEPT - echo -e " ${GREEN}Added: $NYM_CHAIN $protocol port $port${NC}" - fi - fi -} - -install_dependencies() { - if ! dpkg -s iptables-persistent >/dev/null 2>&1; then - echo -e "${YELLOW}Installing iptables-persistent...${NC}" - apt-get update - DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent - echo -e "${GREEN}iptables-persistent installed.${NC}" - else - echo -e "${GREEN}iptables-persistent is already installed.${NC}" - fi - - # Check for other required dependencies - for cmd in iptables ip6tables ip grep sed awk wget curl; do - if ! command -v "$cmd" &>/dev/null; then - echo -e "${YELLOW}Installing $cmd...${NC}" - apt-get install -y "$cmd" - fi - done -} - -configure_ip_forwarding() { - echo -e "${YELLOW}Configuring IP forwarding...${NC}" - - # Remove any existing forwarding settings to avoid duplicates - sed -i "/^net.ipv6.conf.all.forwarding=/d" /etc/sysctl.conf - sed -i "/^net.ipv4.ip_forward=/d" /etc/sysctl.conf - - # Add forwarding settings - echo "net.ipv6.conf.all.forwarding=1" | tee -a /etc/sysctl.conf - echo "net.ipv4.ip_forward=1" | tee -a /etc/sysctl.conf - - # Apply changes - sysctl -p /etc/sysctl.conf - - # Verify settings - ipv4_forwarding=$(cat /proc/sys/net/ipv4/ip_forward) - ipv6_forwarding=$(cat /proc/sys/net/ipv6/conf/all/forwarding) - - if [ "$ipv4_forwarding" == "1" ] && [ "$ipv6_forwarding" == "1" ]; then - echo -e "${GREEN}IP forwarding configured successfully.${NC}" - else - echo -e "${RED}Failed to configure IP forwarding.${NC}" - exit 1 - fi -} - -create_nym_chain() { - echo -e "${YELLOW}Creating Nym exit policy chain...${NC}" - - # Check if the chain already exists - if iptables -L "$NYM_CHAIN" &>/dev/null; then - echo -e "${YELLOW}Chain $NYM_CHAIN already exists. Flushing it...${NC}" - iptables -F "$NYM_CHAIN" - else - echo -e "${YELLOW}Creating chain $NYM_CHAIN...${NC}" - iptables -N "$NYM_CHAIN" - fi - - # Do the same for IPv6 - if ip6tables -L "$NYM_CHAIN" &>/dev/null; then - echo -e "${YELLOW}Chain $NYM_CHAIN already exists in ip6tables. Flushing it...${NC}" - ip6tables -F "$NYM_CHAIN" - else - echo -e "${YELLOW}Creating chain $NYM_CHAIN in ip6tables...${NC}" - ip6tables -N "$NYM_CHAIN" - fi - - # Link it to the FORWARD chain if not already linked - if ! iptables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${YELLOW}Linking $NYM_CHAIN to FORWARD chain...${NC}" - iptables -A FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" - fi - - # Link IPv6 chain - if ! ip6tables -C FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null; then - echo -e "${YELLOW}Linking $NYM_CHAIN to IPv6 FORWARD chain...${NC}" - ip6tables -A FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" - fi -} - -setup_nat_rules() { - echo -e "${YELLOW}Setting up NAT rules...${NC}" - - # Check if NAT rule for IPv4 exists - if ! iptables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null; then - iptables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE - echo -e "${GREEN}Added IPv4 NAT rule.${NC}" - else - echo -e "${GREEN}IPv4 NAT rule already exists.${NC}" - fi - - # Check if NAT rule for IPv6 exists - if ! ip6tables -t nat -C POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE 2>/dev/null; then - ip6tables -t nat -A POSTROUTING -o "$NETWORK_DEVICE" -j MASQUERADE - echo -e "${GREEN}Added IPv6 NAT rule.${NC}" - else - echo -e "${GREEN}IPv6 NAT rule already exists.${NC}" - fi - - # Setup forwarding rules for Wireguard interface - if ! iptables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then - iptables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT - echo -e "${GREEN}Added IPv4 forwarding rule (WG → Internet).${NC}" - fi - - if ! iptables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then - iptables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT - echo -e "${GREEN}Added IPv4 forwarding rule (Internet → WG for established connections).${NC}" - fi - - # IPv6 forwarding rules - if ! ip6tables -C FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT 2>/dev/null; then - ip6tables -A FORWARD -i "$WG_INTERFACE" -o "$NETWORK_DEVICE" -j ACCEPT - echo -e "${GREEN}Added IPv6 forwarding rule (WG → Internet).${NC}" - fi - - if ! ip6tables -C FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null; then - ip6tables -A FORWARD -i "$NETWORK_DEVICE" -o "$WG_INTERFACE" -m state --state RELATED,ESTABLISHED -j ACCEPT - echo -e "${GREEN}Added IPv6 forwarding rule (Internet → WG for established connections).${NC}" - fi -} - -configure_dns_and_icmp() { - echo -e "${YELLOW}Configuring DNS and ICMP rules...${NC}" - - # ICMP rules for ping - if ! iptables -C INPUT -p icmp --icmp-type echo-request -j ACCEPT 2>/dev/null; then - iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT - echo -e "${GREEN}Added IPv4 ICMP rule (allow ping requests).${NC}" - fi - - if ! iptables -C OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT 2>/dev/null; then - iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT - echo -e "${GREEN}Added IPv4 ICMP rule (allow ping replies).${NC}" - fi - - # ICMPv6 rules for ping6 - if ! ip6tables -C INPUT -p ipv6-icmp -j ACCEPT 2>/dev/null; then - ip6tables -A INPUT -p ipv6-icmp -j ACCEPT - echo -e "${GREEN}Added IPv6 ICMP rule (allow ping6).${NC}" - fi - - # DNS rules - if ! iptables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null; then - iptables -A INPUT -p udp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv4 DNS rule (UDP).${NC}" - fi - - if ! iptables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null; then - iptables -A INPUT -p tcp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv4 DNS rule (TCP).${NC}" - fi - - # IPv6 DNS rules - if ! ip6tables -C INPUT -p udp --dport 53 -j ACCEPT 2>/dev/null; then - ip6tables -A INPUT -p udp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv6 DNS rule (UDP).${NC}" - fi - - if ! ip6tables -C INPUT -p tcp --dport 53 -j ACCEPT 2>/dev/null; then - ip6tables -A INPUT -p tcp --dport 53 -j ACCEPT - echo -e "${GREEN}Added IPv6 DNS rule (TCP).${NC}" - fi -} - -# Apply Spamhaus blocklist from the Nym exit policy -apply_spamhaus_blocklist() { - echo -e "${YELLOW}Applying Spamhaus blocklist...${NC}" - - # Create directory if not exists - mkdir -p "$(dirname "$POLICY_FILE")" - - # Try to download the policy file - echo -e "${YELLOW}Downloading exit policy from $EXIT_POLICY_LOCATION${NC}" - if ! wget -q "$EXIT_POLICY_LOCATION" -O "$POLICY_FILE" 2>/dev/null; then - echo -e "${RED}Failed to download exit policy. Using minimal blocklist.${NC}" - - # Create a minimal policy file with a few entries - cat >"$POLICY_FILE" </dev/null; then - iptables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT - fi - - # Apply IPv6 rules for IPv6 addresses - if [[ "$ip_range" == *":"* ]] && ! ip6tables -C "$NYM_CHAIN" -d "$ip_range" -j REJECT 2>/dev/null; then - ip6tables -A "$NYM_CHAIN" -d "$ip_range" -j REJECT - fi - fi - done - - echo -e "${GREEN}Blocklist applied successfully.${NC}" -} - -add_default_reject_rule() { - echo -e "${YELLOW}Adding default reject rule...${NC}" - - # First remove any existing plain reject rules (without specific destinations) - iptables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true - iptables -D "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable 2>/dev/null || true - ip6tables -D "$NYM_CHAIN" -j REJECT 2>/dev/null || true - ip6tables -D "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable 2>/dev/null || true - - # Add the default catch-all reject rule (must be the last rule in the chain) - iptables -A "$NYM_CHAIN" -j REJECT --reject-with icmp-port-unreachable - ip6tables -A "$NYM_CHAIN" -j REJECT --reject-with icmp6-port-unreachable - - echo -e "${GREEN}Default reject rule added successfully.${NC}" -} - -apply_port_allowlist() { - echo -e "${YELLOW}Applying allowed ports...${NC}" - - # Dictionary of services and their ports - declare -A PORT_MAPPINGS=( - ["FTP"]="20-21" - ["SSH"]="22" - ["WHOIS"]="43" - ["DNS"]="53" - ["Finger"]="79" - ["HTTP"]="80-81" - ["Kerberos"]="88" - ["POP3"]="110" - ["NTP"]="123" - ["IMAP"]="143" - ["IMAP3"]="220" - ["LDAP"]="389" - ["HTTPS"]="443" - ["SMBWindowsFileShare"]="445" - ["Kpasswd"]="464" - ["RTSP"]="554" - ["LDAPS"]="636" - ["SILC"]="706" - ["KerberosAdmin"]="749" - ["DNSOverTLS"]="853" - ["Rsync"]="873" - ["VMware"]="902-904" - ["RemoteHTTPS"]="981" - ["FTPOverTLS"]="989-990" - ["NetnewsAdmin"]="991" - ["TelnetOverTLS"]="992" - ["IMAPOverTLS"]="993" - ["POP3OverTLS"]="995" - ["OpenVPN"]="1194" - ["QTServerAdmin"]="1220" - ["PKTKRB"]="1293" - ["MSSQL"]="1433" - ["VLSILicenseManager"]="1500" - ["OracleDB"]="1521" - ["Sametime"]="1533" - ["GroupWise"]="1677" - ["PPTP"]="1723" - ["RTSPAlt"]="1755" - ["MSNP"]="1863" - ["NFS"]="2049" - ["CPanel"]="2082-2083" - ["GNUnet"]="2086-2087" - ["NBX"]="2095-2096" - ["Zephyr"]="2102-2104" - ["XboxLive"]="3074" - ["MySQL"]="3306" - ["SVN"]="3690" - ["RWHOIS"]="4321" - ["Virtuozzo"]="4643" - ["RTPVOIP"]="5000-5005" - ["MMCC"]="5050" - ["ICQ"]="5190" - ["XMPP"]="5222-5223" - ["AndroidMarket"]="5228" - ["PostgreSQL"]="5432" - ["MongoDBDefault"]="27017" - ["Electrum"]="8082" - ["SimplifyMedia"]="8087-8088" - ["Zcash"]="8232-8233" - ["Bitcoin"]="8332-8333" - ["HTTPSALT"]="8443" - ["TeamSpeak"]="8767" - ["MQTTS"]="8883" - ["HTTPProxy"]="8888" - ["TorORPort"]="9001" - ["TorDirPort"]="9030" - ["Tari"]="9053" - ["Gaming"]="9339" - ["Git"]="9418" - ["HTTPSALT2"]="9443" - ["Lightning"]="9735" - ["NDMP"]="10000" - ["OpenPGP"]="11371" - ["Monero"]="18080-18081" - ["MoneroRPC"]="18089" - ["GoogleVoice"]="19294" - ["EnsimControlPanel"]="19638" - ["DarkFiTor"]="25551" - ["Minecraft"]="25565" - ["DarkFi"]="26661" - ["Steam"]="27000-27050" - ["ElectrumSSL"]="50002" - ["MOSH"]="60000-61000" - ["Mumble"]="64738" - ) - - # Add TCP and UDP rules for each allowed port - for service in "${!PORT_MAPPINGS[@]}"; do - port="${PORT_MAPPINGS[$service]}" - echo -e "${YELLOW}Adding rules for $service (Port: $port)${NC}" - - # Add both TCP and UDP rules for all services - add_port_rules iptables "$port" "tcp" - add_port_rules ip6tables "$port" "tcp" - add_port_rules iptables "$port" "udp" - add_port_rules ip6tables "$port" "udp" - done - - add_default_reject_rule - - echo -e "${GREEN}Port allowlist applied successfully.${NC}" -} - -safe_iptables_rule_remove() { - local chain="$1" - local table="${2:-filter}" - local interface="$3" - - # Remove rule if it exists - if iptables -t "$table" -C "$chain" -o "$interface" -j "$NYM_CHAIN" 2>/dev/null; then - iptables -t "$table" -D "$chain" -o "$interface" -j "$NYM_CHAIN" - fi -} - -safe_ip6tables_rule_remove() { - local chain="$1" - local table="${2:-filter}" - local interface="$3" - - # Remove rule if it exists - if ip6tables -t "$table" -C "$chain" -o "$interface" -j "$NYM_CHAIN" 2>/dev/null; then - ip6tables -t "$table" -D "$chain" -o "$interface" -j "$NYM_CHAIN" - fi -} - -clear_rules() { - echo -e "${YELLOW}Clearing Nym exit policy rules...${NC}" - - # Flush all rules in the NYM-EXIT chain - iptables -F "$NYM_CHAIN" 2>/dev/null || true - ip6tables -F "$NYM_CHAIN" 2>/dev/null || true - - # Remove the chain from FORWARD if it exists - iptables -D FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null || true - ip6tables -D FORWARD -o "$WG_INTERFACE" -j "$NYM_CHAIN" 2>/dev/null || true - - # Delete the chains - iptables -X "$NYM_CHAIN" 2>/dev/null || true - ip6tables -X "$NYM_CHAIN" 2>/dev/null || true - - echo -e "${GREEN}Nym exit policy rules cleared successfully.${NC}" -} - -remove_duplicate_rules() { - local interface="$1" - - if [[ -z "$interface" ]]; then - echo -e "${RED}Error: No interface specified. Usage: $0 remove-duplicates ${NC}" >&2 - exit 1 - fi - - echo -e "${YELLOW}Detecting and removing duplicate rules for $interface...${NC}" - - # Verbose duplicate rule detection for IPv4 - echo -e "${YELLOW}Checking IPv4 duplicate rules:${NC}" - iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq -d && { - echo -e "${RED}Duplicate IPv4 rules found! Removing...${NC}" - # Remove duplicates by saving unique rules - iptables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq | while read -r rule; do - # Carefully remove duplicates - full_rule=$(echo "$rule" | sed 's/^-A/iptables -D/') - eval "$full_rule" 2>/dev/null - done - } || echo -e "${GREEN}No duplicate IPv4 rules found.${NC}" - - # Verbose duplicate rule detection for IPv6 - echo -e "${YELLOW}Checking IPv6 duplicate rules:${NC}" - ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq -d && { - echo -e "${RED}Duplicate IPv6 rules found! Removing...${NC}" - # Remove duplicates by saving unique rules - ip6tables-save | grep -E "(-A FORWARD|-A $NYM_CHAIN)" | grep "$interface" | sort | uniq | while read -r rule; do - # Carefully remove duplicates - full_rule=$(echo "$rule" | sed 's/^-A/ip6tables -D/') - eval "$full_rule" 2>/dev/null - done - } || echo -e "${GREEN}No duplicate IPv6 rules found.${NC}" - - # Additional verification - echo -e "\n${YELLOW}Rule verification:${NC}" - echo "IPv4 Rules:" - iptables -L FORWARD -v -n | grep "$interface" - echo "IPv6 Rules:" - ip6tables -L FORWARD -v -n | grep "$interface" - - echo -e "${GREEN}Duplicate rule removal process completed.${NC}" -} - -save_rules() { - echo -e "${YELLOW}Saving iptables rules to make them persistent...${NC}" - - if [ -d "/etc/iptables" ]; then - # For Debian/Ubuntu with iptables-persistent - iptables-save >/etc/iptables/rules.v4 - ip6tables-save >/etc/iptables/rules.v6 - echo -e "${GREEN}Rules saved to /etc/iptables/rules.v4 and /etc/iptables/rules.v6${NC}" - else - # Fallback method - iptables-save >/etc/iptables.rules - ip6tables-save >/etc/ip6tables.rules - echo -e "${GREEN}Rules saved to /etc/iptables.rules and /etc/ip6tables.rules${NC}" - - # Add loading script to rc.local if it doesn't exist - if [ ! -f "/etc/network/if-pre-up.d/iptables" ]; then - cat >/etc/network/if-pre-up.d/iptables </dev/null; then - echo -e "${RED}WARNING: Wireguard interface $WG_INTERFACE not found!${NC}" - return 1 - fi - - # Interface details - echo -e "\n${YELLOW}Interface Details:${NC}" - ip link show "$WG_INTERFACE" - - # IP Addresses - echo -e "\n${YELLOW}IP Addresses:${NC}" - ip -4 addr show dev "$WG_INTERFACE" - ip -6 addr show dev "$WG_INTERFACE" - - # Iptables Chain Status - echo -e "\n${YELLOW}Iptables Chains:${NC}" - { - echo "IPv4 Chain:" - iptables -L "$NYM_CHAIN" -n -v - echo -e "\nIPv6 Chain:" - ip6tables -L "$NYM_CHAIN" -n -v - } || echo "One or both chains not found" - - # Forwarding Status - echo -e "\n${YELLOW}IP Forwarding:${NC}" - echo "IPv4: $(cat /proc/sys/net/ipv4/ip_forward)" - echo "IPv6: $(cat /proc/sys/net/ipv6/conf/all/forwarding)" -} - -test_connectivity() { - echo -e "${YELLOW}Testing connectivity through $WG_INTERFACE...${NC}" - - # More comprehensive interface check - interface_info=$(ip link show "$WG_INTERFACE" 2>/dev/null) - - if [ -z "$interface_info" ]; then - echo -e "${RED}Interface $WG_INTERFACE not found!${NC}" - return 1 - fi - - # Check for multiple possible interface states - if ! echo "$interface_info" | grep -qE "state (UP|UNKNOWN|DORMANT)"; then - echo -e "${RED}Interface $WG_INTERFACE is not in an active state!${NC}" - echo "$interface_info" - return 1 - fi - - # Detailed interface information - echo -e "${GREEN}Interface Details:${NC}" - echo "$interface_info" - - # Get IP addresses with more robust method - ipv4_address=$(ip -4 addr show dev "$WG_INTERFACE" | grep -oP '(?<=inet\s)\d+\.\d+\.\d+\.\d+/\d+' | cut -d'/' -f1 | head -n1) - ipv6_address=$(ip -6 addr show dev "$WG_INTERFACE" scope global | grep -oP '(?<=inet6\s)[0-9a-f:]+/\d+' | cut -d'/' -f1 | head -n1) - - echo -e "${GREEN}IPv4 Address:${NC} ${ipv4_address:-Not found}" - echo -e "${GREEN}IPv6 Address:${NC} ${ipv6_address:-Not found}" - - # Connectivity tests - if [[ -n "$ipv4_address" ]]; then - echo -e "${YELLOW}Testing IPv4 connectivity from $ipv4_address...${NC}" - - # Ping test - if timeout 5 ping -c 3 -I "$ipv4_address" 8.8.8.8 >/dev/null 2>&1; then - echo -e "${GREEN}IPv4 connectivity to 8.8.8.8: Success${NC}" - else - echo -e "${RED}IPv4 connectivity to 8.8.8.8: Failed${NC}" - fi - - # DNS resolution test - if timeout 5 ping -c 3 -I "$ipv4_address" google.com >/dev/null 2>&1; then - echo -e "${GREEN}IPv4 DNS resolution: Success${NC}" - else - echo -e "${RED}IPv4 DNS resolution: Failed${NC}" - fi - - # HTTP(S) connectivity test - if command -v curl &>/dev/null; then - if timeout 5 curl -s --interface "$ipv4_address" -o /dev/null -w "%{http_code}" https://www.google.com | grep -q "200"; then - echo -e "${GREEN}IPv4 HTTPS connectivity: Success${NC}" - else - echo -e "${RED}IPv4 HTTPS connectivity: Failed${NC}" - fi - fi - else - echo -e "${RED}No IPv4 address configured on $WG_INTERFACE${NC}" - fi - - # Similar tests for IPv6 if available - if [[ -n "$ipv6_address" ]]; then - echo -e "${YELLOW}Testing IPv6 connectivity from $ipv6_address...${NC}" - - if timeout 5 ping6 -c 3 -I "$ipv6_address" 2001:4860:4860::8888 >/dev/null 2>&1; then - echo -e "${GREEN}IPv6 connectivity to Google DNS: Success${NC}" - else - echo -e "${RED}IPv6 connectivity to Google DNS: Failed${NC}" - fi - - if timeout 5 ping6 -c 3 -I "$ipv6_address" google.com >/dev/null 2>&1; then - echo -e "${GREEN}IPv6 DNS resolution: Success${NC}" - else - echo -e "${RED}IPv6 DNS resolution: Failed${NC}" - fi - - if command -v curl &>/dev/null; then - if timeout 5 curl -s --interface "$ipv6_address" -o /dev/null -w "%{http_code}" https://www.google.com | grep -q "200"; then - echo -e "${GREEN}IPv6 HTTPS connectivity: Success${NC}" - else - echo -e "${RED}IPv6 HTTPS connectivity: Failed${NC}" - fi - fi - else - echo -e "${YELLOW}No IPv6 address configured on $WG_INTERFACE${NC}" - fi - - echo -e "${GREEN}Connectivity tests completed.${NC}" -} - -main() { - # Check for root privileges - if [ "$(id -u)" -ne 0 ]; then - echo -e "${RED}This script must be run as root${NC}" >&2 - exit 1 - fi - - # Parse command-line arguments - case "$1" in - install) - install_dependencies - configure_ip_forwarding - create_nym_chain - setup_nat_rules - configure_dns_and_icmp - apply_spamhaus_blocklist - apply_port_allowlist - save_rules - echo -e "${GREEN}Nym exit policy installed successfully.${NC}" - ;; - status) - show_status - ;; - test) - test_connectivity - ;; - clear) - clear_rules - echo -e "${GREEN}Nym exit policy rules cleared.${NC}" - ;; - remove-duplicates) - remove_duplicate_rules "$2" - ;; - help | --help | -h) - echo "Usage: $0 [command]" - echo "" - echo "Commands:" - echo " install Install and configure Nym exit policy" - echo " status Show current Nym exit policy status" - echo " test Test connectivity through Wireguard interface" - echo " clear Remove all Nym exit policy rules" - echo " remove-duplicates Remove duplicate iptables rules for an interface" - echo " help Show this help message" - ;; - *) - echo -e "${RED}Invalid command. Use '$0 help' for usage information.${NC}" >&2 - exit 1 - ;; - esac -} - -main "$@" diff --git a/sdk/typescript/codegen/contract-clients/package-lock.json b/sdk/typescript/codegen/contract-clients/package-lock.json index 219b1f89f7..37c65830b5 100644 --- a/sdk/typescript/codegen/contract-clients/package-lock.json +++ b/sdk/typescript/codegen/contract-clients/package-lock.json @@ -1,15 +1,15 @@ { "name": "@nymproject/contract-clients", - "version": "1.0.0", + "version": "1.3.1-rc0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nymproject/contract-clients", - "version": "1.0.0", + "version": "1.3.1-rc0", "license": "Apache-2.0", "devDependencies": { - "@cosmwasm/ts-codegen": "^0.35.3", + "@cosmwasm/ts-codegen": "^1.13.3", "nodemon": "3.0.1", "npm-run-all": "^4.1.5", "reload": "^3.2.1", @@ -17,640 +17,175 @@ "typescript": "^4.6.2" } }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.10.tgz", - "integrity": "sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.22.10", - "chalk": "^2.4.2" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.9.tgz", - "integrity": "sha512-5UamI7xkUcJ3i9qVDS+KFDEK8/7oJ55/sJMB1Ge7IEapr7KfdfV/HErR+koZwOfd+SgtFKOKRhRakdg++DcJpQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz", - "integrity": "sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.10", - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-module-transforms": "^7.18.9", - "@babel/helpers": "^7.18.9", - "@babel/parser": "^7.18.10", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.18.10", - "@babel/types": "^7.18.10", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, "node_modules/@babel/generator": { - "version": "7.18.12", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz", - "integrity": "sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.4.tgz", + "integrity": "sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.18.10", - "@jridgewell/gen-mapping": "^0.3.2", + "@babel/types": "^7.24.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^2.5.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", - "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.10.tgz", - "integrity": "sha512-Av0qubwDQxC56DoUReVDeLfMEjYYSN1nZrTUrWkXd7hpU73ymRANkbuDm3yni9npkn+RXy9nNbEJZEzXr7xrfQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.10" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.10.tgz", - "integrity": "sha512-JMSwHD4J7SLod0idLq5PKgI+6g/hLD/iuWBq08ZX49xE14VpVEojJ5rHWptpirV2j020MvypRLAXAO50igCJ5Q==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.5", - "browserslist": "^4.21.9", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.10.tgz", - "integrity": "sha512-5IBb77txKYQPpOEdUdIhBx8VrZyDCQ+H82H0+5dX1TmuscP5vJKEE3cKurjtIw/vFwzbVH48VweE78kVDBrqjA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.9.tgz", - "integrity": "sha512-+svjVa/tFwsNSG4NEy1h85+HQ5imbT92Q5/bgtS7P0GTQlP8WuFdqsiABmQouhiFGyV66oGxZFpeYHza1rNsKw==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "regexpu-core": "^5.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", - "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz", + "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor/node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz", + "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.24.7", + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz", + "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", - "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", - "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.9.tgz", - "integrity": "sha512-t+WA2Xn5K+rTeGtC8jCsdAH52bjggG5TKRuRrAGNM/mjIbO4GxvlLMFOEz9wXY5I2XQ60PMFsAG2WIcG82dQMQ==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/helper-validator-identifier": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.9.tgz", - "integrity": "sha512-8WWC4oR4Px+tr+Fp0X3RHDVfINGpF3ad1HIbrc8A77epiR6eMMc6jsgozkzT2uDiOOdoS9cLIQ+XD2XvI2WSmQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-wrap-function": "^7.22.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.22.9", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.9.tgz", - "integrity": "sha512-LJIKvvpgPOPUThdYqcX6IXRuIcTkcAub0IaDRGCZH0p5GPUp7PhRU9QVgFcDDd51BaPkk77ZjqFwh6DZTAEmGg==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-member-expression-to-functions": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.22.5" + "@babel/types": "^7.24.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-split-export-declaration/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", - "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", - "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.10.tgz", - "integrity": "sha512-OnMhjWjuGYtdoO3FmsEFWvBStBAe2QOgwOLsLNDjN+aaiMD8InJk1/O3HSD8lkqTjCgg5YI34Tz15KNNA3p+nQ==", - "dev": true, - "dependencies": { - "@babel/helper-function-name": "^7.22.5", - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.10" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.10.tgz", - "integrity": "sha512-a41J4NW8HyZa1I1vAndrraTlPZ/eZoga2ZgS7fEr0tZJGVU4xqdE80CEm0CcNjha5EZ8fTBYLKHF0kqDUuAwQw==", - "dev": true, - "dependencies": { - "@babel/template": "^7.22.5", - "@babel/traverse": "^7.22.10", - "@babel/types": "^7.22.10" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/generator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.10.tgz", - "integrity": "sha512-79KIf7YiWjjdZ81JnLujDRApWtl7BxTqWD88+FFdQEIOG8LJ0etDOM7CXuIgGJa55sGOwZVwuEsaLEm0PJ5/+A==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.10", - "@jridgewell/gen-mapping": "^0.3.2", - "@jridgewell/trace-mapping": "^0.3.17", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/parser": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.10.tgz", - "integrity": "sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/traverse": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.10.tgz", - "integrity": "sha512-Q/urqV4pRByiNNpb/f5OSv28ZlGJiFiiTh+GAHktbIrkPhPbl90+uW6SmpoLyZqutrg9AEaEf3Q/ZBRHBXgxig==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.22.10", - "@babel/generator": "^7.22.10", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.10", - "@babel/types": "^7.22.10", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.10.tgz", - "integrity": "sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.5", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.18.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz", - "integrity": "sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -658,1299 +193,75 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", - "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "node_modules/@babel/parser/node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" } }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", - "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-transform-optional-chaining": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", - "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.21.0.tgz", - "integrity": "sha512-XP5G9MWNUskFuP30IfFSEFB0Z6HzLIUcjYM4bYOPHXl7eiJ9HFv8tWj6TXTN5QODiEhDZAeI4hLok2iHFFV4hw==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-default-from": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.18.10.tgz", - "integrity": "sha512-5H2N3R2aQFxkV4PIBUR/i7PUSwgTZjouJKzI8eKswfIjT0PhvzkPn0t0wIS5zn6maQuvtT0t1oHtMUz61LOuow==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-default-from": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.20.7.tgz", - "integrity": "sha512-y7C7cZgpMIjWlKE5T7eJwp+tnRYM89HmRvWM5EQuB5BoHEONjmQ8lSNmBUwOyy/GFRsohJED51YBF79hE1djug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.18.9.tgz", - "integrity": "sha512-kDDHQ5rflIeY5xl69CEqGEZ0KY369ehsCIEbTGb4siHG5BE9sga/T0r0OUwyZNLMmZE79E1kbsqAjwFCW4ds6Q==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.18.8", - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.18.8" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.21.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.21.0.tgz", - "integrity": "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.11", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", - "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.21.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-default-from": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.22.5.tgz", - "integrity": "sha512-ODAqWWXB/yReh/jVQDag/3/tl6lgBueQkk/TcfW/59Oykm4c8a55XloX0CTk2k2VJiFWMgHby9xNX29IbCv9dQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", - "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz", - "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.22.5.tgz", - "integrity": "sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", - "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", - "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-remap-async-to-generator": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", - "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.10.tgz", - "integrity": "sha512-1+kVpGAOOI1Albt6Vse7c8pHzcZQdQKW+wJH+g8mCaszOdDVwRXa/slHPqIw+oJAJANTKDMuM2cBdV0Dg618Vg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.6.tgz", - "integrity": "sha512-58EgM6nuPNG6Py4Z3zSuu0xWu2VfodiMi72Jt5Kj2FECmaYk1RrTXA45z6KBFsu9tRgwQDwIiY4FXTt+YsSFAQ==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-environment-visitor": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", - "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/template": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.10.tgz", - "integrity": "sha512-dPJrL0VOyxqLM9sritNbMSGx/teueHF/htMKrPT7DNxccXxRDPYqlgPFFdr8u+F+qUZOkZoXue/6rL5O5GduEw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", - "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", - "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", - "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", - "dev": true, - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", - "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", - "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.5", - "@babel/helper-function-name": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", - "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", - "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", - "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", - "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", - "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", - "dev": true, - "dependencies": { - "@babel/helper-hoist-variables": "^7.22.5", - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", - "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", - "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", - "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", - "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.10.tgz", - "integrity": "sha512-MMkQqZAZ+MGj+jGTG3OTuhKeBpNcO+0oCEbrGNEaOmiEn+1MzRyQlYsruGiU8RTK3zV6XwrVJTmwiDOyYK6J9g==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", - "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", - "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz", - "integrity": "sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "regenerator-transform": "^0.15.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", - "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.18.10.tgz", - "integrity": "sha512-q5mMeYAdfEbpBAgzl7tBre/la3LeCxmDO1+wMXRdPWbcoMjR3GiXlCLk7JBZVVye0bqTGNMbt0yYVXX1B1jEWQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.9", - "babel-plugin-polyfill-corejs2": "^0.3.2", - "babel-plugin-polyfill-corejs3": "^0.5.3", - "babel-plugin-polyfill-regenerator": "^0.4.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", - "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", - "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", - "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", - "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", - "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.22.10.tgz", - "integrity": "sha512-7++c8I/ymsDo4QQBAgbraXLzIM6jmfao11KgIBEYZRReWzNWH9NtNgJcyrZiXsOPh523FQm6LfpLyy/U5fn46A==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.22.10", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz", - "integrity": "sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", - "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.22.5", - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.18.10.tgz", - "integrity": "sha512-wVxs1yjFdW3Z/XkNfXKoblxoHgbtUF7/l3PvvP4m02Qz9TZ6uZGxRVYjSQeR87oQmHco9zWitW5J82DJ7sCjvA==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.18.8", - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.18.10", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.18.9", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.18.9", - "@babel/plugin-transform-classes": "^7.18.9", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.18.9", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.18.6", - "@babel/plugin-transform-modules-commonjs": "^7.18.6", - "@babel/plugin-transform-modules-systemjs": "^7.18.9", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.18.6", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.18.8", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.18.9", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.18.10", - "babel-plugin-polyfill-corejs2": "^0.3.2", - "babel-plugin-polyfill-corejs3": "^0.5.3", - "babel-plugin-polyfill-regenerator": "^0.4.0", - "core-js-compat": "^3.22.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6.tgz", - "integrity": "sha512-ID2yj6K/4lKfhuU3+EX4UvNbIt7eACFbHmNUjzA+ep+B5971CknnA/9DEWKbRokfbbtblxxxXFJJrH47UEAMVg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.22.5.tgz", - "integrity": "sha512-YbPaal9LxztSGhmndR46FmAbkJ/1fAsw293tSU+I5E5h+cnJ3d4GTwyUgGYmOXJYdGA+uNePle4qbaRzj2NISQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.5", - "@babel/plugin-syntax-jsx": "^7.22.5", - "@babel/plugin-transform-modules-commonjs": "^7.22.5", - "@babel/plugin-transform-typescript": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", - "dev": true - }, "node_modules/@babel/runtime": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.10.tgz", - "integrity": "sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "dev": true, - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", - "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.22.5", - "@babel/parser": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/template/node_modules/@babel/parser": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.10.tgz", - "integrity": "sha512-lNbdGsQb9ekfsnjFGhEiF4hfFqGgfOP3H3d27re3n+CGhNuTSUEQdfWk556sTLNTloczcdM5TYF2LhzmDQKyvQ==", - "dev": true, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/template/node_modules/@babel/types": { - "version": "7.22.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.10.tgz", - "integrity": "sha512-obaoigiLrlDZ7TUQln/8m4mSqIW2QFeOrCQc9r+xsaHGNoplVNYlRVpsfE8Vj35GEm2ZH4ZhrNYogs/3fj85kg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.5", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.18.11", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz", - "integrity": "sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ==", + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.1.tgz", + "integrity": "sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.18.10", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.18.11", - "@babel/types": "^7.18.10", - "debug": "^4.1.0", + "@babel/code-frame": "^7.24.1", + "@babel/generator": "^7.24.1", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.24.1", + "@babel/types": "^7.24.0", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -1958,320 +269,848 @@ } }, "node_modules/@babel/types": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz", - "integrity": "sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.18.10", - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@cosmwasm/ts-codegen": { - "version": "0.35.3", - "resolved": "https://registry.npmjs.org/@cosmwasm/ts-codegen/-/ts-codegen-0.35.3.tgz", - "integrity": "sha512-L7T4yjAQIWJ/h0YmqVjihzv78ZbqGD/f0G8EJTwccsPXqWzbcnXzQKsvtFXpDp6AzaJKVMchFHfp6ZPRP3boUQ==", + "node_modules/@chain-registry/types": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@chain-registry/types/-/types-0.17.1.tgz", + "integrity": "sha512-O0CgrtJgIlqXvZm1CqDZe/7jZz068O/uuCIoyDXCegFHK03rdHacKcDGwEIUuI0MNUf8YV3jdE4xHQMSAX+79w==", "dev": true, + "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@babel/core": "7.18.10", - "@babel/generator": "7.18.12", - "@babel/parser": "7.18.11", - "@babel/plugin-proposal-class-properties": "7.18.6", - "@babel/plugin-proposal-export-default-from": "7.18.10", - "@babel/plugin-proposal-object-rest-spread": "7.18.9", - "@babel/plugin-transform-runtime": "7.18.10", - "@babel/preset-env": "7.18.10", - "@babel/preset-typescript": "^7.18.6", - "@babel/runtime": "^7.18.9", - "@babel/traverse": "7.18.11", - "@babel/types": "7.18.10", + "@babel/runtime": "^7.21.0" + } + }, + "node_modules/@chain-registry/utils": { + "version": "1.51.261", + "resolved": "https://registry.npmjs.org/@chain-registry/utils/-/utils-1.51.261.tgz", + "integrity": "sha512-spdGKOB4ZdUBcPiFAMIXCwCi8Q5/dAXyRPzLXBCCm0DxBnhSfg6nx1iTin2ViXog1TOMykSthxi0dcdCANvCZA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@chain-registry/types": "^0.50.261", + "bignumber.js": "9.1.2", + "sha.js": "^2.4.11" + } + }, + "node_modules/@chain-registry/utils/node_modules/@chain-registry/types": { + "version": "0.50.261", + "resolved": "https://registry.npmjs.org/@chain-registry/types/-/types-0.50.261.tgz", + "integrity": "sha512-+AEKPWMFdPfu9pBGn/BjjyJkuPB/7AzkvzCVFxGocn/HZsXVVvxvK+q6YwgsDzNAcAtq8GU39S0dNi/Obj/o/g==", + "dev": true, + "license": "SEE LICENSE IN LICENSE" + }, + "node_modules/@chain-registry/v2": { + "version": "1.71.237", + "resolved": "https://registry.npmjs.org/@chain-registry/v2/-/v2-1.71.237.tgz", + "integrity": "sha512-c10uXxFz1+51VR0szeB1phn482O03uJiSQ+mZXpeq7IBtU2Hst6Vj1Ttjbx+nlxekhoSGG1jZ0JQ1ewXWYQ6IA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "peer": true, + "dependencies": { + "@chain-registry/v2-types": "^0.53.146" + } + }, + "node_modules/@chain-registry/v2-types": { + "version": "0.53.146", + "resolved": "https://registry.npmjs.org/@chain-registry/v2-types/-/v2-types-0.53.146.tgz", + "integrity": "sha512-GnqdNnEJ+ZTAc/0S9fQbwL5GpIvF3LZ/P3o6qh6EyITSiRA1Y55FXs59eSddltpPX5EgyWDwNpJjY+Q5uYlAkA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "peer": true + }, + "node_modules/@cosmwasm/ts-codegen": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@cosmwasm/ts-codegen/-/ts-codegen-1.13.3.tgz", + "integrity": "sha512-BD+yJQZjQ6CK6+vuMpAVQKRK24OSxSCATRgpzo5v2NHoXpcALp6HUc6NU0u8hZjULJjjT5uChiSjNuucS1P+YA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@babel/generator": "7.24.4", + "@babel/traverse": "7.24.1", + "@babel/types": "7.24.0", + "@chain-registry/types": "^0.17.1", + "@chain-registry/utils": "^1.51.71", + "@chain-registry/v2": "^1.71.229", + "@cosmwasm/ts-codegen-ast": "^1.9.3", + "@cosmwasm/ts-codegen-types": "^1.4.3", + "@interchainjs/amino": "1.17.1", + "@interchainjs/cosmos": "1.17.1", + "@interchainjs/types": "1.17.1", "@pyramation/json-schema-to-typescript": " 11.0.4", + "@types/rimraf": "3.0.2", + "@types/shelljs": "0.8.15", "case": "1.6.3", "dargs": "7.0.0", "deepmerge": "4.2.2", - "dotty": "0.1.2", "fuzzy": "0.1.3", - "glob": "8.0.3", + "glob": "^10", "inquirerer": "0.1.3", - "long": "^5.2.0", + "interchainjs": "1.17.1", + "minimatch": "^10.0.3", "minimist": "1.2.6", "mkdirp": "1.0.4", + "nested-obj": "0.0.1", "parse-package-name": "1.0.0", "rimraf": "3.0.2", - "shelljs": "0.8.5", - "wasm-ast-types": "^0.26.2" + "shelljs": "0.8.5" }, "bin": { - "cosmwasm-ts-codegen": "main/ts-codegen.js" + "cosmwasm-ts-codegen": "ts-codegen.js", + "ts-codegen": "ts-codegen.js" } }, - "node_modules/@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@cosmwasm/ts-codegen-ast": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@cosmwasm/ts-codegen-ast/-/ts-codegen-ast-1.9.3.tgz", + "integrity": "sha512-ITtwVRbYr4fmjxq3xOc7wKsAK5xaIaLp7bEduGax2cgv54NL8i5/lv0iP84DUI11EEA9bDOWl6QtvwKBd8I12g==", "dev": true, + "license": "SEE LICENSE IN LICENSE", "dependencies": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" + "@babel/types": "7.24.0", + "@cosmwasm/ts-codegen-types": "^1.4.3", + "ast-stringify": "0.2.1", + "case": "1.6.3", + "deepmerge": "4.2.2", + "minimatch": "^10.0.3" + } + }, + "node_modules/@cosmwasm/ts-codegen-types": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@cosmwasm/ts-codegen-types/-/ts-codegen-types-1.4.3.tgz", + "integrity": "sha512-WjNOpg8phEbzCfwo7cB2Iey/ipwKsSqazhx9k2ocTwSG+87zf1G1XGhU0K0FP4XTMvUvNjd8ovtCi4I1+kSMTA==", + "dev": true, + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "minimatch": "^10.0.3" + } + }, + "node_modules/@ethersproject/abstract-provider": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-provider/-/abstract-provider-5.8.0.tgz", + "integrity": "sha512-wC9SFcmh4UK0oKuLJQItoQdzS/qZ51EJegK6EmAWlh+OptpQ/npECOR3QqECd8iGHC0RJb4WKbVdSfif4ammrg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/networks": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/transactions": "^5.8.0", + "@ethersproject/web": "^5.8.0" + } + }, + "node_modules/@ethersproject/abstract-signer": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/abstract-signer/-/abstract-signer-5.8.0.tgz", + "integrity": "sha512-N0XhZTswXcmIZQdYtUnd79VJzvEwXQw6PK0dTl9VoYrEBxxCPXqS0Eod7q5TNKRxe1/5WUMuR0u0nqTF/avdCA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-provider": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0" + } + }, + "node_modules/@ethersproject/address": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/address/-/address-5.8.0.tgz", + "integrity": "sha512-GhH/abcC46LJwshoN+uBNoKVFPxUuZm6dA257z0vZkKmU1+t8xTn8oK7B9qrj8W2rFRMch4gbJl6PmVxjxBEBA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/rlp": "^5.8.0" + } + }, + "node_modules/@ethersproject/base64": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/base64/-/base64-5.8.0.tgz", + "integrity": "sha512-lN0oIwfkYj9LbPx4xEkie6rAMJtySbpOAFXSDVQaBnAzYfB4X2Qr+FXJGxMoc3Bxp2Sm8OwvzMrywxyw0gLjIQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0" + } + }, + "node_modules/@ethersproject/bignumber": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bignumber/-/bignumber-5.8.0.tgz", + "integrity": "sha512-ZyaT24bHaSeJon2tGPKIiHszWjD/54Sz8t57Toch475lCLljC6MgPmxk7Gtzz+ddNN5LuHea9qhAe0x3D+uYPA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "bn.js": "^5.2.1" + } + }, + "node_modules/@ethersproject/bytes": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/bytes/-/bytes-5.8.0.tgz", + "integrity": "sha512-vTkeohgJVCPVHu5c25XWaWQOZ4v+DkGoC42/TS2ond+PARCxTJvgTFUNDZovyQ/uAQ4EcpqqowKydcdmRKjg7A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/constants": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/constants/-/constants-5.8.0.tgz", + "integrity": "sha512-wigX4lrf5Vu+axVTIvNsuL6YrV4O5AXl5ubcURKMEME5TnWBouUh0CDTWxZ2GpnRn1kcCgE7l8O5+VbV9QTTcg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.8.0" + } + }, + "node_modules/@ethersproject/hash": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/hash/-/hash-5.8.0.tgz", + "integrity": "sha512-ac/lBcTbEWW/VGJij0CNSw/wPcw9bSRgCB0AIBz8CvED/jfvDoV9hsIIiWfvWmFEi8RcXtlNwp2jv6ozWOsooA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/abstract-signer": "^5.8.0", + "@ethersproject/address": "^5.8.0", + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@ethersproject/keccak256": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/keccak256/-/keccak256-5.8.0.tgz", + "integrity": "sha512-A1pkKLZSz8pDaQ1ftutZoaN46I6+jvuqugx5KYNeQOPqq+JZ0Txm7dlWesCHB5cndJSu5vP2VKptKf7cksERng==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "js-sha3": "0.8.0" + } + }, + "node_modules/@ethersproject/logger": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/logger/-/logger-5.8.0.tgz", + "integrity": "sha512-Qe6knGmY+zPPWTC+wQrpitodgBfH7XoceCGL5bJVejmH+yCS3R8jJm8iiWuvWbG76RUmyEG53oqv6GMVWqunjA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT" + }, + "node_modules/@ethersproject/networks": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/networks/-/networks-5.8.0.tgz", + "integrity": "sha512-egPJh3aPVAzbHwq8DD7Po53J4OUSsA1MjQp8Vf/OZPav5rlmWUaFLiq8cvQiGK0Z5K6LYzm29+VA/p4RL1FzNg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/properties": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/properties/-/properties-5.8.0.tgz", + "integrity": "sha512-PYuiEoQ+FMaZZNGrStmN7+lWjlsoufGIHdww7454FIaGdbe/p5rnaCXTr5MtBYl3NkeoVhHZuyzChPeGeKIpQw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/rlp": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/rlp/-/rlp-5.8.0.tgz", + "integrity": "sha512-LqZgAznqDbiEunaUvykH2JAoXTT9NV0Atqk8rQN9nx9SEgThA/WMx5DnW8a9FOufo//6FZOCHZ+XiClzgbqV9Q==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/signing-key": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/signing-key/-/signing-key-5.8.0.tgz", + "integrity": "sha512-LrPW2ZxoigFi6U6aVkFN/fa9Yx/+4AtIUe4/HACTvKJdhm0eeb107EVCIQcrLZkxaSIgc/eCrX8Q1GtbH+9n3w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "bn.js": "^5.2.1", + "elliptic": "6.6.1", + "hash.js": "1.1.7" + } + }, + "node_modules/@ethersproject/strings": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/strings/-/strings-5.8.0.tgz", + "integrity": "sha512-qWEAk0MAvl0LszjdfnZ2uC8xbR2wdv4cDabyHiBh3Cldq/T8dPH3V4BbBsAYJUeonwD+8afVXld274Ls+Y1xXg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/logger": "^5.8.0" + } + }, + "node_modules/@ethersproject/transactions": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/transactions/-/transactions-5.8.0.tgz", + "integrity": "sha512-UglxSDjByHG0TuU17bDfCemZ3AnKO2vYrL5/2n2oXvKzvb7Cz+W9gOWXKARjp2URVwcWlQlPOEQyAviKwT4AHg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/address": "^5.8.0", + "@ethersproject/bignumber": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/constants": "^5.8.0", + "@ethersproject/keccak256": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/rlp": "^5.8.0", + "@ethersproject/signing-key": "^5.8.0" + } + }, + "node_modules/@ethersproject/web": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@ethersproject/web/-/web-5.8.0.tgz", + "integrity": "sha512-j7+Ksi/9KfGviws6Qtf9Q7KCqRhpwrYKQPs+JBA/rKVFF/yaWLHJEH3zfVP2plVu+eys0d2DlFmhoQJayFewcw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://gitcoin.co/grants/13/ethersjs-complete-simple-and-tiny-2" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "dependencies": { + "@ethersproject/base64": "^5.8.0", + "@ethersproject/bytes": "^5.8.0", + "@ethersproject/logger": "^5.8.0", + "@ethersproject/properties": "^5.8.0", + "@ethersproject/strings": "^5.8.0" + } + }, + "node_modules/@interchainjs/amino": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/amino/-/amino-1.17.1.tgz", + "integrity": "sha512-MYEye+V4O7tITc/YofgGbiLT0kdXuK0BvuIYMFfq5+Tybq9tgFu/PYQ6CL3nyD6+S5H/rbgz9sUH31sPj7XaZg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@interchainjs/crypto": "1.17.1", + "@interchainjs/encoding": "1.17.1", + "@interchainjs/math": "1.17.1", + "@interchainjs/utils": "1.17.1" + } + }, + "node_modules/@interchainjs/auth": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/auth/-/auth-1.17.1.tgz", + "integrity": "sha512-4khXhRti537vqsSzSPfCwtarFBNvXkc8ISYFvqY43I2j0m0FbzZhNqjrXQn+xnihnjsF9ec/SqckVKdXX2DHFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@interchainjs/crypto": "1.17.1", + "@interchainjs/types": "1.17.1", + "@interchainjs/utils": "1.17.1", + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", + "@scure/bip32": "^1.0.10", + "bech32": "^2.0.0", + "elliptic": "^6.5.4", + "libsodium-wrappers-sumo": "^0.7.11" + } + }, + "node_modules/@interchainjs/auth/node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@interchainjs/cosmos": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/cosmos/-/cosmos-1.17.1.tgz", + "integrity": "sha512-Wzjv5VxaiZUD+sY2ErMv6QCTdrmV+Mus1VFHrMZo3T4gex6Z8fOOV1IaesN5+w44XM4YkF8CX+zSCerfqV6Dvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@interchainjs/auth": "1.17.1", + "@interchainjs/cosmos-types": "1.17.1", + "@interchainjs/crypto": "1.17.1", + "@interchainjs/encoding": "1.17.1", + "@interchainjs/types": "1.17.1", + "@interchainjs/utils": "1.17.1", + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", + "bech32": "^1.1.4", + "bignumber.js": "^9.1.0", + "bip39": "^3.1.0", + "decimal.js": "^10.4.3", + "deepmerge": "4.2.2", + "ws": "^8.14.0" + } + }, + "node_modules/@interchainjs/cosmos-types": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/cosmos-types/-/cosmos-types-1.17.1.tgz", + "integrity": "sha512-GU3dfQ2IHTVvClgctjY0RAdErwcG6fQskBh4Iqm6BDpcgpto2+nzES/DoLslx8ds9vBCujjx5zGFwr90fgScHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@interchainjs/math": "1.17.1", + "@interchainjs/types": "1.17.1", + "@interchainjs/utils": "1.17.1" + } + }, + "node_modules/@interchainjs/cosmos/node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@interchainjs/crypto": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/crypto/-/crypto-1.17.1.tgz", + "integrity": "sha512-vrKtsRq4YX6pUukD73CbLLOD6tgwHb05GMOVhK0S8lGULiSoW8f76LvhOUgX13v5MSWiBzdSiAaVsAfTvkJePw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@interchainjs/encoding": "1.17.1", + "@interchainjs/math": "1.17.1", + "@interchainjs/utils": "1.17.1", + "@noble/hashes": "^1", + "bn.js": "^5.2.0", + "elliptic": "^6.5.4", + "libsodium-wrappers-sumo": "^0.7.11" + } + }, + "node_modules/@interchainjs/encoding": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/encoding/-/encoding-1.17.1.tgz", + "integrity": "sha512-ALRsvzEtSAHBiBw2z+i9bp9ICGpMN8L6cqS0AIpx+B4mFnuFp0TYi4m7CerklcDeV78ld+G6Xf5PbbQUGI0/hA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@interchainjs/math": "1.17.1", + "base64-js": "^1.3.0", + "bech32": "^1.1.4", + "readonly-date": "^1.0.0" + } + }, + "node_modules/@interchainjs/ethereum": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/ethereum/-/ethereum-1.17.1.tgz", + "integrity": "sha512-EOjB3Q8OusoCk5/zg0PlWJUoepKEOjm18DNEeAxUPmG5wDw8CIAp7wkUHF6lfGkE7ZFedwy74Uinc1eZZSDh1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", + "@ethersproject/hash": "^5.7.0", + "@ethersproject/transactions": "^5.7.0", + "@interchainjs/auth": "1.17.1", + "@interchainjs/types": "1.17.1", + "@interchainjs/utils": "1.17.1", + "@noble/curves": "^1.1.0", + "@noble/hashes": "^1.3.1", + "bip39": "^3.1.0", + "deepmerge": "4.2.2", + "ethereum-cryptography": "^3.1.0", + "rlp": "^3.0.0" + } + }, + "node_modules/@interchainjs/math": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/math/-/math-1.17.1.tgz", + "integrity": "sha512-4eOj6m95Iy3iNuhqf9F9iLBlV1U2G5L6CqjpROvn9fiO3H8DInXermZFP1CjFXley1Nrd8QfbV7OSkyfa0u8tA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bn.js": "^5.2.0" + } + }, + "node_modules/@interchainjs/pubkey": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/pubkey/-/pubkey-1.17.1.tgz", + "integrity": "sha512-hdfXFsZSVSg6tPzgd3Htw40dSY/nXTVJLrbZt6cKe6BGRiuPSQmOkka6AZn0Ze5Y4ULYfUMxpipFQc0ukfsd1A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@interchainjs/amino": "1.17.1", + "@interchainjs/cosmos-types": "1.17.1", + "@interchainjs/encoding": "1.17.1", + "@interchainjs/math": "1.17.1", + "@interchainjs/types": "1.17.1" + } + }, + "node_modules/@interchainjs/types": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/types/-/types-1.17.1.tgz", + "integrity": "sha512-EPKa0nFPjYFpgO+a5WUBXrkHbbBeYHyBv8DG55yTFuvvn4Yd6uH86ygQIR9NsEAMgKQSiTD+yQcVj/4fbC/EQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decimal.js": "^10.4.3" + } + }, + "node_modules/@interchainjs/utils": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@interchainjs/utils/-/utils-1.17.1.tgz", + "integrity": "sha512-46W53v0tRMHiThvF8IZbURqAJsCxNwdxMdKFlL1pDwqAUHxgFzM6IfxemijbNCQd2ST9EYKe9c5zmnxEsHioig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@interchainjs/types": "1.17.1", + "bech32": "^2.0.0", + "decimal.js": "^10.4.3" + }, + "peerDependencies": { + "@chain-registry/v2": "^1.71.186", + "@chain-registry/v2-types": "^0.53.115" + } + }, + "node_modules/@interchainjs/utils/node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" }, "engines": { - "node": ">=8" + "node": "20 || >=22" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/schemas": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz", - "integrity": "sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==", + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dev": true, + "license": "ISC", "dependencies": { - "@sinclair/typebox": "^0.24.1" + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "node": ">=12" } }, - "node_modules/@jest/transform": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz", - "integrity": "sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA==", + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^28.1.3", - "@jridgewell/trace-mapping": "^0.3.13", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^28.1.3", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.3", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.1" - }, + "license": "MIT", "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/transform/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/@jest/transform/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@jest/transform/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, + "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "ansi-regex": "^6.0.1" }, "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/transform/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@jest/transform/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/transform/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz", - "integrity": "sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^28.1.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/@jest/types/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@jest/types/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/@jest/types/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/@jest/types/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/types/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -2283,6 +1122,59 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "dev": true }, + "node_modules/@noble/ciphers": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", + "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pyramation/json-schema-ref-parser": { "version": "9.0.6", "resolved": "https://registry.npmjs.org/@pyramation/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz", @@ -2337,6 +1229,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, + "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2383,11 +1276,44 @@ "node": "*" } }, - "node_modules/@sinclair/typebox": { - "version": "0.24.51", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", - "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", - "dev": true + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@types/glob": { "version": "7.2.0", @@ -2399,39 +1325,6 @@ "@types/node": "*" } }, - "node_modules/@types/graceful-fs": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", - "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", - "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", - "dev": true - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", - "dev": true, - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", @@ -2462,20 +1355,27 @@ "integrity": "sha512-+68kP9yzs4LMp7VNh8gdzMSPZFL44MLGqiHWvttYJe+6qnuVr4Ek9wSBQoveqY/r+LwjCcU29kNVkidwim+kYA==", "dev": true }, - "node_modules/@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", + "node_modules/@types/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-F3OznnSLAUxFrCEu/L5PY8+ny8DtcFRjx7fZZ9bycvXRi3KPTRS9HOitGZwvPg0juRhXFWIeKX58cnX5YqLohQ==", "dev": true, + "license": "MIT", "dependencies": { - "@types/yargs-parser": "*" + "@types/glob": "*", + "@types/node": "*" } }, - "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true + "node_modules/@types/shelljs": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/@types/shelljs/-/shelljs-0.8.15.tgz", + "integrity": "sha512-vzmnCHl6hViPu9GNLQJ+DZFd6BQI2DBTUeOvYHqkWQLMfKAAQYMb/xAmZkTogZI/vqXHCWkqDRymDI5p0QTi5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/glob": "~7.2.0", + "@types/node": "*" + } }, "node_modules/abbrev": { "version": "1.1.1", @@ -2581,19 +1481,21 @@ } }, "node_modules/ast-stringify": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ast-stringify/-/ast-stringify-0.1.0.tgz", - "integrity": "sha512-J1PgFYV3RG6r37+M6ySZJH406hR82okwGvFM9hLXpOvdx4WC4GEW8/qiw6pi1hKTrqcRvoHP8a7mp87egYr6iA==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/ast-stringify/-/ast-stringify-0.2.1.tgz", + "integrity": "sha512-wS/5cTSysM/IL1NVAwI2/xhFDI08UEw4PpLsbpDfc1KEPc2ABo94pjvB5p/tIF+Qu7oFOdxx8TdlGhyu4ZOb3A==", "dev": true, - "dependencies": { - "@babel/runtime": "^7.11.2" - } + "license": "SEE LICENSE IN LICENSE" }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2601,67 +1503,50 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", - "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.3.tgz", - "integrity": "sha512-zKsXDh0XjnrUEW0mxIHLfjBfnXSMr5Q/goMe/fxpQnLm07mcOZiIZHBNWCMx60HmdvjxfXcalac0tfFg0wqxyw==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.2", - "core-js-compat": "^3.21.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bech32": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz", + "integrity": "sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -2671,6 +1556,23 @@ "node": ">=8" } }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, + "node_modules/bn.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", + "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -2692,55 +1594,58 @@ "node": ">=8" } }, - "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", - "dev": true, - "dependencies": { - "node-int64": "^0.4.0" - } + "license": "MIT" }, "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2752,40 +1657,12 @@ "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", "dev": true }, - "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001522", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001522.tgz", - "integrity": "sha512-TKiyTVZxJGhsTszLuzb+6vUZSjVOAhClszBr2Ta2k9IwtNBT/4dzmL6aywt0HCgEZlmwJzXJd8yNiob6HgwTRg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ] - }, "node_modules/case": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/case/-/case-1.6.3.tgz", "integrity": "sha512-mzDSXIPaFwVDvZAHqZ9VlbyF4yyXRuX6IvB06WvPYkqJVO24kX1PPhv9bfpKNFZyxYFmmgo03HUiD8iklmJYRQ==", "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", "engines": { "node": ">= 0.8.0" } @@ -2837,21 +1714,6 @@ "fsevents": "~2.3.2" } }, - "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "engines": { - "node": ">=8" - } - }, "node_modules/cli-color": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.3.tgz", @@ -2925,25 +1787,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, - "node_modules/core-js-compat": { - "version": "3.32.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.32.1.tgz", - "integrity": "sha512-GSvKDv4wE0bPnQtjklV101juQ85g6H3rm5PDP20mqlS5j0kXF3pP97YvAu5hl+uFHqMictp3b2VxOHljWMAtuA==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -2989,12 +1832,13 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -3005,15 +1849,41 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/deepmerge": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/define-lazy-prop": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", @@ -3058,11 +1928,27 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/dotty": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dotty/-/dotty-0.1.2.tgz", - "integrity": "sha512-V0EWmKeH3DEhMwAZ+8ZB2Ao4OK6p++Z0hsDtZq3N0+0ZMVqkzrcEGROvOnZpLnvBg5PTNG23JEDLAm64gPaotQ==", - "dev": true + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" }, "node_modules/ee-first": { "version": "1.1.1", @@ -3070,11 +1956,35 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, - "node_modules/electron-to-chromium": { - "version": "1.4.499", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.499.tgz", - "integrity": "sha512-0NmjlYBLKVHva4GABWAaHuPJolnDuL0AhV3h1hES6rcLCWEIbRL6/8TghfsVwkx6TEroQVdliX7+aLysUpKvjw==", - "dev": true + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", + "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" }, "node_modules/encodeurl": { "version": "1.0.2", @@ -3147,6 +2057,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", @@ -3226,15 +2169,6 @@ "es6-symbol": "^3.1.1" } }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -3263,15 +2197,6 @@ "node": ">=4" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -3281,6 +2206,40 @@ "node": ">= 0.6" } }, + "node_modules/ethereum-cryptography": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ethereum-cryptography/-/ethereum-cryptography-3.2.0.tgz", + "integrity": "sha512-Urr5YVsalH+Jo0sYkTkv1MyI9bLYZwW8BENZCeE1QYaTHETEYx0Nv/SVsWkSqpYrzweg6d8KMY1wTjH/1m/BIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/ciphers": "1.3.0", + "@noble/curves": "1.9.0", + "@noble/hashes": "1.8.0", + "@scure/bip32": "1.7.0", + "@scure/bip39": "1.6.0" + }, + "engines": { + "node": "^14.21.3 || >=16", + "npm": ">=9" + } + }, + "node_modules/ethereum-cryptography/node_modules/@noble/curves": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.0.tgz", + "integrity": "sha512-7YDlXiNMdO1YZeH6t/kvopHHbIZzlxrCV9WLqCY6QhcXOoXiNCMDqJIglZ9Yjx5+w7Dz30TITFrlTjnRg7sKEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -3320,21 +2279,6 @@ "node": ">=4" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fb-watchman": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", - "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", - "dev": true, - "dependencies": { - "bser": "2.1.1" - } - }, "node_modules/figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -3392,26 +2336,114 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", "dev": true, + "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/foreground-child/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" }, "engines": { "node": ">=8" } }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "node_modules/foreground-child/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", "dependencies": { - "is-callable": "^1.1.3" + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, "node_modules/fresh": { @@ -3444,10 +2476,14 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -3485,37 +2521,43 @@ "node": ">= 0.6.0" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, "engines": { - "node": ">=8.0.0" + "node": ">= 0.4" } }, "node_modules/get-stdin": { @@ -3547,19 +2589,21 @@ } }, "node_modules/glob": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", - "integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, + "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": ">=12" + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3577,11 +2621,28 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -3602,12 +2663,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3671,12 +2733,13 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.1.1" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3695,10 +2758,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3707,12 +2771,13 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -3721,6 +2786,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -3761,15 +2862,6 @@ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", "dev": true }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3942,6 +3034,26 @@ "inquirer-autocomplete-prompt": "^0.11.1" } }, + "node_modules/interchainjs": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/interchainjs/-/interchainjs-1.17.1.tgz", + "integrity": "sha512-7i22pCVrW+5EKmC16NFoR0lvI0DQ7DQlOLynRY4WeMN5Kc0Y/hZ5M3TzbcE3NXbtv82ygxJYwNIlrL24+xpXpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@interchainjs/cosmos": "1.17.1", + "@interchainjs/cosmos-types": "1.17.1", + "@interchainjs/crypto": "1.17.1", + "@interchainjs/encoding": "1.17.1", + "@interchainjs/ethereum": "1.17.1", + "@interchainjs/math": "1.17.1", + "@interchainjs/pubkey": "1.17.1", + "@interchainjs/types": "1.17.1", + "@interchainjs/utils": "1.17.1", + "@noble/hashes": "^1.3.1", + "decimal.js": "^10.4.3" + } + }, "node_modules/internal-slot": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", @@ -4210,12 +3322,13 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, + "license": "MIT", "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -4260,201 +3373,42 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", - "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" + "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-haste-map": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.3.tgz", - "integrity": "sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^28.0.2", - "jest-util": "^28.1.3", - "jest-worker": "^28.1.3", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" }, "optionalDependencies": { - "fsevents": "^2.3.2" + "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/jest-regex-util": { - "version": "28.0.2", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz", - "integrity": "sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==", + "node_modules/js-sha3": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", + "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==", "dev": true, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-util": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz", - "integrity": "sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ==", - "dev": true, - "dependencies": { - "@jest/types": "^28.1.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-util/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/jest-util/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/jest-util/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/jest-util/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker": { - "version": "28.1.3", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz", - "integrity": "sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g==", - "dev": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } + "license": "MIT" }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4468,6 +3422,7 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -4481,24 +3436,29 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/jsonc-parser": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", "dev": true }, + "node_modules/libsodium-sumo": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-sumo/-/libsodium-sumo-0.7.15.tgz", + "integrity": "sha512-5tPmqPmq8T8Nikpm1Nqj0hBHvsLFCXvdhBFV7SGOitQPZAA6jso8XoL0r4L7vmfKXr486fiQInvErHtEvizFMw==", + "dev": true, + "license": "ISC" + }, + "node_modules/libsodium-wrappers-sumo": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/libsodium-wrappers-sumo/-/libsodium-wrappers-sumo-0.7.15.tgz", + "integrity": "sha512-aSWY8wKDZh5TC7rMvEdTHoyppVq/1dTSAeAR7H6pzd6QRT3vQWcT5pGwCotLcpPEOLXX6VvqihSPkpEhYAjANA==", + "dev": true, + "license": "ISC", + "dependencies": { + "libsodium-sumo": "^0.7.15" + } + }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -4514,44 +3474,18 @@ "node": ">=4" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true - }, - "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", - "dev": true - }, "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } + "license": "ISC" }, "node_modules/lru-queue": { "version": "0.1.0", @@ -4568,15 +3502,6 @@ "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", "dev": true }, - "node_modules/makeerror": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", - "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", - "dev": true, - "dependencies": { - "tmpl": "1.0.5" - } - }, "node_modules/marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -4589,6 +3514,16 @@ "node": ">= 12" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/memoizee": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.15.tgz", @@ -4614,25 +3549,6 @@ "node": ">= 0.10.0" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", @@ -4654,16 +3570,34 @@ "node": ">=4" } }, - "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", "dev": true, + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "dev": true, + "license": "MIT" + }, + "node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { - "node": ">=10" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -4672,6 +3606,16 @@ "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -4685,10 +3629,11 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/mute-stream": { "version": "0.0.7", @@ -4707,6 +3652,13 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nested-obj": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/nested-obj/-/nested-obj-0.0.1.tgz", + "integrity": "sha512-kB1WKTng+IePQhZVs1UXtFaHBx4QEM5a0XKGAzYfCKvdx5DhNjCytNDWMUGpNNpHLotln+tiwcA52kWCIgGq1Q==", + "dev": true, + "license": "SEE LICENSE IN LICENSE" + }, "node_modules/next-tick": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", @@ -4719,18 +3671,6 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, - "node_modules/node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", - "dev": true - }, "node_modules/nodemon": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.1.tgz", @@ -5019,41 +3959,12 @@ "node": ">=0.10.0" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } + "license": "BlueOak-1.0.0" }, "node_modules/parse-json": { "version": "4.0.0", @@ -5083,15 +3994,6 @@ "node": ">= 0.8" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -5116,6 +4018,23 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-type": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", @@ -5129,10 +4048,11 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", @@ -5167,13 +4087,14 @@ "node": ">=4" } }, - "node_modules/pirates": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 6" + "node": ">= 0.4" } }, "node_modules/prettier": { @@ -5232,6 +4153,13 @@ "node": ">=8.10.0" } }, + "node_modules/readonly-date": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/readonly-date/-/readonly-date-1.0.0.tgz", + "integrity": "sha512-tMKIV7hlk0h4mO3JTmmVuIlJVXjKk3Sep9Bf5OH0O+758ruuVkUy2J9SttDLm91IEX/WHlXPSpxMGjPj4beMIQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", @@ -5244,39 +4172,6 @@ "node": ">= 0.10" } }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", - "dev": true, - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", @@ -5294,44 +4189,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", - "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", - "dev": true, - "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dev": true, - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, "node_modules/reload": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/reload/-/reload-3.2.1.tgz", @@ -5368,15 +4225,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -5447,6 +4295,16 @@ "node": "*" } }, + "node_modules/rlp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rlp/-/rlp-3.0.0.tgz", + "integrity": "sha512-PD6U2PGk6Vq2spfgiWZdomLvRGDreBLxi5jv5M8EpRo3pU6VEm31KO+HFxE18Q3vgqfDrQ9pZA3FP95rkijNKw==", + "dev": true, + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp" + } + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -5501,6 +4359,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safe-regex-test": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", @@ -5521,15 +4400,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/send": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", @@ -5569,12 +4439,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", @@ -5590,12 +4454,51 @@ "node": ">= 0.8.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "dev": true }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -5762,15 +4665,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", @@ -5831,6 +4725,55 @@ "node": ">=4" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/ansi-regex": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", @@ -5926,6 +4869,30 @@ "node": ">=6" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", @@ -5972,62 +4939,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", - "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -6077,17 +4988,27 @@ "node": ">=0.6.0" } }, - "node_modules/tmpl": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", - "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", - "dev": true + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -6138,14 +5059,15 @@ "dev": true }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -6243,6 +5165,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6272,46 +5195,6 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6321,36 +5204,6 @@ "node": ">= 0.8" } }, - "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", @@ -6388,29 +5241,6 @@ "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", "dev": true }, - "node_modules/walker": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", - "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", - "dev": true, - "dependencies": { - "makeerror": "1.0.12" - } - }, - "node_modules/wasm-ast-types": { - "version": "0.26.2", - "resolved": "https://registry.npmjs.org/wasm-ast-types/-/wasm-ast-types-0.26.2.tgz", - "integrity": "sha512-DM34W0Sexd/BreN4g1ZjfaDC8hkKIw3FykV7vT9KdhCoroH+Qncz+jvSEI0fEDqbtJkWiP9Z+u61a5LPObHKaA==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.18.9", - "@babel/types": "7.18.10", - "@jest/transform": "28.1.3", - "ast-stringify": "0.1.0", - "case": "1.6.3", - "deepmerge": "4.2.2" - } - }, "node_modules/which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -6440,16 +5270,19 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", "dev": true, + "license": "MIT", "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6458,25 +5291,200 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, - "node_modules/write-file-atomic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", - "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, "node_modules/ws": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", @@ -6497,12 +5505,6 @@ "optional": true } } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true } } } diff --git a/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json b/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json index f3c7b6aad6..02138582d9 100644 --- a/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json +++ b/sdk/typescript/tests/integration-tests/mix-fetch/package-lock.json @@ -642,9 +642,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -1058,9 +1058,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz", - "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "license": "MIT", "dependencies": { "pump": "^3.0.0", diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 1731d38bef..2c11e6ea3f 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -4,7 +4,7 @@ [package] name = "nym-network-requester" license = "GPL-3.0" -version = "1.1.68" +version = "1.1.69" authors.workspace = true edition.workspace = true rust-version = "1.85" diff --git a/tools/echo-server/Cargo.toml b/tools/echo-server/Cargo.toml index 390f746bcc..f58a0e46b3 100644 --- a/tools/echo-server/Cargo.toml +++ b/tools/echo-server/Cargo.toml @@ -26,7 +26,7 @@ serde = { workspace = true, features = ["derive"] } tracing.workspace = true tracing-subscriber = { workspace = true } bytecodec = { workspace = true } -nym-sdk = { path = "../../sdk/rust/nym-sdk/" } +nym-sdk = { path = "../../sdk/rust/nym-sdk" } bytes.workspace = true dirs.workspace = true clap.workspace = true diff --git a/tools/internal/testnet-manager/src/manager/contract.rs b/tools/internal/testnet-manager/src/manager/contract.rs index 5792dbd23e..4c04ade5d0 100644 --- a/tools/internal/testnet-manager/src/manager/contract.rs +++ b/tools/internal/testnet-manager/src/manager/contract.rs @@ -7,6 +7,7 @@ use nym_validator_client::nyxd::cosmwasm_client::types::{ ContractCodeId, InstantiateResult, MigrateResult, UploadResult, }; use nym_validator_client::nyxd::{AccountId, Hash}; +use nym_validator_client::signing::signer::OfflineSigner; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; @@ -145,12 +146,9 @@ impl Account { pub(crate) fn new() -> Account { let mnemonic = bip39::Mnemonic::generate(24).unwrap(); // sure, we're using hardcoded prefix, but realistically this will never change - let wallet = DirectSecp256k1HdWallet::from_mnemonic("n", mnemonic.clone()); - let acc = wallet.try_derive_accounts().unwrap().pop().unwrap(); - Account { - address: acc.address, - mnemonic, - } + let wallet = DirectSecp256k1HdWallet::checked_from_mnemonic("n", mnemonic.clone()).unwrap(); + let address = wallet.signer_addresses().pop().unwrap(); + Account { address, mnemonic } } pub(crate) fn address(&self) -> AccountId { diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs index 8e3aff09c0..3969bfea33 100644 --- a/tools/internal/testnet-manager/src/manager/local_client.rs +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -213,12 +213,14 @@ impl NetworkManager { id, "--enabled-credentials-mode", "true", + "--minimum-gateway-performance", + "0", "--port", &port.to_string(), ]) - .stdout(Stdio::null()) + // .stdout(Stdio::null()) .stdin(Stdio::null()) - .stderr(Stdio::null()) + // .stderr(Stdio::null()) .kill_on_drop(true); if let Some(gateway) = &ctx.gateway { @@ -243,10 +245,10 @@ impl NetworkManager { config_file, r#" -[debug.topology] -minimum_mixnode_performance = 0 -minimum_gateway_performance = 0 -"# + [debug.topology] + minimum_mixnode_performance = 0 + minimum_gateway_performance = 0 + "# )?; ctx.println(format!("\t✅client {id} is ready to use!")); diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs index f710939248..3ed0af70b3 100644 --- a/tools/internal/testnet-manager/src/manager/local_nodes.rs +++ b/tools/internal/testnet-manager/src/manager/local_nodes.rs @@ -577,7 +577,7 @@ impl NetworkManager { )); let id = ctx.nym_node_id(mixnode); cmds.push(format!( - "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise" + "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise --unsafe-disable-replay-protection" )); } @@ -588,7 +588,7 @@ impl NetworkManager { )); let id = ctx.nym_node_id(gateway); cmds.push(format!( - "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise" + "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise --unsafe-disable-replay-protection" )); } diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index 79edd2de27..1a8aa016eb 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.67" +version = "1.1.68" authors.workspace = true edition = "2021" license.workspace = true diff --git a/tools/nymvisor/Cargo.toml b/tools/nymvisor/Cargo.toml index 738a50a88e..7329119eb9 100644 --- a/tools/nymvisor/Cargo.toml +++ b/tools/nymvisor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nymvisor" -version = "0.1.32" +version = "0.1.33" authors.workspace = true repository.workspace = true homepage.workspace = true diff --git a/yarn.lock b/yarn.lock index 246c016a46..0fe96d803b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16349,9 +16349,9 @@ mimic-fn@^3.1.0: integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== min-document@^2.19.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" - integrity sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ== + version "2.19.1" + resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.1.tgz#7083ad4bc8879a6eba6516688e9f5d91d32e2d23" + integrity sha512-8lqe85PkqQJzIcs2iD7xW/WSxcncC3/DPVbTOafKNJDIMXwGfwXS350mH4SJslomntN2iYtFBuC0yNO3CEap6g== dependencies: dom-walk "^0.1.0"