diff --git a/.github/workflows/build-and-upload-binaries-ci.yml b/.github/workflows/build-and-upload-binaries-ci.yml index 849bd32a17..57b897d9c2 100644 --- a/.github/workflows/build-and-upload-binaries-ci.yml +++ b/.github/workflows/build-and-upload-binaries-ci.yml @@ -57,7 +57,7 @@ jobs: echo $OUTPUT_DIR - name: Install Dependencies (Linux) - run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools + run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler continue-on-error: true - name: Install Rust stable @@ -98,7 +98,6 @@ jobs: cp target/release/nym-network-requester $OUTPUT_DIR cp target/release/nym-network-statistics $OUTPUT_DIR cp target/release/nym-cli $OUTPUT_DIR - cp target/release/nym-credential-client $OUTPUT_DIR cp target/release/explorer-api $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm $OUTPUT_DIR diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d346df6296..df3ad85309 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,6 +6,7 @@ on: - 'clients/**' - 'common/**' - 'explorer-api/**' + - 'ephemera/**' - 'gateway/**' - 'integrations/**' - 'mixnode/**' @@ -15,6 +16,7 @@ on: - 'nym-api/**' - 'nym-outfox/**' - 'tools/nym-cli/**' + - 'tools/nym-nr-query/**' - 'tools/ts-rs-cli/**' - 'Cargo.toml' pull_request: @@ -22,6 +24,7 @@ on: - 'clients/**' - 'common/**' - 'explorer-api/**' + - 'ephemera/**' - 'gateway/**' - 'integrations/**' - 'mixnode/**' @@ -31,6 +34,7 @@ on: - 'nym-api/**' - 'nym-outfox/**' - 'tools/nym-cli/**' + - 'tools/nym-nr-query/**' - 'tools/ts-rs-cli/**' - 'Cargo.toml' @@ -42,7 +46,7 @@ jobs: RUSTC_WRAPPER: /home/ubuntu/.cargo/bin/sccache steps: - name: Install Dependencies (Linux) - run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools + run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler continue-on-error: true - name: Check out repository code diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index 1ee391d6a7..84ea9fabec 100644 --- a/.github/workflows/nightly_build.yml +++ b/.github/workflows/nightly_build.yml @@ -24,7 +24,7 @@ jobs: continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' || matrix.rust == 'stable' }} steps: - name: Install Dependencies (Linux) - run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools + run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler continue-on-error: true if: matrix.os == 'ubuntu-20.04' diff --git a/.github/workflows/nightly_build_release.yml b/.github/workflows/nightly_build_release.yml index fa1c465c97..6ddee762e1 100644 --- a/.github/workflows/nightly_build_release.yml +++ b/.github/workflows/nightly_build_release.yml @@ -37,7 +37,7 @@ jobs: continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' || matrix.rust == 'stable' }} steps: - name: Install Dependencies (Linux) - run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools + run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler continue-on-error: true if: matrix.os == 'ubuntu-20.04' diff --git a/.github/workflows/nym-connect-publish-windows10.yml b/.github/workflows/nym-connect-publish-windows10.yml index 12c296a16d..f90ce1697d 100644 --- a/.github/workflows/nym-connect-publish-windows10.yml +++ b/.github/workflows/nym-connect-publish-windows10.yml @@ -100,6 +100,7 @@ jobs: nym-connect/desktop/target/release/bundle/msi/*.msi.zip* - id: release-info name: Prepare release info + shell: bash run: | ref=${{ github.ref_name }} semver="${ref##nym-connect-}" && semver="${semver##v}" diff --git a/.github/workflows/run-api-tests.yml b/.github/workflows/run-api-tests.yml index 1d2afec2bd..fbc3d63eea 100644 --- a/.github/workflows/run-api-tests.yml +++ b/.github/workflows/run-api-tests.yml @@ -32,5 +32,5 @@ jobs: run: yarn - name: Run tests - run: yarn test:qa + run: yarn test:sandbox working-directory: nym-api/tests diff --git a/.github/workflows/upload-nyxd-ci.yml b/.github/workflows/upload-nyxd-ci.yml deleted file mode 100644 index 1668bf3184..0000000000 --- a/.github/workflows/upload-nyxd-ci.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Upload nyxd to CI - -on: - workflow_dispatch: - -jobs: - publish-nyxd: - strategy: - fail-fast: false - matrix: - platform: [ubuntu-20.04] - - runs-on: ${{ matrix.platform }} - steps: - - uses: actions/checkout@v3 - - - name: Prepare build output directory - shell: bash - env: - OUTPUT_DIR: ci-builds/nyxd - run: | - rm -rf ci-builds || true - mkdir -p $OUTPUT_DIR - echo $OUTPUT_DIR - - - name: Install Dependencies (Linux) - run: sudo apt-get update && sudo apt-get -y install build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools git - continue-on-error: true - - - name: Update env variables to include go - run: | - sudo rm -rf /usr/local/go - curl https://dl.google.com/go/go1.19.2.linux-amd64.tar.gz | sudo tar -C/usr/local -zxvf - - cat <<'EOF' >>$HOME/.profile - export GOROOT=/usr/local/go - export GOPATH=$HOME/go - export GO111MODULE=on - export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin - EOF - source $HOME/.profile - - - name: Verify Go is installed - run: go version - - - name: Clone nyxd repo - run: | - git clone https://github.com/tommyv1987/nyxd - cd nyxd - git checkout release/v0.30.2 - - - name: Run nyxd - run: | - pwd - cd nyxd && make build - sleep 10 - ls /home/runner/work/nym/nym/nyxd/build - - - name: Prepare build output - shell: bash - env: - OUTPUT_DIR: ci-builds/nyxd - run: | - cp /home/runner/work/nym/nym/nyxd/build/nyxd $OUTPUT_DIR - WASMVM_SO=$(ldd /home/runner/work/nym/nym/nyxd/build/nyxd | grep "libwasm*" | awk '{ print $3 }') - ls $WASMVM_SO - sleep 3 - cp $(echo $WASMVM_SO) $OUTPUT_DIR - - - name: Deploy nyxd to CI www - continue-on-error: true - uses: easingthemes/ssh-deploy@main - env: - SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }} - ARGS: "-avzr" - SOURCE: "ci-builds/" - REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }} - REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }} - TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/ - EXCLUDE: "/dist/, /node_modules/" diff --git a/.github/workflows/wallet.yml b/.github/workflows/wallet.yml index d77fb9d6fb..34f202d25f 100644 --- a/.github/workflows/wallet.yml +++ b/.github/workflows/wallet.yml @@ -31,7 +31,7 @@ jobs: uses: actions-rs/toolchain@v1 with: profile: minimal - toolchain: stable + toolchain: 1.71.0 override: true components: rustfmt, clippy diff --git a/CHANGELOG.md b/CHANGELOG.md index e728e00506..2df4f7e732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,27 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] -## [1.1.28] (2023-08-22) +## [v1.1.29-snickers] (2023-08-29) + +- Add EXPLORER_API configurable url ([#3810]) +- Bugfix/use correct tendermint dialect ([#3802]) +- Explorer - look up gateways based on geo-location ([#3776]) +- Speedy mode - select the mixnodes based on the location of the NR ([#3775]) +- NR - reduce response time by removing poisson delay ([#3774]) +- [demo] libp2p example with nym-sdk ([#3763]) +- introduced /network/details endpoint to nym-api to return used network information ([#3758]) +- Feature/issue credentials ([#3691]) + +[#3810]: https://github.com/nymtech/nym/pull/3810 +[#3802]: https://github.com/nymtech/nym/pull/3802 +[#3776]: https://github.com/nymtech/nym/issues/3776 +[#3775]: https://github.com/nymtech/nym/issues/3775 +[#3774]: https://github.com/nymtech/nym/issues/3774 +[#3763]: https://github.com/nymtech/nym/pull/3763 +[#3758]: https://github.com/nymtech/nym/pull/3758 +[#3691]: https://github.com/nymtech/nym/pull/3691 + +## [v1.1.28] (2023-08-22) - [final step3]: add [rust] support to nyxd client in wasm ([#3743]) - Feature/ephemera upgrade ([#3791]) @@ -19,7 +39,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// [#3767]: https://github.com/nymtech/nym/pull/3767 -## [1.1.27] (2023-08-16) +## [v1.1.27] (2023-08-16) - fix serialisation of contract types ([#3752]) - Investigate spending credentials from the main API (coconut enabled to a gateway) from feature/ephemera branch ([#3741]) diff --git a/Cargo.lock b/Cargo.lock index cc54eee9dc..6a09ca15d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ "futures-core", "futures-sink", "memchr", - "pin-project-lite", + "pin-project-lite 0.2.12", "tokio", "tokio-util", "tracing", @@ -54,7 +54,7 @@ dependencies = [ "local-channel", "mime", "percent-encoding", - "pin-project-lite", + "pin-project-lite 0.2.12", "rand 0.8.5", "sha1", "smallvec", @@ -123,7 +123,7 @@ checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" dependencies = [ "futures-core", "paste", - "pin-project-lite", + "pin-project-lite 0.2.12", ] [[package]] @@ -133,7 +133,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8" dependencies = [ "local-waker", - "pin-project-lite", + "pin-project-lite 0.2.12", ] [[package]] @@ -166,7 +166,7 @@ dependencies = [ "log", "mime", "once_cell", - "pin-project-lite", + "pin-project-lite 0.2.12", "regex", "serde", "serde_json", @@ -569,6 +569,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "asn1_der" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "155a5a185e42c6b77ac7b88a15143d930a9e9727a5b7b77eed417404ab15c247" + [[package]] name = "assert_matches" version = "1.5.0" @@ -633,7 +639,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", - "pin-project-lite", + "pin-project-lite 0.2.12", ] [[package]] @@ -668,7 +674,7 @@ dependencies = [ "futures-sink", "futures-util", "memchr", - "pin-project-lite", + "pin-project-lite 0.2.12", ] [[package]] @@ -746,7 +752,7 @@ dependencies = [ "memchr", "mime", "percent-encoding", - "pin-project-lite", + "pin-project-lite 0.2.12", "rustversion", "serde", "sync_wrapper", @@ -840,9 +846,9 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.64.0" +version = "0.65.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" dependencies = [ "bitflags 1.3.2", "cexpr", @@ -850,12 +856,13 @@ dependencies = [ "lazy_static", "lazycell", "peeking_take_while", + "prettyplease 0.2.12", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 1.0.109", + "syn 2.0.28", ] [[package]] @@ -883,8 +890,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f2635620bf0b9d4576eb7bb9a38a55df78bd1205d26fa994b25911a69f212f" dependencies = [ "bitcoin_hashes", - "rand 0.6.5", - "rand_core 0.4.2", + "rand 0.8.5", + "rand_core 0.6.4", "serde", "unicode-normalization", "zeroize", @@ -2699,10 +2706,10 @@ dependencies = [ "futures", "futures-util", "lazy_static", - "libp2p", + "libp2p 0.51.3", "libp2p-identity", "log", - "lru", + "lru 0.10.0", "nym-config", "nym-ephemera-common", "nym-task", @@ -2761,7 +2768,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "explorer-api" -version = "1.1.26" +version = "1.1.27" dependencies = [ "chrono", "clap 4.3.21", @@ -2933,6 +2940,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.26" @@ -2940,6 +2953,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", + "libz-sys", "miniz_oxide", ] @@ -3093,7 +3107,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite", + "pin-project-lite 0.2.12", "waker-fn", ] @@ -3150,7 +3164,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite", + "pin-project-lite 0.2.12", "pin-utils", "slab", ] @@ -3610,7 +3624,7 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", - "pin-project-lite", + "pin-project-lite 0.2.12", ] [[package]] @@ -3666,7 +3680,7 @@ dependencies = [ "httparse", "httpdate", "itoa", - "pin-project-lite", + "pin-project-lite 0.2.12", "socket2 0.4.9", "tokio", "tower-service", @@ -3718,7 +3732,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", - "pin-project-lite", + "pin-project-lite 0.2.12", "tokio", "tokio-io-timeout", ] @@ -4262,6 +4276,35 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +[[package]] +name = "libp2p" +version = "0.51.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "bytes", + "futures", + "futures-timer", + "getrandom 0.2.10", + "instant", + "libp2p-core 0.39.0", + "libp2p-dns 0.39.0 (git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6)", + "libp2p-gossipsub 0.44.0", + "libp2p-identify", + "libp2p-mdns 0.43.0", + "libp2p-metrics 0.12.0 (git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6)", + "libp2p-mplex", + "libp2p-noise 0.42.0", + "libp2p-ping", + "libp2p-quic 0.7.0-alpha.2", + "libp2p-swarm 0.42.0", + "libp2p-tcp 0.39.0 (git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6)", + "libp2p-webrtc 0.4.0-alpha.2", + "libp2p-websocket", + "libp2p-yamux 0.43.0", + "multiaddr 0.17.0", + "pin-project", +] + [[package]] name = "libp2p" version = "0.51.3" @@ -4275,21 +4318,21 @@ dependencies = [ "instant", "libp2p-allow-block-list", "libp2p-connection-limits", - "libp2p-core", - "libp2p-dns", - "libp2p-gossipsub", + "libp2p-core 0.39.2", + "libp2p-dns 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libp2p-gossipsub 0.44.4", "libp2p-identity", "libp2p-kad", - "libp2p-mdns", - "libp2p-metrics", - "libp2p-noise", - "libp2p-quic", + "libp2p-mdns 0.43.1", + "libp2p-metrics 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libp2p-noise 0.42.2", + "libp2p-quic 0.7.0-alpha.3", "libp2p-request-response", - "libp2p-swarm", - "libp2p-tcp", - "libp2p-webrtc", - "libp2p-yamux", - "multiaddr", + "libp2p-swarm 0.42.2", + "libp2p-tcp 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libp2p-webrtc 0.4.0-alpha.4", + "libp2p-yamux 0.43.1", + "multiaddr 0.17.1", "pin-project", ] @@ -4299,9 +4342,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "510daa05efbc25184458db837f6f9a5143888f1caa742426d92e1833ddd38a50" dependencies = [ - "libp2p-core", + "libp2p-core 0.39.2", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.42.2", "void", ] @@ -4311,12 +4354,45 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4caa33f1d26ed664c4fe2cca81a08c8e07d4c1c04f2f4ac7655c2dd85467fda0" dependencies = [ - "libp2p-core", + "libp2p-core 0.39.2", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.42.2", "void", ] +[[package]] +name = "libp2p-core" +version = "0.39.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "asn1_der", + "bs58 0.4.0", + "ed25519-dalek", + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "log", + "multiaddr 0.17.0", + "multihash", + "multistream-select 0.12.1 (git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6)", + "once_cell", + "parking_lot 0.12.1", + "pin-project", + "prost", + "prost-build", + "rand 0.8.5", + "rw-stream-sink 0.3.0 (git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6)", + "sec1 0.3.0", + "sha2 0.10.7", + "smallvec", + "thiserror", + "unsigned-varint", + "void", + "zeroize", +] + [[package]] name = "libp2p-core" version = "0.39.2" @@ -4330,15 +4406,15 @@ dependencies = [ "instant", "libp2p-identity", "log", - "multiaddr", + "multiaddr 0.17.1", "multihash", - "multistream-select", + "multistream-select 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", "once_cell", "parking_lot 0.12.1", "pin-project", "quick-protobuf", "rand 0.8.5", - "rw-stream-sink", + "rw-stream-sink 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde", "smallvec", "thiserror", @@ -4353,13 +4429,55 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "146ff7034daae62077c415c2376b8057368042df6ab95f5432ad5e88568b1554" dependencies = [ "futures", - "libp2p-core", + "libp2p-core 0.39.2", "log", "parking_lot 0.12.1", "smallvec", "trust-dns-resolver", ] +[[package]] +name = "libp2p-dns" +version = "0.39.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "futures", + "libp2p-core 0.39.0", + "log", + "parking_lot 0.12.1", + "smallvec", + "trust-dns-resolver", +] + +[[package]] +name = "libp2p-gossipsub" +version = "0.44.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "asynchronous-codec", + "base64 0.21.2", + "byteorder", + "bytes", + "fnv", + "futures", + "hex_fmt", + "instant", + "libp2p-core 0.39.0", + "libp2p-swarm 0.42.0", + "log", + "prometheus-client", + "prost", + "prost-build", + "prost-codec", + "rand 0.8.5", + "regex", + "sha2 0.10.7", + "smallvec", + "thiserror", + "unsigned-varint", + "wasm-timer", +] + [[package]] name = "libp2p-gossipsub" version = "0.44.4" @@ -4375,9 +4493,9 @@ dependencies = [ "futures", "hex_fmt", "instant", - "libp2p-core", + "libp2p-core 0.39.2", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.42.2", "log", "prometheus-client", "quick-protobuf", @@ -4393,6 +4511,27 @@ dependencies = [ "wasm-timer", ] +[[package]] +name = "libp2p-identify" +version = "0.42.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "asynchronous-codec", + "either", + "futures", + "futures-timer", + "libp2p-core 0.39.0", + "libp2p-swarm 0.42.0", + "log", + "lru 0.9.0", + "prost", + "prost-build", + "prost-codec", + "smallvec", + "thiserror", + "void", +] + [[package]] name = "libp2p-identity" version = "0.1.2" @@ -4402,7 +4541,7 @@ dependencies = [ "bs58 0.4.0", "ed25519-dalek", "log", - "multiaddr", + "multiaddr 0.17.1", "multihash", "quick-protobuf", "rand 0.8.5", @@ -4426,9 +4565,9 @@ dependencies = [ "futures", "futures-timer", "instant", - "libp2p-core", + "libp2p-core 0.39.2", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.42.2", "log", "quick-protobuf", "rand 0.8.5", @@ -4441,6 +4580,25 @@ dependencies = [ "void", ] +[[package]] +name = "libp2p-mdns" +version = "0.43.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "data-encoding", + "futures", + "if-watch", + "libp2p-core 0.39.0", + "libp2p-swarm 0.42.0", + "log", + "rand 0.8.5", + "smallvec", + "socket2 0.4.9", + "tokio", + "trust-dns-proto", + "void", +] + [[package]] name = "libp2p-mdns" version = "0.43.1" @@ -4450,9 +4608,9 @@ dependencies = [ "data-encoding", "futures", "if-watch", - "libp2p-core", + "libp2p-core 0.39.2", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.42.2", "log", "rand 0.8.5", "smallvec", @@ -4468,13 +4626,65 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a42ec91e227d7d0dafa4ce88b333cdf5f277253873ab087555c92798db2ddd46" dependencies = [ - "libp2p-core", - "libp2p-gossipsub", + "libp2p-core 0.39.2", + "libp2p-gossipsub 0.44.4", "libp2p-kad", - "libp2p-swarm", + "libp2p-swarm 0.42.2", "prometheus-client", ] +[[package]] +name = "libp2p-metrics" +version = "0.12.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "libp2p-core 0.39.0", + "libp2p-gossipsub 0.44.0", + "libp2p-identify", + "libp2p-ping", + "libp2p-swarm 0.42.0", + "prometheus-client", +] + +[[package]] +name = "libp2p-mplex" +version = "0.39.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "asynchronous-codec", + "bytes", + "futures", + "libp2p-core 0.39.0", + "log", + "nohash-hasher", + "parking_lot 0.12.1", + "rand 0.8.5", + "smallvec", + "unsigned-varint", +] + +[[package]] +name = "libp2p-noise" +version = "0.42.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "bytes", + "curve25519-dalek 3.2.0", + "futures", + "libp2p-core 0.39.0", + "log", + "once_cell", + "prost", + "prost-build", + "rand 0.8.5", + "sha2 0.10.7", + "snow", + "static_assertions", + "thiserror", + "x25519-dalek 1.1.1", + "zeroize", +] + [[package]] name = "libp2p-noise" version = "0.42.2" @@ -4484,7 +4694,7 @@ dependencies = [ "bytes", "curve25519-dalek 3.2.0", "futures", - "libp2p-core", + "libp2p-core 0.39.2", "libp2p-identity", "log", "once_cell", @@ -4498,6 +4708,42 @@ dependencies = [ "zeroize", ] +[[package]] +name = "libp2p-ping" +version = "0.42.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "either", + "futures", + "futures-timer", + "instant", + "libp2p-core 0.39.0", + "libp2p-swarm 0.42.0", + "log", + "rand 0.8.5", + "void", +] + +[[package]] +name = "libp2p-quic" +version = "0.7.0-alpha.2" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "bytes", + "futures", + "futures-timer", + "if-watch", + "libp2p-core 0.39.0", + "libp2p-tls 0.1.0-alpha.2", + "log", + "parking_lot 0.12.1", + "quinn-proto", + "rand 0.8.5", + "rustls 0.20.8", + "thiserror", + "tokio", +] + [[package]] name = "libp2p-quic" version = "0.7.0-alpha.3" @@ -4508,9 +4754,9 @@ dependencies = [ "futures", "futures-timer", "if-watch", - "libp2p-core", + "libp2p-core 0.39.2", "libp2p-identity", - "libp2p-tls", + "libp2p-tls 0.1.0", "log", "parking_lot 0.12.1", "quinn-proto", @@ -4529,13 +4775,32 @@ dependencies = [ "async-trait", "futures", "instant", - "libp2p-core", + "libp2p-core 0.39.2", "libp2p-identity", - "libp2p-swarm", + "libp2p-swarm 0.42.2", "rand 0.8.5", "smallvec", ] +[[package]] +name = "libp2p-swarm" +version = "0.42.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "either", + "fnv", + "futures", + "futures-timer", + "instant", + "libp2p-core 0.39.0", + "libp2p-swarm-derive 0.32.0 (git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6)", + "log", + "rand 0.8.5", + "smallvec", + "tokio", + "void", +] + [[package]] name = "libp2p-swarm" version = "0.42.2" @@ -4547,9 +4812,9 @@ dependencies = [ "futures", "futures-timer", "instant", - "libp2p-core", + "libp2p-core 0.39.2", "libp2p-identity", - "libp2p-swarm-derive", + "libp2p-swarm-derive 0.32.0 (registry+https://github.com/rust-lang/crates.io-index)", "log", "rand 0.8.5", "smallvec", @@ -4568,6 +4833,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "libp2p-swarm-derive" +version = "0.32.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "heck 0.4.1", + "quote", + "syn 1.0.109", +] + [[package]] name = "libp2p-tcp" version = "0.39.0" @@ -4578,12 +4853,44 @@ dependencies = [ "futures-timer", "if-watch", "libc", - "libp2p-core", + "libp2p-core 0.39.2", "log", "socket2 0.4.9", "tokio", ] +[[package]] +name = "libp2p-tcp" +version = "0.39.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "futures", + "futures-timer", + "if-watch", + "libc", + "libp2p-core 0.39.0", + "log", + "socket2 0.4.9", + "tokio", +] + +[[package]] +name = "libp2p-tls" +version = "0.1.0-alpha.2" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "futures", + "futures-rustls", + "libp2p-core 0.39.0", + "rcgen 0.10.0", + "ring", + "rustls 0.20.8", + "thiserror", + "webpki 0.22.0", + "x509-parser 0.14.0", + "yasna", +] + [[package]] name = "libp2p-tls" version = "0.1.0" @@ -4592,7 +4899,7 @@ checksum = "ff08d13d0dc66e5e9ba6279c1de417b84fa0d0adc3b03e5732928c180ec02781" dependencies = [ "futures", "futures-rustls", - "libp2p-core", + "libp2p-core 0.39.2", "libp2p-identity", "rcgen 0.10.0", "ring", @@ -4603,6 +4910,36 @@ dependencies = [ "yasna", ] +[[package]] +name = "libp2p-webrtc" +version = "0.4.0-alpha.2" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "async-trait", + "asynchronous-codec", + "bytes", + "futures", + "futures-timer", + "hex", + "if-watch", + "libp2p-core 0.39.0", + "libp2p-noise 0.42.0", + "log", + "multihash", + "prost", + "prost-build", + "prost-codec", + "rand 0.8.5", + "rcgen 0.9.3", + "serde", + "stun", + "thiserror", + "tinytemplate", + "tokio", + "tokio-util", + "webrtc", +] + [[package]] name = "libp2p-webrtc" version = "0.4.0-alpha.4" @@ -4616,9 +4953,9 @@ dependencies = [ "futures-timer", "hex", "if-watch", - "libp2p-core", + "libp2p-core 0.39.2", "libp2p-identity", - "libp2p-noise", + "libp2p-noise 0.42.2", "log", "multihash", "quick-protobuf", @@ -4634,6 +4971,36 @@ dependencies = [ "webrtc", ] +[[package]] +name = "libp2p-websocket" +version = "0.41.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "either", + "futures", + "futures-rustls", + "libp2p-core 0.39.0", + "log", + "parking_lot 0.12.1", + "quicksink", + "rw-stream-sink 0.3.0 (git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6)", + "soketto", + "url", + "webpki-roots 0.22.6", +] + +[[package]] +name = "libp2p-yamux" +version = "0.43.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "futures", + "libp2p-core 0.39.0", + "log", + "thiserror", + "yamux", +] + [[package]] name = "libp2p-yamux" version = "0.43.1" @@ -4641,7 +5008,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd21d950662700a385d4c6d68e2f5f54d778e97068cdd718522222ef513bda" dependencies = [ "futures", - "libp2p-core", + "libp2p-core 0.39.2", "log", "thiserror", "yamux", @@ -4649,9 +5016,9 @@ dependencies = [ [[package]] name = "librocksdb-sys" -version = "0.10.0+7.9.2" +version = "0.11.0+8.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe4d5874f5ff2bc616e55e8c6086d478fcda13faf9495768a4aa1c22042d30b" +checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" dependencies = [ "bindgen", "bzip2-sys", @@ -4765,6 +5132,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lru" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e7d46de488603ffdd5f30afbc64fbba2378214a2c3a2fb83abf3d33126df17" +dependencies = [ + "hashbrown 0.13.2", +] + [[package]] name = "lru" version = "0.10.0" @@ -4944,6 +5320,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "multiaddr" +version = "0.17.0" +source = "git+https://github.com/ChainSafe/rust-multiaddr.git?rev=2d21365f880622adf1c8e594b72e671dbac9dd64#2d21365f880622adf1c8e594b72e671dbac9dd64" +dependencies = [ + "arrayref", + "byteorder", + "data-encoding", + "multibase", + "multihash", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint", + "url", +] + [[package]] name = "multiaddr" version = "0.17.1" @@ -5003,6 +5396,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + [[package]] name = "multistream-select" version = "0.12.1" @@ -5017,6 +5416,19 @@ dependencies = [ "unsigned-varint", ] +[[package]] +name = "multistream-select" +version = "0.12.1" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "bytes", + "futures", + "log", + "pin-project", + "smallvec", + "unsigned-varint", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -5226,7 +5638,7 @@ dependencies = [ [[package]] name = "nym-api" -version = "1.1.27" +version = "1.1.28" dependencies = [ "actix-web", "anyhow", @@ -5373,7 +5785,7 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.26" +version = "1.1.27" dependencies = [ "anyhow", "base64 0.13.1", @@ -5399,6 +5811,7 @@ dependencies = [ name = "nym-cli-commands" version = "1.0.0" dependencies = [ + "anyhow", "base64 0.13.1", "bip39", "bs58 0.4.0", @@ -5412,10 +5825,16 @@ dependencies = [ "humantime-serde", "k256 0.13.1", "log", + "nym-bandwidth-controller", "nym-bin-common", + "nym-client-core", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", + "nym-config", "nym-contracts-common", + "nym-credential-storage", + "nym-credential-utils", + "nym-credentials", "nym-crypto", "nym-mixnet-contract-common", "nym-multisig-contract-common", @@ -5439,7 +5858,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.26" +version = "1.1.27" dependencies = [ "clap 4.3.21", "dirs 4.0.0", @@ -5479,6 +5898,7 @@ version = "1.1.15" dependencies = [ "async-trait", "base64 0.21.2", + "cfg-if", "dashmap 5.5.0", "dirs 4.0.0", "futures", @@ -5611,26 +6031,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "nym-credential-client" -version = "0.1.0" -dependencies = [ - "clap 4.3.21", - "log", - "nym-bandwidth-controller", - "nym-bin-common", - "nym-client-core", - "nym-config", - "nym-credential-storage", - "nym-credentials", - "nym-network-defaults", - "nym-pemstore", - "nym-validator-client", - "serde", - "thiserror", - "tokio", -] - [[package]] name = "nym-credential-storage" version = "0.1.0" @@ -5642,12 +6042,28 @@ dependencies = [ "tokio", ] +[[package]] +name = "nym-credential-utils" +version = "0.1.0" +dependencies = [ + "log", + "nym-bandwidth-controller", + "nym-client-core", + "nym-config", + "nym-credential-storage", + "nym-credentials", + "nym-validator-client", + "thiserror", + "tokio", +] + [[package]] name = "nym-credentials" version = "0.1.0" dependencies = [ "bls12_381 0.5.0", "cosmrs", + "log", "nym-api-requests", "nym-coconut-interface", "nym-crypto", @@ -5737,7 +6153,7 @@ dependencies = [ [[package]] name = "nym-gateway" -version = "1.1.26" +version = "1.1.27" dependencies = [ "anyhow", "async-trait", @@ -5892,7 +6308,7 @@ dependencies = [ [[package]] name = "nym-mixnode" -version = "1.1.27" +version = "1.1.28" dependencies = [ "anyhow", "bs58 0.4.0", @@ -6010,7 +6426,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.26" +version = "1.1.27" dependencies = [ "anyhow", "async-file-watcher", @@ -6057,7 +6473,7 @@ dependencies = [ [[package]] name = "nym-network-statistics" -version = "1.1.26" +version = "1.1.27" dependencies = [ "dirs 4.0.0", "log", @@ -6155,15 +6571,19 @@ dependencies = [ name = "nym-sdk" version = "0.1.0" dependencies = [ + "anyhow", "async-trait", "bip39", "dotenvy", "futures", + "hex", + "libp2p 0.51.0", "log", "nym-bandwidth-controller", "nym-bin-common", "nym-client-core", "nym-credential-storage", + "nym-credential-utils", "nym-credentials", "nym-crypto", "nym-gateway-requests", @@ -6173,12 +6593,15 @@ dependencies = [ "nym-task", "nym-topology", "nym-validator-client", + "parking_lot 0.12.1", "pretty_env_logger", "rand 0.7.3", "reqwest", "tap", "thiserror", "tokio", + "tokio-stream", + "tokio-util", "toml 0.5.11", "url", ] @@ -6215,7 +6638,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.26" +version = "1.1.27" dependencies = [ "clap 4.3.21", "lazy_static", @@ -6821,7 +7244,7 @@ dependencies = [ "futures-util", "indexmap 1.9.3", "once_cell", - "pin-project-lite", + "pin-project-lite 0.2.12", "thiserror", "urlencoding", ] @@ -7147,6 +7570,16 @@ dependencies = [ "sha2 0.10.7", ] +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.0.0", +] + [[package]] name = "pin-project" version = "1.1.3" @@ -7167,6 +7600,12 @@ dependencies = [ "syn 2.0.28", ] +[[package]] +name = "pin-project-lite" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" + [[package]] name = "pin-project-lite" version = "0.2.12" @@ -7251,7 +7690,7 @@ dependencies = [ "concurrent-queue", "libc", "log", - "pin-project-lite", + "pin-project-lite 0.2.12", "windows-sys 0.48.0", ] @@ -7327,6 +7766,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "prettyplease" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" +dependencies = [ + "proc-macro2", + "syn 2.0.28", +] + [[package]] name = "proc-macro-crate" version = "1.1.3" @@ -7416,6 +7865,40 @@ dependencies = [ "prost-derive", ] +[[package]] +name = "prost-build" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270" +dependencies = [ + "bytes", + "heck 0.4.1", + "itertools", + "lazy_static", + "log", + "multimap", + "petgraph", + "prettyplease 0.1.25", + "prost", + "prost-types", + "regex", + "syn 1.0.109", + "tempfile", + "which", +] + +[[package]] +name = "prost-codec" +version = "0.3.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "asynchronous-codec", + "bytes", + "prost", + "thiserror", + "unsigned-varint", +] + [[package]] name = "prost-derive" version = "0.11.9" @@ -7483,6 +7966,17 @@ dependencies = [ "unsigned-varint", ] +[[package]] +name = "quicksink" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77de3c815e5a160b1539c6592796801df2043ae35e123b46d73380cfa57af858" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project-lite 0.1.12", +] + [[package]] name = "quinn-proto" version = "0.9.3" @@ -7937,7 +8431,7 @@ dependencies = [ "native-tls", "once_cell", "percent-encoding", - "pin-project-lite", + "pin-project-lite 0.2.12", "serde", "serde_json", "serde_urlencoded", @@ -8028,7 +8522,7 @@ dependencies = [ "multer", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite", + "pin-project-lite 0.2.12", "rand 0.8.5", "ref-cast", "rocket_codegen", @@ -8093,7 +8587,7 @@ dependencies = [ "memchr", "pear", "percent-encoding", - "pin-project-lite", + "pin-project-lite 0.2.12", "ref-cast", "serde", "smallvec", @@ -8135,9 +8629,9 @@ dependencies = [ [[package]] name = "rocksdb" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "015439787fce1e75d55f279078d33ff14b4af5d93d995e8838ee4631301c8a99" +checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" dependencies = [ "libc", "librocksdb-sys", @@ -8362,6 +8856,16 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "rw-stream-sink" +version = "0.3.0" +source = "git+https://github.com/ChainSafe/rust-libp2p.git?rev=e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6#e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6" +dependencies = [ + "futures", + "pin-project", + "static_assertions", +] + [[package]] name = "ryu" version = "1.0.15" @@ -8392,7 +8896,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b08f58cf71a58bda5734758eb20051cdb66c06c9243badbc45092ced1be834df" dependencies = [ "macro_rules_attribute", - "prettyplease", + "prettyplease 0.1.25", "proc-macro2", "quote", "syn 1.0.109", @@ -8911,6 +9415,22 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "soketto" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d1c5305e39e09653383c2c7244f2f78b3bcae37cf50c64cb4789c9f5096ec2" +dependencies = [ + "base64 0.13.1", + "bytes", + "flate2", + "futures", + "httparse", + "log", + "rand 0.8.5", + "sha-1 0.9.8", +] + [[package]] name = "sphinx-packet" version = "0.1.0" @@ -9638,7 +10158,7 @@ dependencies = [ "mio", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite", + "pin-project-lite 0.2.12", "signal-hook-registry", "socket2 0.5.3", "tokio-macros", @@ -9652,7 +10172,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ - "pin-project-lite", + "pin-project-lite 0.2.12", "tokio", ] @@ -9718,7 +10238,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite", + "pin-project-lite 0.2.12", "tokio", "tokio-util", ] @@ -9785,7 +10305,7 @@ dependencies = [ "futures-sink", "futures-util", "hashbrown 0.12.3", - "pin-project-lite", + "pin-project-lite 0.2.12", "slab", "tokio", "tracing", @@ -9872,7 +10392,7 @@ dependencies = [ "futures-util", "indexmap 1.9.3", "pin-project", - "pin-project-lite", + "pin-project-lite 0.2.12", "rand 0.8.5", "slab", "tokio", @@ -9902,7 +10422,7 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "log", - "pin-project-lite", + "pin-project-lite 0.2.12", "tracing-attributes", "tracing-core", ] @@ -10048,9 +10568,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "ts-rs" -version = "6.2.1" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4added4070a4fdf9df03457206cd2e4b12417c8560a2954d91ffcbe60177a56a" +checksum = "e1ff1f8c90369bc172200013ac17ae86e7b5def580687df4e6127883454ff2b0" dependencies = [ "thiserror", "ts-rs-macros", @@ -10073,14 +10593,14 @@ dependencies = [ [[package]] name = "ts-rs-macros" -version = "6.2.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f807fdb3151fee75df7485b901a89624358cd07a67a8fb1a5831bf5a07681ff" +checksum = "a6f41cc0aeb7a4a55730188e147d3795a7349b501f8334697fd37629b896cdc2" dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.28", "termcolor", ] @@ -10871,6 +11391,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "widestring" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 696f1bd9f1..2c505c4bfe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ opt-level = 3 resolver = "2" members = [ - "clients/credential", "clients/native", "clients/native/websocket-requests", "clients/socks5", @@ -43,6 +42,7 @@ members = [ "common/cosmwasm-smart-contracts/vesting-contract", "common/credential-storage", "common/credentials", + "common/credential-utils", "common/crypto", "common/dkg", "common/execute", @@ -150,6 +150,7 @@ tap = "1.0.1" tendermint-rpc = "0.32" # same version as used by cosmrs thiserror = "1.0.38" tokio = "1.24.1" +ts-rs = "7.0.0" url = "2.4" zeroize = "1.6.0" diff --git a/Makefile b/Makefile index 2576489be9..6fc92e55f8 100644 --- a/Makefile +++ b/Makefile @@ -77,9 +77,6 @@ $(eval $(call add_cargo_workspace,contracts,contracts,--lib --target wasm32-unkn $(eval $(call add_cargo_workspace,wasm-client,clients/webassembly,--target wasm32-unknown-unknown)) $(eval $(call add_cargo_workspace,wallet,nym-wallet,)) $(eval $(call add_cargo_workspace,connect,nym-connect/desktop)) -ifdef NYM_MOBILE -$(eval $(call add_cargo_workspace,connect-mobile,nym-connect/mobile/src-tauri)) -endif # ----------------------------------------------------------------------------- # Convenience targets for crates that are already part of the main workspace diff --git a/clients/credential/README.md b/clients/credential/README.md deleted file mode 100644 index 9d82b4b839..0000000000 --- a/clients/credential/README.md +++ /dev/null @@ -1,32 +0,0 @@ - - -## Credential binary - -The credential binary is used to acquire coconut bandwidth credentials in exchange for nym tokens. Those credentials are stored in the client's `data` directory, so that they can be used as the client sees fit. - -### Warning - -The credential binary is still experimental software. The infrastructure for using it is not yet deployed to mainnet and it's still in the process of being deployed to sandbox. - -### Building - -From the project's root directory, run: -``` -cargo build -p nym-credential-client -``` -which generates the `nym-credential-client` binary in `target/debug/nym-credential-client`. - - -### Running - -For example, you can get a credential worth 3 nym (3000000 unym) in a socks5 client that was already initialized like so: - -``` -./target/debug/nym-credential-client --config-env-file envs/sandbox.env --client-home-directory ~/.nym/socks5-clients/cred_client --nyxd-url https://sandbox-validator1.nymtech.net --mnemonic $MNEMONIC --recovery-dir /tmp/recovery --amount 3000000 -``` - -More information regarding how to run the binary can be found by running it with the `--help` argument. - diff --git a/clients/credential/src/client.rs b/clients/credential/src/client.rs deleted file mode 100644 index 9eb0a7da75..0000000000 --- a/clients/credential/src/client.rs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::Result; -use bip39::Mnemonic; -use nym_network_defaults::{NymNetworkDetails, VOUCHER_INFO}; -use nym_validator_client::nyxd::contract_traits::CoconutBandwidthSigningClient; -use nym_validator_client::nyxd::{self, DirectSigningHttpRpcNyxdClient}; -use nym_validator_client::nyxd::{Coin, Fee, NyxdClient}; -use std::str::FromStr; -use url::Url; - -pub(crate) struct Client { - nyxd_client: DirectSigningHttpRpcNyxdClient, - mix_denom_base: String, -} - -impl Client { - pub fn new(nyxd_url: &str, mnemonic: &str) -> Self { - let nyxd_url = Url::from_str(nyxd_url).unwrap(); - let mnemonic = Mnemonic::from_str(mnemonic).unwrap(); - let network_details = NymNetworkDetails::new_from_env(); - let config = nyxd::Config::try_from_nym_network_details(&network_details) - .expect("failed to construct valid validator client config with the provided network"); - let nyxd_client = - NyxdClient::connect_with_mnemonic(config, nyxd_url.as_ref(), mnemonic, None).unwrap(); - - Client { - nyxd_client, - mix_denom_base: network_details.chain_details.mix_denom.base, - } - } - - pub async fn deposit( - &self, - amount: u64, - verification_key: String, - encryption_key: String, - fee: Option, - ) -> Result { - let amount = Coin::new(amount as u128, self.mix_denom_base.clone()); - Ok(self - .nyxd_client - .deposit( - amount, - String::from(VOUCHER_INFO), - verification_key, - encryption_key, - fee, - ) - .await? - .transaction_hash - .to_string()) - } -} diff --git a/clients/credential/src/commands.rs b/clients/credential/src/commands.rs deleted file mode 100644 index da603eb84b..0000000000 --- a/clients/credential/src/commands.rs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use clap::{ArgGroup, Args, Subcommand}; -use log::*; -use nym_bandwidth_controller::acquire::state::State; -use nym_bin_common::completions::ArgShell; -use nym_credential_storage::persistent_storage::PersistentStorage; -use nym_validator_client::nyxd::contract_traits::DkgQueryClient; - -use crate::error::Result; -use crate::recovery_storage::RecoveryStorage; - -#[derive(Subcommand)] -pub(crate) enum Command { - /// Run the binary to obtain a credential - Run(Run), - - /// Generate shell completions - Completions(ArgShell), - - /// Generate Fig specification - GenerateFigSpec, -} - -#[derive(Args)] -#[clap(group( -ArgGroup::new("recov") -.required(true) -.args(&["amount", "recovery_mode"]), -))] -pub(crate) struct Run { - /// Home directory of the client that is supposed to use the credential. - #[clap(long)] - pub(crate) client_home_directory: std::path::PathBuf, - - /// A mnemonic for the account that buys the credential - #[clap(long)] - pub(crate) mnemonic: String, - - /// The amount of utokens the credential will hold. If recovery mode is enabled, this value - /// is not needed - #[clap(long, default_value = "0")] - pub(crate) amount: u64, - - /// Path to a directory used to store recovery files for unconsumed deposits - #[clap(long)] - pub(crate) recovery_dir: std::path::PathBuf, - - /// Recovery mode, when enabled, tries to recover any deposit data dumped in recovery_dir - #[clap(long)] - pub(crate) recovery_mode: bool, -} - -pub(crate) async fn recover_credentials( - client: &C, - recovery_storage: &RecoveryStorage, - shared_storage: &PersistentStorage, -) -> Result<()> { - for voucher in recovery_storage.unconsumed_vouchers()? { - let state = State::new(voucher); - if let Err(e) = - nym_bandwidth_controller::acquire::get_credential(&state, client, shared_storage).await - { - error!( - "Could not recover deposit {} due to {:?}, try again later", - state.voucher.tx_hash(), - e - ) - } else { - info!( - "Converted deposit {} to a credential, removing recovery data for it", - state.voucher.tx_hash() - ); - if let Err(e) = recovery_storage.remove_voucher(state.voucher.tx_hash().to_string()) { - warn!("Could not remove recovery data - {:?}", e); - } - } - } - - Ok(()) -} diff --git a/clients/credential/src/error.rs b/clients/credential/src/error.rs deleted file mode 100644 index 1c6ac1521b..0000000000 --- a/clients/credential/src/error.rs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use std::time::SystemTimeError; -use thiserror::Error; - -use nym_credential_storage::error::StorageError; -use nym_credentials::error::Error as CredentialError; -use nym_validator_client::nyxd::error::NyxdError; -use nym_validator_client::ValidatorClientError; - -pub type Result = std::result::Result; - -#[derive(Error, Debug)] -pub enum CredentialClientError { - #[error("IO error: {0}")] - IOError(#[from] std::io::Error), - - #[error("Bandwidth controller error: {0}")] - BandwidthControllerError(#[from] nym_bandwidth_controller::error::BandwidthControllerError), - - #[error("Nyxd error: {0}")] - Nyxd(#[from] NyxdError), - - #[error("Validator client error: {0}")] - ValidatorClientError(#[from] ValidatorClientError), - - #[error("Credential error: {0}")] - Credential(#[from] CredentialError), - - #[error("Could not use shared storage")] - SharedStorageError(#[from] StorageError), - - #[error("Could not get system time")] - SysTimeError(#[from] SystemTimeError), -} diff --git a/clients/credential/src/main.rs b/clients/credential/src/main.rs deleted file mode 100644 index 982ef02823..0000000000 --- a/clients/credential/src/main.rs +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -mod commands; -mod error; -mod recovery_storage; - -use commands::*; -use error::Result; -use log::*; -use nym_bin_common::completions::fig_generate; -use nym_config::DEFAULT_DATA_DIR; -use nym_network_defaults::{setup_env, NymNetworkDetails}; -use std::process::exit; -use std::time::{Duration, SystemTime}; - -use clap::{CommandFactory, Parser}; -use nym_bin_common::logging::setup_logging; -use nym_client_core::config::disk_persistence::CommonClientPaths; -use nym_validator_client::nyxd::contract_traits::DkgQueryClient; -use nym_validator_client::nyxd::{Coin, Config}; -use nym_validator_client::DirectSigningHttpRpcNyxdClient; - -const SAFETY_BUFFER_SECS: u64 = 60; // 1 minute - -#[derive(Parser)] -#[clap(author = "Nymtech", version, about)] -struct Cli { - /// Path pointing to an env file that configures the client. - #[clap(short, long)] - pub(crate) config_env_file: Option, - - #[clap(subcommand)] - pub(crate) command: Command, -} - -async fn block_until_coconut_is_available( - client: &C, -) -> Result<()> { - loop { - let epoch = client.get_current_epoch().await?; - let current_timestamp_secs = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH)? - .as_secs(); - if epoch.state.is_final() { - if current_timestamp_secs + SAFETY_BUFFER_SECS >= epoch.finish_timestamp.seconds() { - info!("In the next {} minute(s), a transition will take place in the coconut system. Deposits should be halted in this time for safety reasons.", SAFETY_BUFFER_SECS / 60); - exit(0); - } - - break; - } else { - // Use 1 additional second to not start the next iteration immediately and spam get_current_epoch queries - let secs_until_final = epoch - .final_timestamp_secs() - .saturating_sub(current_timestamp_secs) - + 1; - info!("Approximately {} seconds until coconut is available. Sleeping until then. You can safely kill the process at any moment.", secs_until_final); - std::thread::sleep(Duration::from_secs(secs_until_final)); - } - } - - Ok(()) -} - -#[tokio::main] -async fn main() -> Result<()> { - let args = Cli::parse(); - setup_logging(); - setup_env(args.config_env_file.as_ref()); - let bin_name = "nym-credential-client"; - - match args.command { - Command::Run(r) => { - // we assume the structure of /data - let data_dir = r.client_home_directory.join(DEFAULT_DATA_DIR); - let paths = CommonClientPaths::new_default(data_dir); - let db_path = paths.credentials_database; - - let shared_storage = - nym_credential_storage::initialise_persistent_storage(db_path).await; - let recovery_storage = recovery_storage::RecoveryStorage::new(r.recovery_dir)?; - - let network_details = NymNetworkDetails::new_from_env(); - let config = Config::try_from_nym_network_details(&network_details).expect( - "failed to construct valid validator client config with the provided network", - ); - let amount = Coin::new( - r.amount as u128, - network_details.chain_details.mix_denom.base, - ); - let endpoint = network_details.endpoints[0].nyxd_url.as_str(); - let client = DirectSigningHttpRpcNyxdClient::connect_with_mnemonic( - config, - endpoint, - r.mnemonic.parse().unwrap(), - )?; - - block_until_coconut_is_available(&client).await?; - info!("Starting depositing funds, don't kill the process"); - - if !r.recovery_mode { - let state = nym_bandwidth_controller::acquire::deposit(&client, amount).await?; - if nym_bandwidth_controller::acquire::get_credential( - &state, - &client, - &shared_storage, - ) - .await - .is_err() - { - warn!("Failed to obtain credential. Dumping recovery data.",); - match recovery_storage.insert_voucher(&state.voucher) { - Ok(file_path) => { - warn!("Dumped recovery data to {:?}. Try using recovery mode to convert it to a credential", file_path); - } - Err(e) => { - error!("Could not dump recovery data to file system due to {:?}, the deposit will be lost!", e) - } - } - } - } else { - recover_credentials(&client, &recovery_storage, &shared_storage).await?; - } - } - Command::Completions(c) => c.generate(&mut Cli::command(), bin_name), - Command::GenerateFigSpec => fig_generate(&mut Cli::command(), bin_name), - } - - Ok(()) -} diff --git a/clients/credential/src/recovery_storage.rs b/clients/credential/src/recovery_storage.rs deleted file mode 100644 index db81d2ff6a..0000000000 --- a/clients/credential/src/recovery_storage.rs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use nym_credentials::coconut::bandwidth::BandwidthVoucher; -use std::fs::{create_dir_all, read_dir, File}; -use std::io::{Read, Write}; -use std::path::PathBuf; - -pub struct RecoveryStorage { - recovery_dir: PathBuf, -} - -impl RecoveryStorage { - pub fn new(recovery_dir: PathBuf) -> std::io::Result { - create_dir_all(&recovery_dir)?; - Ok(Self { recovery_dir }) - } - - pub fn unconsumed_vouchers(&self) -> std::io::Result> { - Ok(read_dir(&self.recovery_dir)? - .filter_map(|entry| entry.ok()) - .filter_map(|entry| { - let path = entry.path(); - if path.is_file() { - Some(path) - } else { - None - } - }) - .filter_map(|path| File::open(path).ok()) - .filter_map(|mut f| { - let mut buff = Vec::new(); - if f.read_to_end(&mut buff).is_ok() { - Some(buff) - } else { - None - } - }) - .filter_map(|buff| BandwidthVoucher::try_from_bytes(&buff).ok())) - } - - pub fn insert_voucher(&self, voucher: &BandwidthVoucher) -> std::io::Result { - let file_name = voucher.tx_hash().to_string(); - let file_path = self.recovery_dir.join(file_name); - let mut file = File::create(&file_path)?; - let buff = voucher.to_bytes(); - file.write_all(&buff)?; - - Ok(file_path) - } - - pub fn remove_voucher(&self, file_name: String) -> std::io::Result<()> { - let file_path = self.recovery_dir.join(file_name); - std::fs::remove_file(file_path) - } -} diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index b94b1e7b76..351f8fb295 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.26" +version = "1.1.27" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/native/websocket-requests/src/requests.rs b/clients/native/websocket-requests/src/requests.rs index 3c4c1a618b..645f2edf9c 100644 --- a/clients/native/websocket-requests/src/requests.rs +++ b/clients/native/websocket-requests/src/requests.rs @@ -105,10 +105,10 @@ impl ClientRequest { let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes(); std::iter::once(ClientRequestTag::Send as u8) - .chain(recipient.to_bytes().into_iter()) // will not be length prefixed because the length is constant - .chain(conn_id_bytes.into_iter()) - .chain(data_len_bytes.into_iter()) - .chain(data.into_iter()) + .chain(recipient.to_bytes()) // will not be length prefixed because the length is constant + .chain(conn_id_bytes) + .chain(data_len_bytes) + .chain(data) .collect() } @@ -180,11 +180,11 @@ impl ClientRequest { let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes(); std::iter::once(ClientRequestTag::SendAnonymous as u8) - .chain(reply_surbs.to_be_bytes().into_iter()) - .chain(recipient.to_bytes().into_iter()) // will not be length prefixed because the length is constant - .chain(conn_id_bytes.into_iter()) - .chain(data_len_bytes.into_iter()) - .chain(data.into_iter()) + .chain(reply_surbs.to_be_bytes()) + .chain(recipient.to_bytes()) // will not be length prefixed because the length is constant + .chain(conn_id_bytes) + .chain(data_len_bytes) + .chain(data) .collect() } @@ -258,10 +258,10 @@ impl ClientRequest { let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes(); std::iter::once(ClientRequestTag::Reply as u8) - .chain(sender_tag.to_bytes().into_iter()) - .chain(conn_id_bytes.into_iter()) - .chain(message_len_bytes.into_iter()) - .chain(message.into_iter()) + .chain(sender_tag.to_bytes()) + .chain(conn_id_bytes) + .chain(message_len_bytes) + .chain(message) .collect() } @@ -332,7 +332,7 @@ impl ClientRequest { fn serialize_closed_connection(connection_id: u64) -> Vec { let conn_id_bytes = connection_id.to_be_bytes(); std::iter::once(ClientRequestTag::ClosedConnection as u8) - .chain(conn_id_bytes.into_iter()) + .chain(conn_id_bytes) .collect() } @@ -359,7 +359,7 @@ impl ClientRequest { fn serialize_get_lane_queue_lengths(connection_id: u64) -> Vec { let conn_id_bytes = connection_id.to_be_bytes(); std::iter::once(ClientRequestTag::GetLaneQueueLength as u8) - .chain(conn_id_bytes.into_iter()) + .chain(conn_id_bytes) .collect() } diff --git a/clients/native/websocket-requests/src/responses.rs b/clients/native/websocket-requests/src/responses.rs index d326388570..713054ecf3 100644 --- a/clients/native/websocket-requests/src/responses.rs +++ b/clients/native/websocket-requests/src/responses.rs @@ -67,15 +67,15 @@ impl ServerResponse { if let Some(sender_tag) = reconstructed_message.sender_tag { std::iter::once(ServerResponseTag::Received as u8) .chain(std::iter::once(true as u8)) - .chain(sender_tag.to_bytes().into_iter()) + .chain(sender_tag.to_bytes()) .chain(message_len_bytes.iter().cloned()) - .chain(reconstructed_message.message.into_iter()) + .chain(reconstructed_message.message) .collect() } else { std::iter::once(ServerResponseTag::Received as u8) .chain(std::iter::once(false as u8)) .chain(message_len_bytes.iter().cloned()) - .chain(reconstructed_message.message.into_iter()) + .chain(reconstructed_message.message) .collect() } } @@ -149,7 +149,7 @@ impl ServerResponse { // SELF_ADDRESS_RESPONSE_TAG || self_address fn serialize_self_address(address: Recipient) -> Vec { std::iter::once(ServerResponseTag::SelfAddress as u8) - .chain(address.to_bytes().into_iter()) + .chain(address.to_bytes()) .collect() } @@ -211,8 +211,8 @@ impl ServerResponse { let message_len_bytes = (error.message.len() as u64).to_be_bytes(); std::iter::once(ServerResponseTag::Error as u8) .chain(std::iter::once(error.kind as u8)) - .chain(message_len_bytes.into_iter()) - .chain(error.message.into_bytes().into_iter()) + .chain(message_len_bytes) + .chain(error.message.into_bytes()) .collect() } diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 8532f9d2b7..a52422b5d0 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.26" +version = "1.1.27" 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/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index 393429246d..d2b37b5a84 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -17,7 +17,7 @@ use nym_client_core::client::base_client::storage::gateway_details::{ }; use nym_client_core::client::key_manager::persistence::OnDiskKeys; use nym_client_core::client::topology_control::geo_aware_provider::CountryGroup; -use nym_client_core::config::{GatewayEndpointConfig, TopologyStructure}; +use nym_client_core::config::{GatewayEndpointConfig, GroupBy, TopologyStructure}; use nym_client_core::error::ClientCoreError; use nym_config::OptionalSet; use nym_sphinx::params::{PacketSize, PacketType}; @@ -101,9 +101,17 @@ pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config { let secondary_packet_size = args.medium_toggle.then_some(PacketSize::ExtendedPacket16); let no_per_hop_delays = args.medium_toggle; - let topology_structure = if args.medium_toggle || args.geo_routing.is_some() { - // TODO: rethink the default group. I just picked one for now. - TopologyStructure::GeoAware(args.geo_routing.unwrap_or(CountryGroup::Europe)) + let topology_structure = if args.medium_toggle { + // Use the location of the network-requester + let address = config + .core + .socks5 + .provider_mix_address + .parse() + .expect("failed to parse provider mix address"); + TopologyStructure::GeoAware(GroupBy::NymAddress(address)) + } else if let Some(code) = args.geo_routing { + TopologyStructure::GeoAware(GroupBy::CountryGroup(code)) } else { TopologyStructure::default() }; diff --git a/clients/webassembly/Cargo.lock b/clients/webassembly/Cargo.lock index 8449b45ff6..271ea93ea8 100644 --- a/clients/webassembly/Cargo.lock +++ b/clients/webassembly/Cargo.lock @@ -2503,6 +2503,7 @@ version = "1.1.15" dependencies = [ "async-trait", "base64 0.21.2", + "cfg-if 1.0.0", "dashmap", "dirs 4.0.0", "futures", @@ -2676,6 +2677,7 @@ version = "0.1.0" dependencies = [ "bls12_381 0.5.0", "cosmrs", + "log", "nym-api-requests", "nym-coconut-interface", "nym-crypto", diff --git a/clients/webassembly/src/client/helpers.rs b/clients/webassembly/src/client/helpers.rs index c3f456a891..7d5373853b 100644 --- a/clients/webassembly/src/client/helpers.rs +++ b/clients/webassembly/src/client/helpers.rs @@ -114,7 +114,7 @@ impl WasmTopologyExt for Arc { let this = Arc::clone(self); future_to_promise(async move { let Some(current_topology) = this.topology_accessor.current_topology().await else { - return Err(WasmClientError::UnavailableNetworkTopology.into()) + return Err(WasmClientError::UnavailableNetworkTopology.into()); }; match current_topology.find_mix_by_identity(&mixnode_identity) { @@ -135,7 +135,7 @@ impl WasmTopologyExt for Arc { let this = Arc::clone(self); future_to_promise(async move { let Some(current_topology) = this.topology_accessor.current_topology().await else { - return Err(WasmClientError::UnavailableNetworkTopology.into()) + return Err(WasmClientError::UnavailableNetworkTopology.into()); }; let Some(mix) = current_topology.find_mix_by_identity(&mixnode_identity) else { diff --git a/clients/webassembly/src/encoded_payload_helper.rs b/clients/webassembly/src/encoded_payload_helper.rs index 84f463ec58..17ae92fbf8 100644 --- a/clients/webassembly/src/encoded_payload_helper.rs +++ b/clients/webassembly/src/encoded_payload_helper.rs @@ -52,7 +52,7 @@ pub fn encode_payload_with_headers( Ok(metadata) => { let metadata = metadata.as_bytes().to_vec(); let size = (metadata.len() as u64).to_be_bytes().to_vec(); - Ok(vec![size, metadata, payload].concat()) + Ok([size, metadata, payload].concat()) } Err(e) => Err(JsValue::from(JsError::new( format!("Could not encode message: {}", e).as_str(), diff --git a/clients/webassembly/src/storage/mod.rs b/clients/webassembly/src/storage/mod.rs index f4ab912657..febbd9ecd5 100644 --- a/clients/webassembly/src/storage/mod.rs +++ b/clients/webassembly/src/storage/mod.rs @@ -1,6 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::rc::Rc; + use crate::client::config::Config; use crate::storage::error::ClientStorageError; use js_sys::Promise; @@ -8,7 +10,6 @@ use nym_client_core::client::base_client::storage::gateway_details::PersistedGat use nym_crypto::asymmetric::{encryption, identity}; use nym_gateway_client::SharedKeys; use nym_sphinx::acknowledgements::AckKey; -use std::sync::Arc; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::future_to_promise; use wasm_utils::storage::{IdbVersionChangeEvent, WasmStorage}; @@ -43,7 +44,7 @@ mod v1 { pub struct ClientStorage { #[allow(dead_code)] pub(crate) name: String, - pub(crate) inner: Arc, + pub(crate) inner: Rc, } #[wasm_bindgen] @@ -88,7 +89,7 @@ impl ClientStorage { .await?; Ok(ClientStorage { - inner: Arc::new(inner), + inner: Rc::new(inner), name, }) } diff --git a/clients/webassembly/src/tester/ephemeral_receiver.rs b/clients/webassembly/src/tester/ephemeral_receiver.rs index 826bfa07b2..2d3117addd 100644 --- a/clients/webassembly/src/tester/ephemeral_receiver.rs +++ b/clients/webassembly/src/tester/ephemeral_receiver.rs @@ -58,7 +58,7 @@ impl<'a> EphemeralTestReceiver<'a> { let Some(received_packet) = packet else { // can't do anything more... console_error!("packet receiver has stopped processing results!"); - return true + return true; }; match received_packet { Received::Message(msg) => { diff --git a/clients/webassembly/src/tester/mod.rs b/clients/webassembly/src/tester/mod.rs index ba70062f3b..6433c029c7 100644 --- a/clients/webassembly/src/tester/mod.rs +++ b/clients/webassembly/src/tester/mod.rs @@ -29,6 +29,7 @@ use nym_validator_client::client::IdentityKey; use nym_validator_client::QueryReqwestRpcNyxdClient; use rand::rngs::OsRng; use std::collections::HashSet; +use std::rc::Rc; use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::sync::{Arc, Mutex as SyncMutex}; use std::time::Duration; @@ -42,7 +43,7 @@ pub(crate) mod helpers; pub type NodeTestMessage = TestMessage; type LockedGatewayClient = - Arc>>; + Rc>>; pub(crate) const DEFAULT_TEST_TIMEOUT: Duration = Duration::from_secs(10); pub(crate) const DEFAULT_TEST_PACKETS: u32 = 20; @@ -210,7 +211,7 @@ impl NymNodeTesterBuilder { test_in_progress: Arc::new(AtomicBool::new(false)), current_test_nonce: Default::default(), tester: Arc::new(SyncMutex::new(tester)), - gateway_client: Arc::new(AsyncMutex::new(gateway_client)), + gateway_client: Rc::new(AsyncMutex::new(gateway_client)), processed_receiver: ReceivedReceiverWrapper::new(processed_receiver), _task_manager: task_manager, }) @@ -343,7 +344,7 @@ impl NymNodeTester { )); let processed_receiver_clone = self.processed_receiver.clone(); - let gateway_client_clone = Arc::clone(&self.gateway_client); + let gateway_client_clone = Rc::clone(&self.gateway_client); let tester_marker = TestMarker::new(Arc::clone(&self.test_in_progress)); // start doing async things (send packets and watch for anything coming back) diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index bcb21899fe..56733b5ab8 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -10,6 +10,7 @@ rust-version = "1.66" [dependencies] async-trait = { workspace = true } base64 = "0.21.2" +cfg-if = "1.0.0" dashmap = "5.4.0" dirs = "4.0" futures = { workspace = true } diff --git a/common/client-core/src/client/base_client/mod.rs b/common/client-core/src/client/base_client/mod.rs index 2f2e7d0072..02615f2b95 100644 --- a/common/client-core/src/client/base_client/mod.rs +++ b/common/client-core/src/client/base_client/mod.rs @@ -354,11 +354,13 @@ where nym_api_urls, env!("CARGO_PKG_VERSION").to_string(), )), - config::TopologyStructure::GeoAware(group) => Box::new(GeoAwareTopologyProvider::new( - nym_api_urls, - env!("CARGO_PKG_VERSION").to_string(), - group, - )), + config::TopologyStructure::GeoAware(group_by) => { + Box::new(GeoAwareTopologyProvider::new( + nym_api_urls, + env!("CARGO_PKG_VERSION").to_string(), + group_by, + )) + } }) } diff --git a/common/client-core/src/client/topology_control/geo_aware_provider.rs b/common/client-core/src/client/topology_control/geo_aware_provider.rs index 268f6612e0..dabece05e5 100644 --- a/common/client-core/src/client/topology_control/geo_aware_provider.rs +++ b/common/client-core/src/client/topology_control/geo_aware_provider.rs @@ -1,23 +1,48 @@ use std::{collections::HashMap, fmt}; use log::{debug, error, info}; -use nym_explorer_api_requests::PrettyDetailedMixNodeBond; +use nym_explorer_api_requests::{PrettyDetailedGatewayBond, PrettyDetailedMixNodeBond}; +use nym_network_defaults::var_names::EXPLORER_API; use nym_topology::{ nym_topology_from_detailed, provider_trait::{async_trait, TopologyProvider}, NymTopology, }; -use nym_validator_client::client::MixId; +use nym_validator_client::client::{IdentityKey, MixId}; use rand::{prelude::SliceRandom, thread_rng}; use serde::{Deserialize, Serialize}; +use tap::TapOptional; use url::Url; +use crate::config::GroupBy; + const MIN_NODES_PER_LAYER: usize = 1; -const EXPLORER_API_MIXNODES_URL: &str = "https://explorer.nymtech.net/api/v1/mix-nodes"; + +#[cfg(target_arch = "wasm32")] +fn reqwest_client() -> Option { + reqwest::Client::builder().build().ok() +} + +#[cfg(not(target_arch = "wasm32"))] +fn reqwest_client() -> Option { + reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .ok() +} // TODO: create a explorer-api-client async fn fetch_mixnodes_from_explorer_api() -> Option> { - reqwest::get(EXPLORER_API_MIXNODES_URL) + let explorer_api_url = std::env::var(EXPLORER_API).ok()?; + let explorer_api_url = Url::parse(&explorer_api_url) + .ok()? + .join("v1/mix-nodes") + .ok()?; + + debug!("Fetching: {}", explorer_api_url); + reqwest_client()? + .get(explorer_api_url) + .send() .await .ok()? .json::>() @@ -25,6 +50,25 @@ async fn fetch_mixnodes_from_explorer_api() -> Option Option> { + let explorer_api_url = std::env::var(EXPLORER_API).ok()?; + let explorer_api_url = Url::parse(&explorer_api_url) + .ok()? + .join("v1/gateways") + .ok()?; + + debug!("Fetching: {}", explorer_api_url); + reqwest_client()? + .get(explorer_api_url) + .send() + .await + .ok()? + .json::>() + .await + .ok() +} + #[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)] pub enum CountryGroup { Europe, @@ -191,6 +235,23 @@ fn group_mixnodes_by_country_code( }) } +fn group_gateways_by_country_code( + gateways: Vec, +) -> HashMap> { + gateways.into_iter().fold( + HashMap::>::new(), + |mut acc, g| { + if let Some(ref location) = g.location { + let country_code = location.two_letter_iso_country_code.clone(); + let group_code = CountryGroup::new(country_code.as_str()); + let gateways = acc.entry(group_code).or_insert_with(Vec::new); + gateways.push(g.gateway.identity_key) + } + acc + }, + ) +} + fn log_mixnode_distribution(mixnodes: &HashMap>) { let mixnode_distribution = mixnodes .iter() @@ -200,6 +261,15 @@ fn log_mixnode_distribution(mixnodes: &HashMap>) { debug!("Mixnode distribution - {}", mixnode_distribution); } +fn log_gateway_distribution(gateways: &HashMap>) { + let gateway_distribution = gateways + .iter() + .map(|(k, v)| format!("{}: {}", k, v.len())) + .collect::>() + .join(", "); + debug!("Gateway distribution - {}", gateway_distribution); +} + fn check_layer_integrity(topology: NymTopology) -> Result<(), ()> { let mixes = topology.mixes(); if mixes.keys().len() < 3 { @@ -222,7 +292,7 @@ fn check_layer_integrity(topology: NymTopology) -> Result<(), ()> { pub struct GeoAwareTopologyProvider { validator_client: nym_validator_client::client::NymApiClient, - filter_on: CountryGroup, + filter_on: GroupBy, client_version: String, } @@ -230,7 +300,7 @@ impl GeoAwareTopologyProvider { pub fn new( mut nym_api_urls: Vec, client_version: String, - filter_on: CountryGroup, + filter_on: GroupBy, ) -> GeoAwareTopologyProvider { log::info!( "Creating geo-aware topology provider with filter on {:?}", @@ -272,6 +342,38 @@ impl GeoAwareTopologyProvider { return None; }; + debug!("Fetching gateways from explorer-api..."); + let Some(gateways_from_explorer_api) = fetch_gateways_from_explorer_api().await else { + error!("failed to get mixnodes from explorer-api"); + return None; + }; + + // Determine what we should filter around + let filter_on = match self.filter_on { + GroupBy::CountryGroup(group) => group, + GroupBy::NymAddress(recipient) => { + // Convert recipient into a country group by extracting out the gateway part and + // using that as the country code. + let gateway = recipient.gateway().to_base58_string(); + + // Lookup the location of this gateway by using the location data from the + // explorer-api + let gateway_location = gateways_from_explorer_api + .iter() + .find(|g| g.gateway.identity_key == gateway) + .and_then(|g| g.location.clone()) + .map(|location| location.two_letter_iso_country_code) + .tap_none(|| error!("No location found for the gateway: {}", gateway))?; + debug!( + "Filtering on nym-address: {}, with location: {}", + recipient, gateway_location + ); + + CountryGroup::new(&gateway_location) + } + }; + debug!("Filter group: {}", filter_on); + // Partition mixnodes_from_explorer_api according to the value of // two_letter_iso_country_code. // NOTE: we construct the full distribution here, but only use the one we're interested in. @@ -280,8 +382,16 @@ impl GeoAwareTopologyProvider { let mixnode_distribution = group_mixnodes_by_country_code(mixnodes_from_explorer_api); log_mixnode_distribution(&mixnode_distribution); - let Some(filtered_mixnode_ids) = mixnode_distribution.get(&self.filter_on) else { - error!("no mixnodes found for: {}", self.filter_on); + let gateway_distribution = group_gateways_by_country_code(gateways_from_explorer_api); + log_gateway_distribution(&gateway_distribution); + + let Some(filtered_mixnode_ids) = mixnode_distribution.get(&filter_on) else { + error!("no mixnodes found for: {}", filter_on); + return None; + }; + + let Some(filtered_gateway_ids) = gateway_distribution.get(&filter_on) else { + error!("no gateways found for: {}", filter_on); return None; }; @@ -290,6 +400,11 @@ impl GeoAwareTopologyProvider { .filter(|m| filtered_mixnode_ids.contains(&m.mix_id())) .collect::>(); + let gateways = gateways + .into_iter() + .filter(|g| filtered_gateway_ids.contains(g.identity())) + .collect::>(); + let topology = nym_topology_from_detailed(mixnodes, gateways) .filter_system_version(&self.client_version); diff --git a/common/client-core/src/config/mod.rs b/common/client-core/src/config/mod.rs index 465c227e19..f3cee8200f 100644 --- a/common/client-core/src/config/mod.rs +++ b/common/client-core/src/config/mod.rs @@ -3,7 +3,10 @@ use nym_config::defaults::NymNetworkDetails; use nym_crypto::asymmetric::identity; -use nym_sphinx::params::{PacketSize, PacketType}; +use nym_sphinx::{ + addressing::clients::Recipient, + params::{PacketSize, PacketType}, +}; use serde::{Deserialize, Serialize}; use std::time::Duration; use url::Url; @@ -480,11 +483,19 @@ pub struct Topology { pub topology_structure: TopologyStructure, } +#[allow(clippy::large_enum_variant)] #[derive(Default, Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum TopologyStructure { #[default] NymApi, - GeoAware(CountryGroup), + GeoAware(GroupBy), +} + +#[allow(clippy::large_enum_variant)] +#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum GroupBy { + CountryGroup(CountryGroup), + NymAddress(Recipient), } impl Default for Topology { diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index f3d2716143..0ee5c8b3af 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -65,7 +65,7 @@ features = ["tokio"] [dev-dependencies] bip39 = { workspace = true } cosmrs = { workspace = true, features = ["bip32"] } -ts-rs = "6.1.2" +ts-rs = { workspace = true } [[example]] name = "offline_signing" diff --git a/common/client-libs/validator-client/examples/offline_signing.rs b/common/client-libs/validator-client/examples/offline_signing.rs index baa2d10035..0e5bce5681 100644 --- a/common/client-libs/validator-client/examples/offline_signing.rs +++ b/common/client-libs/validator-client/examples/offline_signing.rs @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use cosmrs::bank::MsgSend; -use cosmrs::rpc::HttpClient; use cosmrs::tx::Msg; 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::tx_signer::TxSigner; @@ -27,7 +27,7 @@ async fn main() { // possibly remote client that doesn't do ANY signing // (only broadcasts + queries for sequence numbers) - let broadcaster = HttpClient::new(validator).unwrap(); + let broadcaster = http_client(validator).unwrap(); // get signer information let sequence_response = broadcaster.get_sequence(&signer_address).await.unwrap(); diff --git a/common/client-libs/validator-client/examples/query_name_service.rs b/common/client-libs/validator-client/examples/query_name_service.rs index 68e4da4cf2..f705cb3564 100644 --- a/common/client-libs/validator-client/examples/query_name_service.rs +++ b/common/client-libs/validator-client/examples/query_name_service.rs @@ -9,7 +9,7 @@ use nym_validator_client::nyxd::contract_traits::{ #[tokio::main] async fn main() { - setup_env(Some(&"../../../envs/qa-qwerty.env".parse().unwrap())); + setup_env(Some("../../../envs/qa-qwerty.env")); let network_details = NymNetworkDetails::new_from_env(); let config = nym_validator_client::Config::try_from_nym_network_details(&network_details).unwrap(); diff --git a/common/client-libs/validator-client/examples/query_service_provider_directory.rs b/common/client-libs/validator-client/examples/query_service_provider_directory.rs index d0f6f7ce46..e489564e94 100644 --- a/common/client-libs/validator-client/examples/query_service_provider_directory.rs +++ b/common/client-libs/validator-client/examples/query_service_provider_directory.rs @@ -9,7 +9,7 @@ use nym_validator_client::nyxd::contract_traits::{ #[tokio::main] async fn main() { - setup_env(Some(&"../../../envs/qa-qwerty.env".parse().unwrap())); + setup_env(Some("../../../envs/qa-qwerty.env")); let network_details = NymNetworkDetails::new_from_env(); let config = nym_validator_client::Config::try_from_nym_network_details(&network_details).unwrap(); diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index e70cbdaf2f..c5ae83a6bf 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -26,6 +26,8 @@ pub use nym_mixnet_contract_common::{ // re-export the type to not break existing imports pub use crate::coconut::CoconutApiClient; +#[cfg(feature = "http-client")] +use crate::rpc::http_client; #[cfg(feature = "http-client")] use crate::{DirectSigningHttpRpcValidatorClient, HttpRpcClient, QueryHttpRpcValidatorClient}; @@ -95,7 +97,7 @@ impl Client { config: Config, mnemonic: bip39::Mnemonic, ) -> Result { - let rpc_client = HttpRpcClient::new(config.nyxd_url.as_str())?; + 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); @@ -126,7 +128,7 @@ impl Client { #[cfg(feature = "http-client")] impl Client { pub fn new_query(config: Config) -> Result { - let rpc_client = HttpRpcClient::new(config.nyxd_url.as_str())?; + let rpc_client = http_client(config.nyxd_url.as_str())?; Ok(Self::new_with_rpc_client(config, rpc_client)) } @@ -170,6 +172,10 @@ impl Client { // validator-api wrappers impl Client { + pub fn api_url(&self) -> &Url { + self.nym_api.current_url() + } + pub fn change_nym_api(&mut self, new_endpoint: Url) { self.nym_api.change_url(new_endpoint) } @@ -240,6 +246,10 @@ impl NymApiClient { NymApiClient { nym_api } } + pub fn api_url(&self) -> &Url { + self.nym_api.current_url() + } + pub fn change_nym_api(&mut self, new_endpoint: Url) { self.nym_api.change_url(new_endpoint); } diff --git a/common/client-libs/validator-client/src/lib.rs b/common/client-libs/validator-client/src/lib.rs index c6391673a0..b3dca0a473 100644 --- a/common/client-libs/validator-client/src/lib.rs +++ b/common/client-libs/validator-client/src/lib.rs @@ -20,6 +20,8 @@ pub use nym_api_requests::*; #[cfg(feature = "http-client")] pub use cosmrs::rpc::HttpClient as HttpRpcClient; +#[cfg(feature = "http-client")] +pub use rpc::http_client; // some type aliasing 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 7ebff2ef16..ab78a8c418 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 @@ -3,18 +3,25 @@ use crate::nyxd::cosmwasm_client::client_traits::{CosmWasmClient, SigningCosmWasmClient}; use crate::nyxd::error::NyxdError; -use crate::nyxd::{Config, GasPrice}; +use crate::nyxd::{Config, GasPrice, Hash, Height}; use crate::rpc::TendermintRpcClient; use crate::signing::{ signer::{NoSigner, OfflineSigner}, AccountData, }; use async_trait::async_trait; -use tendermint_rpc::{Error as TendermintRpcError, SimpleRequest}; +use cosmrs::tendermint::{abci, evidence::Evidence, Genesis}; +use cosmrs::tx::{Raw, SignDoc}; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt::Debug; +use tendermint_rpc::endpoint::*; +use tendermint_rpc::query::Query; +use tendermint_rpc::{Error as TendermintRpcError, Order, Paging, SimpleRequest}; +#[cfg(feature = "http-client")] +use crate::http_client; #[cfg(feature = "http-client")] use cosmrs::rpc::{HttpClient, HttpClientUrl}; -use cosmrs::tx::{Raw, SignDoc}; pub mod client_traits; mod helpers; @@ -73,7 +80,7 @@ impl MaybeSigningClient { where U: TryInto, { - self.client = HttpClient::new(new_endpoint)?; + self.client = http_client(new_endpoint)?; Ok(()) } } @@ -85,6 +92,216 @@ where C: TendermintRpcClient + Send + Sync, S: Send + Sync, { + async fn abci_info(&self) -> Result { + self.client.abci_info().await + } + + async fn abci_query( + &self, + path: Option, + data: V, + height: Option, + prove: bool, + ) -> Result + where + V: Into> + Send, + { + self.client.abci_query(path, data, height, prove).await + } + + async fn block(&self, height: H) -> Result + where + H: Into + Send, + { + self.client.block(height).await + } + + async fn block_by_hash( + &self, + hash: Hash, + ) -> Result { + self.client.block_by_hash(hash).await + } + + async fn latest_block(&self) -> Result { + self.client.latest_block().await + } + + async fn header(&self, height: H) -> Result + where + H: Into + Send, + { + self.client.header(height).await + } + + async fn header_by_hash( + &self, + hash: Hash, + ) -> Result { + self.client.header_by_hash(hash).await + } + + async fn block_results( + &self, + height: H, + ) -> Result + where + H: Into + Send, + { + self.client.block_results(height).await + } + + async fn latest_block_results(&self) -> Result { + self.client.latest_block_results().await + } + + async fn block_search( + &self, + query: Query, + page: u32, + per_page: u8, + order: Order, + ) -> Result { + self.client.block_search(query, page, per_page, order).await + } + + async fn blockchain( + &self, + min: H, + max: H, + ) -> Result + where + H: Into + Send, + { + self.client.blockchain(min, max).await + } + + async fn broadcast_tx_async( + &self, + tx: T, + ) -> Result + where + T: Into> + Send, + { + self.client.broadcast_tx_async(tx).await + } + + async fn broadcast_tx_sync( + &self, + tx: T, + ) -> Result + where + T: Into> + Send, + { + self.client.broadcast_tx_sync(tx).await + } + + async fn broadcast_tx_commit( + &self, + tx: T, + ) -> Result + where + T: Into> + Send, + { + self.client.broadcast_tx_commit(tx).await + } + + async fn commit(&self, height: H) -> Result + where + H: Into + Send, + { + self.client.commit(height).await + } + + async fn consensus_params( + &self, + height: H, + ) -> Result + where + H: Into + Send, + { + self.client.consensus_params(height).await + } + + async fn consensus_state(&self) -> Result { + self.client.consensus_state().await + } + + async fn validators( + &self, + height: H, + paging: Paging, + ) -> Result + where + H: Into + Send, + { + self.client.validators(height, paging).await + } + + async fn latest_consensus_params( + &self, + ) -> Result { + self.client.latest_consensus_params().await + } + + async fn latest_commit(&self) -> Result { + self.client.latest_commit().await + } + + async fn health(&self) -> Result<(), TendermintRpcError> { + self.client.health().await + } + + async fn genesis(&self) -> Result, TendermintRpcError> + where + AppState: Debug + Serialize + DeserializeOwned + Send, + { + self.client.genesis().await + } + + async fn net_info(&self) -> Result { + self.client.net_info().await + } + + async fn status(&self) -> Result { + self.client.status().await + } + + async fn broadcast_evidence( + &self, + e: Evidence, + ) -> Result { + self.client.broadcast_evidence(e).await + } + + async fn tx(&self, hash: Hash, prove: bool) -> Result { + self.client.tx(hash, prove).await + } + + async fn tx_search( + &self, + query: Query, + prove: bool, + page: u32, + per_page: u8, + order: Order, + ) -> Result { + self.client + .tx_search(query, prove, page, per_page, order) + .await + } + + #[cfg(any( + feature = "tendermint-rpc/http-client", + feature = "tendermint-rpc/websocket-client" + ))] + async fn wait_until_healthy(&self, timeout: T) -> Result<(), Error> + where + T: Into + Send, + { + self.client.wait_until_healthy(timeout).await + } + async fn perform(&self, request: R) -> Result where R: SimpleRequest, diff --git a/common/client-libs/validator-client/src/nyxd/mod.rs b/common/client-libs/validator-client/src/nyxd/mod.rs index 16027a1970..10cca6de7e 100644 --- a/common/client-libs/validator-client/src/nyxd/mod.rs +++ b/common/client-libs/validator-client/src/nyxd/mod.rs @@ -9,19 +9,25 @@ use crate::nyxd::cosmwasm_client::types::{ use crate::nyxd::cosmwasm_client::MaybeSigningClient; use crate::nyxd::error::NyxdError; use crate::nyxd::fee::DEFAULT_SIMULATED_GAS_MULTIPLIER; +use crate::signing::direct_wallet::DirectSecp256k1HdWallet; 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 async_trait::async_trait; use cosmrs::cosmwasm; +use cosmrs::tendermint::{abci, evidence::Evidence, Genesis}; use cosmrs::tx::{Msg, Raw, SignDoc}; use cosmwasm_std::Addr; use nym_network_defaults::{ChainDetails, NymNetworkDetails}; -use serde::Serialize; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt::Debug; use std::time::SystemTime; use tendermint_rpc::endpoint::block::Response as BlockResponse; -use tendermint_rpc::Error as TendermintRpcError; +use tendermint_rpc::endpoint::*; +use tendermint_rpc::{Error as TendermintRpcError, Order}; +use url::Url; pub use crate::nyxd::cosmwasm_client::client_traits::{CosmWasmClient, SigningCosmWasmClient}; pub use crate::nyxd::fee::Fee; @@ -45,14 +51,13 @@ pub use tendermint_rpc::{ }; pub use tendermint_rpc::{Request, Response, SimpleRequest}; -// #[cfg(feature = "http-client")] -use crate::signing::direct_wallet::DirectSecp256k1HdWallet; +#[cfg(feature = "http-client")] +use crate::http_client; #[cfg(feature = "http-client")] use crate::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient}; -use crate::{DirectSigningReqwestRpcNyxdClient, QueryReqwestRpcNyxdClient, ReqwestRpcClient}; #[cfg(feature = "http-client")] use cosmrs::rpc::{HttpClient, HttpClientUrl}; -use url::Url; +use tendermint_rpc::query::Query; pub mod coin; pub mod contract_traits; @@ -97,7 +102,7 @@ impl NyxdClient { where U: TryInto, { - let client = HttpClient::new(endpoint)?; + let client = http_client(endpoint)?; Ok(NyxdClient { client: MaybeSigningClient::new(client, (&config).into()), @@ -140,7 +145,7 @@ impl NyxdClient { where U: TryInto, { - let client = HttpClient::new(endpoint)?; + let client = http_client(endpoint)?; let prefix = &config.chain_details.bech32_account_prefix; let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic); @@ -568,6 +573,216 @@ where C: TendermintRpcClient + Send + Sync, S: Send + Sync, { + async fn abci_info(&self) -> Result { + self.client.abci_info().await + } + + async fn abci_query( + &self, + path: Option, + data: V, + height: Option, + prove: bool, + ) -> Result + where + V: Into> + Send, + { + self.client.abci_query(path, data, height, prove).await + } + + async fn block(&self, height: H) -> Result + where + H: Into + Send, + { + self.client.block(height).await + } + + async fn block_by_hash( + &self, + hash: Hash, + ) -> Result { + self.client.block_by_hash(hash).await + } + + async fn latest_block(&self) -> Result { + self.client.latest_block().await + } + + async fn header(&self, height: H) -> Result + where + H: Into + Send, + { + self.client.header(height).await + } + + async fn header_by_hash( + &self, + hash: Hash, + ) -> Result { + self.client.header_by_hash(hash).await + } + + async fn block_results( + &self, + height: H, + ) -> Result + where + H: Into + Send, + { + self.client.block_results(height).await + } + + async fn latest_block_results(&self) -> Result { + self.client.latest_block_results().await + } + + async fn block_search( + &self, + query: Query, + page: u32, + per_page: u8, + order: Order, + ) -> Result { + self.client.block_search(query, page, per_page, order).await + } + + async fn blockchain( + &self, + min: H, + max: H, + ) -> Result + where + H: Into + Send, + { + self.client.blockchain(min, max).await + } + + async fn broadcast_tx_async( + &self, + tx: T, + ) -> Result + where + T: Into> + Send, + { + TendermintRpcClient::broadcast_tx_async(&self.client, tx).await + } + + async fn broadcast_tx_sync( + &self, + tx: T, + ) -> Result + where + T: Into> + Send, + { + TendermintRpcClient::broadcast_tx_sync(&self.client, tx).await + } + + async fn broadcast_tx_commit( + &self, + tx: T, + ) -> Result + where + T: Into> + Send, + { + TendermintRpcClient::broadcast_tx_commit(&self.client, tx).await + } + + async fn commit(&self, height: H) -> Result + where + H: Into + Send, + { + self.client.commit(height).await + } + + async fn consensus_params( + &self, + height: H, + ) -> Result + where + H: Into + Send, + { + self.client.consensus_params(height).await + } + + async fn consensus_state(&self) -> Result { + self.client.consensus_state().await + } + + async fn validators( + &self, + height: H, + paging: Paging, + ) -> Result + where + H: Into + Send, + { + self.client.validators(height, paging).await + } + + async fn latest_consensus_params( + &self, + ) -> Result { + self.client.latest_consensus_params().await + } + + async fn latest_commit(&self) -> Result { + self.client.latest_commit().await + } + + async fn health(&self) -> Result<(), TendermintRpcError> { + self.client.health().await + } + + async fn genesis(&self) -> Result, TendermintRpcError> + where + AppState: Debug + Serialize + DeserializeOwned + Send, + { + self.client.genesis().await + } + + async fn net_info(&self) -> Result { + self.client.net_info().await + } + + async fn status(&self) -> Result { + self.client.status().await + } + + async fn broadcast_evidence( + &self, + e: Evidence, + ) -> Result { + self.client.broadcast_evidence(e).await + } + + async fn tx(&self, hash: Hash, prove: bool) -> Result { + self.client.tx(hash, prove).await + } + + async fn tx_search( + &self, + query: Query, + prove: bool, + page: u32, + per_page: u8, + order: Order, + ) -> Result { + self.client + .tx_search(query, prove, page, per_page, order) + .await + } + + #[cfg(any( + feature = "tendermint-rpc/http-client", + feature = "tendermint-rpc/websocket-client" + ))] + async fn wait_until_healthy(&self, timeout: T) -> Result<(), Error> + where + T: Into + Send, + { + self.client.wait_until_healthy(timeout).await + } + async fn perform(&self, request: R) -> Result where R: SimpleRequest, diff --git a/common/client-libs/validator-client/src/rpc/mod.rs b/common/client-libs/validator-client/src/rpc/mod.rs index f483f1067c..fd3f7991ba 100644 --- a/common/client-libs/validator-client/src/rpc/mod.rs +++ b/common/client-libs/validator-client/src/rpc/mod.rs @@ -11,8 +11,27 @@ use tendermint_rpc::{ Error, Order, Paging, SimpleRequest, }; +#[cfg(feature = "http-client")] +use crate::error::TendermintRpcError; +#[cfg(feature = "http-client")] +use crate::HttpRpcClient; +#[cfg(feature = "http-client")] +use tendermint_rpc::client::CompatMode; +#[cfg(feature = "http-client")] +use tendermint_rpc::HttpClientUrl; + pub mod reqwest; +#[cfg(feature = "http-client")] +pub fn http_client(url: U) -> Result +where + U: TryInto, +{ + HttpRpcClient::builder(url.try_into()?) + .compat_mode(CompatMode::V0_34) + .build() +} + // we have to create a sealed trait since `TendermintClient` needs T: Send (due to how async trait is created) // which we can't do in wasm #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/common/client-libs/validator-client/src/rpc/reqwest.rs b/common/client-libs/validator-client/src/rpc/reqwest.rs index 9c8411d666..ffad7dd815 100644 --- a/common/client-libs/validator-client/src/rpc/reqwest.rs +++ b/common/client-libs/validator-client/src/rpc/reqwest.rs @@ -3,12 +3,32 @@ use crate::rpc::TendermintRpcClient; use async_trait::async_trait; +use cosmrs::tendermint::{block::Height, evidence::Evidence, Hash}; use reqwest::header::HeaderMap; use reqwest::{header, RequestBuilder}; -use tendermint_rpc::{Error, Response, SimpleRequest}; +use tendermint_rpc::{ + client::CompatMode, + dialect::{self, Dialect}, + endpoint::{self, *}, + query::Query, + Error, Order, Response, SimpleRequest, +}; + use url::Url; +// copied macro from tendermint-rpc crate because that's exactly what we have to do here too +macro_rules! perform_with_compat { + ($self:expr, $request:expr) => {{ + let request = $request; + match $self.compat { + CompatMode::V0_37 => $self.perform_v0_37(request).await, + CompatMode::V0_34 => $self.perform_v0_34(request).await, + } + }}; +} + pub struct ReqwestRpcClient { + compat: CompatMode, inner: reqwest::Client, url: Url, } @@ -16,12 +36,21 @@ pub struct ReqwestRpcClient { impl ReqwestRpcClient { pub fn new(url: Url) -> Self { ReqwestRpcClient { + compat: CompatMode::V0_34, inner: reqwest::Client::new(), url, } } - fn build_request(&self, request: R) -> RequestBuilder { + pub fn set_compat_mode(&mut self, compat: CompatMode) { + self.compat = compat; + } + + fn build_request(&self, request: R) -> RequestBuilder + where + R: SimpleRequest, + S: Dialect, + { let mut headers = HeaderMap::new(); headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap()); headers.insert( @@ -39,6 +68,38 @@ impl ReqwestRpcClient { .body(request.into_json().into_bytes()) .headers(headers) } + + async fn perform_request(&self, request: R) -> Result + where + R: SimpleRequest, + S: Dialect, + { + let request = self.build_request(request); + // that's extremely unfortunate. the trait requires returning tendermint rpc error so we have to make best effort error mapping + let response = request + .send() + .await + .map_err(TendermintRpcErrorMap::into_rpc_err)?; + let bytes = response + .bytes() + .await + .map_err(TendermintRpcErrorMap::into_rpc_err)?; + R::Response::from_string(bytes).map(Into::into) + } + + async fn perform_v0_34(&self, request: R) -> Result + where + R: SimpleRequest, + { + self.perform_request(request).await + } + + async fn perform_v0_37(&self, request: R) -> Result + where + R: SimpleRequest, + { + self.perform_request(request).await + } } trait TendermintRpcErrorMap { @@ -58,17 +119,81 @@ impl TendermintRpcClient for ReqwestRpcClient { where R: SimpleRequest, { - let request = self.build_request(request); - // that's extremely unfortunate. the trait requires returning tendermint rpc error so we have to make best effort error mapping - let response = request - .send() - .await - .map_err(TendermintRpcErrorMap::into_rpc_err)?; - let bytes = response - .bytes() - .await - .map_err(TendermintRpcErrorMap::into_rpc_err)?; - R::Response::from_string(bytes).map(Into::into) + self.perform_request(request).await + } + + async fn block_results(&self, height: H) -> Result + where + H: Into + Send, + { + perform_with_compat!(self, block_results::Request::new(height.into())) + } + + async fn latest_block_results(&self) -> Result { + perform_with_compat!(self, block_results::Request::default()) + } + + async fn header(&self, height: H) -> Result + where + H: Into + Send, + { + let height = height.into(); + match self.compat { + CompatMode::V0_37 => self.perform(endpoint::header::Request::new(height)).await, + CompatMode::V0_34 => { + // Back-fill with a request to /block endpoint and + // taking just the header from the response. + let resp = self.perform_v0_34(block::Request::new(height)).await?; + Ok(resp.into()) + } + } + } + + async fn header_by_hash(&self, hash: Hash) -> Result { + match self.compat { + CompatMode::V0_37 => self.perform(header_by_hash::Request::new(hash)).await, + CompatMode::V0_34 => { + // Back-fill with a request to /block_by_hash endpoint and + // taking just the header from the response. + let resp = self + .perform_v0_34(block_by_hash::Request::new(hash)) + .await?; + Ok(resp.into()) + } + } + } + + /// `/broadcast_evidence`: broadcast an evidence. + async fn broadcast_evidence(&self, e: Evidence) -> Result { + match self.compat { + CompatMode::V0_37 => self.perform(evidence::Request::new(e)).await, + CompatMode::V0_34 => self.perform_v0_34(evidence::Request::new(e)).await, + } + } + + async fn tx(&self, hash: Hash, prove: bool) -> Result { + perform_with_compat!(self, tx::Request::new(hash, prove)) + } + + async fn tx_search( + &self, + query: Query, + prove: bool, + page: u32, + per_page: u8, + order: Order, + ) -> Result { + perform_with_compat!( + self, + tx_search::Request::new(query, prove, page, per_page, order) + ) + } + + async fn broadcast_tx_commit(&self, tx: T) -> Result + where + T: Into> + Send, + { + perform_with_compat!(self, broadcast::tx_commit::Request::new(tx)) } } 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 b26ac39ada..3de09502ae 100644 --- a/common/client-libs/validator-client/src/signing/direct_wallet.rs +++ b/common/client-libs/validator-client/src/signing/direct_wallet.rs @@ -199,16 +199,14 @@ mod tests { #[test] fn generating_account_addresses() { // test vectors produced from our js wallet - let mnemonics = vec![ - "crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove", + 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", - "step income throw wheat mobile ship wave drink pool sudden upset jaguar bar globe rifle spice frost bless glimpse size regular carry aspect ball" - ]; + "step income throw wheat mobile ship wave drink pool sudden upset jaguar bar globe rifle spice frost bless glimpse size regular carry aspect ball"]; let prefix = NymNetworkDetails::new_mainnet() .chain_details .bech32_account_prefix; - let addrs = vec![ + let addrs = [ "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es", "n17n9flp6jflljg6fp05dsy07wcprf2uuu8g40rf", diff --git a/common/coconut-interface/src/lib.rs b/common/coconut-interface/src/lib.rs index 4f69c2c9fa..2322bc3978 100644 --- a/common/coconut-interface/src/lib.rs +++ b/common/coconut-interface/src/lib.rs @@ -8,7 +8,14 @@ use serde::{Deserialize, Serialize}; use error::CoconutInterfaceError; -pub use nym_coconut::*; +// We list these explicity instead of glob export due to shadowing warnings with the pub tests +// module. +pub use nym_coconut::{ + aggregate_signature_shares, aggregate_verification_keys, blind_sign, hash_to_scalar, + prepare_blind_sign, prove_bandwidth_credential, Attribute, Base58, BlindSignRequest, + BlindedSignature, Bytable, CoconutError, KeyPair, Parameters, PrivateAttribute, + PublicAttribute, Signature, SignatureShare, Theta, VerificationKey, +}; #[derive(Debug, Serialize, Deserialize, Getters, CopyGetters, Clone, PartialEq, Eq)] pub struct Credential { @@ -57,7 +64,7 @@ impl Credential { pub fn verify(&self, verification_key: &VerificationKey) -> bool { let params = Parameters::new(self.n_params).unwrap(); - let public_attributes = vec![ + let public_attributes = [ self.voucher_value.to_string().as_bytes(), self.voucher_info.as_bytes(), ] @@ -138,6 +145,8 @@ impl Base58 for Credential {} #[cfg(test)] mod tests { + use nym_coconut::{prove_bandwidth_credential, Signature}; + use super::*; #[test] diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml index 45c9139fe3..df6860641e 100644 --- a/common/commands/Cargo.toml +++ b/common/commands/Cargo.toml @@ -5,6 +5,7 @@ authors.workspace = true edition = "2021" [dependencies] +anyhow = { workspace = true } base64 = "0.13.0" bip39 = { workspace = true } bs58 = "0.4" @@ -33,6 +34,7 @@ nym-bin-common = { path = "../../common/bin-common", features = ["output_format" nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } nym-network-defaults = { path = "../network-defaults" } nym-contracts-common = { path = "../cosmwasm-smart-contracts/contracts-common" } +nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } nym-vesting-contract-common = { path = "../cosmwasm-smart-contracts/vesting-contract" } nym-coconut-bandwidth-contract-common = { path = "../cosmwasm-smart-contracts/coconut-bandwidth-contract" } @@ -41,6 +43,11 @@ nym-multisig-contract-common = { path = "../cosmwasm-smart-contracts/multisig-co nym-service-provider-directory-common = { path = "../cosmwasm-smart-contracts/service-provider-directory" } nym-name-service-common = { path = "../cosmwasm-smart-contracts/name-service" } nym-sphinx = { path = "../../common/nymsphinx" } +nym-client-core = { path = "../../common/client-core" } +nym-config = { path = "../../common/config" } +nym-credentials = { path = "../../common/credentials" } +nym-credential-storage = { path = "../../common/credential-storage" } +nym-credential-utils = { path = "../../common/credential-utils" } nym-pemstore = { path = "../../common/pemstore", version = "0.3.0" } nym-types = { path = "../../common/types" } diff --git a/common/commands/src/coconut/issue_credentials.rs b/common/commands/src/coconut/issue_credentials.rs new file mode 100644 index 0000000000..cd8a9b5f36 --- /dev/null +++ b/common/commands/src/coconut/issue_credentials.rs @@ -0,0 +1,51 @@ +// Copyright 2022-2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use crate::utils::CommonConfigsWrapper; +use anyhow::bail; +use clap::Parser; +use nym_credential_storage::initialise_persistent_storage; +use nym_credential_utils::utils; +use nym_validator_client::nyxd::Coin; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +pub struct Args { + /// Config file of the client that is supposed to use the credential. + #[clap(long)] + pub(crate) client_config: PathBuf, + + /// The amount of utokens the credential will hold. + #[clap(long, default_value = "0")] + pub(crate) amount: u64, + + /// Path to a directory used to store recovery files for unconsumed deposits + #[clap(long)] + pub(crate) recovery_dir: PathBuf, +} + +pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> { + let loaded = CommonConfigsWrapper::try_load(args.client_config)?; + + if let Ok(id) = loaded.try_get_id() { + println!("loaded config file for client '{id}'"); + } + + let Ok(credentials_store) = loaded.try_get_credentials_store() else { + bail!("the loaded config does not have a credentials store information") + }; + + println!( + "using credentials store at '{}'", + credentials_store.display() + ); + + let denom = &client.current_chain_details().mix_denom.base; + let coin = Coin::new(args.amount as u128, denom); + + let persistent_storage = initialise_persistent_storage(credentials_store).await; + utils::issue_credential(&client, coin, &persistent_storage, args.recovery_dir).await?; + + Ok(()) +} diff --git a/common/commands/src/coconut/mod.rs b/common/commands/src/coconut/mod.rs index 6cfc921228..531a316125 100644 --- a/common/commands/src/coconut/mod.rs +++ b/common/commands/src/coconut/mod.rs @@ -1,4 +1,20 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -// TODO: add coconut commands here +use clap::{Args, Subcommand}; + +pub mod issue_credentials; +pub mod recover_credentials; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct Coconut { + #[clap(subcommand)] + pub command: CoconutCommands, +} + +#[derive(Debug, Subcommand)] +pub enum CoconutCommands { + IssueCredentials(issue_credentials::Args), + RecoverCredentials(recover_credentials::Args), +} diff --git a/common/commands/src/coconut/recover_credentials.rs b/common/commands/src/coconut/recover_credentials.rs new file mode 100644 index 0000000000..025ea68c2c --- /dev/null +++ b/common/commands/src/coconut/recover_credentials.rs @@ -0,0 +1,48 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::QueryClient; +use crate::utils::CommonConfigsWrapper; +use anyhow::bail; +use clap::Parser; +use nym_credential_storage::initialise_persistent_storage; +use nym_credential_utils::{recovery_storage, utils}; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +pub struct Args { + /// Config file of the client that is supposed to use the credential. + #[clap(long)] + pub(crate) client_config: PathBuf, + + /// Path to a directory used to store recovery files for unconsumed deposits + #[clap(long)] + pub(crate) recovery_dir: PathBuf, +} + +pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> { + let loaded = CommonConfigsWrapper::try_load(args.client_config)?; + + if let Ok(id) = loaded.try_get_id() { + println!("loaded config file for client '{id}'"); + } + + let Ok(credentials_store) = loaded.try_get_credentials_store() else { + bail!("the loaded config does not have a credentials store information") + }; + + println!( + "using credentials store at '{}'", + credentials_store.display() + ); + + let persistent_storage = initialise_persistent_storage(credentials_store).await; + let recovery_storage = recovery_storage::RecoveryStorage::new(args.recovery_dir)?; + + let recovered = + utils::recover_credentials(&client, &recovery_storage, &persistent_storage).await?; + + // TODO: denom? + println!("recovered {recovered} worth of credentials"); + Ok(()) +} diff --git a/common/commands/src/utils.rs b/common/commands/src/utils.rs index 63f8b6a085..25797c71e9 100644 --- a/common/commands/src/utils.rs +++ b/common/commands/src/utils.rs @@ -1,13 +1,17 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use anyhow::{anyhow, bail}; use cosmrs::AccountId; use cosmwasm_std::{Addr, Coin as CosmWasmCoin, Decimal}; use log::error; +use nym_client_core::config::disk_persistence::CommonClientPaths; use nym_validator_client::nyxd::Coin; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::error::Error; use std::fmt::{Display, Formatter}; +use std::fs; +use std::path::{Path, PathBuf}; // TODO: perhaps it should be moved to some global common crate? pub fn account_id_to_cw_addr(account_id: &AccountId) -> Addr { @@ -75,3 +79,150 @@ impl DataWrapper { DataWrapper { data } } } + +fn find_toml_value<'a>(root: &'a toml::Value, key: &str) -> Option<&'a toml::Value> { + if let toml::Value::Table(table) = root { + for (k, v) in table { + if k == key { + return Some(v); + } + if v.is_table() { + if let Some(res) = find_toml_value(v, key) { + return Some(res); + } + } + } + } + None +} + +#[derive(Deserialize, Debug)] +#[serde(untagged)] +pub(crate) enum CommonConfigsWrapper { + // native, socks5, NR, etc. clients + NymClients(Box), + + // nym-api + NymApi(NymApiConfigLight), + + // anything else that might get get introduced + Unknown(UnknownConfigWrapper), +} + +impl CommonConfigsWrapper { + pub(crate) fn try_load>(path: P) -> anyhow::Result { + let content = fs::read_to_string(path)?; + Ok(toml::from_str(&content)?) + } + + pub(crate) fn try_get_id(&self) -> anyhow::Result<&str> { + match self { + CommonConfigsWrapper::NymClients(cfg) => cfg.try_get_id(), + CommonConfigsWrapper::NymApi(cfg) => Ok(&cfg.base.id), + CommonConfigsWrapper::Unknown(cfg) => cfg.try_get_id(), + } + } + + pub(crate) fn try_get_credentials_store(&self) -> anyhow::Result { + match self { + CommonConfigsWrapper::NymClients(cfg) => { + Ok(cfg.storage_paths.inner.credentials_database.clone()) + } + CommonConfigsWrapper::NymApi(cfg) => Ok(cfg + .network_monitor + .storage_paths + .credentials_database_path + .clone()), + CommonConfigsWrapper::Unknown(cfg) => cfg.try_get_credentials_store(), + } + } +} + +// ideally we would have just imported the full nym-api config structure, but that'd have been an overkill, +// because we'd have to import the whole crate +#[derive(Deserialize, Debug)] +pub(crate) struct NymApiConfigLight { + base: NymApiConfigBaseLight, + network_monitor: NymApiConfigNetworkMonitorLight, +} + +#[derive(Deserialize, Debug)] +struct NymApiConfigBaseLight { + id: String, +} + +#[derive(Deserialize, Debug)] +struct NymApiConfigNetworkMonitorLight { + storage_paths: NetworkMonitorPaths, +} + +#[derive(Deserialize, Debug)] +struct NetworkMonitorPaths { + credentials_database_path: PathBuf, +} + +// a hacky way of reading common data from client configs (native, socks5, etc.) +// it works because all clients follow the same structure for storage paths +// (or so I thought) +#[derive(Deserialize, Debug)] +pub(crate) struct ClientConfigCommonWrapper { + storage_paths: StoragePathsWrapper, + + // ... but they have different structure for `nym_client_core::config::Client` + // native client has it on the top layer, whilsts socks5 has it under 'core' table + #[serde(flatten)] + other: toml::Value, +} + +// wrapper to allow for any additional entries besides the common paths, like allow list for NR +#[derive(Deserialize, Debug)] +struct StoragePathsWrapper { + #[serde(flatten)] + inner: CommonClientPaths, +} + +impl ClientConfigCommonWrapper { + pub(crate) fn try_get_id(&self) -> anyhow::Result<&str> { + let id_val = find_toml_value(&self.other, "id") + .ok_or_else(|| anyhow!("no id field present in the config"))?; + if let toml::Value::String(id) = id_val { + Ok(id) + } else { + bail!("no id field present in the config") + } + } +} + +#[derive(Deserialize, Debug)] +pub(crate) struct UnknownConfigWrapper { + #[serde(flatten)] + inner: toml::Value, +} + +impl UnknownConfigWrapper { + fn find_value(&self, key: &str) -> Option<&toml::Value> { + find_toml_value(&self.inner, key) + } + + pub(crate) fn try_get_id(&self) -> anyhow::Result<&str> { + let id_val = self + .find_value("id") + .ok_or_else(|| anyhow!("no id field present in the config"))?; + if let toml::Value::String(id) = id_val { + Ok(id) + } else { + bail!("no id field present in the config") + } + } + + pub(crate) fn try_get_credentials_store(&self) -> anyhow::Result { + let id_val = self + .find_value("credentials_database_path") + .ok_or_else(|| anyhow!("no 'credentials_database_path' field present in the config"))?; + if let toml::Value::String(credentials_store) = id_val { + Ok(credentials_store.parse()?) + } else { + bail!("no 'credentials_database_path' field present in the config") + } + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index 47397d7b88..e82be77e29 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -26,7 +26,7 @@ humantime-serde = "1.1.1" # TO CHECK WHETHER STILL NEEDED: log = { workspace = true } time = { version = "0.3.6", features = ["parsing", "formatting"] } -ts-rs = { version = "6.1.2", optional = true } +ts-rs = { workspace = true, optional = true } [dev-dependencies] rand_chacha = "0.3" diff --git a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml index f5fa35df5f..3fd7e8a377 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml @@ -15,7 +15,7 @@ mixnet-contract-common = { path = "../mixnet-contract", package = "nym-mixnet-co contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.5.0" } serde = { version = "1.0", features = ["derive"] } thiserror = "1.0" -ts-rs = {version = "6.1.2", optional = true} +ts-rs = { workspace = true, optional = true} [features] schema = ["cw2"] diff --git a/clients/credential/Cargo.toml b/common/credential-utils/Cargo.toml similarity index 54% rename from clients/credential/Cargo.toml rename to common/credential-utils/Cargo.toml index 8ceb14bc98..1949107036 100644 --- a/clients/credential/Cargo.toml +++ b/common/credential-utils/Cargo.toml @@ -1,25 +1,18 @@ [package] -name = "nym-credential-client" +name = "nym-credential-utils" version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -clap = { version = "4.0", features = ["cargo", "derive"] } -log = "0.4" -serde = { workspace = true, features = ["derive"] } -thiserror = "1.0" -tokio = { version = "1.24.1", features = ["rt-multi-thread", "net", "signal", "macros"] } # async runtime - +log = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } -nym-client-core = { path = "../../common/client-core" } -nym-config = { path = "../../common/config" } nym-credentials = { path = "../../common/credentials" } nym-credential-storage = { path = "../../common/credential-storage" } -nym-bin-common = { path = "../../common/bin-common"} -nym-network-defaults = { path = "../../common/network-defaults" } -nym-pemstore = { path = "../../common/pemstore" } nym-validator-client = { path = "../../common/client-libs/validator-client" } - +nym-config = { path = "../../common/config" } +nym-client-core = { path = "../../common/client-core" } diff --git a/common/credential-utils/src/errors.rs b/common/credential-utils/src/errors.rs new file mode 100644 index 0000000000..6407929a99 --- /dev/null +++ b/common/credential-utils/src/errors.rs @@ -0,0 +1,31 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credential_storage::error::StorageError; +use nym_credentials::error::Error as CredentialError; +use nym_validator_client::nyxd::error::NyxdError; +use std::num::ParseIntError; +use thiserror::Error; + +pub type Result = std::result::Result; + +#[derive(Error, Debug)] +pub enum Error { + #[error(transparent)] + IOError(#[from] std::io::Error), + + #[error(transparent)] + BandwidthControllerError(#[from] nym_bandwidth_controller::error::BandwidthControllerError), + + #[error(transparent)] + Nyxd(#[from] NyxdError), + + #[error(transparent)] + Credential(#[from] CredentialError), + + #[error("Could not use shared storage: {0}")] + SharedStorageError(#[from] StorageError), + + #[error("failed to parse credential value: {0}")] + MalformedCredentialValue(#[from] ParseIntError), +} diff --git a/common/credential-utils/src/lib.rs b/common/credential-utils/src/lib.rs new file mode 100644 index 0000000000..6e5fbc3158 --- /dev/null +++ b/common/credential-utils/src/lib.rs @@ -0,0 +1,5 @@ +pub mod errors; +pub mod recovery_storage; +pub mod utils; + +pub use errors::{Error, Result}; diff --git a/common/credential-utils/src/recovery_storage.rs b/common/credential-utils/src/recovery_storage.rs new file mode 100644 index 0000000000..1793553c62 --- /dev/null +++ b/common/credential-utils/src/recovery_storage.rs @@ -0,0 +1,64 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::errors::Result; +use log::error; +use nym_credentials::coconut::bandwidth::BandwidthVoucher; +use std::fs::{create_dir_all, read_dir, File}; +use std::io::{Read, Write}; +use std::path::PathBuf; + +pub struct RecoveryStorage { + recovery_dir: PathBuf, +} + +impl RecoveryStorage { + pub fn new(recovery_dir: PathBuf) -> Result { + create_dir_all(&recovery_dir)?; + Ok(Self { recovery_dir }) + } + + pub fn unconsumed_vouchers(&self) -> Result> { + let entries = read_dir(&self.recovery_dir)?; + + let mut paths = vec![]; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + paths.push(path) + } + } + + let mut vouchers = vec![]; + for path in paths { + if let Ok(mut file) = File::open(&path) { + let mut buff = Vec::new(); + if file.read_to_end(&mut buff).is_ok() { + match BandwidthVoucher::try_from_bytes(&buff) { + Ok(voucher) => vouchers.push(voucher), + Err(err) => { + error!("failed to parse the voucher at {}: {err}", path.display()) + } + } + } + } + } + + Ok(vouchers) + } + + pub fn insert_voucher(&self, voucher: &BandwidthVoucher) -> Result { + let file_name = voucher.tx_hash().to_string(); + let file_path = self.recovery_dir.join(file_name); + let mut file = File::create(&file_path)?; + let buff = voucher.to_bytes(); + file.write_all(&buff)?; + + Ok(file_path) + } + + pub fn remove_voucher(&self, file_name: String) -> Result<()> { + let file_path = self.recovery_dir.join(file_name); + Ok(std::fs::remove_file(file_path)?) + } +} diff --git a/common/credential-utils/src/utils.rs b/common/credential-utils/src/utils.rs new file mode 100644 index 0000000000..2f92b53c90 --- /dev/null +++ b/common/credential-utils/src/utils.rs @@ -0,0 +1,139 @@ +use crate::errors::{Error, Result}; +use crate::recovery_storage::RecoveryStorage; +use log::*; +use nym_bandwidth_controller::acquire::state::State; +use nym_client_core::config::disk_persistence::CommonClientPaths; +use nym_config::DEFAULT_DATA_DIR; +use nym_credential_storage::persistent_storage::PersistentStorage; +use nym_validator_client::nyxd::contract_traits::{CoconutBandwidthSigningClient, DkgQueryClient}; +use nym_validator_client::nyxd::Coin; +use std::path::PathBuf; +use std::process::exit; +use std::time::{Duration, SystemTime}; + +const SAFETY_BUFFER_SECS: u64 = 60; // 1 minute + +pub async fn issue_credential( + client: &C, + amount: Coin, + persistent_storage: &PersistentStorage, + recovery_storage_path: PathBuf, +) -> Result<()> +where + C: DkgQueryClient + CoconutBandwidthSigningClient + Send + Sync, +{ + let recovery_storage = setup_recovery_storage(recovery_storage_path).await; + + block_until_coconut_is_available(client).await?; + info!("Starting to deposit funds, don't kill the process"); + + if let Ok(recovered_amount) = + recover_credentials(client, &recovery_storage, persistent_storage).await + { + if recovered_amount != 0 { + info!( + "Recovered credentials in the amount of {}", + recovered_amount + ); + return Ok(()); + } + }; + + let state = nym_bandwidth_controller::acquire::deposit(client, amount.clone()).await?; + + if nym_bandwidth_controller::acquire::get_credential(&state, client, persistent_storage) + .await + .is_err() + { + warn!("Failed to obtain credential. Dumping recovery data.",); + match recovery_storage.insert_voucher(&state.voucher) { + Ok(file_path) => { + warn!("Dumped recovery data to {}. Try using recovery mode to convert it to a credential", file_path.to_str().unwrap()); + } + Err(e) => { + error!("Could not dump recovery data to file system due to {:?}, the deposit will be lost!", e) + } + } + + return Err(Error::Credential( + nym_credentials::error::Error::BandwidthCredentialError, + )); + } + + info!("Succeeded adding a credential with amount {amount}"); + + Ok(()) +} + +pub async fn setup_recovery_storage(recovery_dir: PathBuf) -> RecoveryStorage { + RecoveryStorage::new(recovery_dir).expect("") +} + +pub async fn setup_persistent_storage(client_home_directory: PathBuf) -> PersistentStorage { + let data_dir = client_home_directory.join(DEFAULT_DATA_DIR); + let paths = CommonClientPaths::new_default(data_dir); + let db_path = paths.credentials_database; + + nym_credential_storage::initialise_persistent_storage(db_path).await +} + +pub async fn block_until_coconut_is_available(client: &C) -> Result<()> +where + C: DkgQueryClient + Send + Sync, +{ + loop { + let epoch = client.get_current_epoch().await?; + let current_timestamp_secs = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("the system clock is set to 01/01/1970 (or earlier)") + .as_secs(); + if epoch.state.is_final() { + if current_timestamp_secs + SAFETY_BUFFER_SECS >= epoch.finish_timestamp.seconds() { + info!("In the next {} minute(s), a transition will take place in the coconut system. Deposits should be halted in this time for safety reasons.", SAFETY_BUFFER_SECS / 60); + exit(0); + } + + break; + } else { + // Use 1 additional second to not start the next iteration immediately and spam get_current_epoch queries + let secs_until_final = epoch + .final_timestamp_secs() + .saturating_sub(current_timestamp_secs) + + 1; + info!("Approximately {} seconds until coconut is available. Sleeping until then. You can safely kill the process at any moment.", secs_until_final); + tokio::time::sleep(Duration::from_secs(secs_until_final)).await; + } + } + + Ok(()) +} + +pub async fn recover_credentials( + client: &C, + recovery_storage: &RecoveryStorage, + shared_storage: &PersistentStorage, +) -> Result +where + C: DkgQueryClient + Send + Sync, +{ + let mut recovered_amount: u128 = 0; + for voucher in recovery_storage.unconsumed_vouchers()? { + let voucher_value = voucher.get_voucher_value(); + recovered_amount += voucher_value.parse::()?; + + let state = State::new(voucher); + let voucher = state.voucher.tx_hash(); + if let Err(e) = + nym_bandwidth_controller::acquire::get_credential(&state, client, shared_storage).await + { + error!("Could not recover deposit {voucher} due to {e}, try again later",) + } else { + info!("Converted deposit {voucher} to a credential, removing recovery data for it",); + if let Err(e) = recovery_storage.remove_voucher(voucher.to_string()) { + warn!("Could not remove recovery data: {e}"); + } + } + } + + Ok(recovered_amount) +} diff --git a/common/credentials/Cargo.toml b/common/credentials/Cargo.toml index 1f4c444b31..aff9cf01ab 100644 --- a/common/credentials/Cargo.toml +++ b/common/credentials/Cargo.toml @@ -8,7 +8,8 @@ edition = "2021" [dependencies] bls12_381 = { version = "0.5", default-features = false, features = ["pairings", "alloc", "experimental"] } cosmrs = { workspace = true } -thiserror = "1.0" +thiserror = { workspace = true } +log = { workspace = true } # I guess temporarily until we get serde support in coconut up and running nym-coconut-interface = { path = "../coconut-interface" } diff --git a/common/credentials/src/coconut/utils.rs b/common/credentials/src/coconut/utils.rs index 025297f167..b7fda42f73 100644 --- a/common/credentials/src/coconut/utils.rs +++ b/common/credentials/src/coconut/utils.rs @@ -1,6 +1,10 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::coconut::bandwidth::{BandwidthVoucher, PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES}; +use crate::coconut::params::{NymApiCredentialEncryptionAlgorithm, NymApiCredentialHkdfAlgorithm}; +use crate::error::Error; +use log::{debug, warn}; use nym_api_requests::coconut::BlindSignRequestBody; use nym_coconut_interface::{ aggregate_signature_shares, aggregate_verification_keys, prove_bandwidth_credential, Attribute, @@ -11,10 +15,6 @@ use nym_crypto::shared_key::recompute_shared_key; use nym_crypto::symmetric::stream_cipher; use nym_validator_client::client::CoconutApiClient; -use crate::coconut::bandwidth::{BandwidthVoucher, PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES}; -use crate::coconut::params::{NymApiCredentialEncryptionAlgorithm, NymApiCredentialHkdfAlgorithm}; -use crate::error::Error; - pub async fn obtain_aggregate_verification_key( api_clients: &[CoconutApiClient], ) -> Result { @@ -107,7 +107,12 @@ pub async fn obtain_aggregate_signature( aggregate_verification_keys(&validators_partial_vks, Some(indices.as_ref()))?; for coconut_api_client in coconut_api_clients.iter() { - if let Ok(signature) = obtain_partial_credential( + debug!( + "attempting to obtain partial credential from {}", + coconut_api_client.api_client.api_url() + ); + + match obtain_partial_credential( params, attributes, &coconut_api_client.api_client, @@ -115,9 +120,17 @@ pub async fn obtain_aggregate_signature( ) .await { - let share = SignatureShare::new(signature, coconut_api_client.node_id); - shares.push(share) - } + Ok(signature) => { + let share = SignatureShare::new(signature, coconut_api_client.node_id); + shares.push(share) + } + Err(err) => { + warn!( + "failed to obtain partial credential from {}: {err}", + coconut_api_client.api_client.api_url() + ); + } + }; } if shares.len() < threshold as usize { return Err(Error::NotEnoughShares); diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index 8de0ed465c..fe353b16ae 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -4,11 +4,11 @@ use crate::var_names::{DEPRECATED_API_VALIDATOR, DEPRECATED_NYMD_VALIDATOR, NYM_API, NYXD}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::path::Path; use std::{ env::{var, VarError}, ffi::OsStr, ops::Not, - path::PathBuf, }; use url::Url; @@ -43,6 +43,7 @@ pub struct NymNetworkDetails { pub chain_details: ChainDetails, pub endpoints: Vec, pub contracts: NymContracts, + pub explorer_api: Option, } // by default we assume the same defaults as mainnet, i.e. same prefixes and denoms @@ -71,6 +72,7 @@ impl NymNetworkDetails { }, endpoints: Default::default(), contracts: Default::default(), + explorer_api: Default::default(), } } @@ -136,6 +138,7 @@ impl NymNetworkDetails { var_names::SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS, )) .with_name_service_contract(get_optional_env(var_names::NAME_SERVICE_CONTRACT_ADDRESS)) + .with_explorer_api(get_optional_env(var_names::EXPLORER_API)) } pub fn new_mainnet() -> Self { @@ -167,6 +170,7 @@ impl NymNetworkDetails { service_provider_directory_contract_address: None, name_service_contract_address: None, }, + explorer_api: parse_optional_str(mainnet::EXPLORER_API), } } @@ -278,6 +282,12 @@ impl NymNetworkDetails { self.contracts.name_service_contract_address = contract.map(Into::into); self } + + #[must_use] + pub fn with_explorer_api>(mut self, endpoint: Option) -> Self { + self.explorer_api = endpoint.map(Into::into); + self + } } #[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)] @@ -378,7 +388,7 @@ fn fix_deprecated_environmental_variables() { } } -pub fn setup_env(config_env_file: Option<&PathBuf>) { +pub fn setup_env>(config_env_file: Option

) { match std::env::var(var_names::CONFIGURED) { // if the configuration is not already set in the env vars Err(std::env::VarError::NotPresent) => { diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index fbc25c4b00..bfb3f42531 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -26,6 +26,8 @@ pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hh pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "https://mainnet-stats.nymte.ch:8090/"; pub const NYXD_URL: &str = "https://rpc.nymtech.net"; pub const NYM_API: &str = "https://validator.nymtech.net/api/"; +pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/"; + pub(crate) fn validators() -> Vec { vec![ValidatorDetails::new(NYXD_URL, Some(NYM_API))] } @@ -99,6 +101,7 @@ pub fn export_to_env() { ); set_var_to_default(var_names::NYXD, NYXD_URL); set_var_to_default(var_names::NYM_API, NYM_API); + set_var_to_default(var_names::EXPLORER_API, EXPLORER_API); } pub fn export_to_env_if_not_set() { @@ -145,4 +148,5 @@ pub fn export_to_env_if_not_set() { ); set_var_conditionally_to_default(var_names::NYXD, NYXD_URL); set_var_conditionally_to_default(var_names::NYM_API, NYM_API); + set_var_conditionally_to_default(var_names::EXPLORER_API, EXPLORER_API); } diff --git a/common/network-defaults/src/var_names.rs b/common/network-defaults/src/var_names.rs index 26b65b7f95..d082045a03 100644 --- a/common/network-defaults/src/var_names.rs +++ b/common/network-defaults/src/var_names.rs @@ -26,6 +26,7 @@ pub const SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS: &str = pub const NAME_SERVICE_CONTRACT_ADDRESS: &str = "NAME_SERVICE_CONTRACT_ADDRESS"; pub const NYXD: &str = "NYXD"; pub const NYM_API: &str = "NYM_API"; +pub const EXPLORER_API: &str = "EXPLORER_API"; pub const DKG_TIME_CONFIGURATION: &str = "DKG_TIME_CONFIGURATION"; diff --git a/common/nymcoconut/src/proofs/mod.rs b/common/nymcoconut/src/proofs/mod.rs index 84c7211aa2..42f923466d 100644 --- a/common/nymcoconut/src/proofs/mod.rs +++ b/common/nymcoconut/src/proofs/mod.rs @@ -354,7 +354,7 @@ impl ProofKappaZeta { let witness_blinder = params.random_scalar(); let witness_serial_number = params.random_scalar(); let witness_binding_number = params.random_scalar(); - let witness_attributes = vec![witness_serial_number, witness_binding_number]; + let witness_attributes = [witness_serial_number, witness_binding_number]; let beta_bytes = verification_key .beta_g2 @@ -417,7 +417,7 @@ impl ProofKappaZeta { .map(|beta_i| beta_i.to_bytes()) .collect::>(); - let response_attributes = vec![self.response_serial_number, self.response_binding_number]; + let response_attributes = [self.response_serial_number, self.response_binding_number]; // re-compute witnesses commitments // Aw = (c * kappa) + (rt * g2) + ((1 - c) * alpha) + (rm[0] * beta[0]) + ... + (rm[i] * beta[i]) let commitment_kappa = kappa * self.challenge diff --git a/common/nymsphinx/acknowledgements/src/identifier.rs b/common/nymsphinx/acknowledgements/src/identifier.rs index aab5a96146..abe1d58368 100644 --- a/common/nymsphinx/acknowledgements/src/identifier.rs +++ b/common/nymsphinx/acknowledgements/src/identifier.rs @@ -17,7 +17,7 @@ pub fn prepare_identifier( let id_ciphertext = encrypt::(key.inner(), &iv, &serialized_id); // IV || ID_CIPHERTEXT - iv.into_iter().chain(id_ciphertext.into_iter()).collect() + iv.into_iter().chain(id_ciphertext).collect() } pub fn recover_identifier( diff --git a/common/nymsphinx/acknowledgements/src/surb_ack.rs b/common/nymsphinx/acknowledgements/src/surb_ack.rs index e623d4e738..732f84b9ba 100644 --- a/common/nymsphinx/acknowledgements/src/surb_ack.rs +++ b/common/nymsphinx/acknowledgements/src/surb_ack.rs @@ -122,7 +122,7 @@ impl SurbAck { .first_hop_address .as_zero_padded_bytes(MAX_NODE_ADDRESS_UNPADDED_LEN) .into_iter() - .chain(self.surb_ack_packet.to_bytes()?.into_iter()) + .chain(self.surb_ack_packet.to_bytes()?) .collect(); Ok((self.expected_total_delay, surb_bytes)) } diff --git a/common/nymsphinx/anonymous-replies/src/reply_surb.rs b/common/nymsphinx/anonymous-replies/src/reply_surb.rs index 2bf5799912..d38b16fc13 100644 --- a/common/nymsphinx/anonymous-replies/src/reply_surb.rs +++ b/common/nymsphinx/anonymous-replies/src/reply_surb.rs @@ -130,7 +130,7 @@ impl ReplySurb { self.encryption_key .to_bytes() .into_iter() - .chain(self.surb.to_bytes().into_iter()) + .chain(self.surb.to_bytes()) .collect() } diff --git a/common/nymsphinx/anonymous-replies/src/requests.rs b/common/nymsphinx/anonymous-replies/src/requests.rs index f4effbf7d6..9dd4c84dc0 100644 --- a/common/nymsphinx/anonymous-replies/src/requests.rs +++ b/common/nymsphinx/anonymous-replies/src/requests.rs @@ -275,7 +275,7 @@ impl RepliableMessageContent { .to_be_bytes() .into_iter() .chain(reply_surbs.into_iter().flat_map(|s| s.to_bytes())) - .chain(message.into_iter()) + .chain(message) .collect() } RepliableMessageContent::AdditionalSurbs { reply_surbs } => { @@ -465,7 +465,7 @@ impl ReplyMessageContent { ReplyMessageContent::SurbRequest { recipient, amount } => recipient .to_bytes() .into_iter() - .chain(amount.to_be_bytes().into_iter()) + .chain(amount.to_be_bytes()) .collect(), } } diff --git a/common/nymsphinx/chunking/src/fragment.rs b/common/nymsphinx/chunking/src/fragment.rs index 74996f29ab..6dccfa053a 100644 --- a/common/nymsphinx/chunking/src/fragment.rs +++ b/common/nymsphinx/chunking/src/fragment.rs @@ -204,7 +204,7 @@ impl Fragment { self.header .to_bytes() .into_iter() - .chain(self.payload.into_iter()) + .chain(self.payload) .collect() } diff --git a/common/nymsphinx/cover/src/lib.rs b/common/nymsphinx/cover/src/lib.rs index 05f4cffe33..66142a4cee 100644 --- a/common/nymsphinx/cover/src/lib.rs +++ b/common/nymsphinx/cover/src/lib.rs @@ -115,7 +115,7 @@ where let packet_payload: Vec<_> = ack_bytes .into_iter() .chain(ephemeral_keypair.public_key().to_bytes().iter().cloned()) - .chain(cover_content.into_iter()) + .chain(cover_content) .collect(); let route = diff --git a/common/nymsphinx/forwarding/src/packet.rs b/common/nymsphinx/forwarding/src/packet.rs index 2276e9a5cd..13026a3aaf 100644 --- a/common/nymsphinx/forwarding/src/packet.rs +++ b/common/nymsphinx/forwarding/src/packet.rs @@ -106,8 +106,8 @@ impl MixPacket { pub fn into_bytes(self) -> Result, MixPacketFormattingError> { Ok(std::iter::once(self.packet_type as u8) - .chain(self.next_hop.as_bytes().into_iter()) - .chain(self.packet.to_bytes()?.into_iter()) + .chain(self.next_hop.as_bytes()) + .chain(self.packet.to_bytes()?) .collect()) } } diff --git a/common/nymsphinx/src/preparer/payload.rs b/common/nymsphinx/src/preparer/payload.rs index 5999026252..bfd1715d57 100644 --- a/common/nymsphinx/src/preparer/payload.rs +++ b/common/nymsphinx/src/preparer/payload.rs @@ -50,8 +50,8 @@ impl NymPayloadBuilder { Ok(NymPayload( surb_ack_bytes .into_iter() - .chain(variant_data.into_iter()) - .chain(fragment_data.into_iter()) + .chain(variant_data) + .chain(fragment_data) .collect(), )) } @@ -61,10 +61,7 @@ impl NymPayloadBuilder { packet_encryption_key: &SurbEncryptionKey, ) -> Result { let key_digest = packet_encryption_key.compute_digest(); - self.build::( - packet_encryption_key.inner(), - key_digest.into_iter(), - ) + self.build::(packet_encryption_key.inner(), key_digest) } pub fn build_regular( diff --git a/common/socks5/requests/src/lib.rs b/common/socks5/requests/src/lib.rs index 58c23dcd9d..c131f132a0 100644 --- a/common/socks5/requests/src/lib.rs +++ b/common/socks5/requests/src/lib.rs @@ -73,7 +73,7 @@ impl SocketDataHeader { .to_be_bytes() .into_iter() .chain(std::iter::once(self.local_socket_closed as u8)) - .chain(self.seq.to_be_bytes().into_iter()) + .chain(self.seq.to_be_bytes()) } pub fn try_from_response_bytes( @@ -107,8 +107,8 @@ impl SocketDataHeader { pub fn into_response_bytes_iter(self) -> impl Iterator { std::iter::once(self.local_socket_closed as u8) - .chain(self.connection_id.to_be_bytes().into_iter()) - .chain(self.seq.to_be_bytes().into_iter()) + .chain(self.connection_id.to_be_bytes()) + .chain(self.seq.to_be_bytes()) } } @@ -170,9 +170,7 @@ impl SocketData { } pub fn into_request_bytes_iter(self) -> impl Iterator { - self.header - .into_request_bytes_iter() - .chain(self.data.into_iter()) + self.header.into_request_bytes_iter().chain(self.data) } pub fn try_from_response_bytes(b: &[u8]) -> Result { @@ -190,9 +188,7 @@ impl SocketData { } pub fn into_response_bytes_iter(self) -> impl Iterator { - self.header - .into_response_bytes_iter() - .chain(self.data.into_iter()) + self.header.into_response_bytes_iter().chain(self.data) } } diff --git a/common/socks5/requests/src/request.rs b/common/socks5/requests/src/request.rs index 0e29fb49d9..530d27e11f 100644 --- a/common/socks5/requests/src/request.rs +++ b/common/socks5/requests/src/request.rs @@ -113,7 +113,7 @@ impl Serializable for Socks5Request { fn into_bytes(self) -> Vec { if let Some(version) = self.protocol_version.as_u8() { std::iter::once(version) - .chain(self.content.into_bytes().into_iter()) + .chain(self.content.into_bytes()) .collect() } else { std::iter::once(Self::LEGACY_TYPE_TAG) @@ -335,12 +335,12 @@ impl Socks5RequestContent { let remote_address_bytes_len = remote_address_bytes.len() as u16; let iter = std::iter::once(RequestFlag::Connect as u8) - .chain(req.conn_id.to_be_bytes().into_iter()) - .chain(remote_address_bytes_len.to_be_bytes().into_iter()) - .chain(remote_address_bytes.into_iter()); + .chain(req.conn_id.to_be_bytes()) + .chain(remote_address_bytes_len.to_be_bytes()) + .chain(remote_address_bytes); if let Some(return_address) = req.return_address { - iter.chain(return_address.to_bytes().into_iter()).collect() + iter.chain(return_address.to_bytes()).collect() } else { iter.collect() } @@ -358,7 +358,7 @@ impl Socks5RequestContent { }) .unwrap_or_default(); std::iter::once(RequestFlag::Query as u8) - .chain(query_bytes.into_iter()) + .chain(query_bytes) .collect() } } @@ -495,7 +495,7 @@ mod request_deserialization_tests { let request_bytes: Vec<_> = request_bytes_prefix .iter() .cloned() - .chain(recipient_bytes.into_iter()) + .chain(recipient_bytes) .collect(); assert!(Socks5RequestContent::try_from_bytes(&request_bytes) .unwrap_err() @@ -530,10 +530,7 @@ mod request_deserialization_tests { let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); let recipient_bytes = recipient.to_bytes(); - let request_bytes: Vec<_> = request_bytes - .into_iter() - .chain(recipient_bytes.into_iter()) - .collect(); + let request_bytes: Vec<_> = request_bytes.into_iter().chain(recipient_bytes).collect(); let request = Socks5RequestContent::try_from_bytes(&request_bytes).unwrap(); match request { @@ -577,10 +574,7 @@ mod request_deserialization_tests { let recipient = Recipient::try_from_base58_string("CytBseW6yFXUMzz4SGAKdNLGR7q3sJLLYxyBGvutNEQV.4QXYyEVc5fUDjmmi8PrHN9tdUFV4PCvSJE1278cHyvoe@4sBbL1ngf1vtNqykydQKTFh26sQCw888GpUqvPvyNB4f").unwrap(); let recipient_bytes = recipient.to_bytes(); - let request_bytes: Vec<_> = request_bytes - .into_iter() - .chain(recipient_bytes.into_iter()) - .collect(); + let request_bytes: Vec<_> = request_bytes.into_iter().chain(recipient_bytes).collect(); let request = Socks5RequestContent::try_from_bytes(&request_bytes).unwrap(); match request { diff --git a/common/socks5/requests/src/response.rs b/common/socks5/requests/src/response.rs index c0ff60fa39..0415035ac5 100644 --- a/common/socks5/requests/src/response.rs +++ b/common/socks5/requests/src/response.rs @@ -84,7 +84,7 @@ impl Serializable for Socks5Response { fn into_bytes(self) -> Vec { if let Some(version) = self.protocol_version.as_u8() { std::iter::once(version) - .chain(self.content.into_bytes().into_iter()) + .chain(self.content.into_bytes()) .collect() } else { self.content.into_bytes() @@ -192,7 +192,7 @@ impl Socks5ResponseContent { } Socks5ResponseContent::ConnectionError(res) => { std::iter::once(ResponseFlag::ConnectionError as u8) - .chain(res.into_bytes().into_iter()) + .chain(res.into_bytes()) .collect() } Socks5ResponseContent::Query(query) => { @@ -204,7 +204,7 @@ impl Socks5ResponseContent { }) .unwrap_or_default(); std::iter::once(ResponseFlag::Query as u8) - .chain(query_bytes.into_iter()) + .chain(query_bytes) .collect() } } @@ -290,7 +290,7 @@ impl ConnectionError { .to_be_bytes() .iter() .copied() - .chain(self.network_requester_error.into_bytes().into_iter()) + .chain(self.network_requester_error.into_bytes()) .collect() } } @@ -339,7 +339,7 @@ mod tests { let bytes: Vec = 42u64 .to_be_bytes() .into_iter() - .chain([0, 159, 146, 150].into_iter()) + .chain([0, 159, 146, 150]) .collect(); let err = ConnectionError::try_from_bytes(&bytes).err().unwrap(); assert!(matches!( diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 45eb2d9d7d..428efd1056 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -131,7 +131,7 @@ impl NymTopology { } pub fn mixes_in_layer(&self, layer: MixLayer) -> Vec { - assert!(vec![1, 2, 3].contains(&layer)); + assert!([1, 2, 3].contains(&layer)); self.mixes.get(&layer).unwrap().to_owned() } diff --git a/common/types/Cargo.toml b/common/types/Cargo.toml index 5515ba5248..4df4087661 100644 --- a/common/types/Cargo.toml +++ b/common/types/Cargo.toml @@ -17,7 +17,7 @@ serde_json = { workspace = true } strum = { version = "0.23", features = ["derive"] } thiserror = { workspace = true } url = { workspace = true } -ts-rs = "6.1.2" +ts-rs = { workspace = true } cosmwasm-std = { workspace = true } cosmrs = { workspace = true } diff --git a/common/wasm-utils/src/storage/mod.rs b/common/wasm-utils/src/storage/mod.rs index 1a5b601ce6..eb17949849 100644 --- a/common/wasm-utils/src/storage/mod.rs +++ b/common/wasm-utils/src/storage/mod.rs @@ -5,7 +5,6 @@ use crate::console_log; use crate::storage::cipher_export::StoredExportedStoreCipher; use crate::storage::error::StorageError; use futures::TryFutureExt; -use indexed_db_futures::IdbDatabase; use nym_store_cipher::{ Aes256Gcm, Algorithm, EncryptedData, KdfInfo, KeySizeUser, Params, StoreCipher, Unsigned, Version, diff --git a/contracts/mixnet/src/interval/storage.rs b/contracts/mixnet/src/interval/storage.rs index 0e35f3908d..04f3aa999a 100644 --- a/contracts/mixnet/src/interval/storage.rs +++ b/contracts/mixnet/src/interval/storage.rs @@ -176,7 +176,7 @@ mod tests { use cosmwasm_std::Order; use rand_chacha::rand_core::RngCore; - fn read_entire_set(storage: &mut dyn Storage) -> HashMap { + fn read_entire_set(storage: &dyn Storage) -> HashMap { REWARDED_SET .range(storage, None, None, Order::Ascending) .map(|r| r.unwrap()) diff --git a/contracts/name-service/src/contract.rs b/contracts/name-service/src/contract.rs index 2001341212..35294382c8 100644 --- a/contracts/name-service/src/contract.rs +++ b/contracts/name-service/src/contract.rs @@ -286,7 +286,6 @@ mod tests { let owner = "steve"; let (name, owner_signature) = new_name_details_with_sign(deps.as_mut(), &mut rng, "my-name", owner, deposit.clone()); - dbg!(&name); // Register let msg = ExecuteMsg::Register { diff --git a/contracts/name-service/src/integration_tests/register.rs b/contracts/name-service/src/integration_tests/register.rs index aeed4add3d..e46499ce32 100644 --- a/contracts/name-service/src/integration_tests/register.rs +++ b/contracts/name-service/src/integration_tests/register.rs @@ -265,7 +265,6 @@ fn can_register_multiple_names_for_the_same_nym_address(mut setup: TestSetup) { let reg_name2 = reg_name2.sign(payload); setup.register(®_name2, &owner); - dbg!(&setup.query_all().names); assert_eq!( setup.query_all().names, vec![ diff --git a/documentation/dev-portal/src/images/profile_picture/minibolt_pp.png b/documentation/dev-portal/src/images/profile_picture/minibolt_pp.png index ed059e6055..455243cc22 100644 Binary files a/documentation/dev-portal/src/images/profile_picture/minibolt_pp.png and b/documentation/dev-portal/src/images/profile_picture/minibolt_pp.png differ diff --git a/documentation/operators/src/nodes/mix-node-setup.md b/documentation/operators/src/nodes/mix-node-setup.md index a50c5cac13..b168329487 100644 --- a/documentation/operators/src/nodes/mix-node-setup.md +++ b/documentation/operators/src/nodes/mix-node-setup.md @@ -63,16 +63,16 @@ To check available configuration options for initializing your node use: ``` ~~~ -Initalise your mix node with the following command, replacing the value of `--id` with the moniker you wish to give your mix node. Your `--host` must be publicly routable on the internet in order to mix packets, and can be either an Ipv4 or IPv6 address. The `$(curl ifconfig.me)` command returns your IP automatically using an external service. If you enter your IP address manually, enter it **without** any port information. +Initalise your mix node with the following command, replacing the value of `--id` with the moniker you wish to give your mix node. Your `--host` must be publicly routable on the internet in order to mix packets, and can be either an Ipv4 or IPv6 address. The `$(curl -4 https://ifconfig.me)` command returns your IP automatically using an external service. If you enter your IP address manually, enter it **without** any port information. ``` -./nym-mixnode init --id --host $(curl ifconfig.me) +./nym-mixnode init --id --host $(curl -4 https://ifconfig.me) ``` ~~~admonish example collapsible=true title="Console output" ``` -.nym-mixnode init --id --host $(curl ifconfig.me) --wallet-address +.nym-mixnode init --id --host $(curl -4 https://ifconfig.me) --wallet-address Initialising mixnode ... diff --git a/envs/mainnet.env b/envs/mainnet.env index 4bf406bd64..9dc5b5272d 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -22,5 +22,6 @@ REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" NYXD="https://rpc.nymtech.net" NYM_API="https://validator.nymtech.net/api/" +EXPLORER_API="https://explorer.nymtech.net/api/" DKG_TIME_CONFIGURATION="259200,300,300,60,60,1209600" diff --git a/envs/sandbox.env b/envs/sandbox.env index 95b0b4d49a..b4a66a70d1 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -23,4 +23,5 @@ EPHEMERA_CONTRACT_ADDRESS="nymt1gwk6muhmzeuxje7df7rjvqwl2vex0kj4t2hwuzmyx5k62kfu STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" NYXD="https://sandbox-validator1.nymtech.net" -NYM_API="https://sandbox-nym-api1.nymtech.net/api/" \ No newline at end of file +NYM_API="https://sandbox-nym-api1.nymtech.net/api/" +EXPLORER_API="https://sandbox-explorer.nymtech.net/api/" diff --git a/ephemera/Cargo.toml b/ephemera/Cargo.toml index c831726134..50743df2fe 100644 --- a/ephemera/Cargo.toml +++ b/ephemera/Cargo.toml @@ -37,7 +37,7 @@ nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" } pretty_env_logger = "0.4" refinery = { version = "0.8.7", features = ["rusqlite"], optional = true } reqwest = { version = "0.11.6", features = ["json"] } -rocksdb = { version = "0.20.1", optional = true } +rocksdb = { version = "0.21.0", optional = true } rusqlite = { version = "0.27.0", features = ["bundled"], optional = true } serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0.149" diff --git a/ephemera/src/core/builder.rs b/ephemera/src/core/builder.rs index db9b3e8254..58d4b7cd08 100644 --- a/ephemera/src/core/builder.rs +++ b/ephemera/src/core/builder.rs @@ -211,7 +211,7 @@ impl EphemeraStarterWithApplication { #[cfg(feature = "rocksdb_storage")] fn connect_rocksdb(&self) -> anyhow::Result { info!("Opening database..."); - RocksDbStorage::open(self.init.config.storage.clone()) + RocksDbStorage::open(&self.init.config.storage) .map_err(|e| anyhow::anyhow!("Failed to open database: {}", e)) } diff --git a/ephemera/src/storage/rocksdb/mod.rs b/ephemera/src/storage/rocksdb/mod.rs index dc86a4bd7f..24e5916bc3 100644 --- a/ephemera/src/storage/rocksdb/mod.rs +++ b/ephemera/src/storage/rocksdb/mod.rs @@ -104,7 +104,7 @@ fn block_hash_key(block_hash: &str) -> String { format!("{PREFIX_BLOCK_HASH}:{block_hash}") } -fn block_height_key(height: &u64) -> String { +fn block_height_key(height: u64) -> String { format!("{PREFIX_BLOCK_HEIGHT}:{height}") } diff --git a/ephemera/src/storage/rocksdb/query.rs b/ephemera/src/storage/rocksdb/query.rs index a2586557e5..a41bde78e9 100644 --- a/ephemera/src/storage/rocksdb/query.rs +++ b/ephemera/src/storage/rocksdb/query.rs @@ -53,7 +53,7 @@ impl Database { pub(crate) fn get_block_by_height(&self, height: u64) -> anyhow::Result> { trace!("Getting block by height: {}", height); - if let Some(block_hash) = self.database.get(block_height_key(&height))? { + if let Some(block_hash) = self.database.get(block_height_key(height))? { let block_hash = String::from_utf8(block_hash)?; self.get_block_by_hash(&block_hash) } else { diff --git a/ephemera/src/storage/rocksdb/store.rs b/ephemera/src/storage/rocksdb/store.rs index 66e1f6d2de..9b65137863 100644 --- a/ephemera/src/storage/rocksdb/store.rs +++ b/ephemera/src/storage/rocksdb/store.rs @@ -12,6 +12,7 @@ use rocksdb::{TransactionDB, WriteBatchWithTransaction}; use crate::utilities::crypto::Certificate; +#[allow(clippy::module_name_repetitions)] pub struct DbStore { connection: Arc, } @@ -34,7 +35,7 @@ impl DbStore { let block_id_key = block_hash_key(&hash_str); let certificates_key = certificates_key(&hash_str); - let height_key = block_height_key(&block.header.height); + let height_key = block_height_key(block.header.height); let members_key = members_key(&hash_str); let merkle_tree_key = merkle_tree_key(&hash_str); diff --git a/explorer-api/.env.dev b/explorer-api/.env.dev new file mode 100644 index 0000000000..0020aba233 --- /dev/null +++ b/explorer-api/.env.dev @@ -0,0 +1,44 @@ +CONFIGURED=true + +RUST_LOG=info +RUST_BACKTRACE=1 + +BECH32_PREFIX=n +MIX_DENOM=unym +MIX_DENOM_DISPLAY=nym +STAKE_DENOM=unyx +STAKE_DENOM_DISPLAY=nyx +DENOMS_EXPONENT=6 + +REWARDING_VALIDATOR_ADDRESS=n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa +MIXNET_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav +VESTING_CONTRACT_ADDRESS=n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz +BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 +COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l +GROUP_CONTRACT_ADDRESS=n1pd7kfgvr5tpcv0xnlv46c4jsq9jg2r799xxrcwqdm4l2jhq2pjwqrmz5ju +MULTISIG_CONTRACT_ADDRESS=n14ph4e660eyqz0j36zlkaey4zgzexm5twkmjlqaequxr2cjm9eprqsmad6k +COCONUT_DKG_CONTRACT_ADDRESS=n1ahg0erc2fs6xx3j5m8sfx3ryuzdjh6kf6qm9plsf865fltekyrfsesac6a +NAME_SERVICE_CONTRACT_ADDRESS=n12ne7qtmdwd0j03t9t5es8md66wq4e5xg9neladrsag8fx3y89rcs36asfp +SERVICE_PROVIDER_DIRECTORY_CONTRACT_ADDRESS=n1ps5yutd7sufwg058qd7ac7ldnlazsvmhzqwucsfxmm445d70u8asqxpur4 + +STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" +NYXD="https://sandbox-validator1.nymtech.net" +NYM_API="https://sandbox-nym-api1.nymtech.net/api" + +GEOIP_DB_PATH=geo_ip/GeoLite2-City.mmdb +# MaxMind account ID +# TODO replace with your own account ID +GEOIPUPDATE_ACCOUNT_ID=xxx +# MaxMind license key +# TODO replace with your own license key +GEOIPUPDATE_LICENSE_KEY=xxx +# List of space-separated database edition IDs. Edition IDs may +# consist of letters, digits, and dashes. For example, GeoIP2-City +# would download the GeoIP2 City database (GeoIP2-City). +GEOIPUPDATE_EDITION_IDS=GeoLite2-City +# The number of hours between geoipupdate runs. If this is not set +# or is set to 0, geoipupdate will run once and exit. +GEOIPUPDATE_FREQUENCY=72 +# The path to the directory where geoipupdate will download the +# database. +GEOIP_DB_DIRECTORY=./geo_ip diff --git a/explorer-api/.gitignore b/explorer-api/.gitignore index 66ab76436b..fd6f795b06 100644 --- a/explorer-api/.gitignore +++ b/explorer-api/.gitignore @@ -1,3 +1,4 @@ target explorer-api-state.json /geo_ip +!.env.dev diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index a39e3cb52b..10ca849b3d 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "explorer-api" -version = "1.1.26" +version = "1.1.27" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/explorer-api/README.md b/explorer-api/README.md index 86e64e4c12..8ab514d616 100644 --- a/explorer-api/README.md +++ b/explorer-api/README.md @@ -8,9 +8,17 @@ Features: - calculates how many nodes are in each country - proxies mixnode API requests to add HTTPS +## Development + +Several environment variables are required. They can be +provisioned via a `.env` file. For convenience a `.env.dev` is +provided, just copy its content into `.env`. + +Follow the steps to setup the geoip database. + ## GeoIP db install/update -First we need to install the geoip database. +A geoip database needs to be installed. We use https://github.com/maxmind/geoipupdate to automatically download and update GeoLite2 binary database. For convenience we @@ -37,13 +45,7 @@ When starting the explorer-api, supply the environment variable `GEOIP_DB_PATH`, pointing to the GeoLite2 binary database file. It should be previously installed thanks to `geoipupdate` service. -For example: - -```shell -GEOIP_DB_PATH=./geo_ip/GeoLite2-City.mmdb cargo run -``` - -Note: explorer-api binary reads the provided `.env` file. +Note: As mentioned above the explorer-api binary reads the provided `.env` file. Run as a service and reverse proxy with `nginx` to add `https` with Lets Encrypt. diff --git a/explorer-api/explorer-api-requests/Cargo.toml b/explorer-api/explorer-api-requests/Cargo.toml index 7d8dc2f803..d7139b9f78 100644 --- a/explorer-api/explorer-api-requests/Cargo.toml +++ b/explorer-api/explorer-api-requests/Cargo.toml @@ -9,4 +9,4 @@ nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mix nym-api-requests = { path = "../../nym-api/nym-api-requests" } schemars = { version = "0.8", features = ["preserve_order"] } serde = { version = "1.0", features = ["derive"] } -ts-rs = { version = "6.1.2", optional = true } +ts-rs = { workspace = true, optional = true } diff --git a/explorer-api/explorer-api-requests/src/lib.rs b/explorer-api/explorer-api-requests/src/lib.rs index 6180d65f11..2109348899 100644 --- a/explorer-api/explorer-api-requests/src/lib.rs +++ b/explorer-api/explorer-api-requests/src/lib.rs @@ -1,6 +1,6 @@ use nym_api_requests::models::NodePerformance; use nym_contracts_common::Percent; -use nym_mixnet_contract_common::{Addr, Coin, Layer, MixId, MixNode}; +use nym_mixnet_contract_common::{Addr, Coin, Gateway, Layer, MixId, MixNode}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -42,3 +42,13 @@ pub struct Location { pub latitude: Option, pub longitude: Option, } + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct PrettyDetailedGatewayBond { + pub pledge_amount: Coin, + pub owner: Addr, + pub block_height: u64, + pub gateway: Gateway, + pub proxy: Option, + pub location: Option, +} diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index a80ebc28e4..858f593986 100644 --- a/explorer-api/src/country_statistics/geolocate.rs +++ b/explorer-api/src/country_statistics/geolocate.rs @@ -17,13 +17,14 @@ impl GeoLocateTask { } pub(crate) fn start(mut self) { - info!("Spawning mix node locator task runner..."); + info!("Spawning locator task runner..."); tokio::spawn(async move { let mut interval_timer = tokio::time::interval(std::time::Duration::from_millis(50)); while !self.shutdown.is_shutdown() { tokio::select! { _ = interval_timer.tick() => { self.locate_mix_nodes().await; + self.locate_gateways().await; } _ = self.shutdown.recv() => { trace!("Listener: Received shutdown"); @@ -107,4 +108,68 @@ impl GeoLocateTask { trace!("All mix nodes located"); } + + async fn locate_gateways(&mut self) { + let gateways = self.state.inner.gateways.get_gateways().await; + + let geo_ip = self.state.inner.geo_ip.0.clone(); + + for (i, cache_item) in gateways.iter().enumerate() { + if self + .state + .inner + .gateways + .is_location_valid(cache_item.identity().to_owned()) + .await + { + // when the cached location is valid, don't locate and continue to next gateway + continue; + } + + match geo_ip.query(&cache_item.gateway.host, Some(cache_item.gateway.mix_port)) { + Ok(opt) => match opt { + Some(location) => { + let location: Location = location.into(); + + trace!( + "{} gateways already located. Ip {} is located in {:#?}", + i, + cache_item.gateway.host, + location.three_letter_iso_country_code, + ); + + if i > 0 && (i % 100) == 0 { + info!("Located {} gateways...", i + 1,); + } + + self.state + .inner + .gateways + .set_location(cache_item.identity().to_owned(), Some(location)) + .await; + + // one node has been located, so return out of the loop + return; + } + None => { + warn!("❌ Location for {} not found.", cache_item.gateway.host); + self.state + .inner + .gateways + .set_location(cache_item.identity().to_owned(), None) + .await; + } + }, + Err(_e) => { + // warn!( + // "❌ Oh no! Location for {} failed. Error: {:#?}", + // cache_item.gateway.host, + // e + // ); + } + }; + } + + trace!("All gateways located"); + } } diff --git a/explorer-api/src/gateways/http.rs b/explorer-api/src/gateways/http.rs index 1f494d0edd..ac0d85c0b1 100644 --- a/explorer-api/src/gateways/http.rs +++ b/explorer-api/src/gateways/http.rs @@ -1,6 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use nym_explorer_api_requests::PrettyDetailedGatewayBond; use rocket::response::status::NotFound; use rocket::serde::json::Json; use rocket::{Route, State}; @@ -9,7 +10,6 @@ use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; use crate::state::ExplorerApiStateContext; -use nym_mixnet_contract_common::GatewayBond; pub fn gateways_make_default_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { openapi_get_routes_spec![settings: list] @@ -19,6 +19,6 @@ pub fn gateways_make_default_routes(settings: &OpenApiSettings) -> (Vec, #[get("/")] pub(crate) async fn list( state: &State, -) -> Result>, NotFound> { - Ok(Json(state.inner.gateways.get_gateways().await)) +) -> Result>, NotFound> { + Ok(Json(state.inner.gateways.get_detailed_gateways().await)) } diff --git a/explorer-api/src/gateways/location.rs b/explorer-api/src/gateways/location.rs new file mode 100644 index 0000000000..68420720b3 --- /dev/null +++ b/explorer-api/src/gateways/location.rs @@ -0,0 +1,8 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_contracts_common::IdentityKey; + +use crate::location::LocationCache; + +pub(crate) type GatewayLocationCache = LocationCache; diff --git a/explorer-api/src/gateways/mod.rs b/explorer-api/src/gateways/mod.rs index 5df938f83c..cbce7ef930 100644 --- a/explorer-api/src/gateways/mod.rs +++ b/explorer-api/src/gateways/mod.rs @@ -1,2 +1,3 @@ pub(crate) mod http; +pub(crate) mod location; pub(crate) mod models; diff --git a/explorer-api/src/gateways/models.rs b/explorer-api/src/gateways/models.rs index ab2712c3fc..a3ed406067 100644 --- a/explorer-api/src/gateways/models.rs +++ b/explorer-api/src/gateways/models.rs @@ -1,12 +1,15 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::cache::Cache; +use crate::{cache::Cache, location::LocationCacheItem}; +use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond}; use nym_mixnet_contract_common::{GatewayBond, IdentityKey}; use serde::Serialize; -use std::sync::Arc; +use std::{sync::Arc, time::SystemTime}; use tokio::sync::RwLock; +use super::location::GatewayLocationCache; + pub(crate) struct GatewayCache { pub(crate) gateways: Cache, } @@ -18,30 +21,93 @@ pub(crate) struct GatewaySummary { #[derive(Clone)] pub(crate) struct ThreadsafeGatewayCache { - inner: Arc>, + gateways: Arc>, + locations: Arc>, } impl ThreadsafeGatewayCache { pub(crate) fn new() -> Self { ThreadsafeGatewayCache { - inner: Arc::new(RwLock::new(GatewayCache { + gateways: Arc::new(RwLock::new(GatewayCache { gateways: Cache::new(), })), + locations: Arc::new(RwLock::new(GatewayLocationCache::new())), + } + } + + fn create_detailed_gateway( + &self, + bond: GatewayBond, + location: Option<&LocationCacheItem>, + ) -> PrettyDetailedGatewayBond { + PrettyDetailedGatewayBond { + pledge_amount: bond.pledge_amount, + owner: bond.owner, + block_height: bond.block_height, + gateway: bond.gateway, + proxy: bond.proxy, + location: location.and_then(|l| l.location.clone()), } } pub(crate) async fn get_gateways(&self) -> Vec { - self.inner.read().await.gateways.get_all() + self.gateways.read().await.gateways.get_all() + } + + pub(crate) async fn get_detailed_gateways(&self) -> Vec { + let gateways_guard = self.gateways.read().await; + let location_guard = self.locations.read().await; + + gateways_guard + .gateways + .get_all() + .iter() + .map(|bond| { + let location = location_guard.get(bond.identity()); + self.create_detailed_gateway(bond.to_owned(), location) + }) + .collect() } pub(crate) async fn get_gateway_summary(&self) -> GatewaySummary { GatewaySummary { - count: self.inner.read().await.gateways.len(), + count: self.gateways.read().await.gateways.len(), } } + pub(crate) fn new_with_location_cache(locations: GatewayLocationCache) -> Self { + ThreadsafeGatewayCache { + gateways: Arc::new(RwLock::new(GatewayCache { + gateways: Cache::new(), + })), + locations: Arc::new(RwLock::new(locations)), + } + } + + pub(crate) async fn is_location_valid(&self, identity_key: IdentityKey) -> bool { + self.locations + .read() + .await + .get(&identity_key) + .map_or(false, |cache_item| { + cache_item.valid_until > SystemTime::now() + }) + } + + pub(crate) async fn get_locations(&self) -> GatewayLocationCache { + self.locations.read().await.clone() + } + + pub(crate) async fn set_location(&self, identy_key: IdentityKey, location: Option) { + // cache the location for this mix node so that it can be used when the mix node list is refreshed + self.locations + .write() + .await + .insert(identy_key, LocationCacheItem::new_from_location(location)); + } + pub(crate) async fn update_cache(&self, gateways: Vec) { - let mut guard = self.inner.write().await; + let mut guard = self.gateways.write().await; for gateway in gateways { guard diff --git a/explorer-api/src/location.rs b/explorer-api/src/location.rs new file mode 100644 index 0000000000..d11572e631 --- /dev/null +++ b/explorer-api/src/location.rs @@ -0,0 +1,40 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_explorer_api_requests::Location; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::time::{Duration, SystemTime}; + +pub(crate) type LocationCache = HashMap; + +#[allow(dead_code)] +#[derive(Debug, Deserialize)] +pub(crate) struct GeoLocation { + pub(crate) ip: String, + pub(crate) country_code: String, + pub(crate) country_name: String, + pub(crate) region_code: String, + pub(crate) region_name: String, + pub(crate) city: String, + pub(crate) zip_code: String, + pub(crate) time_zone: String, + pub(crate) latitude: f32, + pub(crate) longitude: f32, + pub(crate) metro_code: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub(crate) struct LocationCacheItem { + pub(crate) location: Option, + pub(crate) valid_until: SystemTime, +} + +impl LocationCacheItem { + pub(crate) fn new_from_location(location: Option) -> Self { + LocationCacheItem { + location, + valid_until: SystemTime::now() + Duration::from_secs(60 * 60 * 24), // valid for 1 day + } + } +} diff --git a/explorer-api/src/main.rs b/explorer-api/src/main.rs index 676f1c5cea..1c00c1548f 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -19,6 +19,7 @@ mod geo_ip; mod guards; mod helpers; mod http; +mod location; mod mix_node; pub(crate) mod mix_nodes; mod overview; @@ -35,7 +36,7 @@ async fn main() { dotenv().ok(); setup_logging(); let args = commands::Cli::parse(); - setup_env(args.config_env_file.as_ref()); + setup_env(args.config_env_file); let mut explorer_api = ExplorerApi::new(); explorer_api.run().await; } diff --git a/explorer-api/src/mix_nodes/location.rs b/explorer-api/src/mix_nodes/location.rs index 4908564697..de3c2e2fd6 100644 --- a/explorer-api/src/mix_nodes/location.rs +++ b/explorer-api/src/mix_nodes/location.rs @@ -1,41 +1,8 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_explorer_api_requests::Location; use nym_mixnet_contract_common::MixId; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::time::{Duration, SystemTime}; -pub(crate) type LocationCache = HashMap; +use crate::location::LocationCache; -#[allow(dead_code)] -#[derive(Debug, Deserialize)] -pub(crate) struct GeoLocation { - pub(crate) ip: String, - pub(crate) country_code: String, - pub(crate) country_name: String, - pub(crate) region_code: String, - pub(crate) region_name: String, - pub(crate) city: String, - pub(crate) zip_code: String, - pub(crate) time_zone: String, - pub(crate) latitude: f32, - pub(crate) longitude: f32, - pub(crate) metro_code: u32, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub(crate) struct LocationCacheItem { - pub(crate) location: Option, - pub(crate) valid_until: SystemTime, -} - -impl LocationCacheItem { - pub(crate) fn new_from_location(location: Option) -> Self { - LocationCacheItem { - location, - valid_until: SystemTime::now() + Duration::from_secs(60 * 60 * 24), // valid for 1 day - } - } -} +pub(crate) type MixnodeLocationCache = LocationCache; diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index b107bb2168..fe5ae7fde8 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -12,10 +12,11 @@ use serde::Serialize; use tokio::sync::{RwLock, RwLockReadGuard}; use crate::helpers::best_effort_small_dec_to_f64; +use crate::location::LocationCacheItem; use nym_validator_client::models::MixNodeBondAnnotated; +use super::location::MixnodeLocationCache; use super::utils::family_numerical_id; -use crate::mix_nodes::location::{LocationCache, LocationCacheItem}; use crate::mix_nodes::CACHE_ENTRY_TTL; #[derive(Clone, Debug, Serialize, JsonSchema)] @@ -83,18 +84,18 @@ impl MixNodesResult { #[derive(Clone)] pub(crate) struct ThreadsafeMixNodesCache { mixnodes: Arc>, - locations: Arc>, + locations: Arc>, } impl ThreadsafeMixNodesCache { pub(crate) fn new() -> Self { ThreadsafeMixNodesCache { mixnodes: Arc::new(RwLock::new(MixNodesResult::new())), - locations: Arc::new(RwLock::new(LocationCache::new())), + locations: Arc::new(RwLock::new(MixnodeLocationCache::new())), } } - pub(crate) fn new_with_location_cache(locations: LocationCache) -> Self { + pub(crate) fn new_with_location_cache(locations: MixnodeLocationCache) -> Self { ThreadsafeMixNodesCache { mixnodes: Arc::new(RwLock::new(MixNodesResult::new())), locations: Arc::new(RwLock::new(locations)), @@ -111,7 +112,7 @@ impl ThreadsafeMixNodesCache { }) } - pub(crate) async fn get_locations(&self) -> LocationCache { + pub(crate) async fn get_locations(&self) -> MixnodeLocationCache { self.locations.read().await.clone() } diff --git a/explorer-api/src/mix_nodes/utils.rs b/explorer-api/src/mix_nodes/utils.rs index 0f7eb6b991..5273d66f55 100644 --- a/explorer-api/src/mix_nodes/utils.rs +++ b/explorer-api/src/mix_nodes/utils.rs @@ -1,13 +1,13 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use isocountry::CountryCode; use nym_mixnet_contract_common::families::FamilyHead; use rand::Rng; use rand_pcg::Pcg64; use rand_seeder::Seeder; -use crate::mix_nodes::location::GeoLocation; -use isocountry::CountryCode; +use crate::location::GeoLocation; #[allow(dead_code)] pub(crate) fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String { diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index 0011a924d3..8ecee92ae2 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -13,9 +13,10 @@ use nym_validator_client::models::MixNodeBondAnnotated; use crate::country_statistics::country_nodes_distribution::{ CountryNodesDistribution, ThreadsafeCountryNodesDistribution, }; +use crate::gateways::location::GatewayLocationCache; use crate::gateways::models::ThreadsafeGatewayCache; use crate::mix_node::models::ThreadsafeMixNodeCache; -use crate::mix_nodes::location::LocationCache; +use crate::mix_nodes::location::MixnodeLocationCache; use crate::mix_nodes::models::ThreadsafeMixNodesCache; use crate::ping::models::ThreadsafePingCache; use crate::validators::models::ThreadsafeValidatorCache; @@ -46,7 +47,8 @@ impl ExplorerApiState { #[derive(Debug, Serialize, Deserialize)] pub struct ExplorerApiStateOnDisk { pub(crate) country_node_distribution: CountryNodesDistribution, - pub(crate) location_cache: LocationCache, + pub(crate) mixnode_location_cache: MixnodeLocationCache, + pub(crate) gateway_location_cache: GatewayLocationCache, pub(crate) as_at: DateTime, } @@ -76,9 +78,13 @@ impl ExplorerApiStateContext { ThreadsafeCountryNodesDistribution::new_from_distribution( state.country_node_distribution, ), - gateways: ThreadsafeGatewayCache::new(), + gateways: ThreadsafeGatewayCache::new_with_location_cache( + state.gateway_location_cache, + ), mixnode: ThreadsafeMixNodeCache::new(), - mixnodes: ThreadsafeMixNodesCache::new_with_location_cache(state.location_cache), + mixnodes: ThreadsafeMixNodesCache::new_with_location_cache( + state.mixnode_location_cache, + ), ping: ThreadsafePingCache::new(), validators: ThreadsafeValidatorCache::new(), validator_client: ThreadsafeValidatorClient::new(), @@ -109,7 +115,8 @@ impl ExplorerApiStateContext { let file = File::create(json_file_path).expect("unable to create state json file"); let state = ExplorerApiStateOnDisk { country_node_distribution: self.inner.country_node_distribution.get_all().await, - location_cache: self.inner.mixnodes.get_locations().await, + mixnode_location_cache: self.inner.mixnodes.get_locations().await, + gateway_location_cache: self.inner.gateways.get_locations().await, as_at: Utc::now(), }; serde_json::to_writer(file, &state).expect("error writing state to disk"); diff --git a/explorer/.env.dev b/explorer/.env.dev new file mode 100644 index 0000000000..fe4119f671 --- /dev/null +++ b/explorer/.env.dev @@ -0,0 +1,9 @@ +# When running the explorer API locally +#EXPLORER_API_URL=http://localhost:8000/v1 + +EXPLORER_API_URL=https://sandbox-explorer.nymtech.net/api/v1 +NYM_API_URL=https://sandbox-nym-api1.nymtech.net +VALIDATOR_URL=https://sandbox-validator1.nymtech.net +BIG_DIPPER_URL=https://sandbox-blocks.nymtech.net +CURRENCY_DENOM=unym +CURRENCY_STAKING_DENOM=unyx diff --git a/explorer/.gitignore b/explorer/.gitignore index b9a105e264..b8b1827577 100644 --- a/explorer/.gitignore +++ b/explorer/.gitignore @@ -1,3 +1,4 @@ dist .DS_Store -detailAPI.md \ No newline at end of file +detailAPI.md +!.env.dev diff --git a/explorer/README.md b/explorer/README.md index cc8c7f0363..a7bbc2346a 100644 --- a/explorer/README.md +++ b/explorer/README.md @@ -32,6 +32,21 @@ You can then open a browser to http://localhost:3000 and start development. Documentation for developers [can be found here](./docs). +Several environment variables are required. They can be +provisioned via a `.env` file. For convenience a `.env.dev` is +provided, just copy its content into `.env`. + +#### Required env vars + +``` +EXPLORER_API_URL +NYM_API_URL +VALIDATOR_URL +BIG_DIPPER_URL +CURRENCY_DENOM +CURRENCY_STAKING_DENOM +``` + ## Deployment Build the UI with (starting in the repository root): diff --git a/explorer/package.json b/explorer/package.json index 094e705132..885f378254 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -3,6 +3,20 @@ "version": "1.0.7", "private": true, "license": "Apache-2.0", + "scripts": { + "start": "webpack serve --progress --port 3000", + "build": "webpack build --progress --config webpack.prod.js", + "build:serve": "npx serve dist", + "test": "jest", + "test:watch": "jest --watch", + "tsc": "tsc --noEmit true", + "tsc:watch": "tsc --watch --noEmit true", + "lint": "eslint src", + "lint:fix": "eslint src --fix", + "prestorybook": "yarn --cwd .. build", + "storybook": "start-storybook -p 6006", + "storybook:build": "build-storybook" + }, "dependencies": { "@cosmjs/math": "^0.26.2", "@emotion/react": "^11.4.1", @@ -18,6 +32,7 @@ "big.js": "^6.2.1", "d3-scale": "^4.0.0", "date-fns": "^2.24.0", + "lodash": "^4.17.21", "i18n-iso-countries": "^6.8.0", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -95,20 +110,6 @@ "webpack-favicons": "^1.3.8", "webpack-merge": "^5.8.0" }, - "scripts": { - "start": "webpack serve --progress --port 3000", - "build": "webpack build --progress --config webpack.prod.js", - "build:serve": "npx serve dist", - "test": "jest", - "test:watch": "jest --watch", - "tsc": "tsc --noEmit true", - "tsc:watch": "tsc --watch --noEmit true", - "lint": "eslint src", - "lint:fix": "eslint src --fix", - "prestorybook": "yarn --cwd .. build", - "storybook": "start-storybook -p 6006", - "storybook:build": "build-storybook" - }, "browserslist": { "production": [ ">0.2%", diff --git a/explorer/src/api/constants.ts b/explorer/src/api/constants.ts index e3f39a3cae..f00c9ec280 100644 --- a/explorer/src/api/constants.ts +++ b/explorer/src/api/constants.ts @@ -9,6 +9,7 @@ export const OVERVIEW_API = `${API_BASE_URL}/overview`; export const MIXNODE_PING = `${API_BASE_URL}/ping`; export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`; export const MIXNODE_API = `${API_BASE_URL}/mix-node`; +export const GATEWAYS_EXPLORER_API = `${API_BASE_URL}/gateways`; export const GATEWAYS_API = `${NYM_API_BASE_URL}/api/v1/status/gateways/detailed`; export const VALIDATORS_API = `${VALIDATOR_BASE_URL}/validators`; export const BLOCK_API = `${NYM_API_BASE_URL}/block`; diff --git a/explorer/src/api/index.ts b/explorer/src/api/index.ts index ceb5878e97..a2b134d4ae 100644 --- a/explorer/src/api/index.ts +++ b/explorer/src/api/index.ts @@ -1,3 +1,4 @@ +import keyBy from 'lodash/keyBy'; import { API_BASE_URL, BLOCK_API, @@ -11,6 +12,7 @@ import { UPTIME_STORY_API, VALIDATORS_API, SERVICE_PROVIDERS, + GATEWAYS_EXPLORER_API, } from './constants'; import { @@ -32,6 +34,7 @@ import { GatewayBondAnnotated, GatewayBond, DirectoryServiceProvider, + LocatedGateway, } from '../typeDefs/explorer-api'; function getFromCache(key: string) { @@ -98,9 +101,13 @@ export class Api { static fetchGateways = async (): Promise => { const res = await fetch(GATEWAYS_API); const gatewaysAnnotated: GatewayBondAnnotated[] = await res.json(); + const res2 = await fetch(GATEWAYS_EXPLORER_API); + const locatedGateways: LocatedGateway[] = await res2.json(); + const locatedGatewaysByOwner = keyBy(locatedGateways, 'owner'); return gatewaysAnnotated.map(({ gateway_bond, node_performance }) => ({ ...gateway_bond, node_performance, + location: locatedGatewaysByOwner[gateway_bond.owner]?.location, })); }; diff --git a/explorer/src/components/Gateways.ts b/explorer/src/components/Gateways.ts index b78b1320da..4072a33db7 100644 --- a/explorer/src/components/Gateways.ts +++ b/explorer/src/components/Gateways.ts @@ -26,7 +26,7 @@ export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowTy id: gw.owner, owner: gw.owner, identity_key: gw.gateway.identity_key || '', - location: gw?.gateway?.location || '', + location: gw.location?.country_name.toUpperCase() || '', bond: gw.pledge_amount.amount || 0, host: gw.gateway.host || '', version: gw.gateway.version || '', @@ -39,7 +39,7 @@ export function gatewayEnrichedToGridRow(gateway: GatewayBond, report: GatewayRe id: gateway.owner, owner: gateway.owner, identity_key: gateway.gateway.identity_key || '', - location: gateway?.gateway?.location || '', + location: gateway.location?.country_name.toUpperCase() || '', bond: gateway.pledge_amount.amount || 0, host: gateway.gateway.host || '', version: gateway.gateway.version || '', diff --git a/explorer/src/typeDefs/explorer-api.ts b/explorer/src/typeDefs/explorer-api.ts index 736c6e8ffa..9319c33952 100644 --- a/explorer/src/typeDefs/explorer-api.ts +++ b/explorer/src/typeDefs/explorer-api.ts @@ -131,6 +131,7 @@ export interface GatewayBond { owner: string; gateway: Gateway; node_performance: NodePerformance; + location?: Location; } export interface GatewayBondAnnotated { @@ -138,6 +139,23 @@ export interface GatewayBondAnnotated { node_performance: NodePerformance; } +export interface Location { + two_letter_iso_country_code: string; + three_letter_iso_country_code: string; + country_name: string; + latitude?: number; + longitude?: number; +} + +export interface LocatedGateway { + pledge_amount: Amount; + owner: string; + block_height: number; + gateway: Gateway; + proxy?: string; + location?: Location; +} + export type GatewayResponse = GatewayBond[]; export interface GatewayReportResponse { diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 2b71aa1542..8e907a9eee 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-gateway" -version = "1.1.26" +version = "1.1.27" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ", diff --git a/gateway/gateway-requests/src/registration/handshake/gateway.rs b/gateway/gateway-requests/src/registration/handshake/gateway.rs index 2afebbc115..69381dc5d4 100644 --- a/gateway/gateway-requests/src/registration/handshake/gateway.rs +++ b/gateway/gateway-requests/src/registration/handshake/gateway.rs @@ -94,7 +94,7 @@ impl<'a> GatewayHandshake<'a> { .to_bytes() .iter() .cloned() - .chain(material.into_iter()) + .chain(material) .collect() } diff --git a/gateway/gateway-requests/src/registration/handshake/shared_key.rs b/gateway/gateway-requests/src/registration/handshake/shared_key.rs index abe1754e31..d3101017a4 100644 --- a/gateway/gateway-requests/src/registration/handshake/shared_key.rs +++ b/gateway/gateway-requests/src/registration/handshake/shared_key.rs @@ -85,10 +85,7 @@ impl SharedKeys { let mac = compute_keyed_hmac::(self.mac_key(), &encrypted_data); - mac.into_bytes() - .into_iter() - .chain(encrypted_data.into_iter()) - .collect() + mac.into_bytes().into_iter().chain(encrypted_data).collect() } pub fn decrypt_tagged( diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index ec0b3f5f94..0a9edd82b9 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-mixnode" -version = "1.1.27" +version = "1.1.28" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ", diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index c9a0b8e8e4..29be65e7bb 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-api" -version = "1.1.27" +version = "1.1.28" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ", @@ -47,7 +47,7 @@ tokio = { version = "1.24.1", features = [ tokio-stream = "0.1.11" url = { workspace = true } -ts-rs = {version = "6.1", optional = true} +ts-rs = { workspace = true, optional = true} anyhow = "1.0" getset = "0.1.1" diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 37aded5efa..e890aa8e44 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -12,7 +12,7 @@ cosmwasm-std = { workspace = true, default-features = false } getset = "0.1.1" schemars = { version = "0.8", features = ["preserve_order"] } serde = { version = "1.0", features = ["derive"] } -ts-rs = { version = "6.1.2", optional = true } +ts-rs = { workspace = true, optional = true } nym-coconut-interface = { path = "../../common/coconut-interface" } nym-mixnet-contract-common = { path= "../../common/cosmwasm-smart-contracts/mixnet-contract" } diff --git a/nym-api/src/network/mod.rs b/nym-api/src/network/mod.rs index f06bc5bf16..b0264f092c 100644 --- a/nym-api/src/network/mod.rs +++ b/nym-api/src/network/mod.rs @@ -11,6 +11,8 @@ mod routes; pub(crate) fn network_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { openapi_get_routes_spec![ - settings: routes::network_details, routes::nym_contracts, routes::nym_contracts_detailed + settings: routes::network_details, + routes::nym_contracts, + routes::nym_contracts_detailed ] } diff --git a/nym-api/tests/package.json b/nym-api/tests/package.json index e93160b7b6..31731d6b16 100644 --- a/nym-api/tests/package.json +++ b/nym-api/tests/package.json @@ -7,7 +7,7 @@ "test": "test" }, "scripts": { - "test:qa": "TEST_ENV=qa jest --forceExit --detectOpenHandles --passWithNoTests", + "test:sandbox": "TEST_ENV=sandbox jest --forceExit --detectOpenHandles --passWithNoTests", "test:prod": "TEST_ENV=prod jest --forceExit --detectOpenHandles --passWithNoTests", "build": "tsc", "lint": "eslint --ext .js,.ts,.tsx .", diff --git a/nym-api/tests/src/config/config.yaml b/nym-api/tests/src/config/config.yaml index d0f51eff0c..85603a7efa 100644 --- a/nym-api/tests/src/config/config.yaml +++ b/nym-api/tests/src/config/config.yaml @@ -2,12 +2,13 @@ common: request_headers: Accept: application/json Content-Type: application/json -qa: - api_base_url: https://qwerty-validator-api.qa.nymte.ch/api/v1 - mix_id: 63 - identity_key: 4Yr4qmEHd9sgsuQ83191FR2hD88RfsbMmB4tzhhZWriz - gateway_identity: 336yuXAeGEgedRfqTJZsG2YV7P13QH1bHv1SjCZYarc9 +sandbox: + api_base_url: https://sandbox-nym-api1.nymtech.net/api/v1 + mix_id: 7 + identity_key: 2wEzyW6Pihk19xbCK9kB81vYuDFUvTzTAr77arvxdfSg + gateway_identity: HjNEDJuotWV8VD4ufeA1jeheTnfNJ7Jorevp57hgaZua log_level: error + time_zone: utc prod: api_base_url: https://validator.nymtech.net/api/v1 mix_id: 730 diff --git a/nym-api/tests/src/config/configHandler.ts b/nym-api/tests/src/config/configHandler.ts index 44177f0866..1d29402012 100644 --- a/nym-api/tests/src/config/configHandler.ts +++ b/nym-api/tests/src/config/configHandler.ts @@ -6,7 +6,7 @@ import YAML from "yaml"; class ConfigHandler { private static instance: ConfigHandler; - private validEnvironments = ["qa", "prod"]; + private validEnvironments = ["sandbox", "prod"]; public commonConfig: { request_headers: object }; @@ -23,7 +23,7 @@ class ConfigHandler { private constructor() { this.setCommonConfig(); - this.setEnvironmentConfig(process.env.TEST_ENV || "qa" || "prod"); + this.setEnvironmentConfig(process.env.TEST_ENV || "sandbox" || "prod"); } public static getInstance(): ConfigHandler { diff --git a/nym-connect/desktop/CHANGELOG.md b/nym-connect/desktop/CHANGELOG.md index 20534c13ef..ab9c1da100 100644 --- a/nym-connect/desktop/CHANGELOG.md +++ b/nym-connect/desktop/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +## [v1.1.19-snickers] (2023-08-29) + +- NymConnect sometimes fails to connect because the gateway it fetches from the validator-api to use is running an old version (of the gateway binary) ([#3788]) + +[#3788]: https://github.com/nymtech/nym/issues/3788 + ## [1.1.18] (2023-08-22) - refactor(nc-desktop): use userdata storage to save user gateway&sp ([#3723]) diff --git a/nym-connect/desktop/Cargo.lock b/nym-connect/desktop/Cargo.lock index 47977894ba..ecfe3bf851 100644 --- a/nym-connect/desktop/Cargo.lock +++ b/nym-connect/desktop/Cargo.lock @@ -3884,6 +3884,7 @@ version = "1.1.15" dependencies = [ "async-trait", "base64 0.21.2", + "cfg-if", "dashmap", "dirs 4.0.0", "futures", @@ -3991,7 +3992,7 @@ dependencies = [ [[package]] name = "nym-connect" -version = "1.1.17" +version = "1.1.18" dependencies = [ "anyhow", "bip39", @@ -4067,6 +4068,7 @@ version = "0.1.0" dependencies = [ "bls12_381 0.5.0", "cosmrs", + "log", "nym-api-requests", "nym-coconut-interface", "nym-crypto", @@ -7557,9 +7559,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "ts-rs" -version = "6.2.1" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4added4070a4fdf9df03457206cd2e4b12417c8560a2954d91ffcbe60177a56a" +checksum = "e1ff1f8c90369bc172200013ac17ae86e7b5def580687df4e6127883454ff2b0" dependencies = [ "thiserror", "ts-rs-macros", @@ -7567,14 +7569,14 @@ dependencies = [ [[package]] name = "ts-rs-macros" -version = "6.2.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f807fdb3151fee75df7485b901a89624358cd07a67a8fb1a5831bf5a07681ff" +checksum = "a6f41cc0aeb7a4a55730188e147d3795a7349b501f8334697fd37629b896cdc2" dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.28", "termcolor", ] diff --git a/nym-connect/desktop/package.json b/nym-connect/desktop/package.json index a3a74ab218..a69bc51486 100644 --- a/nym-connect/desktop/package.json +++ b/nym-connect/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@nym/nym-connect", - "version": "1.1.18", + "version": "1.1.19", "main": "index.js", "license": "MIT", "scripts": { diff --git a/nym-connect/desktop/src-tauri/Cargo.toml b/nym-connect/desktop/src-tauri/Cargo.toml index 1844688af9..ae940cd32d 100644 --- a/nym-connect/desktop/src-tauri/Cargo.toml +++ b/nym-connect/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-connect" -version = "1.1.18" +version = "1.1.19" description = "nym-connect" authors = ["Nym Technologies SA"] license = "" @@ -61,7 +61,7 @@ nym-task = { path = "../../../common/task" } nym-validator-client = { path = "../../../common/client-libs/validator-client" } [dev-dependencies] -ts-rs = "6.1.2" +ts-rs = "7.0.0" tempfile = "3.3.0" [features] diff --git a/nym-connect/desktop/src-tauri/src/operations/directory/mod.rs b/nym-connect/desktop/src-tauri/src/operations/directory/mod.rs index dc001dac2e..3842720161 100644 --- a/nym-connect/desktop/src-tauri/src/operations/directory/mod.rs +++ b/nym-connect/desktop/src-tauri/src/operations/directory/mod.rs @@ -8,6 +8,7 @@ use crate::{ }; use itertools::Itertools; use nym_api_requests::models::GatewayBondAnnotated; +use nym_bin_common::version_checker::is_minor_version_compatible; use nym_config::defaults::var_names::{NETWORK_NAME, NYM_API}; use nym_contracts_common::types::Percent; use nym_validator_client::nym_api::Client as ApiClient; @@ -120,7 +121,17 @@ fn filter_out_inactive_services( async fn fetch_gateways() -> Result> { let api_client = ApiClient::new(Url::from_str(&std::env::var(NYM_API)?)?); - Ok(api_client.get_gateways_detailed().await?) + let gateways = api_client.get_gateways_detailed().await?; + let our_version = env!("CARGO_PKG_VERSION"); + log::debug!( + "Our version that we use to filter compatible gateways: {}", + our_version + ); + let gateways = gateways + .into_iter() + .filter(|g| is_minor_version_compatible(&g.gateway_bond.gateway.version, our_version)) + .collect(); + Ok(gateways) } #[tauri::command] diff --git a/nym-connect/desktop/src-tauri/src/tasks.rs b/nym-connect/desktop/src-tauri/src/tasks.rs index 06b0809c2f..4af366ca48 100644 --- a/nym-connect/desktop/src-tauri/src/tasks.rs +++ b/nym-connect/desktop/src-tauri/src/tasks.rs @@ -1,12 +1,9 @@ use futures::{channel::mpsc, StreamExt}; use nym_client_core::{ - client::{ - base_client::storage::{ - gateway_details::GatewayDetailsStore, MixnetClientStorage, OnDiskPersistent, - }, - topology_control::geo_aware_provider::CountryGroup, + client::base_client::storage::{ + gateway_details::GatewayDetailsStore, MixnetClientStorage, OnDiskPersistent, }, - config::{GatewayEndpointConfig, TopologyStructure}, + config::{GatewayEndpointConfig, GroupBy, TopologyStructure}, error::ClientCoreStatusMessage, }; use nym_socks5_client_core::{NymClient as Socks5NymClient, Socks5ControlMessageSender}; @@ -54,12 +51,17 @@ fn override_config_from_env(config: &mut Config, privacy_level: &PrivacyLevel) { config.core.base.set_no_per_hop_delays(); // TODO: selectable in the UI - let default_country_group = CountryGroup::Europe; - log::warn!("Using geo-aware mixnode selection: {default_country_group}"); + let address = config + .core + .socks5 + .provider_mix_address + .parse() + .expect("failed to parse provider mix address"); + log::warn!("Using geo-aware mixnode selection baseon the location of: {address}"); config .core .base - .set_topology_structure(TopologyStructure::GeoAware(default_country_group)); + .set_topology_structure(TopologyStructure::GeoAware(GroupBy::NymAddress(address))); } } diff --git a/nym-connect/desktop/src-tauri/tauri.conf.json b/nym-connect/desktop/src-tauri/tauri.conf.json index bfbceb21be..555daaa1b7 100644 --- a/nym-connect/desktop/src-tauri/tauri.conf.json +++ b/nym-connect/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "nym-connect", - "version": "1.1.18" + "version": "1.1.19" }, "build": { "distDir": "../dist", diff --git a/nym-connect/native/android/README.md b/nym-connect/native/android/README.md index b2a92f2454..c0092aedd5 100644 --- a/nym-connect/native/android/README.md +++ b/nym-connect/native/android/README.md @@ -37,7 +37,7 @@ RELEASE=true ./build-android.sh aarch64 x86_64 The shared library for each ABIs will be automatically moved into `app/src/main/jniLibs/*` directories. -### APK build (from terminal) +### APK/AAB build (from terminal) This project is setup with multiple [product flavors](app/build.gradle) to build for specific architectures.\ @@ -48,12 +48,18 @@ Supported archs: - x86_64 - x86 -For example to build for _arm64_ in _release_ mode use +For example to build an APK for _arm64_ in _release_ mode use ```shell ./gradlew :app:assembleArm64Release ``` +Instead of building an APK, to build an app bundle (`.aab`) run + +```shell +./gradlew :app:bundleArm64Release +``` + **NOTE**: you likely want _arch64_ (`arm64` & `x86_64`) for APK distribution To build a _universal_ APK which includes all ABI use diff --git a/nym-connect/native/android/app/build.gradle.kts b/nym-connect/native/android/app/build.gradle.kts index decadfc6dc..b1c912729f 100644 --- a/nym-connect/native/android/app/build.gradle.kts +++ b/nym-connect/native/android/app/build.gradle.kts @@ -22,6 +22,18 @@ android { } } + bundle { + language { + enableSplit = true + } + density { + enableSplit = true + } + abi { + enableSplit = true + } + } + buildTypes { release { isMinifyEnabled = true diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index fbe8b1d3f4..3cb32da98f 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -5864,9 +5864,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "ts-rs" -version = "6.2.1" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4added4070a4fdf9df03457206cd2e4b12417c8560a2954d91ffcbe60177a56a" +checksum = "e1ff1f8c90369bc172200013ac17ae86e7b5def580687df4e6127883454ff2b0" dependencies = [ "thiserror", "ts-rs-macros", @@ -5874,14 +5874,14 @@ dependencies = [ [[package]] name = "ts-rs-macros" -version = "6.2.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f807fdb3151fee75df7485b901a89624358cd07a67a8fb1a5831bf5a07681ff" +checksum = "a6f41cc0aeb7a4a55730188e147d3795a7349b501f8334697fd37629b896cdc2" dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.28", "termcolor", ] diff --git a/nym-wallet/nym-wallet-types/Cargo.toml b/nym-wallet/nym-wallet-types/Cargo.toml index 670739ad5f..ca422a9832 100644 --- a/nym-wallet/nym-wallet-types/Cargo.toml +++ b/nym-wallet/nym-wallet-types/Cargo.toml @@ -9,7 +9,7 @@ hex-literal = "0.3.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" strum = { version = "0.23", features = ["derive"] } -ts-rs = "6.1.2" +ts-rs = "7.0.0" cosmwasm-std = "1.3.0" cosmrs = "=0.14.0" diff --git a/nym-wallet/nym-wallet-types/src/network/qa.rs b/nym-wallet/nym-wallet-types/src/network/qa.rs index be282384f5..605cc63fdd 100644 --- a/nym-wallet/nym-wallet-types/src/network/qa.rs +++ b/nym-wallet/nym-wallet-types/src/network/qa.rs @@ -39,6 +39,8 @@ pub(crate) fn validators() -> Vec { )] } +pub(crate) const EXPLORER_API: &str = "https://qa-explorer.qa.nymte.ch/api/"; + pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { nym_network_defaults::NymNetworkDetails { network_name: NETWORK_NAME.into(), @@ -63,5 +65,6 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { ), name_service_contract_address: parse_optional_str(NAME_SERVICE_CONTRACT_ADDRESS), }, + explorer_api: parse_optional_str(EXPLORER_API), } } diff --git a/nym-wallet/nym-wallet-types/src/network/sandbox.rs b/nym-wallet/nym-wallet-types/src/network/sandbox.rs index 0499fa5420..ace2ff7663 100644 --- a/nym-wallet/nym-wallet-types/src/network/sandbox.rs +++ b/nym-wallet/nym-wallet-types/src/network/sandbox.rs @@ -35,6 +35,8 @@ pub(crate) fn validators() -> Vec { )] } +pub(crate) const EXPLORER_API: &str = "https://sandbox-explorer.nymtech.net/api/"; + pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { nym_network_defaults::NymNetworkDetails { network_name: NETWORK_NAME.into(), @@ -57,5 +59,6 @@ pub(crate) fn network_details() -> nym_network_defaults::NymNetworkDetails { service_provider_directory_contract_address: None, name_service_contract_address: None, }, + explorer_api: parse_optional_str(EXPLORER_API), } } diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index b0bbf153c1..cb3548ca59 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -66,7 +66,7 @@ nym-store-cipher = { path = "../../common/store-cipher", features = ["json"] } nym-crypto = { path = "../../common/crypto", features = ["rand"] } rand_chacha = "0.2" tempfile = "3.3.0" -ts-rs = "6.1.2" +ts-rs = "7.0.0" [features] default = ["custom-protocol"] diff --git a/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts index b36ff2d095..6935b6e623 100644 --- a/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts +++ b/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts @@ -28,7 +28,7 @@ export const gatewayValidationSchema = Yup.object().shape({ location: Yup.string() .required('A location is required') - .test('valid-location', 'A valid version is required', (locationValueTest) => + .test('valid-location', 'A valid location is required', (locationValueTest) => locationValueTest ? validateLocation(locationValueTest) : false, ), diff --git a/sdk/lib/socks5-listener/src/lib.rs b/sdk/lib/socks5-listener/src/lib.rs index 1a3e22ca6c..98dac2755d 100644 --- a/sdk/lib/socks5-listener/src/lib.rs +++ b/sdk/lib/socks5-listener/src/lib.rs @@ -58,7 +58,7 @@ async fn is_shutdown_handle_set() -> bool { fn set_default_env() { if !ENV_SET.swap(true, Ordering::SeqCst) { - setup_env(None); + setup_env::<&str>(None); } } diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index e2b5d86d7e..6986e0ef15 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -14,6 +14,7 @@ nym-gateway-requests = { path = "../../../gateway/gateway-requests" } nym-bandwidth-controller = { path = "../../../common/bandwidth-controller" } nym-credentials = { path = "../../../common/credentials" } nym-credential-storage = { path = "../../../common/credential-storage" } +nym-credential-utils = { path = "../../../common/credential-utils" } nym-network-defaults = { path = "../../../common/network-defaults" } nym-sphinx = { path = "../../../common/nymsphinx" } nym-task = { path = "../../../common/task" } @@ -30,9 +31,20 @@ url = { workspace = true } toml = "0.5.10" [dev-dependencies] +anyhow = { workspace = true } dotenvy = { workspace = true } pretty_env_logger = "0.4.0" reqwest = { workspace = true, features = ["json", "socks"] } thiserror = { workspace = true } -tokio = { version = "1", features = ["full"] } +tokio = { workspace = true, features = ["full"] } nym-bin-common = { path = "../../../common/bin-common" } + +# extra dependencies for libp2p examples +libp2p = { git = "https://github.com/ChainSafe/rust-libp2p.git", rev = "e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6", features = [ "identify", "macros", "ping", "tokio", "tcp", "dns", "websocket", "noise", "mplex", "yamux", "gossipsub" ]} +tokio-stream = "0.1.12" +tokio-util = { version = "0.7", features = ["codec"] } +parking_lot = "0.12" +hex = "0.4" + +[features] +libp2p-vanilla = [] \ No newline at end of file diff --git a/sdk/rust/nym-sdk/examples/bandwidth.rs b/sdk/rust/nym-sdk/examples/bandwidth.rs index b7f452fa92..377dd48527 100644 --- a/sdk/rust/nym-sdk/examples/bandwidth.rs +++ b/sdk/rust/nym-sdk/examples/bandwidth.rs @@ -1,46 +1,43 @@ +use futures::StreamExt; +use nym_network_defaults::setup_env; use nym_sdk::mixnet; use nym_sdk::mixnet::MixnetMessageSender; #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { nym_bin_common::logging::setup_logging(); // right now, only sandbox has coconut setup // this should be run from the `sdk/rust/nym-sdk` directory - dotenvy::from_path("../../../envs/sandbox.env").unwrap(); + setup_env(Some("../../../envs/sandbox.env")); let sandbox_network = mixnet::NymNetworkDetails::new_from_env(); + let mnemonic = String::from("my super secret mnemonic"); let mixnet_client = mixnet::MixnetClientBuilder::new_ephemeral() .network_details(sandbox_network) .enable_credentials_mode() .build() - .await - .unwrap(); + .await?; - let bandwidth_client = mixnet_client - .create_bandwidth_client(String::from("very secret mnemonic")) - .unwrap(); + let bandwidth_client = mixnet_client.create_bandwidth_client(mnemonic)?; // Get a bandwidth credential worth 1000000 unym for the mixnet_client - bandwidth_client.acquire(1000000).await.unwrap(); + bandwidth_client.acquire(1000000).await?; // Connect using paid bandwidth credential - let mut client = mixnet_client.connect_to_mixnet().await.unwrap(); + let mut client = mixnet_client.connect_to_mixnet().await?; let our_address = client.nym_address(); // Send a message throughout the mixnet to ourselves client .send_plain_message(*our_address, "hello there") - .await - .unwrap(); + .await?; println!("Waiting for message"); - if let Some(received) = client.wait_for_messages().await { - for r in received { - println!("Received: {}", String::from_utf8_lossy(&r.message)); - } - } + let received = client.next().await.unwrap(); + println!("Received: {}", String::from_utf8_lossy(&received.message)); client.disconnect().await; + Ok(()) } diff --git a/sdk/rust/nym-sdk/examples/libp2p_chat/README.md b/sdk/rust/nym-sdk/examples/libp2p_chat/README.md new file mode 100644 index 0000000000..0287329769 --- /dev/null +++ b/sdk/rust/nym-sdk/examples/libp2p_chat/README.md @@ -0,0 +1,64 @@ +# rust-libp2p-nym + +This repo contains an example implementation of a libp2p transport using the Nym mixnet. It relies on the ChainSafe's fork of libp2p: https://github.com/ChainSafe/rust-libp2p + +## Requirements + +- Rust 1.68.2 +- `Protoc` protobuf compiler. On Debian/Ubuntu distributed via `apt` as `protobuf-compiler` & on Arch/Manjaro via AUR as `[python-protobuf-compiler](https://aur.archlinux.org/packages/python-protobuf-compiler)`. + +## Usage + +To instantiate a libp2p swarm using the transport: + +```rust +use libp2p::core::{muxing::StreamMuxerBox, transport::Transport}; +use libp2p::swarm::{keep_alive::Behaviour, SwarmBuilder}; +use libp2p::{identity, PeerId}; +use nym_sdk::mixnet::MixnetClient; +use rust_libp2p_nym::transport::NymTransport; +use rust_libp2p_nym::test_utils::create_nym_client; +use std::error::Error; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let local_key = identity::Keypair::generate_ed25519(); + let local_peer_id = PeerId::from(local_key.public()); + info!("Local peer id: {local_peer_id:?}"); + + let nym_id = rand::random::().to_string(); + let nym_client = MixnetClient::connect_new().await.unwrap(); + let transport = NymTransport::new(nym_client, local_key.clone()).await?; + let _swarm = SwarmBuilder::with_tokio_executor( + transport + .map(|a, _| (a.0, StreamMuxerBox::new(a.1))) + .boxed(), + Behaviour::default(), + local_peer_id, + ) + .build(); + Ok(()) +} +``` + +## Chat example + +To run the libp2p chat example, run the following in one terminal: +```bash +cargo run --example libp2p_chat +# Local peer id: PeerId("12D3KooWLukBu6q2FerWPFhFFhiYaJkhn2sBmceh9UCaXe6hJf5D") +# Listening on "/nym/FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve" +``` + +In another terminal, run ping again, passing the Nym multiaddress printed previously: +```bash +cargo run --example libp2p_chat -- /nym/FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve +# Local peer id: PeerId("12D3KooWNsuRwG6DHnFJCDR8B3zdvja6xLcfnbtKCsQWJ8eppyWC") +# Dialed /nym/FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve +# Listening on "/nym/2oiRW5C9ivyF3Bo3Gpm4H9EqSKH7A6GpcrRRwVSDVUQ9.EajgCnhzimsP6KskUwKcEj8VFCmHR78s2J6FHWcZ4etR@Fo4f4SQLdoyoGkFae5TpVhRVoXCF8UiypLVGtGjujVPf" +``` + +You should see that the nodes connected and sent messages to each other: +```bash +# 2023-08-10T14:06:28.116Z INFO libp2p_chat > Got message: 'hello world' with id: 37393732353836333838333537303637303237 from peer: 12D3KooWB6k8ZGDF44N4FMRhgVBNihwk1wMYSumosxiZq9pUTbAz +``` diff --git a/sdk/rust/nym-sdk/examples/libp2p_chat/main.rs b/sdk/rust/nym-sdk/examples/libp2p_chat/main.rs new file mode 100644 index 0000000000..04e1ecca57 --- /dev/null +++ b/sdk/rust/nym-sdk/examples/libp2p_chat/main.rs @@ -0,0 +1,171 @@ +// Copyright 2018 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! A basic chat application with logs demonstrating libp2p and the gossipsub protocol +//! combined with mDNS for the discovery of peers to gossip with. +//! +//! Using two terminal windows, start two instances, typing the following in each: +//! +//! ```sh +//! cargo run +//! ``` +//! +//! Mutual mDNS discovery may take a few seconds. When each peer does discover the other +//! it will print a message like: +//! +//! ```sh +//! mDNS discovered a new peer: {peerId} +//! ``` +//! +//! Type a message and hit return: the message is sent and printed in the other terminal. +//! Close with Ctrl-c. +//! +//! You can open more terminal windows and add more peers using the same line above. +//! +//! Once an additional peer is mDNS discovered it can participate in the conversation +//! and all peers will receive messages sent from it. +//! +//! If a participant exits (Control-C or otherwise) the other peers will receive an mDNS expired +//! event and remove the expired peer from the list of known peers. + +use crate::rust_libp2p_nym::transport::NymTransport; +use futures::{prelude::*, select}; +use libp2p::Multiaddr; +use libp2p::{ + core::muxing::StreamMuxerBox, + gossipsub, identity, + swarm::NetworkBehaviour, + swarm::{SwarmBuilder, SwarmEvent}, + PeerId, Transport, +}; +use log::{error, info, LevelFilter}; +use nym_sdk::mixnet::MixnetClient; +use std::collections::hash_map::DefaultHasher; +use std::error::Error; +use std::hash::{Hash, Hasher}; +use std::time::Duration; +use tokio::io; +use tokio_util::codec; + +#[path = "../libp2p_shared/lib.rs"] +mod rust_libp2p_nym; + +// We create a custom network behaviour that uses Gossipsub +#[derive(NetworkBehaviour)] +struct Behaviour { + gossipsub: gossipsub::Behaviour, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + pretty_env_logger::formatted_timed_builder() + .filter_level(LevelFilter::Warn) + .filter(Some("libp2p_chat"), LevelFilter::Info) + .init(); + + // Create a random PeerId + let id_keys = identity::Keypair::generate_ed25519(); + let local_peer_id = PeerId::from(id_keys.public()); + info!("Local peer id: {local_peer_id}"); + + // To content-address message, we can take the hash of message and use it as an ID. + let message_id_fn = |message: &gossipsub::Message| { + let mut s = DefaultHasher::new(); + message.data.hash(&mut s); + gossipsub::MessageId::from(s.finish().to_string()) + }; + + // Set a custom gossipsub configuration + let gossipsub_config = gossipsub::ConfigBuilder::default() + .heartbeat_interval(Duration::from_secs(10)) // This is set to aid debugging by not cluttering the log space + .validation_mode(gossipsub::ValidationMode::Strict) // This sets the kind of message validation. The default is Strict (enforce message signing) + .message_id_fn(message_id_fn) // content-address messages. No two messages of the same content will be propagated. + .build() + .expect("Valid config"); + + // build a gossipsub network behaviour + let mut gossipsub = gossipsub::Behaviour::new( + gossipsub::MessageAuthenticity::Signed(id_keys), + gossipsub_config, + ) + .expect("Correct configuration"); + // Create a Gossipsub topic + let topic = gossipsub::IdentTopic::new("test-net"); + // subscribes to our topic + gossipsub.subscribe(&topic)?; + + let client = MixnetClient::connect_new().await.unwrap(); + info!("client address: {}", client.nym_address()); + + let local_key = identity::Keypair::generate_ed25519(); + let local_peer_id = PeerId::from(local_key.public()); + info!("Local peer id: {local_peer_id:?}"); + + let transport = NymTransport::new(client, local_key).await?; + + let mut swarm = SwarmBuilder::with_tokio_executor( + transport + .map(|a, _| (a.0, StreamMuxerBox::new(a.1))) + .boxed(), + Behaviour { gossipsub }, + local_peer_id, + ) + .build(); + + if let Some(addr) = std::env::args().nth(1) { + let remote: Multiaddr = addr.parse()?; + swarm.dial(remote)?; + info!("Dialed {addr}") + } + + // Read full lines from stdin + let mut stdin = codec::FramedRead::new(io::stdin(), codec::LinesCodec::new()).fuse(); + + info!("Enter messages via STDIN and they will be sent to connected peers using Gossipsub"); + + // Kick it off + loop { + select! { + line = stdin.select_next_some() => { + if let Err(e) = swarm + .behaviour_mut().gossipsub + .publish(topic.clone(), line.expect("Stdin not to close").as_bytes()) { + error!("Publish error: {e:?}"); + } + }, + event = swarm.select_next_some() => { + match event { + SwarmEvent::Behaviour(BehaviourEvent::Gossipsub(gossipsub::Event::Message { + propagation_source: peer_id, + message_id: id, + message, + })) => info!( + "Got message: '{}' with id: {id} from peer: {peer_id}", + String::from_utf8_lossy(&message.data), + ), + SwarmEvent::NewListenAddr { address, .. } => { + info!("Local node is listening on {address}"); + } + other => {info!("other event: {:?}", other)} + } + } + } + } +} diff --git a/sdk/rust/nym-sdk/examples/libp2p_ping/README.md b/sdk/rust/nym-sdk/examples/libp2p_ping/README.md new file mode 100644 index 0000000000..2218793e89 --- /dev/null +++ b/sdk/rust/nym-sdk/examples/libp2p_ping/README.md @@ -0,0 +1,79 @@ +# rust-libp2p-nym + +This repo contains an example implementation of a libp2p transport using the Nym mixnet. It relies on the ChainSafe's fork of libp2p: https://github.com/ChainSafe/rust-libp2p + +## Requirements + +- Rust 1.68.2 +- `Protoc` protobuf compiler. On Debian/Ubuntu distributed via `apt` as `protobuf-compiler` & on Arch/Manjaro via AUR as `[python-protobuf-compiler](https://aur.archlinux.org/packages/python-protobuf-compiler)`. + +## Usage + +To instantiate a libp2p swarm using the transport: + +```rust +use libp2p::core::{muxing::StreamMuxerBox, transport::Transport}; +use libp2p::swarm::{keep_alive::Behaviour, SwarmBuilder}; +use libp2p::{identity, PeerId}; +use nym_sdk::mixnet::MixnetClient; +use rust_libp2p_nym::transport::NymTransport; +use rust_libp2p_nym::test_utils::create_nym_client; +use std::error::Error; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let local_key = identity::Keypair::generate_ed25519(); + let local_peer_id = PeerId::from(local_key.public()); + info!("Local peer id: {local_peer_id:?}"); + + let nym_id = rand::random::().to_string(); + let nym_client = MixnetClient::connect_new().await.unwrap(); + let transport = NymTransport::new(nym_client, local_key.clone()).await?; + let _swarm = SwarmBuilder::with_tokio_executor( + transport + .map(|a, _| (a.0, StreamMuxerBox::new(a.1))) + .boxed(), + Behaviour::default(), + local_peer_id, + ) + .build(); + Ok(()) +} +``` + +## Ping example + +To run the libp2p ping example, run the following in one terminal: +```bash +cargo run --example libp2p_ping +# Local peer id: PeerId("12D3KooWLukBu6q2FerWPFhFFhiYaJkhn2sBmceh9UCaXe6hJf5D") +# Listening on "/nym/FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve" +``` + +In another terminal, run ping again, passing the Nym multiaddress printed previously: +```bash +cargo run --example libp2p_ping -- /nym/FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve +# Local peer id: PeerId("12D3KooWNsuRwG6DHnFJCDR8B3zdvja6xLcfnbtKCsQWJ8eppyWC") +# Dialed /nym/FhtkzizQg2JbZ19kGkRKXdjV2QnFbT5ww88ZAKaD4nkF.7Remi4UVYzn1yL3qYtEcQBGh6tzTYxMdYB4uqyHVc5Z4@62F81C9GrHDRja9WCqozemRFSzFPMecY85MbGwn6efve +# Listening on "/nym/2oiRW5C9ivyF3Bo3Gpm4H9EqSKH7A6GpcrRRwVSDVUQ9.EajgCnhzimsP6KskUwKcEj8VFCmHR78s2J6FHWcZ4etR@Fo4f4SQLdoyoGkFae5TpVhRVoXCF8UiypLVGtGjujVPf" +``` + +You should see that the nodes connected and pinged each other: +```bash +# Mar 30 22:56:36.400 INFO ping: BehaviourEvent: Event { peer: PeerId("12D3KooWGf2oYd6U2nrLzfDrN9zxsjSQjPsMh2oDJPUQ9hiHMNtf"), result: Ok(Ping { rtt: 1.06836675s }) } +``` +```bash +# Mar 30 22:56:35.595 INFO ping: BehaviourEvent: Event { peer: PeerId("12D3KooWMd5ak31DXuZq7x1JuFSR6toA5RDQrPaHrfXEhy7vqqpC"), result: Ok(Pong) } +``` + +In order to run the ping example with vanilla libp2p, which uses tcp, pass the +`--features libp2p-vanilla` flag to the example and follow the instructions on the +rust-libp2p project as usual. + +```bash +RUST_LOG=ping=debug cargo run --example ping --features libp2p-vanilla +``` + +```bash +RUST_LOG=ping=debug cargo run --example ping --features libp2p-vanilla -- "/ip4/127.0.0.1/tcp/$PORT" +``` diff --git a/sdk/rust/nym-sdk/examples/libp2p_ping/main.rs b/sdk/rust/nym-sdk/examples/libp2p_ping/main.rs new file mode 100644 index 0000000000..24ea089c10 --- /dev/null +++ b/sdk/rust/nym-sdk/examples/libp2p_ping/main.rs @@ -0,0 +1,142 @@ +// Copyright 2018 Parity Technologies (UK) Ltd. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the "Software"), +// to deal in the Software without restriction, including without limitation +// the rights to use, copy, modify, merge, publish, distribute, sublicense, +// and/or sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Ping example +//! +//! See ../src/tutorial.rs for a step-by-step guide building the example below. +//! +//! In the first terminal window, run: +//! +//! ```sh +//! cargo run --example ping --features=full +//! ``` +//! +//! It will print the PeerId and the listening addresses, e.g. `Listening on +//! "/ip4/0.0.0.0/tcp/24915"` +//! +//! In the second terminal window, start a new instance of the example with: +//! +//! ```sh +//! cargo run --example ping --features=full -- /ip4/127.0.0.1/tcp/24915 +//! ``` +//! +//! The two nodes establish a connection, negotiate the ping protocol +//! and begin pinging each other. + +use libp2p::futures::StreamExt; +use libp2p::ping::Success; +use libp2p::swarm::{keep_alive, NetworkBehaviour, SwarmEvent}; +use libp2p::{identity, ping, Multiaddr, PeerId}; +use log::{debug, info, LevelFilter}; +use nym_sdk::mixnet::MixnetClient; +use std::error::Error; +use std::time::Duration; + +#[path = "../libp2p_shared/lib.rs"] +mod rust_libp2p_nym; + +#[tokio::main] +async fn main() -> Result<(), Box> { + pretty_env_logger::formatted_timed_builder() + .filter_level(LevelFilter::Warn) + .filter(Some("libp2p_ping"), LevelFilter::Debug) + .init(); + + let local_key = identity::Keypair::generate_ed25519(); + let local_peer_id = PeerId::from(local_key.public()); + info!("Local peer id: {local_peer_id:?}"); + + #[cfg(not(feature = "libp2p-vanilla"))] + let mut swarm = { + debug!("Running `ping` example using NymTransport"); + use libp2p::core::{muxing::StreamMuxerBox, transport::Transport}; + use libp2p::swarm::SwarmBuilder; + use rust_libp2p_nym::transport::NymTransport; + + let client = MixnetClient::connect_new().await.unwrap(); + + let transport = NymTransport::new(client, local_key.clone()).await?; + SwarmBuilder::with_tokio_executor( + transport + .map(|a, _| (a.0, StreamMuxerBox::new(a.1))) + .boxed(), + Behaviour::default(), + local_peer_id, + ) + .build() + }; + + #[cfg(feature = "libp2p-vanilla")] + let mut swarm = { + debug!("Running `ping` example using the vanilla libp2p tokio_development_transport"); + let transport = libp2p::tokio_development_transport(local_key)?; + let mut swarm = + libp2p::Swarm::with_tokio_executor(transport, Behaviour::default(), local_peer_id); + swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?; + swarm + }; + + // Dial the peer identified by the multi-address given as the second + // command-line argument, if any. + if let Some(addr) = std::env::args().nth(1) { + let remote: Multiaddr = addr.parse()?; + swarm.dial(remote)?; + info!("Dialed {addr}") + } + + let mut total_ping_rtt: Duration = Duration::from_micros(0); + let mut counter: u128 = 0; + loop { + match swarm.select_next_some().await { + SwarmEvent::NewListenAddr { address, .. } => info!("Listening on {address:?}"), + SwarmEvent::Behaviour(event) => { + // Get the round-trip duration for the pings. + // This value is already captured in the BehaviourEvent::Ping's `Success::Ping` + // field. + debug!("{event:?}"); + if let BehaviourEvent::Ping(ping_event) = event { + let result: Success = ping_event.result?; + match result { + Success::Ping { rtt } => { + counter += 1; + total_ping_rtt += rtt; + let average_ping_rtt = Duration::from_micros( + (total_ping_rtt.as_micros() / counter).try_into().unwrap(), + ); + info!("Ping RTT: {rtt:?} AVERAGE RTT: ({counter} pings): {average_ping_rtt:?}"); + } + Success::Pong => info!("Pong Event"), + } + } + } + _ => {} + } + } +} + +/// Our network behaviour. +/// +/// For illustrative purposes, this includes the [`KeepAlive`](behaviour::KeepAlive) behaviour so a continuous sequence of +/// pings can be observed. +#[derive(NetworkBehaviour, Default)] +struct Behaviour { + keep_alive: keep_alive::Behaviour, + ping: ping::Behaviour, +} diff --git a/sdk/rust/nym-sdk/examples/libp2p_shared/connection.rs b/sdk/rust/nym-sdk/examples/libp2p_shared/connection.rs new file mode 100644 index 0000000000..fd5c41ccd1 --- /dev/null +++ b/sdk/rust/nym-sdk/examples/libp2p_shared/connection.rs @@ -0,0 +1,418 @@ +use libp2p::core::{muxing::StreamMuxerEvent, PeerId, StreamMuxer}; +use log::debug; +use nym_sphinx::addressing::clients::Recipient; +use std::{ + collections::{HashMap, HashSet}, + pin::Pin, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + task::{Context, Poll, Waker}, +}; +use tokio::sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + oneshot, +}; + +use super::error::Error; +use super::message::{ + ConnectionId, Message, OutboundMessage, SubstreamId, SubstreamMessage, SubstreamMessageType, + TransportMessage, +}; +use super::substream::Substream; + +/// Connection represents the result of a connection setup process. +/// It implements `StreamMuxer` and thus has stream multiplexing built in. +#[derive(Debug)] +pub struct Connection { + pub(crate) peer_id: PeerId, + pub(crate) remote_recipient: Recipient, + pub(crate) id: ConnectionId, + + /// receive inbound messages from the `InnerConnection` + pub(crate) inbound_rx: UnboundedReceiver, + + /// substream ID -> outbound pending substream exists + /// the key is deleted when the response is received, or the request times out + pending_substreams: HashSet, + + /// substream ID -> substream's inbound_tx channel + substream_inbound_txs: HashMap>>, + + /// substream ID -> substream's close_tx channel + substream_close_txs: HashMap>, + + /// send messages to the mixnet + /// used for sending `SubstreamMessageType::OpenRequest` messages + /// also passed to each substream so they can write to the mixnet + pub(crate) mixnet_outbound_tx: UnboundedSender, + + /// inbound substream open requests; used in poll_inbound + inbound_open_tx: UnboundedSender, + inbound_open_rx: UnboundedReceiver, + + /// closed substream IDs; used in poll_close + close_tx: UnboundedSender, + close_rx: UnboundedReceiver, + + /// message nonce contains the next nonce that should be used when + /// sending a message over the connection + pub(crate) message_nonce: Arc, + + waker: Option, +} + +impl Connection { + pub(crate) fn new( + peer_id: PeerId, + remote_recipient: Recipient, + id: ConnectionId, + inbound_rx: UnboundedReceiver, + mixnet_outbound_tx: UnboundedSender, + ) -> Self { + let (inbound_open_tx, inbound_open_rx) = unbounded_channel(); + let (close_tx, close_rx) = unbounded_channel(); + + Connection { + peer_id, + remote_recipient, + id, + inbound_rx, + pending_substreams: HashSet::new(), + substream_inbound_txs: HashMap::new(), + substream_close_txs: HashMap::new(), + mixnet_outbound_tx, + inbound_open_tx, + inbound_open_rx, + close_tx, + close_rx, + message_nonce: Arc::new(AtomicU64::new(1)), + waker: None, + } + } + + fn new_outbound_substream(&mut self) -> Result { + let substream_id = SubstreamId::generate(); + let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst); + + // send the substream open request that requests to open a substream with the given ID + self.mixnet_outbound_tx + .send(OutboundMessage { + recipient: self.remote_recipient, + message: Message::TransportMessage(TransportMessage { + nonce, + id: self.id.clone(), + message: SubstreamMessage { + substream_id: substream_id.clone(), + message_type: SubstreamMessageType::OpenRequest, + }, + }), + }) + .map_err(|e| Error::OutboundSendError(e.to_string()))?; + + // track pending outbound substreams + // TODO we should probably lock this? storing map values should be atomic + let res = self.new_substream(substream_id.clone()); + if res.is_ok() { + self.pending_substreams.insert(substream_id); + } + res + } + + // creates a new substream instance with the given ID. + fn new_substream(&mut self, id: SubstreamId) -> Result { + // check we don't already have a substream with this ID + if self.substream_inbound_txs.get(&id).is_some() { + return Err(Error::SubstreamIdExists(id)); + } + + let (inbound_tx, inbound_rx) = unbounded_channel::>(); + let (close_tx, close_rx) = oneshot::channel::<()>(); + self.substream_inbound_txs.insert(id.clone(), inbound_tx); + self.substream_close_txs.insert(id.clone(), close_tx); + + if let Some(waker) = self.waker.take() { + waker.wake(); + } + + Ok(Substream::new( + self.remote_recipient, + self.id.clone(), + id, + inbound_rx, + self.mixnet_outbound_tx.clone(), + close_rx, + self.message_nonce.clone(), + )) + } + + fn handle_close(&mut self, substream_id: SubstreamId) -> Result<(), Error> { + if self.substream_inbound_txs.remove(&substream_id).is_none() { + return Err(Error::SubstreamIdDoesNotExist(substream_id)); + } + + // notify substream that it's closed + let close_tx = self.substream_close_txs.remove(&substream_id); + close_tx.unwrap().send(()).unwrap(); + + // notify poll_close that the substream is closed + self.close_tx + .send(substream_id) + .map_err(|e| Error::InboundSendError(e.to_string())) + } +} + +impl StreamMuxer for Connection { + type Substream = Substream; + type Error = Error; + + fn poll_inbound( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + if let Poll::Ready(Some(substream)) = self.inbound_open_rx.poll_recv(cx) { + return Poll::Ready(Ok(substream)); + } + + Poll::Pending + } + + fn poll_outbound( + mut self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(self.new_outbound_substream()) + } + + fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if let Poll::Ready(Some(_)) = self.close_rx.poll_recv(cx) { + return Poll::Ready(Ok(())); + } + + Poll::Pending + } + + fn poll( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + while let Poll::Ready(Some(msg)) = self.inbound_rx.poll_recv(cx) { + match msg.message_type { + SubstreamMessageType::OpenRequest => { + // create a new substream with the given ID + let substream = self.new_substream(msg.substream_id.clone())?; + let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst); + + // send the response to the remote peer + self.mixnet_outbound_tx + .send(OutboundMessage { + recipient: self.remote_recipient, + message: Message::TransportMessage(TransportMessage { + nonce, + id: self.id.clone(), + message: SubstreamMessage { + substream_id: msg.substream_id.clone(), + message_type: SubstreamMessageType::OpenResponse, + }, + }), + }) + .map_err(|e| Error::OutboundSendError(e.to_string()))?; + debug!("wrote OpenResponse for substream: {:?}", &msg.substream_id); + + // send the substream to our own channel to be returned in poll_inbound + self.inbound_open_tx + .send(substream) + .map_err(|e| Error::InboundSendError(e.to_string()))?; + + debug!("new inbound substream: {:?}", &msg.substream_id); + } + SubstreamMessageType::OpenResponse => { + if !self.pending_substreams.remove(&msg.substream_id) { + debug!( + "SubstreamMessageType::OpenResponse no substream pending for ID: {:?}", + &msg.substream_id + ); + } + } + SubstreamMessageType::Close => { + self.handle_close(msg.substream_id)?; + } + SubstreamMessageType::Data(data) => { + debug!("SubstreamMessageType::Data: {:?}", &data); + let inbound_tx = self + .substream_inbound_txs + .get_mut(&msg.substream_id) + .expect("must have a substream channel for substream"); + + // NOTE: this ignores channel closed errors, which is fine because the substream + // might have been closed/dropped + inbound_tx.send(data).ok(); + } + } + } + + self.waker = Some(cx.waker().clone()); + Poll::Pending + } +} + +/// PendingConnection represents a connection that's been initiated, but not completed. +pub(crate) struct PendingConnection { + pub(crate) remote_recipient: Recipient, + pub(crate) connection_tx: oneshot::Sender, +} + +impl PendingConnection { + pub(crate) fn new( + remote_recipient: Recipient, + connection_tx: oneshot::Sender, + ) -> Self { + PendingConnection { + remote_recipient, + connection_tx, + } + } +} + +#[cfg(test)] +mod test { + use super::super::message::InboundMessage; + use super::super::mixnet::initialize_mixnet; + use super::*; + use futures::future::poll_fn; + use futures::{AsyncReadExt, AsyncWriteExt, FutureExt}; + use nym_sdk::mixnet::MixnetClient; + + async fn inbound_receive_and_send( + connection_id: ConnectionId, + mixnet_inbound_rx: &mut UnboundedReceiver, + inbound_tx: &UnboundedSender, + expected_nonce: u64, + ) { + let recv_msg = mixnet_inbound_rx.recv().await.unwrap(); + match recv_msg.0 { + Message::TransportMessage(TransportMessage { + nonce, + id, + message: msg, + }) => { + assert_eq!(nonce, expected_nonce); + assert_eq!(id, connection_id); + inbound_tx.send(msg).unwrap(); + } + _ => panic!("unexpected message"), + } + } + + #[tokio::test] + async fn test_connection_stream_muxer() { + let client = MixnetClient::connect_new().await.unwrap(); + let (sender_address, mut sender_mixnet_inbound_rx, sender_outbound_tx) = + initialize_mixnet(client, None).await.unwrap(); + + let client2 = MixnetClient::connect_new().await.unwrap(); + + let (recipient_address, mut recipient_mixnet_inbound_rx, recipient_outbound_tx) = + initialize_mixnet(client2, None).await.unwrap(); + + let connection_id = ConnectionId::generate(); + + let recipient_peer_id = PeerId::random(); + let sender_peer_id = PeerId::random(); + + // create the connections + let (sender_inbound_tx, sender_inbound_rx) = unbounded_channel::(); + let mut sender_connection = Connection::new( + recipient_peer_id, + recipient_address, + connection_id.clone(), + sender_inbound_rx, + sender_outbound_tx, + ); + let (recipient_inbound_tx, recipient_inbound_rx) = unbounded_channel::(); + let mut recipient_connection = Connection::new( + sender_peer_id, + sender_address, + connection_id.clone(), + recipient_inbound_rx, + recipient_outbound_tx, + ); + + // send the substream OpenRequest to the mixnet + let mut sender_substream = sender_connection.new_outbound_substream().unwrap(); + assert!(sender_connection + .pending_substreams + .contains(&sender_substream.substream_id)); + assert_eq!(sender_connection.message_nonce.load(Ordering::SeqCst), 2); + + // poll the recipient inbound stream; should receive the OpenRequest and create the substream + inbound_receive_and_send( + connection_id.clone(), + &mut recipient_mixnet_inbound_rx, + &recipient_inbound_tx, + 1, + ) + .await; + poll_fn(|cx| Pin::new(&mut recipient_connection).as_mut().poll(cx)).now_or_never(); + assert_eq!(recipient_connection.message_nonce.load(Ordering::SeqCst), 2); + + // poll recipient's poll_inbound to receive the substream + let maybe_recipient_substream = poll_fn(|cx| { + Pin::new(&mut recipient_connection) + .as_mut() + .poll_inbound(cx) + }) + .now_or_never(); + let mut recipient_substream = maybe_recipient_substream.unwrap().unwrap(); + + // poll sender's connection to receive the OpenResponse and send it to the Connection inbound channel + inbound_receive_and_send( + connection_id.clone(), + &mut sender_mixnet_inbound_rx, + &sender_inbound_tx, + 1, + ) + .await; + + // poll sender's poll_outbound to get the substream + poll_fn(|cx| Pin::new(&mut sender_connection).as_mut().poll(cx)).now_or_never(); + assert!(sender_connection.pending_substreams.is_empty()); + + // finally, write message to the substream + let data = b"hello world"; + sender_substream.write_all(data).await.unwrap(); + assert_eq!(sender_connection.message_nonce.load(Ordering::SeqCst), 3); + + // receive message from the mixnet, push to the recipient Connection inbound channel + inbound_receive_and_send( + connection_id.clone(), + &mut recipient_mixnet_inbound_rx, + &recipient_inbound_tx, + 2, + ) + .await; + + // poll the sender's connection to send the msg from the connection inbound channel to the substream's + poll_fn(|cx| Pin::new(&mut sender_connection).as_mut().poll(cx)).now_or_never(); + + // poll the recipient's connection to read the msg from the mixnet and mux it into the substream + poll_fn(|cx| Pin::new(&mut recipient_connection).as_mut().poll(cx)).now_or_never(); + + let mut buf = [0u8; 11]; + let n = recipient_substream.read(&mut buf).await.unwrap(); + assert_eq!(n, 11); + assert_eq!(buf, data[..]); + + // test closing the stream; assert the stream is closed on both sides + sender_substream.close().await.unwrap(); + assert_eq!(sender_connection.message_nonce.load(Ordering::SeqCst), 4); + inbound_receive_and_send( + connection_id.clone(), + &mut recipient_mixnet_inbound_rx, + &recipient_inbound_tx, + 3, + ) + .await; + } +} diff --git a/sdk/rust/nym-sdk/examples/libp2p_shared/error.rs b/sdk/rust/nym-sdk/examples/libp2p_shared/error.rs new file mode 100644 index 0000000000..bf1ebc8175 --- /dev/null +++ b/sdk/rust/nym-sdk/examples/libp2p_shared/error.rs @@ -0,0 +1,64 @@ +use libp2p::core::multiaddr; +use nym_sphinx::addressing::clients::RecipientFormattingError; + +use super::message::SubstreamId; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("unimplemented")] + Unimplemented, + #[error("failed to format multiaddress from nym address")] + FailedToFormatMultiaddr(#[from] multiaddr::Error), + #[error("unexpected protocol in multiaddress")] + InvalidProtocolForMultiaddr, + #[error("failed to decode message")] + InvalidMessageBytes, + #[error("no connection found for ConnectionResponse")] + NoConnectionForResponse, + #[error("received ConnectionResponse but connection was already established")] + ConnectionAlreadyEstablished, + #[error("received None recipient in ConnectionRequest")] + NoneRecipientInConnectionRequest, + #[error("cannot handle connection request; already have connection with given ID")] + ConnectionIDExists, + #[error("no connection found for TransportMessage")] + NoConnectionForTransportMessage, + #[error("failed to decode ConnectionMessage; too short")] + ConnectionMessageBytesTooShort, + #[error("failed to decode ConnectionMessage; no recipient")] + ConnectionMessageBytesNoRecipient, + #[error("failed to decode ConnectionMessage; no peer ID")] + ConnectionMessageBytesNoPeerId, + #[error("invalid peer ID bytes")] + InvalidPeerIdBytes, + #[error("invalid recipient bytes")] + InvalidRecipientBytes(#[from] RecipientFormattingError), + #[error("invalid recipient prefix byte")] + InvalidRecipientPrefixByte, + #[error("failed to decode TransportMessage; too short")] + TransportMessageBytesTooShort, + #[error("failed to decode TransportMessage; invalid nonce")] + InvalidNonce, + #[error("invalid substream ID")] + InvalidSubstreamMessageBytes, + #[error("invalid substream message type byte")] + InvalidSubstreamMessageType, + #[error("substrean with given ID already exists")] + SubstreamIdExists(SubstreamId), + #[error("no substream found for given ID")] + SubstreamIdDoesNotExist(SubstreamId), + #[error("recv error: channel closed")] + OneshotRecvError(#[from] tokio::sync::oneshot::error::RecvError), + #[error("recv error: channel closed")] + RecvError, + #[error("outbound send error")] + OutboundSendError(String), + #[error("inbound send error")] + InboundSendError(String), + #[error("failed to send new connection; receiver dropped")] + ConnectionSendError, + #[error("failed to send initial TransportEvent::NewAddress")] + SendErrorTransportEvent, + #[error("dial timed out")] + DialTimeout(#[from] tokio::time::error::Elapsed), +} diff --git a/sdk/rust/nym-sdk/examples/libp2p_shared/lib.rs b/sdk/rust/nym-sdk/examples/libp2p_shared/lib.rs new file mode 100644 index 0000000000..ba33f1d91f --- /dev/null +++ b/sdk/rust/nym-sdk/examples/libp2p_shared/lib.rs @@ -0,0 +1,10 @@ +pub(crate) mod connection; +pub mod error; +pub(crate) mod message; +pub(crate) mod mixnet; +pub(crate) mod queue; +pub mod substream; +pub mod transport; + +/// The deafult timeout secs for [`transport::Upgrade`] future. +const DEFAULT_HANDSHAKE_TIMEOUT_SECS: u64 = 5; diff --git a/sdk/rust/nym-sdk/examples/libp2p_shared/message.rs b/sdk/rust/nym-sdk/examples/libp2p_shared/message.rs new file mode 100644 index 0000000000..9dc4c7085b --- /dev/null +++ b/sdk/rust/nym-sdk/examples/libp2p_shared/message.rs @@ -0,0 +1,335 @@ +use libp2p::core::PeerId; +use nym_sphinx::addressing::clients::Recipient; +use rand::rngs::OsRng; +use rand::RngCore; +use std::fmt::{Debug, Formatter}; + +use super::error::Error; + +const RECIPIENT_LENGTH: usize = Recipient::LEN; +const CONNECTION_ID_LENGTH: usize = 32; +const SUBSTREAM_ID_LENGTH: usize = 32; + +const NONCE_BYTES_LEN: usize = 8; // length of u64 +const MIN_CONNECTION_MESSAGE_LEN: usize = CONNECTION_ID_LENGTH + NONCE_BYTES_LEN; + +/// ConnectionId is a unique, randomly-generated per-connection ID that's used to +/// identify which connection a message belongs to. +#[derive(Clone, Default, Eq, Hash, PartialEq)] +pub(crate) struct ConnectionId([u8; 32]); + +impl ConnectionId { + pub(crate) fn generate() -> Self { + let mut bytes = [0u8; 32]; + OsRng.fill_bytes(&mut bytes); + ConnectionId(bytes) + } + + fn from_bytes(bytes: &[u8]) -> Self { + let mut id = [0u8; 32]; + id[..].copy_from_slice(&bytes[0..CONNECTION_ID_LENGTH]); + ConnectionId(id) + } +} + +impl Debug for ConnectionId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + +/// SubstreamId is a unique, randomly-generated per-substream ID that's used to +/// identify which substream a message belongs to. +#[derive(Clone, Default, Eq, Hash, PartialEq)] +pub struct SubstreamId(pub(crate) [u8; 32]); + +impl SubstreamId { + pub(crate) fn generate() -> Self { + let mut bytes = [0u8; 32]; + OsRng.fill_bytes(&mut bytes); + SubstreamId(bytes) + } + + fn from_bytes(bytes: &[u8]) -> Self { + let mut id = [0u8; 32]; + id[..].copy_from_slice(&bytes[0..SUBSTREAM_ID_LENGTH]); + SubstreamId(id) + } +} + +impl Debug for SubstreamId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", hex::encode(self.0)) + } +} + +#[derive(Debug)] +#[allow(clippy::enum_variant_names)] +pub(crate) enum Message { + ConnectionRequest(ConnectionMessage), + ConnectionResponse(ConnectionMessage), + TransportMessage(TransportMessage), +} + +/// ConnectionMessage is exchanged to open a new connection. +#[derive(Debug)] +pub(crate) struct ConnectionMessage { + pub(crate) peer_id: PeerId, + pub(crate) id: ConnectionId, + /// recipient is the sender's Nym address. + /// only required if this is a ConnectionRequest. + pub(crate) recipient: Option, +} + +/// TransportMessage is sent over a connection after establishment. +#[derive(Debug, Clone)] +pub(crate) struct TransportMessage { + /// increments by 1 for every TransportMessage sent over a connection. + /// required for ordering, since Nym does not guarantee ordering. + /// ConnectionMessages do not need nonces, as we know that they will + /// be the first messages sent over a connection. + /// the first TransportMessage sent over a connection will have nonce 1. + pub(crate) nonce: u64, + pub(crate) message: SubstreamMessage, + pub(crate) id: ConnectionId, +} + +impl Message { + fn try_from_bytes(bytes: Vec) -> Result { + if bytes.len() < 2 { + return Err(Error::InvalidMessageBytes); + } + + Ok(match bytes[0] { + 0 => Message::ConnectionRequest(ConnectionMessage::try_from_bytes(&bytes[1..])?), + 1 => Message::ConnectionResponse(ConnectionMessage::try_from_bytes(&bytes[1..])?), + 2 => Message::TransportMessage(TransportMessage::try_from_bytes(&bytes[1..])?), + _ => return Err(Error::InvalidMessageBytes), + }) + } +} + +impl ConnectionMessage { + fn to_bytes(&self) -> Vec { + let mut bytes = self.id.0.to_vec(); + match self.recipient { + Some(recipient) => { + bytes.push(1u8); + bytes.append(&mut recipient.to_bytes().to_vec()); + } + None => bytes.push(0u8), + } + bytes.append(&mut self.peer_id.to_bytes()); + bytes + } + + fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < CONNECTION_ID_LENGTH + 1 { + return Err(Error::ConnectionMessageBytesTooShort); + } + + let id = ConnectionId::from_bytes(&bytes[0..CONNECTION_ID_LENGTH]); + let recipient = match bytes[CONNECTION_ID_LENGTH] { + 0u8 => None, + 1u8 => { + if bytes.len() < CONNECTION_ID_LENGTH + 1 + RECIPIENT_LENGTH { + return Err(Error::ConnectionMessageBytesNoRecipient); + } + + let mut recipient_bytes = [0u8; RECIPIENT_LENGTH]; + recipient_bytes[..].copy_from_slice( + &bytes[CONNECTION_ID_LENGTH + 1..CONNECTION_ID_LENGTH + 1 + RECIPIENT_LENGTH], + ); + Some( + Recipient::try_from_bytes(recipient_bytes) + .map_err(Error::InvalidRecipientBytes)?, + ) + } + _ => { + return Err(Error::InvalidRecipientPrefixByte); + } + }; + let peer_id = match recipient { + Some(_) => { + if bytes.len() < CONNECTION_ID_LENGTH + RECIPIENT_LENGTH + 2 { + return Err(Error::ConnectionMessageBytesNoPeerId); + } + PeerId::from_bytes(&bytes[CONNECTION_ID_LENGTH + 1 + RECIPIENT_LENGTH..]) + .map_err(|_| Error::InvalidPeerIdBytes)? + } + None => { + if bytes.len() < CONNECTION_ID_LENGTH + 2 { + return Err(Error::ConnectionMessageBytesNoPeerId); + } + PeerId::from_bytes(&bytes[CONNECTION_ID_LENGTH + 1..]) + .map_err(|_| Error::InvalidPeerIdBytes)? + } + }; + Ok(ConnectionMessage { + peer_id, + recipient, + id, + }) + } +} + +impl TransportMessage { + fn to_bytes(&self) -> Vec { + let mut bytes = self.nonce.to_be_bytes().to_vec(); + bytes.extend_from_slice(self.id.0.as_ref()); + bytes.extend_from_slice(&self.message.to_bytes()); + bytes + } + + fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < MIN_CONNECTION_MESSAGE_LEN + 1 { + return Err(Error::TransportMessageBytesTooShort); + } + + let nonce = u64::from_be_bytes( + bytes[0..NONCE_BYTES_LEN] + .to_vec() + .try_into() + .map_err(|_| Error::InvalidNonce)?, + ); + let id = ConnectionId::from_bytes(&bytes[NONCE_BYTES_LEN..MIN_CONNECTION_MESSAGE_LEN]); + let message = SubstreamMessage::try_from_bytes(&bytes[MIN_CONNECTION_MESSAGE_LEN..])?; + Ok(TransportMessage { nonce, message, id }) + } +} + +impl Ord for TransportMessage { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.nonce.cmp(&other.nonce) + } +} + +impl std::cmp::PartialOrd for TransportMessage { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl std::cmp::Eq for TransportMessage {} + +impl std::cmp::PartialEq for TransportMessage { + fn eq(&self, other: &Self) -> bool { + self.nonce == other.nonce + } +} + +#[derive(Debug, Clone)] +pub(crate) enum SubstreamMessageType { + OpenRequest, + OpenResponse, + Close, + Data(Vec), +} + +impl SubstreamMessageType { + fn to_u8(&self) -> u8 { + match self { + SubstreamMessageType::OpenRequest => 0, + SubstreamMessageType::OpenResponse => 1, + SubstreamMessageType::Close => 2, + SubstreamMessageType::Data(_) => 3, + } + } +} + +/// SubstreamMessage is a message sent over a substream. +#[derive(Debug, Clone)] +pub(crate) struct SubstreamMessage { + pub(crate) substream_id: SubstreamId, + pub(crate) message_type: SubstreamMessageType, +} + +impl SubstreamMessage { + pub(crate) fn new_with_data(substream_id: SubstreamId, message: Vec) -> Self { + SubstreamMessage { + substream_id, + message_type: SubstreamMessageType::Data(message), + } + } + + pub(crate) fn new_close(substream_id: SubstreamId) -> Self { + SubstreamMessage { + substream_id, + message_type: SubstreamMessageType::Close, + } + } + + pub(crate) fn to_bytes(&self) -> Vec { + let mut bytes = self.substream_id.0.clone().to_vec(); + bytes.push(self.message_type.to_u8()); + if let SubstreamMessageType::Data(message) = &self.message_type { + bytes.extend_from_slice(message); + } + bytes + } + + pub(crate) fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() < SUBSTREAM_ID_LENGTH + 1 { + return Err(Error::InvalidSubstreamMessageBytes); + } + + let substream_id = SubstreamId::from_bytes(&bytes[0..SUBSTREAM_ID_LENGTH]); + let message_type = match bytes[SUBSTREAM_ID_LENGTH] { + 0 => SubstreamMessageType::OpenRequest, + 1 => SubstreamMessageType::OpenResponse, + 2 => SubstreamMessageType::Close, + 3 => { + if bytes.len() < SUBSTREAM_ID_LENGTH + 2 { + return Err(Error::InvalidSubstreamMessageBytes); + } + SubstreamMessageType::Data(bytes[SUBSTREAM_ID_LENGTH + 1..].to_vec()) + } + _ => return Err(Error::InvalidSubstreamMessageType), + }; + + Ok(SubstreamMessage { + substream_id, + message_type, + }) + } +} + +impl Message { + pub(crate) fn to_bytes(&self) -> Vec { + match self { + Message::ConnectionRequest(msg) => { + let mut bytes = 0_u8.to_be_bytes().to_vec(); + bytes.append(&mut msg.to_bytes()); + bytes + } + Message::ConnectionResponse(msg) => { + let mut bytes = 1_u8.to_be_bytes().to_vec(); + bytes.append(&mut msg.to_bytes()); + bytes + } + Message::TransportMessage(msg) => { + let mut bytes = 2_u8.to_be_bytes().to_vec(); + bytes.append(&mut msg.to_bytes()); + bytes + } + } + } +} + +/// InboundMessage represents an inbound mixnet message. +pub(crate) struct InboundMessage(pub(crate) Message); + +/// OutboundMessage represents an outbound mixnet message. +#[derive(Debug)] +pub(crate) struct OutboundMessage { + pub(crate) message: Message, + pub(crate) recipient: Recipient, +} + +pub(crate) fn parse_message_data(data: &[u8]) -> Result { + if data.len() < 2 { + return Err(Error::InvalidMessageBytes); + } + let msg = Message::try_from_bytes(data.to_vec())?; + Ok(InboundMessage(msg)) +} diff --git a/sdk/rust/nym-sdk/examples/libp2p_shared/mixnet.rs b/sdk/rust/nym-sdk/examples/libp2p_shared/mixnet.rs new file mode 100644 index 0000000000..f75048203b --- /dev/null +++ b/sdk/rust/nym-sdk/examples/libp2p_shared/mixnet.rs @@ -0,0 +1,164 @@ +use futures::{pin_mut, select}; +use futures::{FutureExt, StreamExt}; +use log::debug; +use nym_sdk::mixnet::{IncludedSurbs, MixnetClient, MixnetClientSender, MixnetMessageSender}; +use nym_sphinx::addressing::clients::Recipient; +use nym_sphinx::receiver::ReconstructedMessage; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; + +use super::error::Error; +use super::message::*; + +/// initialize_mixnet initializes a read/write connection to a Nym websockets endpoint. +/// It starts a task that listens for inbound messages from the endpoint and writes outbound messages to the endpoint. +pub(crate) async fn initialize_mixnet( + client: MixnetClient, + notify_inbound_tx: Option>, +) -> Result< + ( + Recipient, + UnboundedReceiver, + UnboundedSender, + ), + Error, +> { + let recipient = *client.nym_address(); + + // a channel of inbound messages from the mixnet.. + // the transport reads from (listens) to the inbound_rx. + // TODO: this is probably a DOS vector; we should limit the size of the channel. + let (inbound_tx, inbound_rx) = unbounded_channel::(); + + // a channel of outbound messages to be written to the mixnet. + // the transport writes to outbound_tx. + let (outbound_tx, mut outbound_rx) = unbounded_channel::(); + + let sink = client.split_sender(); + let mut stream = client; + + tokio::task::spawn(async move { + loop { + let t1 = check_inbound(&mut stream, &inbound_tx, ¬ify_inbound_tx).fuse(); + let t2 = check_outbound(&sink, &mut outbound_rx).fuse(); + + pin_mut!(t1, t2); + + select! { + _ = t1 => {}, + _ = t2 => {}, + }; + } + }); + + Ok((recipient, inbound_rx, outbound_tx)) +} + +async fn check_inbound( + client: &mut MixnetClient, + inbound_tx: &UnboundedSender, + notify_inbound_tx: &Option>, +) -> Result<(), Error> { + if let Some(msg) = client.next().await { + if let Some(notify_tx) = notify_inbound_tx { + notify_tx + .send(()) + .map_err(|e| Error::InboundSendError(e.to_string()))?; + } + + handle_inbound(msg, inbound_tx).await?; + } + + Err(Error::Unimplemented) +} + +async fn handle_inbound( + msg: ReconstructedMessage, + inbound_tx: &UnboundedSender, +) -> Result<(), Error> { + let data = parse_message_data(&msg.message)?; + inbound_tx + .send(data) + .map_err(|e| Error::InboundSendError(e.to_string()))?; + Ok(()) +} + +async fn check_outbound( + mixnet_sender: &MixnetClientSender, + outbound_rx: &mut UnboundedReceiver, +) -> Result<(), Error> { + match outbound_rx.recv().await { + Some(message) => { + write_bytes( + mixnet_sender, + message.recipient, + &message.message.to_bytes(), + ) + .await + } + None => Err(Error::RecvError), + } +} + +async fn write_bytes( + mixnet_sender: &MixnetClientSender, + recipient: Recipient, + message: &[u8], +) -> Result<(), Error> { + if let Err(_err) = mixnet_sender + .send_message(recipient, message, IncludedSurbs::ExposeSelfAddress) + .await + { + return Err(Error::Unimplemented); + } + + debug!( + "wrote message to mixnet: recipient: {:?}", + recipient.to_string() + ); + Ok(()) +} + +#[cfg(test)] +mod test { + use super::super::message::{ + self, ConnectionId, Message, SubstreamId, SubstreamMessage, SubstreamMessageType, + TransportMessage, + }; + use super::super::mixnet::initialize_mixnet; + use nym_sdk::mixnet::MixnetClient; + + #[tokio::test] + async fn test_mixnet_poll_inbound_and_outbound() { + let client = MixnetClient::connect_new().await.unwrap(); + let (self_address, mut inbound_rx, outbound_tx) = + initialize_mixnet(client, None).await.unwrap(); + let msg_inner = "hello".as_bytes(); + let substream_id = SubstreamId::generate(); + let msg = Message::TransportMessage(TransportMessage { + nonce: 1, // arbitrary + id: ConnectionId::generate(), + message: SubstreamMessage::new_with_data(substream_id.clone(), msg_inner.to_vec()), + }); + + // send a message to ourselves through the mixnet + let out_msg = message::OutboundMessage { + message: msg, + recipient: self_address, + }; + + outbound_tx.send(out_msg).unwrap(); + + // receive the message from ourselves over the mixnet + let received_msg = inbound_rx.recv().await.unwrap(); + if let Message::TransportMessage(recv_msg) = received_msg.0 { + assert_eq!(substream_id, recv_msg.message.substream_id); + if let SubstreamMessageType::Data(data) = recv_msg.message.message_type { + assert_eq!(msg_inner, data.as_slice()); + } else { + panic!("expected SubstreamMessage::Data") + } + } else { + panic!("expected Message::TransportMessage") + } + } +} diff --git a/sdk/rust/nym-sdk/examples/libp2p_shared/queue.rs b/sdk/rust/nym-sdk/examples/libp2p_shared/queue.rs new file mode 100644 index 0000000000..2b59c82877 --- /dev/null +++ b/sdk/rust/nym-sdk/examples/libp2p_shared/queue.rs @@ -0,0 +1,140 @@ +use log::{debug, warn}; +use std::collections::BTreeSet; + +use super::message::TransportMessage; + +/// MessageQueue is a queue of messages, ordered by nonce, that we've +/// received but are not yet able to process because we're waiting for +/// a message with the next expected nonce first. +/// This is required because Nym does not guarantee any sort of message +/// ordering, only delivery. +/// TODO: is there a DOS vector here where a malicious peer sends us +/// messages only with nonce higher than the next expected nonce? +pub(crate) struct MessageQueue { + /// nonce of the next message we expect to receive on the + /// connection. + /// any messages with a nonce greater than this are pushed into + /// the queue. + /// if we get a message with a nonce equal to this, then we + /// immediately handle it in the transport and increment the nonce. + next_expected_nonce: u64, + + /// the actual queue of messages, ordered by nonce. + /// the head of the queue's nonce is always greater + /// than the next expected nonce. + queue: BTreeSet, +} + +impl MessageQueue { + pub(crate) fn new() -> Self { + MessageQueue { + next_expected_nonce: 0, + queue: BTreeSet::new(), + } + } + + pub(crate) fn print_nonces(&self) { + let nonces = self.queue.iter().map(|msg| msg.nonce).collect::>(); + debug!("MessageQueue: {:?}", nonces); + } + + /// sets the next expected nonce to 1, indicating that we've received + /// a ConnectionRequest or ConnectionResponse. + pub(crate) fn set_connection_message_received(&mut self) { + if self.next_expected_nonce != 0 { + panic!("connection message received twice"); + } + + self.next_expected_nonce = self.next_expected_nonce.wrapping_add(1); + } + + /// tries to push a message into the queue. + /// if the message has the next expected nonce, then the message is returned, + /// and should be processed by the caller. + /// in that case, the internal queue's next expected nonce is incremented. + pub(crate) fn try_push(&mut self, msg: TransportMessage) -> Option { + if msg.nonce == self.next_expected_nonce { + self.next_expected_nonce = self.next_expected_nonce.wrapping_add(1); + Some(msg) + } else { + if msg.nonce < self.next_expected_nonce { + // this shouldn't happen normally, only if the other node + // is not following the protocol + warn!("received a message with a nonce that is too low"); + return None; + } + + if !self.queue.insert(msg) { + // this shouldn't happen normally, only if the other node + // is not following the protocol + warn!("received a message with a duplicate nonce"); + return None; + } + + None + } + } + + pub(crate) fn pop(&mut self) -> Option { + let Some(head) = self.queue.first() else { + return None; + }; + + if head.nonce == self.next_expected_nonce { + self.next_expected_nonce = self.next_expected_nonce.wrapping_add(1); + Some(self.queue.pop_first().unwrap()) + } else { + None + } + } +} + +#[cfg(test)] +mod test { + use super::super::message::{ConnectionId, SubstreamId, SubstreamMessage}; + + use super::*; + + impl TransportMessage { + fn new(nonce: u64, message: SubstreamMessage, id: ConnectionId) -> Self { + TransportMessage { nonce, message, id } + } + } + + #[test] + fn test_message_queue() { + let mut queue = MessageQueue::new(); + + let test_substream_message = + SubstreamMessage::new_with_data(SubstreamId::generate(), vec![1, 2, 3]); + let connection_id = ConnectionId::generate(); + + let msg1 = TransportMessage::new(1, test_substream_message.clone(), connection_id.clone()); + let msg2 = TransportMessage::new(2, test_substream_message.clone(), connection_id.clone()); + let msg3 = TransportMessage::new(3, test_substream_message.clone(), connection_id.clone()); + + assert_eq!(queue.try_push(msg1.clone()), None); + assert_eq!(queue.try_push(msg3.clone()), None); + assert_eq!(queue.try_push(msg2.clone()), None); + + assert_eq!(queue.pop(), None); + + // set expected nonce to 1 + queue.set_connection_message_received(); + assert_eq!(queue.pop(), Some(msg1)); + + let msg4 = TransportMessage::new(4, test_substream_message.clone(), connection_id.clone()); + assert_eq!(queue.try_push(msg4.clone()), None); + + assert_eq!(queue.pop(), Some(msg2)); + assert_eq!(queue.pop(), Some(msg3)); + assert_eq!(queue.pop(), Some(msg4)); + assert_eq!(queue.pop(), None); + assert_eq!(queue.next_expected_nonce, 5); + + // should just return the message and increment nonce when message nonce = next expected nonce + let msg5 = TransportMessage::new(5, test_substream_message, connection_id); + assert_eq!(queue.try_push(msg5.clone()), Some(msg5)); + assert_eq!(queue.next_expected_nonce, 6); + } +} diff --git a/sdk/rust/nym-sdk/examples/libp2p_shared/substream.rs b/sdk/rust/nym-sdk/examples/libp2p_shared/substream.rs new file mode 100644 index 0000000000..f7620a835c --- /dev/null +++ b/sdk/rust/nym-sdk/examples/libp2p_shared/substream.rs @@ -0,0 +1,422 @@ +use super::message::{ + ConnectionId, Message, OutboundMessage, SubstreamId, SubstreamMessage, TransportMessage, +}; +use futures::{ + io::{Error as IoError, ErrorKind}, + AsyncRead, AsyncWrite, +}; +use log::debug; +use nym_sphinx::addressing::clients::Recipient; +use parking_lot::Mutex; +use std::{ + pin::Pin, + sync::{ + atomic::{AtomicU64, Ordering}, + Arc, + }, + task::{Context, Poll}, +}; +use tokio::sync::{ + mpsc::{UnboundedReceiver, UnboundedSender}, + oneshot::Receiver, +}; + +#[derive(Debug)] +pub struct Substream { + remote_recipient: Recipient, + connection_id: ConnectionId, + pub(crate) substream_id: SubstreamId, + + /// inbound messages; inbound_tx is in the corresponding Connection + pub(crate) inbound_rx: UnboundedReceiver>, + + /// outbound messages; go directly to the mixnet + outbound_tx: UnboundedSender, + + /// used to signal when the substream is closed + close_rx: Receiver<()>, + closed: Mutex, + + // buffer of data that's been written to the stream, + // but not yet read by the application. + unread_data: Mutex>, + + message_nonce: Arc, +} + +impl Substream { + pub(crate) fn new( + remote_recipient: Recipient, + connection_id: ConnectionId, + substream_id: SubstreamId, + inbound_rx: UnboundedReceiver>, + outbound_tx: UnboundedSender, + close_rx: Receiver<()>, + message_nonce: Arc, + ) -> Self { + Substream { + remote_recipient, + connection_id, + substream_id, + inbound_rx, + outbound_tx, + close_rx, + closed: Mutex::new(false), + unread_data: Mutex::new(vec![]), + message_nonce, + } + } + + fn check_closed(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Result<(), IoError> { + let closed_err = IoError::new(ErrorKind::Other, "stream closed"); + + // close_rx will return an error if the channel is closed (ie. sender was dropped), + // or if it's empty + let received_closed = self.close_rx.try_recv(); + + let mut closed = self.closed.lock(); + if *closed { + return Err(closed_err); + } + + if received_closed.is_ok() { + *closed = true; + return Err(closed_err); + } + + Ok(()) + } +} + +impl AsyncRead for Substream { + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut [u8], + ) -> Poll> { + let closed_result = self.as_mut().check_closed(cx); + if let Err(e) = closed_result { + return Poll::Ready(Err(e)); + } + + let inbound_rx_data = self.inbound_rx.poll_recv(cx); + + // first, write any previously unread data to the buf + let mut unread_data = self.unread_data.lock(); + let filled_len = if unread_data.len() > 0 { + let unread_len = unread_data.len(); + let buf_len = buf.len(); + let copy_len = std::cmp::min(unread_len, buf_len); + buf[..copy_len].copy_from_slice(&unread_data[..copy_len]); + *unread_data = unread_data[copy_len..].to_vec(); + copy_len + } else { + 0 + }; + + if let Poll::Ready(Some(data)) = inbound_rx_data { + if filled_len == buf.len() { + // we've filled the buffer, so we'll have to save the rest for later + let mut new = vec![]; + new.extend(unread_data.drain(..)); + new.extend(data.iter()); + *unread_data = new; + return Poll::Ready(Ok(filled_len)); + } + + // otherwise, there's still room in the buffer, so we'll copy the rest of the data + let remaining_len = buf.len() - filled_len; + let data_len = data.len(); + + // we have more data than buffer room remaining, save the extra for later + if remaining_len < data_len { + unread_data.extend_from_slice(&data[remaining_len..]); + } + + let copied = std::cmp::min(remaining_len, data_len); + buf[filled_len..filled_len + copied].copy_from_slice(&data[..copied]); + debug!("poll_read copied {} bytes: data {:?}", copied, buf); + return Poll::Ready(Ok(copied)); + } + + if filled_len > 0 { + debug!("poll_read copied {} bytes: data {:?}", filled_len, buf); + return Poll::Ready(Ok(filled_len)); + } + + Poll::Pending + } +} + +impl AsyncWrite for Substream { + fn poll_write( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + if let Err(e) = self.as_mut().check_closed(cx) { + return Poll::Ready(Err(e)); + } + + let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst); + + self.outbound_tx + .send(OutboundMessage { + recipient: self.remote_recipient, + message: Message::TransportMessage(TransportMessage { + nonce, + id: self.connection_id.clone(), + message: SubstreamMessage::new_with_data( + self.substream_id.clone(), + buf.to_vec(), + ), + }), + }) + .map_err(|e| { + IoError::new( + ErrorKind::Other, + format!("poll_write outbound_tx error: {}", e), + ) + })?; + + Poll::Ready(Ok(buf.len())) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + if let Err(e) = self.check_closed(cx) { + return Poll::Ready(Err(e)); + } + + Poll::Ready(Ok(())) + } + + fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst); + + let mut closed = self.closed.lock(); + if *closed { + return Poll::Ready(Err(IoError::new(ErrorKind::Other, "stream closed"))); + } + + *closed = true; + + // send a close message to the mixnet + self.outbound_tx + .send(OutboundMessage { + recipient: self.remote_recipient, + message: Message::TransportMessage(TransportMessage { + nonce, + id: self.connection_id.clone(), + message: SubstreamMessage::new_close(self.substream_id.clone()), + }), + }) + .map_err(|e| { + IoError::new( + ErrorKind::Other, + format!("poll_close outbound_rx error: {}", e), + ) + })?; + + Poll::Ready(Ok(())) + } +} + +#[cfg(test)] +mod test { + use super::super::message::{ + ConnectionId, Message, SubstreamId, SubstreamMessage, TransportMessage, + }; + use super::super::mixnet::initialize_mixnet; + use super::Substream; + use futures::{AsyncReadExt, AsyncWriteExt}; + use nym_sdk::mixnet::MixnetClient; + use nym_sphinx::addressing::clients::Recipient; + use std::sync::atomic::AtomicU64; + use std::sync::Arc; + + #[tokio::test] + async fn test_substream_poll_read_unread_data() { + let (outbound_tx, _) = tokio::sync::mpsc::unbounded_channel(); + let connection_id = ConnectionId::generate(); + let substream_id = SubstreamId::generate(); + + let (inbound_tx, inbound_rx) = tokio::sync::mpsc::unbounded_channel(); + let (_, close_rx) = tokio::sync::oneshot::channel(); + + let mut substream = Substream::new( + Recipient::try_from_base58_string("D1rrpsysCGCYXy9saP8y3kmNpGtJZUXN9SvFoUcqAsM9.9Ssso1ea5NfkbMASdiseDSjTN1fSWda5SgEVjdSN4CvV@GJqd3ZxpXWSNxTfx7B1pPtswpetH4LnJdFeLeuY5KUuN").unwrap(), + connection_id, + substream_id, + inbound_rx, + outbound_tx, + close_rx, + Arc::new(AtomicU64::new(1)), + ); + + // test writing and reading w/ same length data + let data = b"hello".to_vec(); + inbound_tx.send(data.clone()).unwrap(); + let mut buf = [0u8; 5]; + let read_len = substream.read(&mut buf).await.unwrap(); + assert_eq!(read_len, data.len()); + assert_eq!(buf.to_vec(), data); + + // test writing data longer than read buffer + let data = b"nootwashere".to_vec(); + inbound_tx.send(data.clone()).unwrap(); + + let mut buf = [0u8; 4]; + let read_len = substream.read(&mut buf).await.unwrap(); + assert_eq!(read_len, buf.len()); + assert_eq!(buf.to_vec(), b"noot".to_vec()); + + let mut buf = [0u8; 7]; + let read_len = substream.read(&mut buf).await.unwrap(); + assert_eq!(read_len, buf.len()); + assert_eq!(buf.to_vec(), b"washere".to_vec()); + + // test read buffer larger than written data + let data = b"nootwashere".to_vec(); + inbound_tx.send(data.clone()).unwrap(); + let mut buf = [0u8; 16]; + let read_len = substream.read(&mut buf).await.unwrap(); + assert_eq!(read_len, data.len()); + assert_eq!(buf[..data.len()], data); + assert_eq!(buf[data.len()..].to_vec(), vec![0u8; 16 - data.len()]); + + // test writing data longer than read buffer multiple times + let data = b"nootwashere".to_vec(); + inbound_tx.send(data.clone()).unwrap(); + + let mut buf = [0u8; 4]; + let read_len = substream.read(&mut buf).await.unwrap(); + assert_eq!(read_len, buf.len()); + assert_eq!(buf.to_vec(), b"noot".to_vec()); + + let data = b"asdf".to_vec(); + inbound_tx.send(data.clone()).unwrap(); + + let mut buf = [0u8; 4]; + let read_len = substream.read(&mut buf).await.unwrap(); + assert_eq!(read_len, buf.len()); + assert_eq!(buf.to_vec(), b"wash".to_vec()); + + let mut buf = [0u8; 8]; + let read_len = substream.read(&mut buf).await.unwrap(); + assert_eq!(read_len, 7); + assert_eq!(buf[..7], b"ereasdf".to_vec()); + } + + #[tokio::test] + async fn test_substream_read_write() { + let client = MixnetClient::connect_new().await.unwrap(); + let (self_address, mut mixnet_inbound_rx, outbound_tx) = + initialize_mixnet(client, None).await.unwrap(); + + const MSG_INNER: &[u8] = "hello".as_bytes(); + let connection_id = ConnectionId::generate(); + let substream_id = SubstreamId::generate(); + + let (inbound_tx, inbound_rx) = tokio::sync::mpsc::unbounded_channel(); + let (_, close_rx) = tokio::sync::oneshot::channel(); + + let mut substream = Substream::new( + self_address, + connection_id, + substream_id, + inbound_rx, + outbound_tx, + close_rx, + Arc::new(AtomicU64::new(1)), + ); + + // send message to ourselves over the mixnet + substream.write_all(MSG_INNER).await.unwrap(); + + // receive full message over the mixnet + let recv_msg = mixnet_inbound_rx.recv().await.unwrap(); + match recv_msg.0 { + Message::TransportMessage(TransportMessage { + nonce, + id: _, + message: + SubstreamMessage { + substream_id: _, + message_type: msg, + }, + }) => { + assert_eq!(nonce, 1); + match msg { + super::super::message::SubstreamMessageType::Data(data) => { + assert_eq!(data, MSG_INNER); + // send message to substream inbound channel + inbound_tx.send(data).unwrap(); + } + _ => panic!("unexpected message type"), + } + } + _ => panic!("unexpected message"), + } + + // read message from substream + let mut buf = [0u8; MSG_INNER.len()]; + substream.read_exact(&mut buf).await.unwrap(); + assert_eq!(buf, MSG_INNER); + + // close substream + substream.close().await.unwrap(); + + // try to read/write to closed substream; should error + substream.write_all(MSG_INNER).await.unwrap_err(); + substream.read_exact(&mut buf).await.unwrap_err(); + + // assert a close message was sent over the mixnet + let recv_msg = mixnet_inbound_rx.recv().await.unwrap(); + match recv_msg.0 { + Message::TransportMessage(TransportMessage { + nonce: _, + id: _, + message: + SubstreamMessage { + substream_id: _, + message_type: msg, + }, + }) => match msg { + super::super::message::SubstreamMessageType::Close => {} + _ => panic!("unexpected message type"), + }, + _ => panic!("unexpected message: {:?}", recv_msg.0), + } + } + + #[tokio::test] + async fn test_substream_recv_close() { + let client = MixnetClient::connect_new().await.unwrap(); + let (self_address, _, outbound_tx) = initialize_mixnet(client, None).await.unwrap(); + + const MSG_INNER: &[u8] = "hello".as_bytes(); + let connection_id = ConnectionId::generate(); + let substream_id = SubstreamId::generate(); + + let (_, inbound_rx) = tokio::sync::mpsc::unbounded_channel(); + let (close_tx, close_rx) = tokio::sync::oneshot::channel(); + + let mut substream = Substream::new( + self_address, + connection_id, + substream_id, + inbound_rx, + outbound_tx, + close_rx, + Arc::new(AtomicU64::new(1)), + ); + + // close substream + close_tx.send(()).unwrap(); + + // try to read/write to closed substream; should error + substream.write_all(MSG_INNER).await.unwrap_err(); + let mut buf = [0u8; MSG_INNER.len()]; + substream.read_exact(&mut buf).await.unwrap_err(); + } +} diff --git a/sdk/rust/nym-sdk/examples/libp2p_shared/transport.rs b/sdk/rust/nym-sdk/examples/libp2p_shared/transport.rs new file mode 100644 index 0000000000..88142cc37f --- /dev/null +++ b/sdk/rust/nym-sdk/examples/libp2p_shared/transport.rs @@ -0,0 +1,897 @@ +use futures::prelude::*; +use libp2p::core::{ + identity::Keypair, + multiaddr::{Multiaddr, Protocol}, + transport::{ListenerId, TransportError, TransportEvent}, + PeerId, Transport, +}; +use log::debug; +use nym_sdk::mixnet::MixnetClient; +use nym_sphinx::addressing::clients::Recipient; +use std::{ + collections::HashMap, + pin::Pin, + str::FromStr, + task::{Context, Poll, Waker}, +}; +use tokio::{ + sync::{ + mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, + oneshot, + }, + time::{timeout, Duration}, +}; +use tokio_stream::wrappers::UnboundedReceiverStream; + +use super::connection::{Connection, PendingConnection}; +use super::error::Error; +use super::message::{ + ConnectionId, ConnectionMessage, InboundMessage, Message, OutboundMessage, SubstreamMessage, + TransportMessage, +}; +use super::mixnet::initialize_mixnet; +use super::queue::MessageQueue; +use super::DEFAULT_HANDSHAKE_TIMEOUT_SECS; + +/// InboundTransportEvent represents an inbound event from the mixnet. +pub enum InboundTransportEvent { + ConnectionRequest(Upgrade), + ConnectionResponse, + TransportMessage, +} + +/// NymTransport implements the Transport trait using the Nym mixnet. +pub struct NymTransport { + /// our Nym address + self_address: Recipient, + pub(crate) listen_addr: Multiaddr, + pub(crate) listener_id: ListenerId, + + /// our libp2p keypair; currently not really used + keypair: Keypair, + + /// established connections -> channel which sends messages received from + /// the mixnet to the corresponding Connection + connections: HashMap>, + + /// outbound pending dials + pending_dials: HashMap, + + /// connection message queues + message_queues: HashMap, + + /// inbound mixnet messages + inbound_stream: UnboundedReceiverStream, + + /// outbound mixnet messages + outbound_tx: UnboundedSender, + + /// inbound messages for Transport.poll() + poll_rx: UnboundedReceiver>, + + /// outbound messages to Transport.poll() + poll_tx: UnboundedSender>, + + waker: Option, + + /// Timeout for the [`Upgrade`] future. + handshake_timeout: Duration, +} + +impl NymTransport { + /// New transport. + pub async fn new(client: MixnetClient, keypair: Keypair) -> Result { + Self::new_maybe_with_notify_inbound(client, keypair, None, None).await + } + + /// New transport with a timeout. + #[allow(dead_code)] + pub async fn new_with_timeout( + client: MixnetClient, + keypair: Keypair, + timeout: Duration, + ) -> Result { + Self::new_maybe_with_notify_inbound(client, keypair, None, Some(timeout)).await + } + + /// Add timeout to transport and return self. + #[allow(dead_code)] + pub fn with_timeout(mut self, timeout: Duration) -> Self { + self.handshake_timeout = timeout; + self + } + + async fn new_maybe_with_notify_inbound( + client: MixnetClient, + keypair: Keypair, + notify_inbound_tx: Option>, + timeout: Option, + ) -> Result { + let (self_address, inbound_rx, outbound_tx) = + initialize_mixnet(client, notify_inbound_tx).await?; + let listen_addr = nym_address_to_multiaddress(self_address)?; + let listener_id = ListenerId::new(); + + let (poll_tx, poll_rx) = unbounded_channel::>(); + + poll_tx + .send(TransportEvent::NewAddress { + listener_id, + listen_addr: listen_addr.clone(), + }) + .map_err(|_| Error::SendErrorTransportEvent)?; + + let inbound_stream = UnboundedReceiverStream::new(inbound_rx); + let handshake_timeout = + timeout.unwrap_or_else(|| Duration::from_secs(DEFAULT_HANDSHAKE_TIMEOUT_SECS)); + + Ok(Self { + self_address, + listen_addr, + listener_id, + keypair, + connections: HashMap::new(), + pending_dials: HashMap::new(), + message_queues: HashMap::new(), + inbound_stream, + outbound_tx, + poll_rx, + poll_tx, + waker: None, + handshake_timeout, + }) + } + + pub(crate) fn peer_id(&self) -> PeerId { + PeerId::from_public_key(&self.keypair.public()) + } + + fn handle_message_queue_on_connection_initiation( + &mut self, + id: &ConnectionId, + ) -> Result<(), Error> { + debug!("handle_message_queue_on_connection_initiation"); + let Some(inbound_tx) = self.connections.get(id) else { + // this should not happen + return Err(Error::NoConnectionForTransportMessage); + }; + + match self.message_queues.get_mut(id) { + Some(queue) => { + // update expected nonce + queue.set_connection_message_received(); + + // push pending inbound some messages in this case + while let Some(msg) = queue.pop() { + debug!( + "popped queued message with nonce {} for connection", + msg.nonce + ); + inbound_tx + .send(msg.message.clone()) + .map_err(|e| Error::InboundSendError(e.to_string()))?; + } + } + None => { + // no queue exists for this connection, create one + let queue = MessageQueue::new(); + self.message_queues.insert(id.clone(), queue); + let queue = self.message_queues.get_mut(id).unwrap(); + queue.set_connection_message_received(); + } + }; + + debug!("returning from handle_message_queue_on_connection_initiation"); + Ok(()) + } + + // handle_connection_response resolves the pending connection corresponding to the response + // (if there is one) into a Connection. + fn handle_connection_response(&mut self, msg: &ConnectionMessage) -> Result<(), Error> { + if self.connections.contains_key(&msg.id) { + return Err(Error::ConnectionAlreadyEstablished); + } + + if let Some(pending_conn) = self.pending_dials.remove(&msg.id) { + // resolve connection and put into pending_conn channel + let (conn, conn_tx) = self.create_connection_types( + msg.peer_id, + pending_conn.remote_recipient, + msg.id.clone(), + ); + + self.connections.insert(msg.id.clone(), conn_tx); + self.handle_message_queue_on_connection_initiation(&msg.id)?; + + pending_conn + .connection_tx + .send(conn) + .map_err(|_| Error::ConnectionSendError)?; + + if let Some(waker) = self.waker.take() { + waker.wake(); + } + + Ok(()) + } else { + Err(Error::NoConnectionForResponse) + } + } + + /// handle_connection_request handles an incoming connection request, sends back a + /// connection response, and finally completes the upgrade into a Connection. + fn handle_connection_request(&mut self, msg: &ConnectionMessage) -> Result { + if msg.recipient.is_none() { + return Err(Error::NoneRecipientInConnectionRequest); + } + + // ensure we don't already have a conn with the same id + if self.connections.get(&msg.id).is_some() { + return Err(Error::ConnectionIDExists); + } + + let (conn, conn_tx) = + self.create_connection_types(msg.peer_id, msg.recipient.unwrap(), msg.id.clone()); + self.connections.insert(msg.id.clone(), conn_tx); + self.handle_message_queue_on_connection_initiation(&msg.id)?; + + let resp = ConnectionMessage { + peer_id: self.peer_id(), + recipient: None, + id: msg.id.clone(), + }; + + self.outbound_tx + .send(OutboundMessage { + message: Message::ConnectionResponse(resp), + recipient: msg.recipient.unwrap(), + }) + .map_err(|e| Error::OutboundSendError(e.to_string()))?; + + if let Some(waker) = self.waker.take() { + waker.wake(); + } + + Ok(conn) + } + + fn handle_transport_message(&mut self, msg: TransportMessage) -> Result<(), Error> { + let queue = match self.message_queues.get_mut(&msg.id) { + Some(queue) => queue, + None => { + // no queue exists for this connection, create one + let queue = MessageQueue::new(); + self.message_queues.insert(msg.id.clone(), queue); + self.message_queues.get_mut(&msg.id).unwrap() + } + }; + + queue.print_nonces(); + + let nonce = msg.nonce; + let Some(msg) = queue.try_push(msg) else { + // don't push the message yet, it's been queued + debug!("message with nonce {} queued for connection", nonce); + return Ok(()); + }; + + let Some(inbound_tx) = self.connections.get(&msg.id) else { + return Err(Error::NoConnectionForTransportMessage); + }; + + // send original message + debug!( + "sending original message with nonce {} for connection", + nonce + ); + inbound_tx + .send(msg.message.clone()) + .map_err(|e| Error::InboundSendError(e.to_string()))?; + + // try to pop queued messages and send them on inbound channel + while let Some(msg) = queue.pop() { + debug!( + "popped queued message with nonce {} for connection", + msg.nonce + ); + inbound_tx + .send(msg.message.clone()) + .map_err(|e| Error::InboundSendError(e.to_string()))?; + } + + if let Some(waker) = self.waker.clone().take() { + waker.wake(); + } + + Ok(()) + } + + fn create_connection_types( + &self, + remote_peer_id: PeerId, + recipient: Recipient, + id: ConnectionId, + ) -> (Connection, UnboundedSender) { + let (inbound_tx, inbound_rx) = unbounded_channel::(); + + // representation of a connection; this contains channels for applications to read/write to. + let conn = Connection::new( + remote_peer_id, + recipient, + id, + inbound_rx, + self.outbound_tx.clone(), + ); + + // inbound_tx is what we write to when receiving messages on the mixnet, + (conn, inbound_tx) + } + + /// handle_inbound handles an inbound message from the mixnet, received via self.inbound_stream. + fn handle_inbound(&mut self, msg: Message) -> Result { + match msg { + Message::ConnectionRequest(inner) => { + debug!("got inbound connection request {:?}", inner); + match self.handle_connection_request(&inner) { + Ok(conn) => { + let (connection_tx, connection_rx) = + oneshot::channel::<(PeerId, Connection)>(); + let upgrade = Upgrade::new(connection_rx); + connection_tx + .send((inner.peer_id, conn)) + .map_err(|_| Error::ConnectionSendError)?; + Ok(InboundTransportEvent::ConnectionRequest(upgrade)) + } + Err(e) => Err(e), + } + } + Message::ConnectionResponse(msg) => { + debug!("got inbound connection response {:?}", msg); + self.handle_connection_response(&msg) + .map(|_| InboundTransportEvent::ConnectionResponse) + } + Message::TransportMessage(msg) => { + debug!("got inbound TransportMessage: {:?}", msg); + self.handle_transport_message(msg) + .map(|_| InboundTransportEvent::TransportMessage) + } + } + } +} + +/// Upgrade represents a transport listener upgrade. +/// Note: we immediately upgrade a connection request to a connection, +/// so this only contains a channel for receiving that connection. +pub struct Upgrade { + connection_tx: oneshot::Receiver<(PeerId, Connection)>, +} + +impl Upgrade { + fn new(connection_tx: oneshot::Receiver<(PeerId, Connection)>) -> Upgrade { + Upgrade { connection_tx } + } +} + +impl Future for Upgrade { + type Output = Result<(PeerId, Connection), Error>; + + // poll checks if the upgrade has turned into a connection yet + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.connection_tx + .poll_unpin(cx) + .map_err(|_| Error::RecvError) + } +} + +impl Transport for NymTransport { + type Output = (PeerId, Connection); + type Error = Error; + type ListenerUpgrade = Upgrade; + type Dial = Pin> + Send>>; + + fn listen_on(&mut self, _: Multiaddr) -> Result> { + // we should only allow listening on the multiaddress containing our Nym address + Ok(self.listener_id) + } + + fn remove_listener(&mut self, id: ListenerId) -> bool { + if self.listener_id != id { + return false; + } + + // TODO: close channels? + self.poll_tx + .send(TransportEvent::ListenerClosed { + listener_id: id, + reason: Ok(()), + }) + .expect("failed to send listener closed event"); + true + } + + fn dial(&mut self, addr: Multiaddr) -> Result> { + debug!("dialing {}", addr); + + let id = ConnectionId::generate(); + + // create remote recipient address + let recipient = multiaddress_to_nym_address(addr).map_err(TransportError::Other)?; + + // create pending conn structs and store + let (connection_tx, connection_rx) = oneshot::channel::(); + + let inner_pending_conn = PendingConnection::new(recipient, connection_tx); + self.pending_dials.insert(id.clone(), inner_pending_conn); + + // put ConnectionRequest message into outbound message channel + let msg = ConnectionMessage { + peer_id: self.peer_id(), + recipient: Some(self.self_address), + id, + }; + + let outbound_tx = self.outbound_tx.clone(); + + let mut waker = self.waker.clone(); + let handshake_timeout = self.handshake_timeout; + Ok(async move { + outbound_tx + .send(OutboundMessage { + message: Message::ConnectionRequest(msg), + recipient, + }) + .map_err(|e| Error::OutboundSendError(e.to_string()))?; + + debug!("sent outbound ConnectionRequest"); + if let Some(waker) = waker.take() { + waker.wake(); + }; + + let conn = timeout(handshake_timeout, connection_rx).await??; + Ok((conn.peer_id, conn)) + } + .boxed()) + } + + // dial_as_listener currently just calls self.dial(). + fn dial_as_listener( + &mut self, + addr: Multiaddr, + ) -> Result> { + self.dial(addr) + } + + fn poll( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + // new addresses + listener close events + if let Poll::Ready(Some(res)) = self.poll_rx.recv().boxed().poll_unpin(cx) { + return Poll::Ready(res); + } + + // check for and handle inbound messages + while let Poll::Ready(Some(msg)) = self.inbound_stream.poll_next_unpin(cx) { + match self.handle_inbound(msg.0) { + Ok(event) => match event { + InboundTransportEvent::ConnectionRequest(upgrade) => { + debug!("InboundTransportEvent::ConnectionRequest"); + return Poll::Ready(TransportEvent::Incoming { + listener_id: self.listener_id, + upgrade, + local_addr: self.listen_addr.clone(), + send_back_addr: self.listen_addr.clone(), + }); + } + InboundTransportEvent::ConnectionResponse => { + debug!("InboundTransportEvent::ConnectionResponse"); + } + InboundTransportEvent::TransportMessage => { + debug!("InboundTransportEvent::TransportMessage"); + } + }, + Err(e) => { + return Poll::Ready(TransportEvent::ListenerError { + listener_id: self.listener_id, + error: e, + }); + } + }; + } + + self.waker = Some(cx.waker().clone()); + Poll::Pending + } + + fn address_translation(&self, _listen: &Multiaddr, _observed: &Multiaddr) -> Option { + None + } +} + +fn nym_address_to_multiaddress(addr: Recipient) -> Result { + Multiaddr::from_str(&format!("/nym/{}", addr)).map_err(Error::FailedToFormatMultiaddr) +} + +fn multiaddress_to_nym_address(multiaddr: Multiaddr) -> Result { + let mut multiaddr = multiaddr; + match multiaddr.pop().unwrap() { + Protocol::Nym(addr) => Recipient::from_str(&addr).map_err(Error::InvalidRecipientBytes), + _ => Err(Error::InvalidProtocolForMultiaddr), + } +} + +#[cfg(test)] +mod test { + use super::super::connection::Connection; + use super::super::error::Error; + use super::super::message::{ + Message, OutboundMessage, SubstreamId, SubstreamMessage, SubstreamMessageType, + TransportMessage, + }; + use super::super::substream::Substream; + use super::{nym_address_to_multiaddress, NymTransport}; + use futures::{future::poll_fn, AsyncReadExt, AsyncWriteExt, FutureExt}; + use libp2p::core::{ + identity::Keypair, + transport::{Transport, TransportEvent}, + Multiaddr, StreamMuxer, + }; + use log::info; + use nym_bin_common::logging::setup_logging; + use nym_sdk::mixnet::MixnetClient; + use std::{pin::Pin, str::FromStr, sync::atomic::Ordering}; + use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; + + impl Connection { + fn write(&self, msg: SubstreamMessage) -> Result<(), Error> { + let nonce = self.message_nonce.fetch_add(1, Ordering::SeqCst); + self.mixnet_outbound_tx + .send(OutboundMessage { + recipient: self.remote_recipient, + message: Message::TransportMessage(TransportMessage { + nonce, + id: self.id.clone(), + message: msg, + }), + }) + .map_err(|e| Error::OutboundSendError(e.to_string()))?; + Ok(()) + } + } + + impl NymTransport { + async fn new_with_notify_inbound( + client: MixnetClient, + notify_inbound_tx: UnboundedSender<()>, + ) -> Result { + let local_key = Keypair::generate_ed25519(); + Self::new_maybe_with_notify_inbound(client, local_key, Some(notify_inbound_tx), None) + .await + } + } + + #[tokio::test] + async fn test_transport_connection() { + setup_logging(); + + let client = MixnetClient::connect_new().await.unwrap(); + let (dialer_notify_inbound_tx, mut dialer_notify_inbound_rx) = unbounded_channel(); + let mut dialer_transport = + NymTransport::new_with_notify_inbound(client, dialer_notify_inbound_tx) + .await + .unwrap(); + + let client2 = MixnetClient::connect_new().await.unwrap(); + let (listener_notify_inbound_tx, mut listener_notify_inbound_rx) = unbounded_channel(); + let mut listener_transport = + NymTransport::new_with_notify_inbound(client2, listener_notify_inbound_tx) + .await + .unwrap(); + let listener_multiaddr = + nym_address_to_multiaddress(listener_transport.self_address).unwrap(); + assert_new_address_event(Pin::new(&mut dialer_transport)).await; + assert_new_address_event(Pin::new(&mut listener_transport)).await; + + // dial the remote peer + let mut dial = dialer_transport.dial(listener_multiaddr).unwrap(); + + // poll the dial to send the connection request message + assert!(poll_fn(|cx| Pin::new(&mut dial).as_mut().poll_unpin(cx)) + .now_or_never() + .is_none()); + listener_notify_inbound_rx.recv().await.unwrap(); + + // should receive the connection request from the mixnet and send the connection response + let res = poll_fn(|cx| Pin::new(&mut listener_transport).as_mut().poll(cx)).await; + let mut upgrade = match res { + TransportEvent::Incoming { + listener_id, + upgrade, + local_addr, + send_back_addr, + } => { + assert_eq!(listener_id, listener_transport.listener_id); + assert_eq!(local_addr, listener_transport.listen_addr); + assert_eq!(send_back_addr, listener_transport.listen_addr); + upgrade + } + _ => panic!("expected TransportEvent::Incoming, got {:?}", res), + }; + dialer_notify_inbound_rx.recv().await.unwrap(); + + // should receive the connection response from the mixnet + assert!( + poll_fn(|cx| Pin::new(&mut dialer_transport).as_mut().poll(cx)) + .now_or_never() + .is_none() + ); + info!("waiting for connections..."); + + // should be able to resolve the connections now + let (_, mut listener_conn) = poll_fn(|cx| Pin::new(&mut upgrade).as_mut().poll_unpin(cx)) + .now_or_never() + .expect("the upgrade should be ready") + .expect("the upgrade should not error"); + let (_, mut dialer_conn) = poll_fn(|cx| Pin::new(&mut dial).as_mut().poll_unpin(cx)) + .now_or_never() + .expect("the upgrade should be ready") + .expect("the upgrade should not error"); + info!("connections established"); + + // write messages from the dialer to the listener and vice versa + send_and_receive_over_conns( + b"hello".to_vec(), + &mut dialer_conn, + &mut listener_conn, + Pin::new(&mut listener_transport), + &mut listener_notify_inbound_rx, + ) + .await; + send_and_receive_over_conns( + b"hi".to_vec(), + &mut dialer_conn, + &mut listener_conn, + Pin::new(&mut listener_transport), + &mut listener_notify_inbound_rx, + ) + .await; + send_and_receive_over_conns( + b"world".to_vec(), + &mut listener_conn, + &mut dialer_conn, + Pin::new(&mut dialer_transport), + &mut dialer_notify_inbound_rx, + ) + .await; + } + + async fn assert_new_address_event(mut transport: Pin<&mut NymTransport>) { + match poll_fn(|cx| transport.as_mut().poll(cx)).await { + TransportEvent::NewAddress { + listener_id, + listen_addr, + } => { + assert_eq!(listener_id, transport.listener_id); + assert_eq!(listen_addr, transport.listen_addr); + } + _ => panic!("expected TransportEvent::NewAddress"), + } + } + + async fn send_and_receive_over_conns( + msg: Vec, + conn1: &mut Connection, + conn2: &mut Connection, + mut transport2: Pin<&mut NymTransport>, + notify_inbound_rx: &mut UnboundedReceiver<()>, + ) { + // send message over conn1 to conn2 + let substream_id = SubstreamId::generate(); + conn1 + .write(SubstreamMessage::new_with_data( + substream_id.clone(), + msg.clone(), + )) + .unwrap(); + notify_inbound_rx.recv().await.unwrap(); + + // poll transport2 to push message from transport to connection + assert!(poll_fn(|cx| transport2.as_mut().poll(cx)) + .now_or_never() + .is_none()); + let substream_msg = conn2.inbound_rx.recv().await.unwrap(); + if let SubstreamMessageType::Data(data) = substream_msg.message_type { + assert_eq!(data, msg); + } else { + panic!("expected data message"); + } + } + + #[tokio::test] + async fn test_transport_substream() { + let client = MixnetClient::connect_new().await.unwrap(); + + let (dialer_notify_inbound_tx, mut dialer_notify_inbound_rx) = unbounded_channel(); + let mut dialer_transport = + NymTransport::new_with_notify_inbound(client, dialer_notify_inbound_tx) + .await + .unwrap(); + + let client2 = MixnetClient::connect_new().await.unwrap(); + + let (listener_notify_inbound_tx, mut listener_notify_inbound_rx) = unbounded_channel(); + let mut listener_transport = + NymTransport::new_with_notify_inbound(client2, listener_notify_inbound_tx) + .await + .unwrap(); + let listener_multiaddr = + nym_address_to_multiaddress(listener_transport.self_address).unwrap(); + assert_new_address_event(Pin::new(&mut dialer_transport)).await; + assert_new_address_event(Pin::new(&mut listener_transport)).await; + + // dial the remote peer + let mut dial = dialer_transport.dial(listener_multiaddr).unwrap(); + + // poll the dial to send the connection request message + assert!(poll_fn(|cx| Pin::new(&mut dial).as_mut().poll_unpin(cx)) + .now_or_never() + .is_none()); + listener_notify_inbound_rx.recv().await.unwrap(); + + // should receive the connection request from the mixnet and send the connection response + let res = poll_fn(|cx| Pin::new(&mut listener_transport).as_mut().poll(cx)).await; + let mut upgrade = match res { + TransportEvent::Incoming { + listener_id, + upgrade, + local_addr, + send_back_addr, + } => { + assert_eq!(listener_id, listener_transport.listener_id); + assert_eq!(local_addr, listener_transport.listen_addr); + assert_eq!(send_back_addr, listener_transport.listen_addr); + upgrade + } + _ => panic!("expected TransportEvent::Incoming, got {:?}", res), + }; + dialer_notify_inbound_rx.recv().await.unwrap(); + + // should receive the connection response from the mixnet + assert!( + poll_fn(|cx| Pin::new(&mut dialer_transport).as_mut().poll(cx)) + .now_or_never() + .is_none() + ); + info!("waiting for connections..."); + + // should be able to resolve the connections now + let (_, mut listener_conn) = poll_fn(|cx| Pin::new(&mut upgrade).as_mut().poll_unpin(cx)) + .now_or_never() + .unwrap() + .unwrap(); + let (_, mut dialer_conn) = poll_fn(|cx| Pin::new(&mut dial).as_mut().poll_unpin(cx)) + .now_or_never() + .unwrap() + .unwrap(); + info!("connections established"); + + // initiate a new substream from the dialer + let mut dialer_substream = + poll_fn(|cx| Pin::new(&mut dialer_conn).as_mut().poll_outbound(cx)) + .await + .unwrap(); + listener_notify_inbound_rx.recv().await.unwrap(); + + // accept the substream on the listener + assert!( + poll_fn(|cx| Pin::new(&mut listener_transport).as_mut().poll(cx)) + .now_or_never() + .is_none() + ); + poll_fn(|cx| Pin::new(&mut listener_conn).as_mut().poll(cx)).now_or_never(); + + // poll recipient's poll_inbound to receive the substream; sends a response to the sender + let mut listener_substream = + poll_fn(|cx| Pin::new(&mut listener_conn).as_mut().poll_inbound(cx)) + .now_or_never() + .unwrap() + .unwrap(); + info!("got listener substream"); + dialer_notify_inbound_rx.recv().await.unwrap(); + + // poll sender to finalize the substream + assert!( + poll_fn(|cx| Pin::new(&mut dialer_transport).as_mut().poll(cx)) + .now_or_never() + .is_none() + ); + poll_fn(|cx| Pin::new(&mut dialer_conn).as_mut().poll(cx)).now_or_never(); + info!("got dialer substream"); + + // write message from dialer to listener + send_and_receive_substream_message( + b"hello world".to_vec(), + Pin::new(&mut dialer_substream), + Pin::new(&mut listener_substream), + Pin::new(&mut listener_transport), + Pin::new(&mut listener_conn), + &mut listener_notify_inbound_rx, + ) + .await; + + // write message from listener to dialer + send_and_receive_substream_message( + b"hello back".to_vec(), + Pin::new(&mut listener_substream), + Pin::new(&mut dialer_substream), + Pin::new(&mut dialer_transport), + Pin::new(&mut dialer_conn), + &mut dialer_notify_inbound_rx, + ) + .await; + + // close the substream from the dialer side + info!("closing dialer substream"); + dialer_substream.close().await.unwrap(); + listener_notify_inbound_rx.recv().await.unwrap(); + info!("dialer substream closed"); + + // assert we can't read or write to either substream + dialer_substream.write_all(b"hello").await.unwrap_err(); + // poll listener transport and conn to receive the substream close message + poll_fn(|cx| Pin::new(&mut listener_transport).as_mut().poll(cx)).now_or_never(); + poll_fn(|cx| Pin::new(&mut listener_conn).as_mut().poll(cx)).now_or_never(); + listener_substream.write_all(b"hello").await.unwrap_err(); + let mut buf = vec![0u8; 5]; + dialer_substream.read(&mut buf).await.unwrap_err(); + listener_substream.read(&mut buf).await.unwrap_err(); + dialer_substream.close().await.unwrap_err(); + listener_substream.close().await.unwrap_err(); + } + + async fn send_and_receive_substream_message( + data: Vec, + mut sender_substream: Pin<&mut Substream>, + mut recipient_substream: Pin<&mut Substream>, + mut recipient_transport: Pin<&mut NymTransport>, + mut recipient_conn: Pin<&mut Connection>, + recipient_notify_inbound_rx: &mut UnboundedReceiver<()>, + ) { + // write message + sender_substream.write_all(&data).await.unwrap(); + recipient_notify_inbound_rx.recv().await.unwrap(); + + // poll recipient for message + poll_fn(|cx| recipient_transport.as_mut().poll(cx)).now_or_never(); + poll_fn(|cx| recipient_conn.as_mut().poll(cx)).now_or_never(); + let mut buf = vec![0u8; data.len()]; + let n = recipient_substream.read(&mut buf).await.unwrap(); + assert_eq!(n, data.len()); + assert_eq!(buf, data[..]); + } + + #[tokio::test] + async fn test_transport_timeout() { + let client = MixnetClient::connect_new().await.unwrap(); + + let (dialer_notify_inbound_tx, _) = unbounded_channel(); + let mut dialer_transport = + NymTransport::new_with_notify_inbound(client, dialer_notify_inbound_tx) + .await + .unwrap() + .with_timeout(std::time::Duration::from_millis(100)); + + // mock a transport that will never resolve the connection. + let empty_addr = Multiaddr::from_str( + "/nym/Hmer6Ndt3PV13YW53HM8ri4NvqqtfDQUQBhzvKqb1dag.2g478dyxtrQXGWc1Mk2VEqdPcWXpz7EhAcjhdAJtVZdA@AnnYnEtBjB2a5sHmeRCnBq43qxyHDf95Bqd7cwQyKNLR" + ) + .expect("unable to parse multiaddress"); + + let dial = dialer_transport.dial(empty_addr).unwrap(); + assert!(dial + .await + .expect_err("should have timed out") + .to_string() + .contains("dial timed out")); + } +} diff --git a/sdk/rust/nym-sdk/src/mixnet/paths.rs b/sdk/rust/nym-sdk/src/mixnet/paths.rs index 5c09752280..288218f5c5 100644 --- a/sdk/rust/nym-sdk/src/mixnet/paths.rs +++ b/sdk/rust/nym-sdk/src/mixnet/paths.rs @@ -51,7 +51,8 @@ impl StoragePaths { /// /// This function will return an error if it is passed a path to an existing file instead of a /// directory. - pub fn new_from_dir(dir: &Path) -> Result { + pub fn new_from_dir>(dir: P) -> Result { + let dir = dir.as_ref(); if dir.is_file() { return Err(Error::ExpectedDirectory(dir.to_owned())); } diff --git a/service-providers/common/src/interface/control.rs b/service-providers/common/src/interface/control.rs index 8e808d6fdd..d6a0a28a68 100644 --- a/service-providers/common/src/interface/control.rs +++ b/service-providers/common/src/interface/control.rs @@ -130,7 +130,7 @@ impl Serializable for ControlResponse { fn into_bytes(self) -> Vec { std::iter::once(self.tag() as u8) - .chain(self.serialize_inner().into_iter()) + .chain(self.serialize_inner()) .collect() } diff --git a/service-providers/common/src/interface/request.rs b/service-providers/common/src/interface/request.rs index fefa1ffae6..12a884d79e 100644 --- a/service-providers/common/src/interface/request.rs +++ b/service-providers/common/src/interface/request.rs @@ -81,7 +81,7 @@ where pub fn into_bytes(self) -> Vec { if let Some(version) = self.interface_version.as_u8() { std::iter::once(version) - .chain(self.content.into_bytes(self.interface_version).into_iter()) + .chain(self.content.into_bytes(self.interface_version)) .collect() } else { self.content.into_bytes(self.interface_version) @@ -137,7 +137,7 @@ where } } else { std::iter::once(self.tag() as u8) - .chain(self.serialize_inner().into_iter()) + .chain(self.serialize_inner()) .collect() } } diff --git a/service-providers/common/src/interface/response.rs b/service-providers/common/src/interface/response.rs index 5aff60c5ad..ad502e8b30 100644 --- a/service-providers/common/src/interface/response.rs +++ b/service-providers/common/src/interface/response.rs @@ -71,7 +71,7 @@ where pub fn into_bytes(self) -> Vec { if let Some(version) = self.interface_version.as_u8() { std::iter::once(version) - .chain(self.content.into_bytes(self.interface_version).into_iter()) + .chain(self.content.into_bytes(self.interface_version)) .collect() } else { self.content.into_bytes(self.interface_version) @@ -127,7 +127,7 @@ where } } else { std::iter::once(self.tag() as u8) - .chain(self.serialize_inner().into_iter()) + .chain(self.serialize_inner()) .collect() } } diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 9f1fc07c97..cf66ae325e 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-network-requester" -version = "1.1.26" +version = "1.1.27" authors.workspace = true edition.workspace = true rust-version = "1.65" diff --git a/service-providers/network-requester/src/allowed_hosts/hosts.rs b/service-providers/network-requester/src/allowed_hosts/hosts.rs index 11aa3dd276..b9d534fe89 100644 --- a/service-providers/network-requester/src/allowed_hosts/hosts.rs +++ b/service-providers/network-requester/src/allowed_hosts/hosts.rs @@ -100,7 +100,7 @@ impl HostsStore { .unwrap(); if let Err(e) = writeln!(file, "{text}") { - eprintln!("Couldn't write to file: {e}"); + log::error!("Couldn't write to file: {e}"); } } diff --git a/service-providers/network-requester/src/cli/init.rs b/service-providers/network-requester/src/cli/init.rs index a1eb89f518..30797dc26e 100644 --- a/service-providers/network-requester/src/cli/init.rs +++ b/service-providers/network-requester/src/cli/init.rs @@ -106,7 +106,7 @@ fn init_paths(id: &str) -> io::Result<()> { } pub(crate) async fn execute(args: &Init) -> Result<(), NetworkRequesterError> { - eprintln!("Initialising client..."); + log::info!("Initialising client..."); let id = &args.id; @@ -114,7 +114,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), NetworkRequesterError> { // in case we're using old config, try to upgrade it // (if we're using the current version, it's a no-op) try_upgrade_config(id)?; - eprintln!("Client \"{id}\" was already initialised before"); + log::info!("Client \"{id}\" was already initialised before"); true } else { init_paths(&args.id)?; @@ -125,7 +125,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), NetworkRequesterError> { // re-registering if wanted. let user_wants_force_register = args.force_register_gateway; if user_wants_force_register { - eprintln!("Instructed to force registering gateway. This might overwrite keys!"); + log::warn!("Instructed to force registering gateway. This might overwrite keys!"); } // If the client was already initialized, don't generate new keys and don't re-register with @@ -142,6 +142,7 @@ pub(crate) async fn execute(args: &Init) -> Result<(), NetworkRequesterError> { // Load and potentially override config let config = override_config(Config::new(id), OverrideConfig::from(args.clone())); + log::debug!("Using config: {:#?}", config); // Setup gateway by either registering a new one, or creating a new config from the selected // one but with keys kept, or reusing the gateway configuration. @@ -156,21 +157,21 @@ pub(crate) async fn execute(args: &Init) -> Result<(), NetworkRequesterError> { Some(&config.base.client.nym_api_urls), ) .await - .tap_err(|err| eprintln!("Failed to setup gateway\nError: {err}"))? + .tap_err(|err| log::error!("Failed to setup gateway\nError: {err}"))? .details; let config_save_location = config.default_location(); config.save_to_default_location().tap_err(|_| { log::error!("Failed to save the config file"); })?; - eprintln!( + log::info!( "Saved configuration file to {}", config_save_location.display() ); let address = init_details.client_address()?; - eprintln!("Client configuration completed.\n"); + log::info!("Client configuration completed.\n"); let init_results = InitResults::new(&config, &address, &init_details.gateway_details); println!("{}", args.output.format(&init_results)); diff --git a/service-providers/network-requester/src/cli/mod.rs b/service-providers/network-requester/src/cli/mod.rs index 80dd4751a5..1eeed74c28 100644 --- a/service-providers/network-requester/src/cli/mod.rs +++ b/service-providers/network-requester/src/cli/mod.rs @@ -84,7 +84,12 @@ pub(crate) struct OverrideConfig { } pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config { - let disable_cover_traffic_with_keepalive = args.medium_toggle; + // These flags have overlapping effects, meaning the order matters here. Making it a bit messy. + // Since a big chunk of these are hidden experimental flags there is hope we can remove them + // soonish and clean this up. + + let disable_cover_traffic_with_keepalive = + config.network_requester.disable_poisson_rate || args.medium_toggle; let secondary_packet_size = args.medium_toggle.then_some(PacketSize::ExtendedPacket16); let no_per_hop_delays = args.medium_toggle; diff --git a/service-providers/network-requester/src/cli/run.rs b/service-providers/network-requester/src/cli/run.rs index 3c3e31ecbb..6827c3ed82 100644 --- a/service-providers/network-requester/src/cli/run.rs +++ b/service-providers/network-requester/src/cli/run.rs @@ -84,6 +84,7 @@ pub(crate) async fn execute(args: &Run) -> Result<(), NetworkRequesterError> { let mut config = try_load_current_config(&args.id)?; config = override_config(config, OverrideConfig::from(args.clone())); + log::debug!("Using config: {:#?}", config); if !version_check(&config) { error!("failed the local version check"); diff --git a/service-providers/network-requester/src/config/mod.rs b/service-providers/network-requester/src/config/mod.rs index ee7b4ce60e..e4ba1fa9da 100644 --- a/service-providers/network-requester/src/config/mod.rs +++ b/service-providers/network-requester/src/config/mod.rs @@ -159,9 +159,19 @@ impl Config { } } -#[derive(Debug, Default, Clone, Deserialize, PartialEq, Serialize)] +#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] #[serde(default, deny_unknown_fields)] -pub struct NetworkRequester {} +pub struct NetworkRequester { + pub disable_poisson_rate: bool, +} + +impl Default for NetworkRequester { + fn default() -> Self { + NetworkRequester { + disable_poisson_rate: true, + } + } +} #[derive(Debug, Clone, Copy, Deserialize, PartialEq, Serialize)] #[serde(default, deny_unknown_fields)] diff --git a/service-providers/network-requester/src/config/old_config_v1_1_20_2.rs b/service-providers/network-requester/src/config/old_config_v1_1_20_2.rs index 92defcd702..20e3634132 100644 --- a/service-providers/network-requester/src/config/old_config_v1_1_20_2.rs +++ b/service-providers/network-requester/src/config/old_config_v1_1_20_2.rs @@ -79,7 +79,7 @@ pub struct NetworkRequesterV1_1_20_2 {} impl From for NetworkRequester { fn from(_value: NetworkRequesterV1_1_20_2) -> Self { - NetworkRequester {} + NetworkRequester::default() } } diff --git a/service-providers/network-requester/src/config/template.rs b/service-providers/network-requester/src/config/template.rs index 34df005821..811c9602f4 100644 --- a/service-providers/network-requester/src/config/template.rs +++ b/service-providers/network-requester/src/config/template.rs @@ -37,7 +37,7 @@ nym_api_urls = [ {{/each}} ] -[storage_paths] +[storage_paths] # Path to file containing private identity key. keys.private_identity_key_file = '{{ storage_paths.keys.private_identity_key_file }}' @@ -76,6 +76,14 @@ allowed_list_location = '{{ storage_paths.allowed_list_location }}' unknown_list_location = '{{ storage_paths.unknown_list_location }}' +[network_requester] + +# Disable Poisson sending rate, and only send cover traffic occasionally as keepalive messages. +# This is equivalent to setting debug.traffic.disable_main_poisson_packet_distribution = true, +# and debug.cover_traffic.loop_cover_traffic_average_delay = 5s. +disable_poisson_rate = {{ network_requester.disable_poisson_rate }} + + ##### logging configuration options ##### [logging] diff --git a/service-providers/network-statistics/Cargo.toml b/service-providers/network-statistics/Cargo.toml index 7ff628cdc6..197ee6e49a 100644 --- a/service-providers/network-statistics/Cargo.toml +++ b/service-providers/network-statistics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-network-statistics" -version = "1.1.26" +version = "1.1.27" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index e63dcc8225..d571f87d7f 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-cli" -version = "1.1.26" +version = "1.1.27" authors.workspace = true edition = "2021" diff --git a/tools/nym-cli/src/coconut/mod.rs b/tools/nym-cli/src/coconut/mod.rs new file mode 100644 index 0000000000..ea4fd45381 --- /dev/null +++ b/tools/nym-cli/src/coconut/mod.rs @@ -0,0 +1,26 @@ +use nym_cli_commands::context::{create_query_client, create_signing_client, ClientArgs}; +use nym_network_defaults::NymNetworkDetails; + +pub(crate) async fn execute( + global_args: ClientArgs, + coconut: nym_cli_commands::coconut::Coconut, + network_details: &NymNetworkDetails, +) -> anyhow::Result<()> { + match coconut.command { + nym_cli_commands::coconut::CoconutCommands::IssueCredentials(args) => { + nym_cli_commands::coconut::issue_credentials::execute( + args, + create_signing_client(global_args, network_details)?, + ) + .await? + } + nym_cli_commands::coconut::CoconutCommands::RecoverCredentials(args) => { + nym_cli_commands::coconut::recover_credentials::execute( + args, + create_query_client(network_details)?, + ) + .await? + } + } + Ok(()) +} diff --git a/tools/nym-cli/src/main.rs b/tools/nym-cli/src/main.rs index a5acae77b9..16b771d33e 100644 --- a/tools/nym-cli/src/main.rs +++ b/tools/nym-cli/src/main.rs @@ -7,6 +7,7 @@ use nym_bin_common::logging::setup_logging; use nym_cli_commands::context::{get_network_details, ClientArgs}; use nym_validator_client::nyxd::AccountId; +mod coconut; mod completion; mod validator; @@ -61,6 +62,8 @@ pub(crate) enum Commands { Account(nym_cli_commands::validator::account::Account), /// Sign and verify messages Signature(nym_cli_commands::validator::signature::Signature), + /// Coconut related stuff + Coconut(nym_cli_commands::coconut::Coconut), /// Query chain blocks Block(nym_cli_commands::validator::block::Block), /// Manage and execute WASM smart contracts @@ -101,6 +104,7 @@ async fn execute(cli: Cli) -> anyhow::Result<()> { Commands::Signature(signature) => { validator::signature::execute(signature, &network_details, mnemonic).await? } + Commands::Coconut(coconut) => coconut::execute(args, coconut, &network_details).await?, Commands::Block(block) => validator::block::execute(block, &network_details).await?, Commands::Cosmwasm(cosmwasm) => { validator::cosmwasm::execute(args, cosmwasm, &network_details).await? diff --git a/tools/ts-rs-cli/Cargo.toml b/tools/ts-rs-cli/Cargo.toml index 41f9ead98d..5e315a761c 100644 --- a/tools/ts-rs-cli/Cargo.toml +++ b/tools/ts-rs-cli/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] anyhow = "1" -ts-rs = "6.1.2" +ts-rs = { workspace = true } walkdir = "2" nym-validator-client = { path = "../../common/client-libs/validator-client", features = [ diff --git a/yarn.lock b/yarn.lock index 10f93b7eaf..ca5ac7a967 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1724,23 +1724,6 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@emotion/babel-plugin@^11.10.6": - version "11.10.6" - resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.6.tgz#a68ee4b019d661d6f37dec4b8903255766925ead" - integrity sha512-p2dAqtVrkhSa7xz1u/m9eHYdLi+en8NowrmXeF/dKtJpU8lCWli8RUAati7NcSl0afsBott48pdnANuD0wh9QQ== - dependencies: - "@babel/helper-module-imports" "^7.16.7" - "@babel/runtime" "^7.18.3" - "@emotion/hash" "^0.9.0" - "@emotion/memoize" "^0.8.0" - "@emotion/serialize" "^1.1.1" - babel-plugin-macros "^3.1.0" - convert-source-map "^1.5.0" - escape-string-regexp "^4.0.0" - find-root "^1.1.0" - source-map "^0.5.7" - stylis "4.1.3" - "@emotion/babel-plugin@^11.11.0": version "11.11.0" resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz#c2d872b6a7767a9d176d007f5b31f7d504bb5d6c" @@ -1769,19 +1752,19 @@ "@emotion/weak-memoize" "^0.3.1" stylis "4.2.0" -"@emotion/hash@^0.9.0", "@emotion/hash@^0.9.1": +"@emotion/hash@^0.9.1": version "0.9.1" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43" integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== -"@emotion/is-prop-valid@^1.2.0", "@emotion/is-prop-valid@^1.2.1": +"@emotion/is-prop-valid@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz#23116cf1ed18bfeac910ec6436561ecb1a3885cc" integrity sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw== dependencies: "@emotion/memoize" "^0.8.1" -"@emotion/memoize@^0.8.0", "@emotion/memoize@^0.8.1": +"@emotion/memoize@^0.8.1": version "0.8.1" resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17" integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA== @@ -1800,7 +1783,7 @@ "@emotion/weak-memoize" "^0.3.1" hoist-non-react-statics "^3.3.1" -"@emotion/serialize@^1.1.1", "@emotion/serialize@^1.1.2": +"@emotion/serialize@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.2.tgz#017a6e4c9b8a803bd576ff3d52a0ea6fa5a62b51" integrity sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA== @@ -1828,29 +1811,17 @@ "@emotion/use-insertion-effect-with-fallbacks" "^1.0.1" "@emotion/utils" "^1.2.1" -"@emotion/styled@^11.7.0": - version "11.10.6" - resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.10.6.tgz#d886afdc51ef4d66c787ebde848f3cc8b117ebba" - integrity sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og== - dependencies: - "@babel/runtime" "^7.18.3" - "@emotion/babel-plugin" "^11.10.6" - "@emotion/is-prop-valid" "^1.2.0" - "@emotion/serialize" "^1.1.1" - "@emotion/use-insertion-effect-with-fallbacks" "^1.0.0" - "@emotion/utils" "^1.2.0" - "@emotion/unitless@^0.8.1": version "0.8.1" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3" integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ== -"@emotion/use-insertion-effect-with-fallbacks@^1.0.0", "@emotion/use-insertion-effect-with-fallbacks@^1.0.1": +"@emotion/use-insertion-effect-with-fallbacks@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz#08de79f54eb3406f9daaf77c76e35313da963963" integrity sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw== -"@emotion/utils@^1.2.0", "@emotion/utils@^1.2.1": +"@emotion/utils@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.2.1.tgz#bbab58465738d31ae4cb3dbb6fc00a5991f755e4" integrity sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg== @@ -1942,11 +1913,6 @@ resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-2.9.11.tgz#9ce96e7746625a89239f68ca57c4f654264c17ef" integrity sha512-bA3aZ79UgcHj7tFV7RlgThzwSSHZgvfbt2wprldRkYBcMopdMvHyO17Wwp/twcJasNFischFfS7oz8Katz8DdQ== -"@hookform/resolvers@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.1.0.tgz#ff83ef4aa6078173201da131ceea4c3583b67034" - integrity sha512-z0A8K+Nxq+f83Whm/ajlwE6VtQlp/yPHZnXw7XWVPIGm1Vx0QV8KThU3BpbBRfAZ7/dYqCKKBNnQh85BkmBKkA== - "@humanwhocodes/config-array@^0.11.10": version "0.11.10" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" @@ -2691,20 +2657,6 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" -"@mui/base@5.0.0-beta.0": - version "5.0.0-beta.0" - resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.0.tgz#4145f8a700e04d68d703da70d3c26c62d278f4e5" - integrity sha512-ap+juKvt8R8n3cBqd/pGtZydQ4v2I/hgJKnvJRGjpSh3RvsvnDHO4rXov8MHQlH6VqpOekwgilFLGxMZjNTucA== - dependencies: - "@babel/runtime" "^7.21.0" - "@emotion/is-prop-valid" "^1.2.0" - "@mui/types" "^7.2.4" - "@mui/utils" "^5.12.3" - "@popperjs/core" "^2.11.7" - clsx "^1.2.1" - prop-types "^15.8.1" - react-is "^18.2.0" - "@mui/base@5.0.0-beta.4": version "5.0.0-beta.4" resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.4.tgz#e3f4f4a056b88ab357194a245e223177ce35e0b0" @@ -2733,11 +2685,6 @@ prop-types "^15.8.1" react-is "^18.2.0" -"@mui/core-downloads-tracker@^5.13.0": - version "5.13.7" - resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.13.7.tgz#f4d9af5fe113b80b98b2cb158263d7b8f77e61c7" - integrity sha512-/suIo4WoeL/OyO3KUsFVpdOmKiSAr6NpWXmQ4WLSxwKrTiha1FJxM6vwAki5W/5kR9WnVLw5E8JC4oHHsutT8w== - "@mui/core-downloads-tracker@^5.13.4": version "5.13.4" resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.13.4.tgz#7e4b491d8081b6d45ae51556d82cb16b31315a19" @@ -2750,13 +2697,6 @@ dependencies: "@babel/runtime" "^7.21.0" -"@mui/icons-material@^5.11.11": - version "5.11.11" - resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.11.11.tgz#d4e01bd405b0dac779f5e060586277f91f3acb6e" - integrity sha512-Eell3ADmQVE8HOpt/LZ3zIma8JSvPh3XgnhwZLT0k5HRqZcd6F/QDHc7xsWtgz09t+UEFvOYJXjtrwKmLdwwpw== - dependencies: - "@babel/runtime" "^7.21.0" - "@mui/lab@^5.0.0-alpha.72": version "5.0.0-alpha.134" resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.134.tgz#e48c108fce91fbb89446dcf86ca35e7e761bc078" @@ -2789,25 +2729,7 @@ react-is "^18.2.0" react-transition-group "^4.4.5" -"@mui/material@^5.11.15": - version "5.13.0" - resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.13.0.tgz#23c63a9d117dca22b81bb82db6e9c4d761c8fbf1" - integrity sha512-ckS+9tCpAzpdJdaTF+btF0b6mF9wbXg/EVKtnoAWYi0UKXoXBAVvEUMNpLGA5xdpCdf+A6fPbVUEHs9TsfU+Yw== - dependencies: - "@babel/runtime" "^7.21.0" - "@mui/base" "5.0.0-beta.0" - "@mui/core-downloads-tracker" "^5.13.0" - "@mui/system" "^5.12.3" - "@mui/types" "^7.2.4" - "@mui/utils" "^5.12.3" - "@types/react-transition-group" "^4.4.6" - clsx "^1.2.1" - csstype "^3.1.2" - prop-types "^15.8.1" - react-is "^18.2.0" - react-transition-group "^4.4.5" - -"@mui/private-theming@^5.13.1", "@mui/private-theming@^5.13.7": +"@mui/private-theming@^5.13.1": version "5.13.7" resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.13.7.tgz#2f8ef5da066f3c6c6423bd4260d003a28d10b099" integrity sha512-qbSr+udcij5F9dKhGX7fEdx2drXchq7htLNr2Qg2Ma+WJ6q0ERlEqGSBiPiVDJkptcjeVL4DGmcf1wl5+vD4EA== @@ -2863,20 +2785,6 @@ csstype "^3.1.2" prop-types "^15.8.1" -"@mui/system@^5.11.15", "@mui/system@^5.12.3": - version "5.13.7" - resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.13.7.tgz#b02e6284bbaab4201b142546ebbb2012ec0fa63d" - integrity sha512-7R2KdI6vr8KtnauEfg9e9xQmPk6Gg/1vGNiALYyhSI+cYztxN6WmlSqGX4bjWn/Sygp1TUE1jhFEgs7MWruhkQ== - dependencies: - "@babel/runtime" "^7.22.5" - "@mui/private-theming" "^5.13.7" - "@mui/styled-engine" "^5.13.2" - "@mui/types" "^7.2.4" - "@mui/utils" "^5.13.7" - clsx "^1.2.1" - csstype "^3.1.2" - prop-types "^15.8.1" - "@mui/types@^7.2.4": version "7.2.4" resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.4.tgz#b6fade19323b754c5c6de679a38f068fd50b9328" @@ -2893,7 +2801,7 @@ prop-types "^15.8.1" react-is "^18.2.0" -"@mui/utils@^5.12.3", "@mui/utils@^5.13.7": +"@mui/utils@^5.13.7": version "5.13.7" resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.13.7.tgz#7e6a8336e05eb2642667a5c02eb605351e27ec20" integrity sha512-/3BLptG/q0u36eYED7Nhf4fKXmcKb6LjjT7ZMwhZIZSdSxVqDqSTmATW3a56n3KEPQUXCU9TpxAfCBQhs6brVA== @@ -3211,9 +3119,6 @@ dependencies: nx "15.9.4" -"@nymproject/extension-storage@file:./nym-browser-extension/storage/pkg": - version "0.1.0" - "@nymproject/nym-validator-client@^0.18.0": version "0.18.0" resolved "https://registry.yarnpkg.com/@nymproject/nym-validator-client/-/nym-validator-client-0.18.0.tgz#4dd72bafdf6c72b603242f32c0bb9a1f9e475b98" @@ -3386,7 +3291,7 @@ schema-utils "^3.0.0" source-map "^0.7.3" -"@popperjs/core@^2.11.7", "@popperjs/core@^2.11.8": +"@popperjs/core@^2.11.8": version "2.11.8" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== @@ -3449,11 +3354,6 @@ resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.0.tgz#550a8d5760b78efc5d60f62b5829b0f74c1fde81" integrity sha512-Eu1V3kz3mV0wUpVTiFHuaT8UD1gj/0VnoFHQYX35xlslQUpe8CuYoKFn9d4WZFHm3yDywz6ALZuGdnUPKrNeAw== -"@remix-run/router@1.7.1": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.1.tgz#fea7ac35ae4014637c130011f59428f618730498" - integrity sha512-bgVQM4ZJ2u2CM8k1ey70o1ePFXsEzYVZoWghh6WjM8p59jQ7HxzbHW4SbnWFG7V9ig9chLawQxDTZ3xzOF8MkQ== - "@rollup/plugin-commonjs@^24.0.1": version "24.1.0" resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz#79e54bd83bb64396761431eee6c44152ef322100" @@ -4548,7 +4448,7 @@ react-docgen-typescript "^2.1.1" tslib "^2.0.0" -"@storybook/react@^6.5.15", "@storybook/react@^6.5.16": +"@storybook/react@^6.5.15": version "6.5.16" resolved "https://registry.yarnpkg.com/@storybook/react/-/react-6.5.16.tgz#f7b82ba87f5bb73b4e4e83cce298a98710a88398" integrity sha512-cBtNlOzf/MySpNLBK22lJ8wFU22HnfTB2xJyBk7W7Zi71Lm7Uxkhv1Pz8HdiQndJ0SlsAAQOWjQYsSZsGkZIaA== @@ -4915,20 +4815,6 @@ lz-string "^1.5.0" pretty-format "^27.0.2" -"@testing-library/dom@^9.0.0": - version "9.2.0" - resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-9.2.0.tgz#0e1f45e956f2a16f471559c06edd8827c4832f04" - integrity sha512-xTEnpUKiV/bMyEsE5bT4oYA0x0Z/colMtxzUY8bKyPXBNLn/e0V4ZjBZkEhms0xE4pv9QsPfSRu9AWS4y5wGvA== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/runtime" "^7.12.5" - "@types/aria-query" "^5.0.1" - aria-query "^5.0.0" - chalk "^4.1.0" - dom-accessibility-api "^0.5.9" - lz-string "^1.5.0" - pretty-format "^27.0.2" - "@testing-library/jest-dom@^5.14.1": version "5.16.5" resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-5.16.5.tgz#3912846af19a29b2dbf32a6ae9c31ef52580074e" @@ -4953,15 +4839,6 @@ "@testing-library/dom" "^8.0.0" "@types/react-dom" "<18.0.0" -"@testing-library/react@^14.0.0": - version "14.0.0" - resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-14.0.0.tgz#59030392a6792450b9ab8e67aea5f3cc18d6347c" - integrity sha512-S04gSNJbYE30TlIMLTzv6QCTzt9AqIF5y6s6SzVFILNcNvbV/jU96GeiTPillGQo+Ny64M/5PV7klNYYgv5Dfg== - dependencies: - "@babel/runtime" "^7.12.5" - "@testing-library/dom" "^9.0.0" - "@types/react-dom" "^18.0.0" - "@testing-library/user-event@^13.2.1": version "13.5.0" resolved "https://registry.yarnpkg.com/@testing-library/user-event/-/user-event-13.5.0.tgz#69d77007f1e124d55314a2b73fd204b333b13295" @@ -5104,11 +4981,6 @@ dependencies: "@types/node" "*" -"@types/crypto-js@4.1.1": - version "4.1.1" - resolved "https://registry.yarnpkg.com/@types/crypto-js/-/crypto-js-4.1.1.tgz#602859584cecc91894eb23a4892f38cfa927890d" - integrity sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA== - "@types/d3-array@^3.0.3": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.0.5.tgz#857c1afffd3f51319bbc5b301956aca68acaa7b8" @@ -5475,11 +5347,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.37.tgz#a1f8728e4dc30163deb41e9b7aba65d0c2d4eda1" integrity sha512-ql+4dw4PlPFBP495k8JzUX/oMNRI2Ei4PrMHgj8oT4VhGlYUzF4EYr0qk2fW+XBVGIrq8Zzk13m4cvyXZuv4pA== -"@types/node@^18.16.1": - version "18.16.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.16.7.tgz#86d0ba2541f808cb78d4dc5d3e40242a349d6db8" - integrity sha512-MFg7ua/bRtnA1hYE3pVyWxGd/r7aMqjNOdHvlSsXV3n8iaeGKkOaPzpJh6/ovf4bEXWcojkeMJpTsq3mzXW4IQ== - "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -5539,13 +5406,6 @@ dependencies: "@types/react" "^17" -"@types/react-dom@^18.0.0": - version "18.2.4" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.4.tgz#13f25bfbf4e404d26f62ac6e406591451acba9e0" - integrity sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw== - dependencies: - "@types/react" "*" - "@types/react-dom@^18.0.10": version "18.2.6" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.6.tgz#ad621fa71a8db29af7c31b41b2ea3d8a6f4144d1" @@ -6133,11 +5993,6 @@ resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== -"@webpack-cli/configtest@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-2.0.1.tgz#a69720f6c9bad6aef54a8fa6ba9c3533e7ef4c7f" - integrity sha512-njsdJXJSiS2iNbQVS0eT8A/KPnmyH4pv1APj2K0d1wrZcBLw+yppxOy4CGqa0OxDJkzfL/XELDhD8rocnIwB5A== - "@webpack-cli/info@^1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" @@ -6145,21 +6000,11 @@ dependencies: envinfo "^7.7.3" -"@webpack-cli/info@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-2.0.1.tgz#eed745799c910d20081e06e5177c2b2569f166c0" - integrity sha512-fE1UEWTwsAxRhrJNikE7v4EotYflkEhBL7EbajfkPlf6E37/2QshOy/D48Mw8G5XMFlQtS6YV42vtbG9zBpIQA== - "@webpack-cli/serve@^1.7.0": version "1.7.0" resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== -"@webpack-cli/serve@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.2.tgz#10aa290e44a182c02e173a89452781b1acbc86d9" - integrity sha512-S9h3GmOmzUseyeFW3tYNnWS7gNUuwxZ3mmMq0JyW78Vx1SGKPSkt5bT4pB0rUnVfHjP0EL9gW2bOzmtiTfQt0A== - "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" @@ -8079,11 +7924,6 @@ commander@2, commander@^2.19.0, commander@^2.20.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== -commander@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-10.0.1.tgz#881ee46b4f77d1c1dccc5823433aa39b022cbe06" - integrity sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug== - commander@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" @@ -8351,18 +8191,6 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== -copy-webpack-plugin@^11.0.0: - version "11.0.0" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" - integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== - dependencies: - fast-glob "^3.2.11" - glob-parent "^6.0.1" - globby "^13.1.1" - normalize-path "^3.0.0" - schema-utils "^4.0.0" - serialize-javascript "^6.0.0" - core-js-compat@^3.30.1, core-js-compat@^3.30.2, core-js-compat@^3.8.1: version "3.31.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.31.0.tgz#4030847c0766cc0e803dcdfb30055d7ef2064bf1" @@ -8532,11 +8360,6 @@ crypto-browserify@^3.11.0: randombytes "^2.0.0" randomfill "^1.0.3" -crypto-js@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf" - integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw== - crypto-random-string@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" @@ -9422,13 +9245,6 @@ dotenv-webpack@^7.0.3: dependencies: dotenv-defaults "^2.0.2" -dotenv-webpack@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/dotenv-webpack/-/dotenv-webpack-8.0.1.tgz#6656550460a8076fab20e5ac2eac867e72478645" - integrity sha512-CdrgfhZOnx4uB18SgaoP9XHRN2v48BbjuXQsZY5ixs5A8579NxQkmMxRtI7aTwSiSQcM2ao12Fdu+L3ZS3bG4w== - dependencies: - dotenv-defaults "^2.0.2" - dotenv@^16.0.3: version "16.3.1" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e" @@ -10491,17 +10307,6 @@ fast-glob@^2.2.6: merge2 "^1.2.3" micromatch "^3.1.10" -fast-glob@^3.2.11, fast-glob@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.0.tgz#7c40cb491e1e2ed5664749e87bfb516dbe8727c0" - integrity sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" @@ -11200,7 +11005,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^6.0.1, glob-parent@^6.0.2: +glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== @@ -11331,17 +11136,6 @@ globby@11.1.0, globby@^11.0.1, globby@^11.0.2, globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -globby@^13.1.1: - version "13.2.2" - resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" - integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== - dependencies: - dir-glob "^3.0.1" - fast-glob "^3.3.0" - ignore "^5.2.4" - merge2 "^1.4.1" - slash "^4.0.0" - globby@^6.1.0: version "6.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -11945,7 +11739,7 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.0.4, ignore@^5.2.0, ignore@^5.2.4: +ignore@^5.0.4, ignore@^5.2.0: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== @@ -12105,11 +11899,6 @@ interpret@^2.2.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== -interpret@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" - integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== - ip@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" @@ -12170,7 +11959,7 @@ is-alphanumerical@^2.0.0: is-alphabetical "^2.0.0" is-decimal "^2.0.0" -is-arguments@^1.0.4, is-arguments@^1.1.1: +is-arguments@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== @@ -12361,13 +12150,6 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - is-glob@^3.0.0, is-glob@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" @@ -12594,7 +12376,7 @@ is-text-path@^1.0.1: dependencies: text-extensions "^1.0.0" -is-typed-array@^1.1.10, is-typed-array@^1.1.3, is-typed-array@^1.1.9: +is-typed-array@^1.1.10, is-typed-array@^1.1.9: version "1.1.10" resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== @@ -17577,11 +17359,6 @@ qrcode.react@^1.0.1: prop-types "^15.6.0" qr.js "0.0.0" -qrcode.react@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8" - integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q== - qs@6.11.0: version "6.11.0" resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" @@ -17729,13 +17506,6 @@ react-error-boundary@^3.1.3, react-error-boundary@^3.1.4: dependencies: "@babel/runtime" "^7.12.5" -react-error-boundary@^4.0.3: - version "4.0.4" - resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-4.0.4.tgz#d2e84505b0a67cec7a6bf33b0146faadfe31597d" - integrity sha512-AbqMFx8bCsob8rCHZvJYQ42MQijK0/034RUvan9qrqyJCpazr8d9vKHrysbxcr6odoHLZvQEcYomFPoIqH9fow== - dependencies: - "@babel/runtime" "^7.12.5" - react-google-charts@^3.0.15: version "3.0.15" resolved "https://registry.yarnpkg.com/react-google-charts/-/react-google-charts-3.0.15.tgz#30759a470f48336e744fd383d054122b039a1ff2" @@ -17743,7 +17513,7 @@ react-google-charts@^3.0.15: dependencies: react-load-script "^0.0.6" -react-hook-form@^7.14.2, react-hook-form@^7.43.9: +react-hook-form@^7.14.2: version "7.45.1" resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.45.1.tgz#e352c7f4dbc7540f0756abbb4dcfd1122fecc9bb" integrity sha512-6dWoFJwycbuFfw/iKMcl+RdAOAOHDiF11KWYhNDRN/OkUt+Di5qsZHwA0OwsVnu9y135gkHpTw9DJA+WzCeR9w== @@ -17818,11 +17588,6 @@ react-refresh-typescript@^2.0.2, react-refresh-typescript@^2.0.3: resolved "https://registry.yarnpkg.com/react-refresh-typescript/-/react-refresh-typescript-2.0.9.tgz#f8a86efcb34f8d717100230564b9b57477d74b10" integrity sha512-chAnOO4vpxm/3WkgOVmti+eN8yUtkJzeGkOigV6UA9eDFz12W34e/SsYe2H5+RwYJ3+sfSZkVbiXcG1chEBxlg== -react-refresh-typescript@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/react-refresh-typescript/-/react-refresh-typescript-2.0.8.tgz#84d086f9da742d7de87ad15824301763bf8135ba" - integrity sha512-7zsaM8jIpUZ2hxMdTo9GNZRauTI11AD5YNNaNXVGb8mtYVwdSbc6IQV8YaHevSWYbx+ZpZispezdCfaJuTrh3w== - react-refresh@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.10.0.tgz#2f536c9660c0b9b1d500684d9e52a65e7404f7e3" @@ -17833,11 +17598,6 @@ react-refresh@^0.11.0: resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.11.0.tgz#77198b944733f0f1f1a90e791de4541f9f074046" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== -react-refresh@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.0.tgz#4e02825378a5f227079554d4284889354e5f553e" - integrity sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ== - react-resize-detector@^8.0.4: version "8.1.0" resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-8.1.0.tgz#1c7817db8bc886e2dbd3fbe3b26ea8e56be0524a" @@ -17853,14 +17613,6 @@ react-router-dom@6, react-router-dom@^6.7.0: "@remix-run/router" "1.7.0" react-router "6.14.0" -react-router-dom@^6.9.0: - version "6.14.1" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.14.1.tgz#0ad7ba7abdf75baa61169d49f096f0494907a36f" - integrity sha512-ssF6M5UkQjHK70fgukCJyjlda0Dgono2QGwqGvuk7D+EDGHdacEN3Yke2LTMjkrpHuFwBfDFsEjGVXBDmL+bWw== - dependencies: - "@remix-run/router" "1.7.1" - react-router "6.14.1" - react-router@6.14.0: version "6.14.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.0.tgz#1c3e8e922d934d43a253fd862c72c82167c0a7f1" @@ -17868,13 +17620,6 @@ react-router@6.14.0: dependencies: "@remix-run/router" "1.7.0" -react-router@6.14.1: - version "6.14.1" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.1.tgz#5e82bcdabf21add859dc04b1859f91066b3a5810" - integrity sha512-U4PfgvG55LdvbQjg5Y9QRWyVxIdO1LlpYT7x+tMAxd9/vmiPuJhIwdxZuIQLN/9e3O4KFDHYfR9gzGeYMasW8g== - dependencies: - "@remix-run/router" "1.7.1" - react-simple-maps@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/react-simple-maps/-/react-simple-maps-2.3.0.tgz#7fdb55120335a9b1ef5bf62e0dcb669511705f3e" @@ -18134,13 +17879,6 @@ rechoir@^0.7.0: dependencies: resolve "^1.9.0" -rechoir@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.8.0.tgz#49f866e0d32146142da3ad8f0eff352b3215ff22" - integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== - dependencies: - resolve "^1.20.0" - redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" @@ -19086,11 +18824,6 @@ slash@^2.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== -slash@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" - integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== - slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -19675,11 +19408,6 @@ stylehacks@^5.1.1: browserslist "^4.21.4" postcss-selector-parser "^6.0.4" -stylis@4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" - integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== - stylis@4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.2.0.tgz#79daee0208964c8fe695a42fcffcac633a211a51" @@ -20855,17 +20583,6 @@ util@^0.11.0: dependencies: inherits "2.0.3" -util@^0.12.5: - version "0.12.5" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - which-typed-array "^1.1.2" - utila@~0.4: version "0.4.0" resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c" @@ -21165,25 +20882,6 @@ webpack-cli@^4.8.0: rechoir "^0.7.0" webpack-merge "^5.7.3" -webpack-cli@^5.0.1: - version "5.0.2" - resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-5.0.2.tgz#2954c10ecb61c5d4dad6f68ee2d77f051741946c" - integrity sha512-4y3W5Dawri5+8dXm3+diW6Mn1Ya+Dei6eEVAdIduAmYNLzv1koKVAqsfgrrc9P2mhrYHQphx5htnGkcNwtubyQ== - dependencies: - "@discoveryjs/json-ext" "^0.5.0" - "@webpack-cli/configtest" "^2.0.1" - "@webpack-cli/info" "^2.0.1" - "@webpack-cli/serve" "^2.0.2" - colorette "^2.0.14" - commander "^10.0.1" - cross-spawn "^7.0.3" - envinfo "^7.7.3" - fastest-levenshtein "^1.0.12" - import-local "^3.0.2" - interpret "^3.1.1" - rechoir "^0.8.0" - webpack-merge "^5.7.3" - webpack-dev-middleware@^3.7.3: version "3.7.3" resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz#0639372b143262e2b84ab95d3b91a7597061c2c5" @@ -21439,7 +21137,7 @@ which-collection@^1.0.1: is-weakmap "^2.0.1" is-weakset "^2.0.1" -which-typed-array@^1.1.2, which-typed-array@^1.1.9: +which-typed-array@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== @@ -21801,11 +21499,6 @@ yup@^1.2.0: toposort "^2.0.2" type-fest "^2.19.0" -zod@^3.21.4: - version "3.21.4" - resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db" - integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw== - zwitch@^1.0.0: version "1.0.5" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-1.0.5.tgz#d11d7381ffed16b742f6af7b3f223d5cd9fe9920"