diff --git a/.env.sample-dev b/.env.sample-dev index 2d24384081..d77245ab12 100644 --- a/.env.sample-dev +++ b/.env.sample-dev @@ -3,3 +3,21 @@ RUST_LOG=info RUST_BACKTRACE=1 + +######################################### +# geoipupdate (needed for explorer-api) # +######################################### +# MaxMind account ID (change it to a valid account ID) +GEOIPUPDATE_ACCOUNT_ID=xxx +# MaxMind license key (change it to a valid 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-Country +# 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=./explorer-api/geo_ip diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index 087f379984..8475b83b8b 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 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 if: matrix.os == 'ubuntu-latest' - name: Check out repository code diff --git a/.github/workflows/nym-release-publish.yml b/.github/workflows/nym-release-publish.yml index 32fb811493..7fd455dd46 100644 --- a/.github/workflows/nym-release-publish.yml +++ b/.github/workflows/nym-release-publish.yml @@ -2,6 +2,12 @@ name: Publish Nym binaries on: workflow_dispatch: + inputs: + add_tokio_unstable: + description: 'True to add RUSTFLAGS="--cfg tokio_unstable"' + required: true + default: false + type: boolean release: types: [created] @@ -25,6 +31,11 @@ jobs: with: script: | core.setFailed('Release tag did not start with nym-binaries-...') + + - name: Sets env vars for tokio if set in manual dispatch inputs + run: | + echo 'RUSTFLAGS="--cfg tokio_unstable"' >> $GITHUB_ENV + if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true - name: Install Rust stable uses: actions-rs/toolchain@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index f340a152dd..4b4e5d91ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,24 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - validator-client: added `query_contract_smart` and `query_contract_raw` on `NymdClient` ([#1558]) - network-requester: added additional Blockstream Green wallet endpoint to `example.allowed.list` ([#1611](https://github.com/nymtech/nym/pull/1611)) - common/ledger: new library for communicating with a Ledger device ([#1640]) +- native-client/socks5-client: `disable_loop_cover_traffic_stream` Debug config option to disable the separate loop cover traffic stream ([#1666]) +- native-client/socks5-client: `disable_main_poisson_packet_distribution` Debug config option to make the client ignore poisson distribution in the main packet stream and ONLY send real message (and as fast as they come) ([#1664]) +- native-client/socks5-client: `use_extended_packet_size` Debug config option to make the client use 'ExtendedPacketSize' for its traffic (32kB as opposed to 2kB in 1.0.2) ([#1671]) +- wasm-client: uses updated wasm-compatible `client-core` so that it's now capable of packet retransmission, cover traffic and poisson delay (among other things!) ([#1673]) +- validator-api: add `interval_operating_cost` and `profit_margin_percent` to cmpute reward estimation endpoint +- vesting-contract: optional locked token pledge cap per account ([#1687]), defaults to 100_000 NYM + +### Fixed + +- validator-api, mixnode, gateway should now prefer values in config.toml over mainnet defaults ([#1645]) ### Changed - validator-client: made `fee` argument optional for `execute` and `execute_multiple` ([#1541]) - socks5 client: graceful shutdown should fix error on disconnect in nym-connect ([#1591]) - wasm-client: fixed build errors on MacOS and changed example JS code to use mainnet ([#1585]) +- gateway-client: will attempt to read now as many as 8 websocket messages at once, assuming they're already available on the socket ([#1669]) +- moved `Percent` struct to to `contracts-common`, change affects explorer-api [#1541]: https://github.com/nymtech/nym/pull/1541 [#1558]: https://github.com/nymtech/nym/pull/1558 @@ -23,6 +35,13 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// [#1585]: https://github.com/nymtech/nym/pull/1585 [#1591]: https://github.com/nymtech/nym/pull/1591 [#1640]: https://github.com/nymtech/nym/pull/1640 +[#1645]: https://github.com/nymtech/nym/pull/1645 +[#1664]: https://github.com/nymtech/nym/pull/1664 +[#1666]: https://github.com/nymtech/nym/pull/1645 +[#1669]: https://github.com/nymtech/nym/pull/1669 +[#1671]: https://github.com/nymtech/nym/pull/1671 +[#1673]: https://github.com/nymtech/nym/pull/1673 +[#1687]: https://github.com/nymtech/nym/pull/1687 ## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2) diff --git a/Cargo.lock b/Cargo.lock index 4534ee11cd..bfe183e51c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -102,12 +102,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - [[package]] name = "arrayvec" version = "0.7.2" @@ -283,28 +277,16 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitvec" -version = "0.20.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" -dependencies = [ - "funty 1.1.0", - "radium 0.6.2", - "tap", - "wyz 0.2.0", -] - [[package]] name = "bitvec" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1489fcb93a5bb47da0462ca93ad252ad6af2145cce58d10d46a83931ba9f016b" dependencies = [ - "funty 2.0.0", - "radium 0.7.0", + "funty", + "radium", "tap", - "wyz 0.5.0", + "wyz", ] [[package]] @@ -326,7 +308,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" dependencies = [ "arrayref", - "arrayvec 0.7.2", + "arrayvec", "cc", "cfg-if 1.0.0", "constant_time_eq", @@ -434,12 +416,6 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" -[[package]] -name = "byte-slice-cast" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c5fdd0166095e1d463fc6cc01aa8ce547ad77a4e84d42eb6762b084e28067e" - [[package]] name = "byte-tools" version = "0.3.1" @@ -610,6 +586,7 @@ dependencies = [ "futures", "gateway-client", "gateway-requests", + "gloo-timers", "humantime-serde", "log", "nonexhaustive-delayqueue", @@ -626,6 +603,9 @@ dependencies = [ "topology", "url", "validator-client", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-timer", ] [[package]] @@ -681,6 +661,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "completions" +version = "0.1.0" +dependencies = [ + "clap 3.2.8", + "clap_complete", + "clap_complete_fig", +] + [[package]] name = "config" version = "0.1.0" @@ -748,15 +737,11 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - [[package]] name = "cookie" version = "0.16.0" @@ -919,6 +904,7 @@ dependencies = [ "cfg-if 0.1.10", "clap 3.2.8", "coconut-interface", + "completions", "config", "credential-storage", "credentials", @@ -1322,19 +1308,6 @@ dependencies = [ "const-oid", ] -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version 0.4.0", - "syn", -] - [[package]] name = "devise" version = "0.3.1" @@ -1421,7 +1394,7 @@ dependencies = [ name = "dkg" version = "0.1.0" dependencies = [ - "bitvec 1.0.0", + "bitvec", "bls12_381 0.6.0", "bs58", "criterion", @@ -1592,49 +1565,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "ethabi" -version = "14.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01317735d563b3bad2d5f90d2e1799f414165408251abb762510f40e790e69a" -dependencies = [ - "anyhow", - "ethereum-types", - "hex", - "serde", - "serde_json", - "sha3", - "thiserror", - "uint", -] - -[[package]] -name = "ethbloom" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb684ac8fa8f6c5759f788862bb22ec6fe3cb392f6bfd08e3c64b603661e3f8" -dependencies = [ - "crunchy", - "fixed-hash", - "impl-rlp", - "impl-serde", - "tiny-keccak", -] - -[[package]] -name = "ethereum-types" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64b5df66a228d85e4b17e5d6c6aa43b0310898ffe8a85988c4c032357aaabfd" -dependencies = [ - "ethbloom", - "fixed-hash", - "impl-rlp", - "impl-serde", - "primitive-types", - "uint", -] - [[package]] name = "event-listener" version = "2.5.3" @@ -1655,10 +1585,13 @@ version = "1.0.1" dependencies = [ "chrono", "clap 3.2.8", + "dotenv", + "contracts-common", "humantime-serde", "isocountry", "itertools", "log", + "maxminddb", "mixnet-contract-common", "network-defaults", "okapi", @@ -1735,18 +1668,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "fixed-hash" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" -dependencies = [ - "byteorder", - "rand 0.8.5", - "rustc-hex", - "static_assertions", -] - [[package]] name = "fixedbitset" version = "0.4.1" @@ -1787,21 +1708,6 @@ dependencies = [ "spin 0.9.2", ] -[[package]] -name = "fluvio-wasm-timer" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b768c170dc045fa587a8f948c91f9bcfb87f774930477c6215addf54317f137f" -dependencies = [ - "futures", - "js-sys", - "parking_lot 0.11.2", - "pin-utils", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1855,12 +1761,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" -[[package]] -name = "funty" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" - [[package]] name = "funty" version = "2.0.0" @@ -1949,12 +1849,6 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" -[[package]] -name = "futures-timer" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" - [[package]] name = "futures-util" version = "0.3.24" @@ -1991,7 +1885,6 @@ dependencies = [ "credential-storage", "credentials", "crypto", - "fluvio-wasm-timer", "futures", "gateway-requests", "getrandom 0.2.6", @@ -2000,7 +1893,6 @@ dependencies = [ "nymsphinx", "pemstore", "rand 0.7.3", - "secp256k1", "task", "thiserror", "tokio", @@ -2011,8 +1903,8 @@ dependencies = [ "validator-client", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-timer", "wasm-utils", - "web3", ] [[package]] @@ -2139,6 +2031,18 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "gloo-timers" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "group" version = "0.10.0" @@ -2526,44 +2430,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "impl-codec" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" -dependencies = [ - "parity-scale-codec", -] - -[[package]] -name = "impl-rlp" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" -dependencies = [ - "rlp", -] - -[[package]] -name = "impl-serde" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4551f042f3438e64dbd6226b20527fc84a6e1fe65688b58746a2f53623f25f5c" -dependencies = [ - "serde", -] - -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "inclusion-probability" version = "0.1.0" @@ -2632,6 +2498,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35e70ee094dc02fd9c13fdad4940090f22dbd6ac7c9e7094a46cf0232a50bc7c" +[[package]] +name = "ipnetwork" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4088d739b183546b239688ddbc79891831df421773df95e236daf7867866d355" +dependencies = [ + "serde", +] + [[package]] name = "ipnetwork" version = "0.20.0" @@ -2690,21 +2565,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonrpc-core" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" -dependencies = [ - "futures", - "futures-executor", - "futures-util", - "log", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "k256" version = "0.10.4" @@ -2905,6 +2765,18 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "maxminddb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe2ba61113f9f7a9f0e87c519682d39c43a6f3f79c2cc42c3ba3dda83b1fa334" +dependencies = [ + "ipnetwork 0.18.0", + "log", + "memchr", + "serde", +] + [[package]] name = "maybe-uninit" version = "2.0.0" @@ -2950,25 +2822,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", - "miow", - "ntapi", "wasi 0.11.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", + "windows-sys 0.36.1", ] [[package]] @@ -2989,6 +2850,7 @@ dependencies = [ "bs58", "contracts-common", "cosmwasm-std", + "humantime-serde", "log", "rand_chacha 0.3.1", "schemars", @@ -3114,9 +2976,12 @@ dependencies = [ name = "nonexhaustive-delayqueue" version = "0.1.0" dependencies = [ + "futures-core", + "slab", "tokio", "tokio-stream", "tokio-util 0.7.3", + "wasm-timer", ] [[package]] @@ -3178,6 +3043,21 @@ dependencies = [ "libc", ] +[[package]] +name = "nym-bity-integration" +version = "0.1.0" +dependencies = [ + "anyhow", + "cosmrs", + "eyre", + "k256", + "nym-cli-commands", + "serde", + "serde_json", + "thiserror", + "validator-client", +] + [[package]] name = "nym-cli" version = "1.0.0" @@ -3236,6 +3116,7 @@ dependencies = [ "clap 3.2.8", "client-core", "coconut-interface", + "completions", "config", "credential-storage", "credentials", @@ -3275,6 +3156,7 @@ dependencies = [ "clap 3.2.8", "coconut-interface", "colored", + "completions", "config", "credentials", "crypto", @@ -3317,6 +3199,7 @@ dependencies = [ "bs58", "clap 3.2.8", "colored", + "completions", "config", "crypto", "cupid", @@ -3355,9 +3238,10 @@ version = "1.0.2" dependencies = [ "async-trait", "clap 3.2.8", + "completions", "dirs", "futures", - "ipnetwork", + "ipnetwork 0.20.0", "log", "network-defaults", "nymsphinx", @@ -3400,6 +3284,7 @@ dependencies = [ "clap 3.2.8", "client-core", "coconut-interface", + "completions", "config", "credential-storage", "credentials", @@ -3467,6 +3352,7 @@ dependencies = [ "coconut-interface", "config", "console-subscriber", + "contracts-common", "cosmwasm-std", "credential-storage", "credentials", @@ -3520,7 +3406,9 @@ dependencies = [ "config", "cosmrs", "cosmwasm-std", + "hex-literal", "mixnet-contract-common", + "network-defaults", "nym-types", "serde", "serde_json", @@ -3770,32 +3658,6 @@ dependencies = [ "group 0.11.0", ] -[[package]] -name = "parity-scale-codec" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" -dependencies = [ - "arrayvec 0.7.2", - "bitvec 0.20.4", - "byte-slice-cast", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "serde", -] - -[[package]] -name = "parity-scale-codec-derive" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "parking_lot" version = "0.11.2" @@ -3841,7 +3703,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec 1.8.0", - "windows-sys", + "windows-sys 0.34.0", ] [[package]] @@ -4104,29 +3966,6 @@ dependencies = [ "log", ] -[[package]] -name = "primitive-types" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06345ee39fbccfb06ab45f3a1a5798d9dafa04cb8921a76d227040003a234b0e" -dependencies = [ - "fixed-hash", - "impl-codec", - "impl-rlp", - "impl-serde", - "uint", -] - -[[package]] -name = "proc-macro-crate" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" -dependencies = [ - "thiserror", - "toml", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -4309,12 +4148,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" - [[package]] name = "radium" version = "0.7.0" @@ -4691,16 +4524,6 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "rlp" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5" -dependencies = [ - "bytes", - "rustc-hex", -] - [[package]] name = "rocket" version = "0.5.0-rc.2" @@ -4827,12 +4650,6 @@ dependencies = [ "syn", ] -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - [[package]] name = "rustc_version" version = "0.2.3" @@ -4998,24 +4815,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secp256k1" -version = "0.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" -dependencies = [ - "secp256k1-sys", -] - -[[package]] -name = "secp256k1-sys" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957da2573cde917463ece3570eab4a0b3f19de6f1646cde62e6fd3868f566036" -dependencies = [ - "cc", -] - [[package]] name = "security-framework" version = "2.6.1" @@ -5402,21 +5201,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "soketto" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4919971d141dbadaa0e82b5d369e2d7666c98e4625046140615ca363e50d4daa" -dependencies = [ - "base64", - "bytes", - "futures", - "httparse", - "log", - "rand 0.8.5", - "sha-1 0.9.8", -] - [[package]] name = "sphinx" version = "0.1.0" @@ -6021,15 +5805,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "tinytemplate" version = "1.2.1" @@ -6042,16 +5817,16 @@ dependencies = [ [[package]] name = "tokio" -version = "1.19.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95eec79ea28c00a365f539f1961e9278fbcaf81c0ff6aaf0e93c181352446948" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ + "autocfg 1.1.0", "bytes", "libc", "memchr", "mio", "num_cpus", - "once_cell", "parking_lot 0.12.0", "pin-project-lite", "signal-hook-registry", @@ -6172,7 +5947,6 @@ checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" dependencies = [ "bytes", "futures-core", - "futures-io", "futures-sink", "log", "pin-project-lite", @@ -6704,6 +6478,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", @@ -6717,7 +6492,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", + "log", "mixnet-contract-common", "schemars", "serde", @@ -6765,9 +6542,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -6775,13 +6552,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -6802,9 +6579,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6812,9 +6589,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -6825,9 +6602,23 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "git+https://github.com/mmsinclair/wasm-timer?rev=b9d1a54ad514c2f230a026afe0dde341e98cd7b6#b9d1a54ad514c2f230a026afe0dde341e98cd7b6" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] [[package]] name = "wasm-utils" @@ -6851,52 +6642,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web3" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd24abe6f2b68e0677f843059faea87bcbd4892e39f02886f366d8222c3c540d" -dependencies = [ - "arrayvec 0.5.2", - "base64", - "bytes", - "derive_more", - "ethabi", - "ethereum-types", - "futures", - "futures-timer", - "headers", - "hex", - "jsonrpc-core", - "log", - "parking_lot 0.11.2", - "pin-project", - "reqwest", - "rlp", - "secp256k1", - "serde", - "serde_json", - "soketto", - "tiny-keccak", - "tokio", - "tokio-stream", - "tokio-util 0.6.9", - "url", - "web3-async-native-tls", -] - -[[package]] -name = "web3-async-native-tls" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f6d8d1636b2627fe63518d5a9b38a569405d9c9bc665c43c9c341de57227ebb" -dependencies = [ - "native-tls", - "thiserror", - "tokio", - "url", -] - [[package]] name = "webpki" version = "0.21.4" @@ -6992,11 +6737,24 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5acdd78cb4ba54c0045ac14f62d8f94a03d10047904ae2a40afa1e99d8f70825" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.34.0", + "windows_i686_gnu 0.34.0", + "windows_i686_msvc 0.34.0", + "windows_x86_64_gnu 0.34.0", + "windows_x86_64_msvc 0.34.0", +] + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] [[package]] @@ -7005,30 +6763,60 @@ version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d" +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + [[package]] name = "windows_i686_gnu" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed" +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + [[package]] name = "windows_i686_msvc" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956" +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + [[package]] name = "windows_x86_64_gnu" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4" +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + [[package]] name = "windows_x86_64_msvc" version = "0.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + [[package]] name = "winreg" version = "0.10.1" @@ -7038,12 +6826,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "wyz" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" - [[package]] name = "wyz" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index b6c45da88b..deb41ec808 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -63,9 +63,11 @@ members = [ "common/topology", "common/types", "common/wasm-utils", + "common/completions", "explorer-api", "gateway", "gateway/gateway-requests", + "integrations/bity", "mixnode", "service-providers/network-requester", "service-providers/network-statistics", diff --git a/Makefile b/Makefile index 8d85d2b5c5..95d163a7ca 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,12 @@ test: clippy-all cargo-test wasm fmt test-all: test cargo-test-expensive no-clippy: build cargo-test wasm fmt happy: fmt clippy-happy test -clippy-all: clippy-main clippy-coconut clippy-all-contracts clippy-all-wallet clippy-all-connect +clippy-all: clippy-main clippy-coconut clippy-all-contracts clippy-all-wallet clippy-all-connect clippy-all-wasm-client clippy-happy: clippy-happy-main clippy-happy-contracts clippy-happy-wallet clippy-happy-connect -cargo-test: test-main test-contracts test-wallet test-connect test-coconut +cargo-test: test-main test-contracts test-wallet test-connect test-coconut test-wasm-client cargo-test-expensive: test-main-expensive test-contracts-expensive test-wallet-expensive test-connect-expensive test-coconut-expensive -build: build-contracts build-wallet build-main build-connect -fmt: fmt-main fmt-contracts fmt-wallet fmt-connect +build: build-contracts build-wallet build-main build-connect build-wasm-client +fmt: fmt-main fmt-contracts fmt-wallet fmt-connect fmt-wasm-client clippy-happy-main: cargo clippy @@ -40,6 +40,9 @@ clippy-all-wallet: clippy-all-connect: cargo clippy --workspace --manifest-path nym-connect/Cargo.toml --all-features -- -D warnings +clippy-all-wasm-client: + cargo clippy --workspace --manifest-path clients/webassembly/Cargo.toml --all-features --target wasm32-unknown-unknown -- -D warnings + test-main: cargo test --workspace @@ -68,6 +71,9 @@ test-wallet: test-wallet-expensive: cargo test --manifest-path nym-wallet/Cargo.toml --all-features -- --ignored +test-wasm-client: + cargo test --workspace --manifest-path clients/webassembly/Cargo.toml --all-features + test-connect: cargo test --manifest-path nym-connect/Cargo.toml --all-features @@ -86,6 +92,9 @@ build-wallet: build-connect: cargo build --manifest-path nym-connect/Cargo.toml --workspace +build-wasm-client: + cargo build --manifest-path clients/webassembly/Cargo.toml --workspace --target wasm32-unknown-unknown + build-nym-cli: cargo build --release --manifest-path tools/nym-cli/Cargo.toml @@ -101,9 +110,15 @@ fmt-wallet: fmt-connect: cargo fmt --manifest-path nym-connect/Cargo.toml --all +fmt-wasm-client: + cargo fmt --manifest-path clients/webassembly/Cargo.toml --all + wasm: RUSTFLAGS='-C link-arg=-s' cargo build --manifest-path contracts/Cargo.toml --release --target wasm32-unknown-unknown +mixnet-opt: wasm + cd contracts/mixnet && make opt + generate-typescript: cd tools/ts-rs-cli && cargo run && cd ../.. yarn types:lint:fix diff --git a/clients/client-core/Cargo.toml b/clients/client-core/Cargo.toml index 9cd7d16e4b..0f279f4425 100644 --- a/clients/client-core/Cargo.toml +++ b/clients/client-core/Cargo.toml @@ -13,26 +13,47 @@ humantime-serde = "1.0" log = "0.4" rand = { version = "0.7.3", features = ["wasm-bindgen"] } serde = { version = "1.0", features = ["derive"] } -sled = "0.34" +sled = { version = "0.34", optional = true } thiserror = "1.0.34" -tokio = { version = "1.19.1", features = ["macros"] } url = { version ="2.2", features = ["serde"] } # internal config = { path = "../../common/config" } crypto = { path = "../../common/crypto" } gateway-client = { path = "../../common/client-libs/gateway-client" } +#gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm", "coconut"] } gateway-requests = { path = "../../gateway/gateway-requests" } nonexhaustive-delayqueue = { path = "../../common/nonexhaustive-delayqueue" } nymsphinx = { path = "../../common/nymsphinx" } pemstore = { path = "../../common/pemstore" } -task = { path = "../../common/task" } topology = { path = "../../common/topology" } -validator-client = { path = "../../common/client-libs/validator-client" } +validator-client = { path = "../../common/client-libs/validator-client", default-features = false } tap = "1.0.1" +tokio = { version = "1.21.2", features = ["time", "macros"]} + +[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen-futures] +version = "0.4" + +[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen] +version = "0.2.83" + +[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-timer] +git = "https://github.com/mmsinclair/wasm-timer" +rev = "b9d1a54ad514c2f230a026afe0dde341e98cd7b6" + +[target."cfg(target_arch = \"wasm32\")".dependencies.gloo-timers] +version = "0.2.4" +features = ["futures"] + +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.task] +path = "../../common/task" + [dev-dependencies] tempfile = "3.1.0" [features] +default = ["reply-surb"] +wasm = ["gateway-client/wasm"] coconut = ["gateway-client/coconut", "gateway-requests/coconut"] +reply-surb = ["sled"] \ No newline at end of file diff --git a/clients/client-core/src/client/cover_traffic_stream.rs b/clients/client-core/src/client/cover_traffic_stream.rs index 8e25357e00..0d362491ee 100644 --- a/clients/client-core/src/client/cover_traffic_stream.rs +++ b/clients/client-core/src/client/cover_traffic_stream.rs @@ -3,20 +3,26 @@ use crate::client::mix_traffic::BatchMixMessageSender; use crate::client::topology_control::TopologyAccessor; +use crate::spawn_future; use futures::task::{Context, Poll}; use futures::{Future, Stream, StreamExt}; use log::*; use nymsphinx::acknowledgements::AckKey; use nymsphinx::addressing::clients::Recipient; use nymsphinx::cover::generate_loop_cover_packet; +use nymsphinx::params::PacketSize; use nymsphinx::utils::sample_poisson_duration; use rand::{rngs::OsRng, CryptoRng, Rng}; use std::pin::Pin; use std::sync::Arc; -use task::ShutdownListener; -use tokio::task::JoinHandle; +use std::time::Duration; + +#[cfg(not(target_arch = "wasm32"))] use tokio::time; +#[cfg(target_arch = "wasm32")] +use wasm_timer; + pub struct LoopCoverTrafficStream where R: CryptoRng + Rng, @@ -25,18 +31,22 @@ where ack_key: Arc, /// Average delay an acknowledgement packet is going to get delay at a single mixnode. - average_ack_delay: time::Duration, + average_ack_delay: Duration, /// Average delay a data packet is going to get delay at a single mixnode. - average_packet_delay: time::Duration, + average_packet_delay: Duration, /// Average delay between sending subsequent cover packets. - average_cover_message_sending_delay: time::Duration, + average_cover_message_sending_delay: Duration, /// Internal state, determined by `average_message_sending_delay`, /// used to keep track of when a next packet should be sent out. + #[cfg(not(target_arch = "wasm32"))] next_delay: Pin>, + #[cfg(target_arch = "wasm32")] + next_delay: Pin>, + /// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them /// out to the network without any further delays. mix_tx: BatchMixMessageSender, @@ -50,8 +60,8 @@ where /// Accessor to the common instance of network topology. topology_access: TopologyAccessor, - /// Listen to shutdown signals. - shutdown: ShutdownListener, + /// Predefined packet size used for the loop cover messages. + packet_size: PacketSize, } impl Stream for LoopCoverTrafficStream @@ -73,13 +83,21 @@ where // we know it's time to send a message, so let's prepare delay for the next one // Get the `now` by looking at the current `delay` deadline let avg_delay = self.average_cover_message_sending_delay; - let now = self.next_delay.deadline(); let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_delay); // The next interval value is `next_poisson_delay` after the one that just // yielded. - let next = now + next_poisson_delay; - self.next_delay.as_mut().reset(next); + #[cfg(not(target_arch = "wasm32"))] + { + let now = self.next_delay.deadline(); + let next = now + next_poisson_delay; + self.next_delay.as_mut().reset(next); + } + + #[cfg(target_arch = "wasm32")] + { + self.next_delay.as_mut().reset(next_poisson_delay); + } Poll::Ready(Some(())) } @@ -91,30 +109,39 @@ impl LoopCoverTrafficStream { #[allow(clippy::too_many_arguments)] pub fn new( ack_key: Arc, - average_ack_delay: time::Duration, - average_packet_delay: time::Duration, - average_cover_message_sending_delay: time::Duration, + average_ack_delay: Duration, + average_packet_delay: Duration, + average_cover_message_sending_delay: Duration, mix_tx: BatchMixMessageSender, our_full_destination: Recipient, topology_access: TopologyAccessor, - shutdown: ShutdownListener, ) -> Self { let rng = OsRng; + #[cfg(not(target_arch = "wasm32"))] + let next_delay = Box::pin(time::sleep(Default::default())); + + #[cfg(target_arch = "wasm32")] + let next_delay = Box::pin(wasm_timer::Delay::new(Default::default())); + LoopCoverTrafficStream { ack_key, average_ack_delay, average_packet_delay, average_cover_message_sending_delay, - next_delay: Box::pin(time::sleep(Default::default())), + next_delay, mix_tx, our_full_destination, rng, topology_access, - shutdown, + packet_size: Default::default(), } } + pub fn set_custom_packet_size(&mut self, packet_size: PacketSize) { + self.packet_size = packet_size; + } + async fn on_new_message(&mut self) { trace!("next cover message!"); @@ -136,10 +163,11 @@ impl LoopCoverTrafficStream { let cover_message = generate_loop_cover_packet( &mut self.rng, topology_ref, - &*self.ack_key, + &self.ack_key, &self.our_full_destination, self.average_ack_delay, self.average_packet_delay, + self.packet_size, ) .expect("Somehow failed to generate a loop cover message with a valid topology"); @@ -156,40 +184,56 @@ impl LoopCoverTrafficStream { // JS: due to identical logical structure to OutQueueControl::on_message(), this is also // presumably required to prevent bugs in the future. Exact reason is still unknown to me. + + // TODO: temporary and BAD workaround for wasm (we should find a way to yield here in wasm) + #[cfg(not(target_arch = "wasm32"))] tokio::task::yield_now().await; } - async fn run(&mut self) { + #[cfg(not(target_arch = "wasm32"))] + pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) { // we should set initial delay only when we actually start the stream - self.next_delay = Box::pin(time::sleep(sample_poisson_duration( - &mut self.rng, - self.average_cover_message_sending_delay, - ))); + let sampled = + sample_poisson_duration(&mut self.rng, self.average_cover_message_sending_delay); + self.next_delay = Box::pin(time::sleep(sampled)); - let mut shutdown = self.shutdown.clone(); - while !shutdown.is_shutdown() { - tokio::select! { - biased; - _ = shutdown.recv() => { - log::trace!("LoopCoverTrafficStream: Received shutdown"); - } - next = self.next() => { - if next.is_some() { - self.on_new_message().await; - } else { - log::trace!("LoopCoverTrafficStream: Stopping since channel closed"); - break; + spawn_future(async move { + debug!("Started LoopCoverTrafficStream with graceful shutdown support"); + + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + log::trace!("LoopCoverTrafficStream: Received shutdown"); + } + next = self.next() => { + if next.is_some() { + self.on_new_message().await; + } else { + log::trace!("LoopCoverTrafficStream: Stopping since channel closed"); + break; + } } } } - } - assert!(self.shutdown.is_shutdown_poll()); - log::debug!("LoopCoverTrafficStream: Exiting"); + assert!(shutdown.is_shutdown_poll()); + log::debug!("LoopCoverTrafficStream: Exiting"); + }) } - pub fn start(mut self) -> JoinHandle<()> { - tokio::spawn(async move { - self.run().await; + #[cfg(target_arch = "wasm32")] + pub fn start(mut self) { + // we should set initial delay only when we actually start the stream + let sampled = + sample_poisson_duration(&mut self.rng, self.average_cover_message_sending_delay); + self.next_delay = Box::pin(wasm_timer::Delay::new(sampled)); + + spawn_future(async move { + debug!("Started LoopCoverTrafficStream without graceful shutdown support"); + + while self.next().await.is_some() { + self.on_new_message().await; + } }) } } diff --git a/clients/client-core/src/client/mix_traffic.rs b/clients/client-core/src/client/mix_traffic.rs index 9a3e70a0d6..cc1d0619ae 100644 --- a/clients/client-core/src/client/mix_traffic.rs +++ b/clients/client-core/src/client/mix_traffic.rs @@ -1,13 +1,12 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::spawn_future; use futures::channel::mpsc; use futures::StreamExt; use gateway_client::GatewayClient; use log::*; use nymsphinx::forwarding::packet::MixPacket; -use task::ShutdownListener; -use tokio::task::JoinHandle; pub type BatchMixMessageSender = mpsc::UnboundedSender>; pub type BatchMixMessageReceiver = mpsc::UnboundedReceiver>; @@ -19,7 +18,6 @@ pub struct MixTrafficController { // later on gateway_client will need to be accessible by other entities gateway_client: GatewayClient, mix_rx: BatchMixMessageReceiver, - shutdown: ShutdownListener, // TODO: this is temporary work-around. // in long run `gateway_client` will be moved away from `MixTrafficController` anyway. @@ -30,12 +28,10 @@ impl MixTrafficController { pub fn new( mix_rx: BatchMixMessageReceiver, gateway_client: GatewayClient, - shutdown: ShutdownListener, ) -> MixTrafficController { MixTrafficController { gateway_client, mix_rx, - shutdown, consecutive_gateway_failure_count: 0, } } @@ -69,30 +65,40 @@ impl MixTrafficController { } } - pub async fn run(&mut self) { - while !self.shutdown.is_shutdown() { - tokio::select! { - mix_packets = self.mix_rx.next() => match mix_packets { - Some(mix_packets) => { - self.on_messages(mix_packets).await; + #[cfg(not(target_arch = "wasm32"))] + pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) { + spawn_future(async move { + debug!("Started MixTrafficController with graceful shutdown support"); + + while !shutdown.is_shutdown() { + tokio::select! { + mix_packets = self.mix_rx.next() => match mix_packets { + Some(mix_packets) => { + self.on_messages(mix_packets).await; + }, + None => { + log::trace!("MixTrafficController: Stopping since channel closed"); + break; + } }, - None => { - log::trace!("MixTrafficController: Stopping since channel closed"); - break; + _ = shutdown.recv() => { + log::trace!("MixTrafficController: Received shutdown"); } - }, - _ = self.shutdown.recv() => { - log::trace!("MixTrafficController: Received shutdown"); } } - } - assert!(self.shutdown.is_shutdown_poll()); - log::debug!("MixTrafficController: Exiting"); + assert!(shutdown.is_shutdown_poll()); + log::debug!("MixTrafficController: Exiting"); + }) } - pub fn start(mut self) -> JoinHandle<()> { - tokio::spawn(async move { - self.run().await; + #[cfg(target_arch = "wasm32")] + pub fn start(mut self) { + spawn_future(async move { + debug!("Started MixTrafficController without graceful shutdown support"); + + while let Some(mix_packets) = self.mix_rx.next().await { + self.on_messages(mix_packets).await; + } }) } } diff --git a/clients/client-core/src/client/mod.rs b/clients/client-core/src/client/mod.rs index 6edb810bc8..bce1a089dc 100644 --- a/clients/client-core/src/client/mod.rs +++ b/clients/client-core/src/client/mod.rs @@ -6,6 +6,7 @@ pub mod key_manager; pub mod mix_traffic; pub mod real_messages_control; pub mod received_buffer; +#[cfg(feature = "reply-surb")] pub mod reply_key_storage; pub mod topology_control; diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs index 97f80474bb..2f9a6053ec 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs @@ -10,7 +10,6 @@ use nymsphinx::{ chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}, }; use std::sync::Arc; -use task::ShutdownListener; /// Module responsible for listening for any data resembling acknowledgements from the network /// and firing actions to remove them from the 'Pending' state. @@ -18,7 +17,6 @@ pub(super) struct AcknowledgementListener { ack_key: Arc, ack_receiver: AcknowledgementReceiver, action_sender: ActionSender, - shutdown: ShutdownListener, } impl AcknowledgementListener { @@ -26,13 +24,11 @@ impl AcknowledgementListener { ack_key: Arc, ack_receiver: AcknowledgementReceiver, action_sender: ActionSender, - shutdown: ShutdownListener, ) -> Self { AcknowledgementListener { ack_key, ack_receiver, action_sender, - shutdown, } } @@ -67,28 +63,41 @@ impl AcknowledgementListener { .unwrap(); } - pub(super) async fn run(&mut self) { - debug!("Started AcknowledgementListener"); - while !self.shutdown.is_shutdown() { + async fn handle_ack_receiver_item(&mut self, item: Vec>) { + // realistically we would only be getting one ack at the time + for ack in item { + self.on_ack(ack).await; + } + } + + #[cfg(not(target_arch = "wasm32"))] + pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + debug!("Started AcknowledgementListener with graceful shutdown support"); + + while !shutdown.is_shutdown() { tokio::select! { acks = self.ack_receiver.next() => match acks { - Some(acks) => { - // realistically we would only be getting one ack at the time - for ack in acks { - self.on_ack(ack).await; - } - }, + Some(acks) => self.handle_ack_receiver_item(acks).await, None => { log::trace!("AcknowledgementListener: Stopping since channel closed"); break; } }, - _ = self.shutdown.recv() => { + _ = shutdown.recv() => { log::trace!("AcknowledgementListener: Received shutdown"); } } } - assert!(self.shutdown.is_shutdown_poll()); + assert!(shutdown.is_shutdown_poll()); log::debug!("AcknowledgementListener: Exiting"); } + + #[cfg(target_arch = "wasm32")] + pub(super) async fn run(&mut self) { + debug!("Started AcknowledgementListener without graceful shutdown support"); + + while let Some(acks) = self.ack_receiver.next().await { + self.handle_ack_receiver_item(acks).await + } + } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs index ca22780d92..2b21639230 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs @@ -12,7 +12,6 @@ use nymsphinx::Delay as SphinxDelay; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; -use task::ShutdownListener; pub(crate) type ActionSender = UnboundedSender; @@ -100,16 +99,12 @@ pub(super) struct ActionController { /// Channel for notifying `RetransmissionRequestListener` about expired acknowledgements. retransmission_sender: RetransmissionRequestSender, - - /// Listen for shutdown notifications - shutdown: ShutdownListener, } impl ActionController { pub(super) fn new( config: Config, retransmission_sender: RetransmissionRequestSender, - shutdown: ShutdownListener, ) -> (Self, ActionSender) { let (sender, receiver) = mpsc::unbounded(); ( @@ -119,7 +114,6 @@ impl ActionController { pending_acks_timers: NonExhaustiveDelayQueue::new(), incoming_actions: receiver, retransmission_sender, - shutdown, }, sender, ) @@ -251,8 +245,11 @@ impl ActionController { } } - pub(super) async fn run(&mut self) { - while !self.shutdown.is_shutdown() { + #[cfg(not(target_arch = "wasm32"))] + pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + debug!("Started ActionController with graceful shutdown support"); + + while !shutdown.is_shutdown() { tokio::select! { action = self.incoming_actions.next() => match action { Some(action) => self.process_action(action), @@ -270,12 +267,24 @@ impl ActionController { break; } }, - _ = self.shutdown.recv() => { + _ = shutdown.recv() => { log::trace!("ActionController: Received shutdown"); } } } - assert!(self.shutdown.is_shutdown_poll()); + assert!(shutdown.is_shutdown_poll()); log::debug!("ActionController: Exiting"); } + + #[cfg(target_arch = "wasm32")] + pub(super) async fn run(&mut self) { + debug!("Started ActionController without graceful shutdown support"); + + loop { + tokio::select! { + action = self.incoming_actions.next() => self.process_action(action.unwrap()), + expired_ack = self.pending_acks_timers.next() => self.handle_expired_ack_timer(expired_ack.unwrap()) + } + } + } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs index 611b43d02b..57e1162a72 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs @@ -3,7 +3,6 @@ use super::action_controller::{Action, ActionSender}; use super::PendingAcknowledgement; -use crate::client::reply_key_storage::ReplyKeyStorage; use crate::client::{ inbound_messages::{InputMessage, InputMessageReceiver}, real_messages_control::real_traffic_stream::{BatchRealMessageSender, RealMessage}, @@ -16,7 +15,9 @@ use nymsphinx::preparer::MessagePreparer; use nymsphinx::{acknowledgements::AckKey, addressing::clients::Recipient}; use rand::{CryptoRng, Rng}; use std::sync::Arc; -use task::ShutdownListener; + +#[cfg(feature = "reply-surb")] +use crate::client::reply_key_storage::ReplyKeyStorage; /// Module responsible for dealing with the received messages: splitting them, creating acknowledgements, /// putting everything into sphinx packets, etc. @@ -32,8 +33,8 @@ where action_sender: ActionSender, real_message_sender: BatchRealMessageSender, topology_access: TopologyAccessor, + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, - shutdown: ShutdownListener, } impl InputMessageListener @@ -51,8 +52,7 @@ where action_sender: ActionSender, real_message_sender: BatchRealMessageSender, topology_access: TopologyAccessor, - reply_key_storage: ReplyKeyStorage, - shutdown: ShutdownListener, + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, ) -> Self { InputMessageListener { ack_key, @@ -62,8 +62,8 @@ where action_sender, real_message_sender, topology_access, + #[cfg(feature = "reply-surb")] reply_key_storage, - shutdown, } } @@ -121,12 +121,16 @@ where .prepare_and_split_message(content, with_reply_surb, topology) .expect("somehow the topology was invalid after all!"); + #[cfg(feature = "reply-surb")] if let Some(reply_key) = reply_key { self.reply_key_storage .insert_encryption_key(reply_key) .expect("Failed to insert surb reply key to the store!") } + #[cfg(not(feature = "reply-surb"))] + let _reply_key = reply_key; + // encrypt chunks, put them inside sphinx packets and generate acks let mut pending_acks = Vec::with_capacity(split_message.len()); let mut real_messages = Vec::with_capacity(split_message.len()); @@ -184,9 +188,11 @@ where } } - pub(super) async fn run(&mut self) { - debug!("Started InputMessageListener"); - while !self.shutdown.is_shutdown() { + #[cfg(not(target_arch = "wasm32"))] + pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + debug!("Started InputMessageListener with graceful shutdown support"); + + while !shutdown.is_shutdown() { tokio::select! { input_msg = self.input_receiver.next() => match input_msg { Some(input_msg) => { @@ -197,12 +203,20 @@ where break; } }, - _ = self.shutdown.recv() => { + _ = shutdown.recv() => { log::trace!("InputMessageListener: Received shutdown"); } } } - assert!(self.shutdown.is_shutdown_poll()); + assert!(shutdown.is_shutdown_poll()); log::debug!("InputMessageListener: Exiting"); } + + #[cfg(target_arch = "wasm32")] + pub(super) async fn run(&mut self) { + debug!("Started InputMessageListener without graceful shutdown support"); + while let Some(input_msg) = self.input_receiver.next().await { + self.on_input_message(input_msg).await; + } + } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs index 9314519a8a..c946716091 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs @@ -8,11 +8,12 @@ use self::{ sent_notification_listener::SentNotificationListener, }; use super::real_traffic_stream::BatchRealMessageSender; -use crate::client::reply_key_storage::ReplyKeyStorage; use crate::client::{inbound_messages::InputMessageReceiver, topology_control::TopologyAccessor}; +use crate::spawn_future; use futures::channel::mpsc; use gateway_client::AcknowledgementReceiver; use log::*; +use nymsphinx::params::PacketSize; use nymsphinx::{ acknowledgements::AckKey, addressing::clients::Recipient, @@ -25,8 +26,9 @@ use std::{ sync::{Arc, Weak}, time::Duration, }; -use task::ShutdownListener; -use tokio::task::JoinHandle; + +#[cfg(feature = "reply-surb")] +use crate::client::reply_key_storage::ReplyKeyStorage; mod acknowledgement_listener; mod action_controller; @@ -120,6 +122,9 @@ pub(super) struct Config { /// Average delay a data packet is going to get delayed at a single mixnode. average_packet_delay: Duration, + + /// Predefined packet size used for the encapsulated messages. + packet_size: PacketSize, } impl Config { @@ -134,19 +139,25 @@ impl Config { ack_wait_multiplier, average_ack_delay, average_packet_delay, + packet_size: Default::default(), } } + + pub fn with_custom_packet_size(mut self, packet_size: PacketSize) -> Self { + self.packet_size = packet_size; + self + } } pub(super) struct AcknowledgementController where R: CryptoRng + Rng, { - acknowledgement_listener: Option, - input_message_listener: Option>, - retransmission_request_listener: Option>, - sent_notification_listener: Option, - action_controller: Option, + acknowledgement_listener: AcknowledgementListener, + input_message_listener: InputMessageListener, + retransmission_request_listener: RetransmissionRequestListener, + sent_notification_listener: SentNotificationListener, + action_controller: ActionController, } impl AcknowledgementController @@ -160,30 +171,29 @@ where topology_access: TopologyAccessor, ack_key: Arc, ack_recipient: Recipient, - reply_key_storage: ReplyKeyStorage, connectors: AcknowledgementControllerConnectors, - shutdown: ShutdownListener, + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, ) -> Self { let (retransmission_tx, retransmission_rx) = mpsc::unbounded(); let action_config = action_controller::Config::new(config.ack_wait_addition, config.ack_wait_multiplier); let (action_controller, action_sender) = - ActionController::new(action_config, retransmission_tx, shutdown.clone()); + ActionController::new(action_config, retransmission_tx); let message_preparer = MessagePreparer::new( rng, ack_recipient, config.average_packet_delay, config.average_ack_delay, - ); + ) + .with_custom_real_message_packet_size(config.packet_size); // will listen for any acks coming from the network let acknowledgement_listener = AcknowledgementListener::new( Arc::clone(&ack_key), connectors.ack_receiver, action_sender.clone(), - shutdown.clone(), ); // will listen for any new messages from the client @@ -195,8 +205,8 @@ where action_sender.clone(), connectors.real_message_sender.clone(), topology_access.clone(), + #[cfg(feature = "reply-surb")] reply_key_storage, - shutdown.clone(), ); // will listen for any ack timeouts and trigger retransmission @@ -208,75 +218,95 @@ where connectors.real_message_sender, retransmission_rx, topology_access, - shutdown.clone(), ); // will listen for events indicating the packet was sent through the network so that // the retransmission timer should be started. let sent_notification_listener = - SentNotificationListener::new(connectors.sent_notifier, action_sender, shutdown); + SentNotificationListener::new(connectors.sent_notifier, action_sender); AcknowledgementController { - acknowledgement_listener: Some(acknowledgement_listener), - input_message_listener: Some(input_message_listener), - retransmission_request_listener: Some(retransmission_request_listener), - sent_notification_listener: Some(sent_notification_listener), - action_controller: Some(action_controller), + acknowledgement_listener, + input_message_listener, + retransmission_request_listener, + sent_notification_listener, + action_controller, } } - pub(super) async fn run(&mut self) { - let mut acknowledgement_listener = self.acknowledgement_listener.take().unwrap(); - let mut input_message_listener = self.input_message_listener.take().unwrap(); - let mut retransmission_request_listener = - self.retransmission_request_listener.take().unwrap(); - let mut sent_notification_listener = self.sent_notification_listener.take().unwrap(); - let mut action_controller = self.action_controller.take().unwrap(); + #[cfg(not(target_arch = "wasm32"))] + pub(super) fn start_with_shutdown(self, shutdown: task::ShutdownListener) { + let mut acknowledgement_listener = self.acknowledgement_listener; + let mut input_message_listener = self.input_message_listener; + let mut retransmission_request_listener = self.retransmission_request_listener; + let mut sent_notification_listener = self.sent_notification_listener; + let mut action_controller = self.action_controller; - // the below are log messages are errors as at the current stage we do not expect any of - // the task to ever finish. This will of course change once we introduce - // graceful shutdowns. - let ack_listener_fut = tokio::spawn(async move { - acknowledgement_listener.run().await; - debug!("The acknowledgement listener has finished execution!"); + let shutdown_handle = shutdown.clone(); + spawn_future(async move { acknowledgement_listener - }); - let input_listener_fut = tokio::spawn(async move { - input_message_listener.run().await; - debug!("The input listener has finished execution!"); - input_message_listener - }); - let retransmission_req_fut = tokio::spawn(async move { - retransmission_request_listener.run().await; - debug!("The retransmission request listener has finished execution!"); - retransmission_request_listener - }); - let sent_notification_fut = tokio::spawn(async move { - sent_notification_listener.run().await; - debug!("The sent notification listener has finished execution!"); - sent_notification_listener - }); - let action_controller_fut = tokio::spawn(async move { - action_controller.run().await; - debug!("The controller has finished execution!"); - action_controller + .run_with_shutdown(shutdown_handle) + .await; + debug!("The acknowledgement listener has finished execution!"); }); - // technically we don't have to bring `AcknowledgementController` back to a valid state - // but we can do it, so why not? Perhaps it might be useful if we wanted to allow - // for restarts of certain modules without killing the entire process. - self.acknowledgement_listener = Some(ack_listener_fut.await.unwrap()); - self.input_message_listener = Some(input_listener_fut.await.unwrap()); - self.retransmission_request_listener = Some(retransmission_req_fut.await.unwrap()); - self.sent_notification_listener = Some(sent_notification_fut.await.unwrap()); - self.action_controller = Some(action_controller_fut.await.unwrap()); + let shutdown_handle = shutdown.clone(); + spawn_future(async move { + input_message_listener + .run_with_shutdown(shutdown_handle) + .await; + debug!("The input listener has finished execution!"); + }); + + let shutdown_handle = shutdown.clone(); + spawn_future(async move { + retransmission_request_listener + .run_with_shutdown(shutdown_handle) + .await; + debug!("The retransmission request listener has finished execution!"); + }); + + let shutdown_handle = shutdown.clone(); + spawn_future(async move { + sent_notification_listener + .run_with_shutdown(shutdown_handle) + .await; + debug!("The sent notification listener has finished execution!"); + }); + + spawn_future(async move { + action_controller.run_with_shutdown(shutdown).await; + debug!("The controller has finished execution!"); + }); } - #[allow(dead_code)] - pub(super) fn start(mut self) -> JoinHandle { - tokio::spawn(async move { - self.run().await; - self - }) + #[cfg(target_arch = "wasm32")] + pub(super) fn start(self) { + let mut acknowledgement_listener = self.acknowledgement_listener; + let mut input_message_listener = self.input_message_listener; + let mut retransmission_request_listener = self.retransmission_request_listener; + let mut sent_notification_listener = self.sent_notification_listener; + let mut action_controller = self.action_controller; + + spawn_future(async move { + acknowledgement_listener.run().await; + error!("The acknowledgement listener has finished execution!"); + }); + spawn_future(async move { + input_message_listener.run().await; + error!("The input listener has finished execution!"); + }); + spawn_future(async move { + retransmission_request_listener.run().await; + error!("The retransmission request listener has finished execution!"); + }); + spawn_future(async move { + sent_notification_listener.run().await; + error!("The sent notification listener has finished execution!"); + }); + spawn_future(async move { + action_controller.run().await; + error!("The controller has finished execution!"); + }); } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index d5d433fa9f..edd3b862bf 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -14,7 +14,6 @@ use nymsphinx::preparer::MessagePreparer; use nymsphinx::{acknowledgements::AckKey, addressing::clients::Recipient}; use rand::{CryptoRng, Rng}; use std::sync::{Arc, Weak}; -use task::ShutdownListener; // responsible for packet retransmission upon fired timer pub(super) struct RetransmissionRequestListener @@ -28,7 +27,6 @@ where real_message_sender: BatchRealMessageSender, request_receiver: RetransmissionRequestReceiver, topology_access: TopologyAccessor, - shutdown: ShutdownListener, } impl RetransmissionRequestListener @@ -44,7 +42,6 @@ where real_message_sender: BatchRealMessageSender, request_receiver: RetransmissionRequestReceiver, topology_access: TopologyAccessor, - shutdown: ShutdownListener, ) -> Self { RetransmissionRequestListener { ack_key, @@ -54,7 +51,6 @@ where real_message_sender, request_receiver, topology_access, - shutdown, } } @@ -124,10 +120,11 @@ where .unwrap(); } - pub(super) async fn run(&mut self) { - debug!("Started RetransmissionRequestListener"); + #[cfg(not(target_arch = "wasm32"))] + pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + debug!("Started RetransmissionRequestListener with graceful shutdown support"); - while !self.shutdown.is_shutdown() { + while !shutdown.is_shutdown() { tokio::select! { timed_out_ack = self.request_receiver.next() => match timed_out_ack { Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack).await, @@ -136,12 +133,21 @@ where break; } }, - _ = self.shutdown.recv() => { + _ = shutdown.recv() => { log::trace!("RetransmissionRequestListener: Received shutdown"); } } } - assert!(self.shutdown.is_shutdown_poll()); + assert!(shutdown.is_shutdown_poll()); log::debug!("RetransmissionRequestListener: Exiting"); } + + #[cfg(target_arch = "wasm32")] + pub(super) async fn run(&mut self) { + debug!("Started RetransmissionRequestListener without graceful shutdown support"); + + while let Some(timed_out_ack) = self.request_receiver.next().await { + self.on_retransmission_request(timed_out_ack).await; + } + } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs index 692a28a6ab..5206a602ce 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/sent_notification_listener.rs @@ -6,7 +6,6 @@ use super::SentPacketNotificationReceiver; use futures::StreamExt; use log::*; use nymsphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}; -use task::ShutdownListener; /// Module responsible for starting up retransmission timers. /// It is required because when we send our packet to the `real traffic stream` controlled @@ -15,19 +14,16 @@ use task::ShutdownListener; pub(super) struct SentNotificationListener { sent_notifier: SentPacketNotificationReceiver, action_sender: ActionSender, - shutdown: ShutdownListener, } impl SentNotificationListener { pub(super) fn new( sent_notifier: SentPacketNotificationReceiver, action_sender: ActionSender, - shutdown: ShutdownListener, ) -> Self { SentNotificationListener { sent_notifier, action_sender, - shutdown, } } @@ -46,9 +42,11 @@ impl SentNotificationListener { .unwrap(); } - pub(super) async fn run(&mut self) { - debug!("Started SentNotificationListener"); - while !self.shutdown.is_shutdown() { + #[cfg(not(target_arch = "wasm32"))] + pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + debug!("Started SentNotificationListener with graceful shutdown support"); + + while !shutdown.is_shutdown() { tokio::select! { frag_id = self.sent_notifier.next() => match frag_id { Some(frag_id) => { @@ -59,12 +57,21 @@ impl SentNotificationListener { break; } }, - _ = self.shutdown.recv() => { + _ = shutdown.recv() => { log::trace!("SentNotificationListener: Received shutdown"); } } } - assert!(self.shutdown.is_shutdown_poll()); + assert!(shutdown.is_shutdown_poll()); log::debug!("SentNotificationListener: Exiting"); } + + #[cfg(target_arch = "wasm32")] + pub(super) async fn run(&mut self) { + debug!("Started SentNotificationListener without graceful shutdown support"); + + while let Some(frag_id) = self.sent_notifier.next().await { + self.on_sent_message(frag_id).await; + } + } } diff --git a/clients/client-core/src/client/real_messages_control/mod.rs b/clients/client-core/src/client/real_messages_control/mod.rs index 9068b51e06..d97e27bdc3 100644 --- a/clients/client-core/src/client/real_messages_control/mod.rs +++ b/clients/client-core/src/client/real_messages_control/mod.rs @@ -9,21 +9,23 @@ use self::{ acknowledgement_control::AcknowledgementController, real_traffic_stream::OutQueueControl, }; use crate::client::real_messages_control::acknowledgement_control::AcknowledgementControllerConnectors; -use crate::client::reply_key_storage::ReplyKeyStorage; use crate::client::{ inbound_messages::InputMessageReceiver, mix_traffic::BatchMixMessageSender, topology_control::TopologyAccessor, }; +use crate::spawn_future; use futures::channel::mpsc; use gateway_client::AcknowledgementReceiver; use log::*; use nymsphinx::acknowledgements::AckKey; use nymsphinx::addressing::clients::Recipient; +use nymsphinx::params::PacketSize; use rand::{rngs::OsRng, CryptoRng, Rng}; use std::sync::Arc; use std::time::Duration; -use task::ShutdownListener; -use tokio::task::JoinHandle; + +#[cfg(feature = "reply-surb")] +use crate::client::reply_key_storage::ReplyKeyStorage; mod acknowledgement_control; mod real_traffic_stream; @@ -50,9 +52,18 @@ pub struct Config { /// Average delay an acknowledgement packet is going to get delayed at a single mixnode. average_ack_delay_duration: Duration, + + /// Controls whether the main packet stream constantly produces packets according to the predefined + /// poisson distribution. + disable_main_poisson_packet_distribution: bool, + + /// Predefined packet size used for the encapsulated messages. + packet_size: PacketSize, } impl Config { + // TODO: change the config into a builder + #[allow(clippy::too_many_arguments)] pub fn new( ack_key: Arc, ack_wait_multiplier: f64, @@ -60,6 +71,7 @@ impl Config { average_ack_delay_duration: Duration, average_message_sending_delay: Duration, average_packet_delay_duration: Duration, + disable_main_poisson_packet_distribution: bool, self_recipient: Recipient, ) -> Self { Config { @@ -70,16 +82,22 @@ impl Config { average_message_sending_delay, average_packet_delay_duration, average_ack_delay_duration, + disable_main_poisson_packet_distribution, + packet_size: Default::default(), } } + + pub fn set_custom_packet_size(&mut self, packet_size: PacketSize) { + self.packet_size = packet_size; + } } pub struct RealMessagesController where R: CryptoRng + Rng, { - out_queue_control: Option>, - ack_control: Option>, + out_queue_control: OutQueueControl, + ack_control: AcknowledgementController, } // obviously when we finally make shared rng that is on 'higher' level, this should become @@ -91,8 +109,7 @@ impl RealMessagesController { input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, topology_access: TopologyAccessor, - reply_key_storage: ReplyKeyStorage, - shutdown: ShutdownListener, + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, ) -> Self { let rng = OsRng; @@ -111,7 +128,8 @@ impl RealMessagesController { config.ack_wait_multiplier, config.average_ack_delay_duration, config.average_packet_delay_duration, - ); + ) + .with_custom_packet_size(config.packet_size); let ack_control = AcknowledgementController::new( ack_control_config, @@ -119,16 +137,18 @@ impl RealMessagesController { topology_access.clone(), Arc::clone(&config.ack_key), config.self_recipient, - reply_key_storage, ack_controller_connectors, - shutdown.clone(), + #[cfg(feature = "reply-surb")] + reply_key_storage, ); let out_queue_config = real_traffic_stream::Config::new( config.average_ack_delay_duration, config.average_packet_delay_duration, config.average_message_sending_delay, - ); + config.disable_main_poisson_packet_distribution, + ) + .with_custom_cover_packet_size(config.packet_size); let out_queue_control = OutQueueControl::new( out_queue_config, @@ -139,44 +159,36 @@ impl RealMessagesController { rng, config.self_recipient, topology_access, - shutdown, ); RealMessagesController { - out_queue_control: Some(out_queue_control), - ack_control: Some(ack_control), + out_queue_control, + ack_control, } } - pub(super) async fn run(&mut self) { - let mut out_queue_control = self.out_queue_control.take().unwrap(); - let mut ack_control = self.ack_control.take().unwrap(); + #[cfg(not(target_arch = "wasm32"))] + pub fn start_with_shutdown(self, shutdown: task::ShutdownListener) { + let mut out_queue_control = self.out_queue_control; + let ack_control = self.ack_control; - // the below are log messages are errors as at the current stage we do not expect any of - // the task to ever finish. This will of course change once we introduce - // graceful shutdowns. - let out_queue_control_fut = tokio::spawn(async move { - out_queue_control.run_out_queue_control().await; + let shutdown_handle = shutdown.clone(); + spawn_future(async move { + out_queue_control.run_with_shutdown(shutdown_handle).await; debug!("The out queue controller has finished execution!"); - out_queue_control }); - let ack_control_fut = tokio::spawn(async move { - ack_control.run().await; - debug!("The acknowledgement controller has finished execution!"); - ack_control - }); - - // technically we don't have to bring `RealMessagesController` back to a valid state - // but we can do it, so why not? Perhaps it might be useful if we wanted to allow - // for restarts of certain modules without killing the entire process. - self.out_queue_control = Some(out_queue_control_fut.await.unwrap()); - self.ack_control = Some(ack_control_fut.await.unwrap()); + ack_control.start_with_shutdown(shutdown); } - pub fn start(mut self) -> JoinHandle { - tokio::spawn(async move { - self.run().await; - self - }) + #[cfg(target_arch = "wasm32")] + pub fn start(self) { + let mut out_queue_control = self.out_queue_control; + let ack_control = self.ack_control; + + spawn_future(async move { + out_queue_control.run().await; + debug!("The out queue controller has finished execution!"); + }); + ack_control.start(); } } diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index 5585044654..ceb93764db 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -13,15 +13,20 @@ use nymsphinx::addressing::clients::Recipient; use nymsphinx::chunking::fragment::FragmentIdentifier; use nymsphinx::cover::generate_loop_cover_packet; use nymsphinx::forwarding::packet::MixPacket; +use nymsphinx::params::PacketSize; use nymsphinx::utils::sample_poisson_duration; use rand::{CryptoRng, Rng}; use std::collections::VecDeque; use std::pin::Pin; use std::sync::Arc; use std::time::Duration; -use task::ShutdownListener; + +#[cfg(not(target_arch = "wasm32"))] use tokio::time; +#[cfg(target_arch = "wasm32")] +use wasm_timer; + /// Configurable parameters of the `OutQueueControl` pub(crate) struct Config { /// Average delay an acknowledgement packet is going to get delay at a single mixnode. @@ -32,6 +37,13 @@ pub(crate) struct Config { /// Average delay between sending subsequent packets. average_message_sending_delay: Duration, + + /// Controls whether the stream constantly produces packets according to the predefined + /// poisson distribution. + disable_poisson_packet_distribution: bool, + + /// Predefined packet size used for the loop cover messages. + cover_packet_size: PacketSize, } impl Config { @@ -39,13 +51,21 @@ impl Config { average_ack_delay: Duration, average_packet_delay: Duration, average_message_sending_delay: Duration, + disable_poisson_packet_distribution: bool, ) -> Self { Config { average_ack_delay, average_packet_delay, average_message_sending_delay, + disable_poisson_packet_distribution, + cover_packet_size: Default::default(), } } + + pub fn with_custom_cover_packet_size(mut self, packet_size: PacketSize) -> Self { + self.cover_packet_size = packet_size; + self + } } pub(crate) struct OutQueueControl @@ -63,7 +83,11 @@ where /// Internal state, determined by `average_message_sending_delay`, /// used to keep track of when a next packet should be sent out. - next_delay: Pin>, + #[cfg(not(target_arch = "wasm32"))] + next_delay: Option>>, + + #[cfg(target_arch = "wasm32")] + next_delay: Option>>, /// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them /// out to the network without any further delays. @@ -84,9 +108,6 @@ where /// Buffer containing all real messages received. It is first exhausted before more are pulled. received_buffer: VecDeque, - - /// Listens for shutdown signals - shutdown: ShutdownListener, } pub(crate) struct RealMessage { @@ -113,55 +134,6 @@ pub(crate) enum StreamMessage { Real(Box), } -impl Stream for OutQueueControl -where - R: CryptoRng + Rng + Unpin, -{ - type Item = StreamMessage; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - // it is not yet time to return a message - if self.next_delay.as_mut().poll(cx).is_pending() { - return Poll::Pending; - }; - - // we know it's time to send a message, so let's prepare delay for the next one - // Get the `now` by looking at the current `delay` deadline - let avg_delay = self.config.average_message_sending_delay; - let now = self.next_delay.deadline(); - let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_delay); - - // The next interval value is `next_poisson_delay` after the one that just - // yielded. - let next = now + next_poisson_delay; - self.next_delay.as_mut().reset(next); - - // check if we have anything immediately available - if let Some(real_available) = self.received_buffer.pop_front() { - return Poll::Ready(Some(StreamMessage::Real(Box::new(real_available)))); - } - - // decide what kind of message to send - match Pin::new(&mut self.real_receiver).poll_next(cx) { - // in the case our real message channel stream was closed, we should also indicate we are closed - // (and whoever is using the stream should panic) - Poll::Ready(None) => Poll::Ready(None), - - // if there are more messages available, return first one and store the rest - Poll::Ready(Some(real_messages)) => { - self.received_buffer = real_messages.into(); - // we MUST HAVE received at least ONE message - Poll::Ready(Some(StreamMessage::Real(Box::new( - self.received_buffer.pop_front().unwrap(), - )))) - } - - // otherwise construct a dummy one - Poll::Pending => Poll::Ready(Some(StreamMessage::Cover)), - } - } -} - impl OutQueueControl where R: CryptoRng + Rng + Unpin, @@ -178,20 +150,18 @@ where rng: R, our_full_destination: Recipient, topology_access: TopologyAccessor, - shutdown: ShutdownListener, ) -> Self { OutQueueControl { config, ack_key, sent_notifier, - next_delay: Box::pin(time::sleep(Default::default())), + next_delay: None, mix_tx, real_receiver, our_full_destination, rng, topology_access, received_buffer: VecDeque::with_capacity(0), // we won't be putting any data into this guy directly - shutdown, } } @@ -228,10 +198,11 @@ where generate_loop_cover_packet( &mut self.rng, topology_ref, - &*self.ack_key, + &self.ack_key, &self.our_full_destination, self.config.average_ack_delay, self.config.average_packet_delay, + self.config.cover_packet_size, ) .expect("Somehow failed to generate a loop cover message with a valid topology") } @@ -246,13 +217,10 @@ where // - the receiver channel is closed // in either case there's no recovery and we can only panic if let Err(err) = self.mix_tx.unbounded_send(vec![next_message]) { - if self.shutdown.is_shutdown_poll() { - log::info!("Failed to send (shutdown detected)"); - } else { - // We don't try to limp along, panic to avoid continuing in a potentially - // inconsistent state - panic!("{err}"); - } + log::warn!( + "Failed to send {} packets (possible process shutdown?)", + err.into_inner().len() + ); } // JS: Not entirely sure why or how it fixes stuff, but without the yield call, @@ -260,18 +228,125 @@ where // JS2: Basically it was the case that with high enough rate, the stream had already a next value // ready and hence was immediately re-scheduled causing other tasks to be starved; // yield makes it go back the scheduling queue regardless of its value availability + + // TODO: temporary and BAD workaround for wasm (we should find a way to yield here in wasm) + #[cfg(not(target_arch = "wasm32"))] tokio::task::yield_now().await; } - // Send messages at certain rate and if no real traffic is available, send cover message. - async fn run_normal_out_queue(&mut self) { - // we should set initial delay only when we actually start the stream - self.next_delay = Box::pin(time::sleep(sample_poisson_duration( - &mut self.rng, - self.config.average_message_sending_delay, - ))); + fn poll_poisson(&mut self, cx: &mut Context<'_>) -> Poll> { + if let Some(ref mut next_delay) = &mut self.next_delay { + // it is not yet time to return a message + if next_delay.as_mut().poll(cx).is_pending() { + return Poll::Pending; + }; + + // we know it's time to send a message, so let's prepare delay for the next one + // Get the `now` by looking at the current `delay` deadline + let avg_delay = self.config.average_message_sending_delay; + let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_delay); + + // The next interval value is `next_poisson_delay` after the one that just + // yielded. + #[cfg(not(target_arch = "wasm32"))] + { + let now = next_delay.deadline(); + let next = now + next_poisson_delay; + next_delay.as_mut().reset(next); + } + + #[cfg(target_arch = "wasm32")] + { + next_delay.as_mut().reset(next_poisson_delay); + } + + // check if we have anything immediately available + if let Some(real_available) = self.received_buffer.pop_front() { + return Poll::Ready(Some(StreamMessage::Real(Box::new(real_available)))); + } + + // decide what kind of message to send + match Pin::new(&mut self.real_receiver).poll_next(cx) { + // in the case our real message channel stream was closed, we should also indicate we are closed + // (and whoever is using the stream should panic) + Poll::Ready(None) => Poll::Ready(None), + + // if there are more messages available, return first one and store the rest + Poll::Ready(Some(real_messages)) => { + self.received_buffer = real_messages.into(); + // we MUST HAVE received at least ONE message + Poll::Ready(Some(StreamMessage::Real(Box::new( + self.received_buffer.pop_front().unwrap(), + )))) + } + + // otherwise construct a dummy one + Poll::Pending => Poll::Ready(Some(StreamMessage::Cover)), + } + } else { + // we never set an initial delay - let's do it now + cx.waker().wake_by_ref(); + + let sampled = + sample_poisson_duration(&mut self.rng, self.config.average_message_sending_delay); + + #[cfg(not(target_arch = "wasm32"))] + let next_delay = Box::pin(time::sleep(sampled)); + + #[cfg(target_arch = "wasm32")] + let next_delay = Box::pin(wasm_timer::Delay::new(sampled)); + + self.next_delay = Some(next_delay); + + Poll::Pending + } + } + + fn poll_immediate(&mut self, cx: &mut Context<'_>) -> Poll> { + // check if we have anything immediately available + if let Some(real_available) = self.received_buffer.pop_front() { + // if there are more messages immediately available, notify the runtime + // because we should be polled again + if !self.received_buffer.is_empty() { + cx.waker().wake_by_ref() + } + return Poll::Ready(Some(StreamMessage::Real(Box::new(real_available)))); + } + + match Pin::new(&mut self.real_receiver).poll_next(cx) { + // in the case our real message channel stream was closed, we should also indicate we are closed + // (and whoever is using the stream should panic) + Poll::Ready(None) => Poll::Ready(None), + + // if there are more messages available, return first one and store the rest + Poll::Ready(Some(real_messages)) => { + self.received_buffer = real_messages.into(); + // we MUST HAVE received at least ONE message + Poll::Ready(Some(StreamMessage::Real(Box::new( + self.received_buffer.pop_front().unwrap(), + )))) + } + + // if there's nothing, then there's nothing + Poll::Pending => Poll::Pending, + } + } + + fn poll_next_message( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + if self.config.disable_poisson_packet_distribution { + self.poll_immediate(cx) + } else { + self.poll_poisson(cx) + } + } + + #[cfg(not(target_arch = "wasm32"))] + pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + debug!("Started OutQueueControl with graceful shutdown support"); - let mut shutdown = self.shutdown.clone(); while !shutdown.is_shutdown() { tokio::select! { biased; @@ -293,8 +368,23 @@ where log::debug!("OutQueueControl: Exiting"); } - pub(crate) async fn run_out_queue_control(&mut self) { - debug!("Starting out queue controller..."); - self.run_normal_out_queue().await + #[cfg(target_arch = "wasm32")] + pub(super) async fn run(&mut self) { + debug!("Started OutQueueControl without graceful shutdown support"); + + while let Some(next_message) = self.next().await { + self.on_message(next_message).await; + } + } +} + +impl Stream for OutQueueControl +where + R: CryptoRng + Rng + Unpin, +{ + type Item = StreamMessage; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.poll_next_message(cx) } } diff --git a/clients/client-core/src/client/received_buffer.rs b/clients/client-core/src/client/received_buffer.rs index 567302121f..9830f0a869 100644 --- a/clients/client-core/src/client/received_buffer.rs +++ b/clients/client-core/src/client/received_buffer.rs @@ -1,24 +1,25 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::reply_key_storage::ReplyKeyStorage; -use crate::client::SHUTDOWN_HAS_BEEN_SIGNALLED; +use crate::spawn_future; use crypto::asymmetric::encryption; -use crypto::symmetric::stream_cipher; -use crypto::Digest; use futures::channel::mpsc; use futures::lock::Mutex; use futures::StreamExt; use gateway_client::MixnetMessageReceiver; use log::*; -use nymsphinx::anonymous_replies::{encryption_key::EncryptionKeyDigest, SurbEncryptionKey}; -use nymsphinx::params::{ReplySurbEncryptionAlgorithm, ReplySurbKeyDigestAlgorithm}; use nymsphinx::receiver::{MessageReceiver, MessageRecoveryError, ReconstructedMessage}; use std::collections::HashSet; -use std::sync::atomic::Ordering; use std::sync::Arc; -use task::ShutdownListener; -use tokio::task::JoinHandle; + +#[cfg(feature = "reply-surb")] +use crate::client::reply_key_storage::ReplyKeyStorage; +#[cfg(feature = "reply-surb")] +use crypto::{symmetric::stream_cipher, Digest}; +#[cfg(feature = "reply-surb")] +use nymsphinx::anonymous_replies::{encryption_key::EncryptionKeyDigest, SurbEncryptionKey}; +#[cfg(feature = "reply-surb")] +use nymsphinx::params::{ReplySurbEncryptionAlgorithm, ReplySurbKeyDigestAlgorithm}; // Buffer Requests to say "hey, send any reconstructed messages to this channel" // or to say "hey, I'm going offline, don't send anything more to me. Just buffer them instead" @@ -116,13 +117,14 @@ struct ReceivedMessagesBuffer { /// Storage containing keys to all [`ReplySURB`]s ever sent out that we did not receive back. // There's no need to put it behind a Mutex since it's already properly concurrent + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, } impl ReceivedMessagesBuffer { fn new( local_encryption_keypair: Arc, - reply_key_storage: ReplyKeyStorage, + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, ) -> Self { ReceivedMessagesBuffer { inner: Arc::new(Mutex::new(ReceivedMessagesBufferInner { @@ -132,6 +134,7 @@ impl ReceivedMessagesBuffer { message_sender: None, recently_reconstructed: HashSet::new(), })), + #[cfg(feature = "reply-surb")] reply_key_storage, } } @@ -180,6 +183,7 @@ impl ReceivedMessagesBuffer { self.inner.lock().await.messages.extend(msgs) } + #[cfg(feature = "reply-surb")] fn process_received_reply( reply_ciphertext: &[u8], reply_key: SurbEncryptionKey, @@ -212,38 +216,50 @@ impl ReceivedMessagesBuffer { let mut completed_messages = Vec::new(); let mut inner_guard = self.inner.lock().await; - let reply_surb_digest_size = ReplySurbKeyDigestAlgorithm::output_size(); - // first check if this is a reply or a chunked message // TODO: verify with @AP if this way of doing it is safe or whether it could // cause some attacks due to, I don't know, stupid edge case collisions? // Update: this DOES introduce a possible leakage: https://github.com/nymtech/nym/issues/296 for msg in msgs { - let possible_key_digest = - EncryptionKeyDigest::clone_from_slice(&msg[..reply_surb_digest_size]); + // TODO: + // 1. make it nicer + // 2. make it not feature-locked - // check first `HasherOutputSize` bytes if they correspond to known encryption key - // if yes - this is a reply message - - // TODO: this might be a bottleneck - since the keys are stored on disk we, presumably, - // are doing a disk operation every single received fragment - if let Some(reply_encryption_key) = self - .reply_key_storage - .get_and_remove_encryption_key(possible_key_digest) - .expect("storage operation failed!") + #[cfg(feature = "reply-surb")] { - if let Some(completed_message) = Self::process_received_reply( - &msg[reply_surb_digest_size..], - reply_encryption_key, - ) { - completed_messages.push(completed_message) - } - } else { - // otherwise - it's a 'normal' message - if let Some(completed_message) = inner_guard.process_received_fragment(msg) { - completed_messages.push(completed_message) + let reply_surb_digest_size = ReplySurbKeyDigestAlgorithm::output_size(); + + let possible_key_digest = + EncryptionKeyDigest::clone_from_slice(&msg[..reply_surb_digest_size]); + + // check first `HasherOutputSize` bytes if they correspond to known encryption key + // if yes - this is a reply message + + // TODO: this might be a bottleneck - since the keys are stored on disk we, presumably, + // are doing a disk operation every single received fragment + if let Some(reply_encryption_key) = self + .reply_key_storage + .get_and_remove_encryption_key(possible_key_digest) + .expect("storage operation failed!") + { + if let Some(completed_message) = Self::process_received_reply( + &msg[reply_surb_digest_size..], + reply_encryption_key, + ) { + completed_messages.push(completed_message) + } + } else { + // otherwise - it's a 'normal' message + if let Some(completed_message) = inner_guard.process_received_fragment(msg) { + completed_messages.push(completed_message) + } } } + + #[cfg(not(feature = "reply-surb"))] + if let Some(completed_message) = inner_guard.process_received_fragment(msg) { + completed_messages.push(completed_message) + } } if !completed_messages.is_empty() { @@ -293,72 +309,97 @@ impl RequestReceiver { } } - fn start(mut self) -> JoinHandle<()> { - tokio::spawn(async move { - loop { - tokio::select! { - request = self.query_receiver.next() => { - match request { - Some(ReceivedBufferMessage::ReceiverAnnounce(sender)) => { - self.received_buffer.connect_sender(sender).await; - } - Some(ReceivedBufferMessage::ReceiverDisconnect) => { - self.received_buffer.disconnect_sender().await - } - None => { - log::trace!("RequestReceiver: Stopping since channel closed"); - break; - }, - } - }, - }; + async fn handle_message(&mut self, message: ReceivedBufferMessage) { + match message { + ReceivedBufferMessage::ReceiverAnnounce(sender) => { + self.received_buffer.connect_sender(sender).await; } + ReceivedBufferMessage::ReceiverDisconnect => { + self.received_buffer.disconnect_sender().await + } + } + } - assert!(SHUTDOWN_HAS_BEEN_SIGNALLED.load(Ordering::Relaxed)); - log::debug!("RequestReceiver: Exiting"); - }) + #[cfg(not(target_arch = "wasm32"))] + async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + debug!("Started RequestReceiver with graceful shutdown support"); + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + log::trace!("RequestReceiver: Received shutdown"); + } + request = self.query_receiver.next() => { + match request { + Some(message) => self.handle_message(message).await, + None => { + log::trace!("RequestReceiver: Stopping since channel closed"); + break; + }, + } + }, + } + } + assert!(shutdown.is_shutdown_poll()); + log::debug!("RequestReceiver: Exiting"); + } + + #[cfg(target_arch = "wasm32")] + async fn run(&mut self) { + debug!("Started RequestReceiver without graceful shutdown support"); + + while let Some(message) = self.query_receiver.next().await { + self.handle_message(message).await + } } } struct FragmentedMessageReceiver { received_buffer: ReceivedMessagesBuffer, mixnet_packet_receiver: MixnetMessageReceiver, - shutdown: ShutdownListener, } impl FragmentedMessageReceiver { fn new( received_buffer: ReceivedMessagesBuffer, mixnet_packet_receiver: MixnetMessageReceiver, - shutdown: ShutdownListener, ) -> Self { FragmentedMessageReceiver { received_buffer, mixnet_packet_receiver, - shutdown, } } - fn start(mut self) -> JoinHandle<()> { - tokio::spawn(async move { - while !self.shutdown.is_shutdown() { - tokio::select! { - new_messages = self.mixnet_packet_receiver.next() => match new_messages { - Some(new_messages) => { - self.received_buffer.handle_new_received(new_messages).await; - } - None => { - log::trace!("FragmentedMessageReceiver: Stopping since channel closed"); - break; - } - }, - _ = self.shutdown.recv() => { - log::trace!("FragmentedMessageReceiver: Received shutdown"); + + #[cfg(not(target_arch = "wasm32"))] + async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + debug!("Started FragmentedMessageReceiver with graceful shutdown support"); + while !shutdown.is_shutdown() { + tokio::select! { + new_messages = self.mixnet_packet_receiver.next() => match new_messages { + Some(new_messages) => { + self.received_buffer.handle_new_received(new_messages).await; } + None => { + log::trace!("FragmentedMessageReceiver: Stopping since channel closed"); + break; + } + }, + _ = shutdown.recv() => { + log::trace!("FragmentedMessageReceiver: Received shutdown"); } } - assert!(self.shutdown.is_shutdown_poll()); - log::debug!("FragmentedMessageReceiver: Exiting"); - }) + } + assert!(shutdown.is_shutdown_poll()); + log::debug!("FragmentedMessageReceiver: Exiting"); + } + + #[cfg(target_arch = "wasm32")] + async fn run(&mut self) { + debug!("Started FragmentedMessageReceiver without graceful shutdown support"); + + while let Some(new_messages) = self.mixnet_packet_receiver.next().await { + self.received_buffer.handle_new_received(new_messages).await; + } } } @@ -372,25 +413,48 @@ impl ReceivedMessagesBufferController { local_encryption_keypair: Arc, query_receiver: ReceivedBufferRequestReceiver, mixnet_packet_receiver: MixnetMessageReceiver, - reply_key_storage: ReplyKeyStorage, - shutdown: ShutdownListener, + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, ) -> Self { - let received_buffer = - ReceivedMessagesBuffer::new(local_encryption_keypair, reply_key_storage); + let received_buffer = ReceivedMessagesBuffer::new( + local_encryption_keypair, + #[cfg(feature = "reply-surb")] + reply_key_storage, + ); ReceivedMessagesBufferController { fragmented_message_receiver: FragmentedMessageReceiver::new( received_buffer.clone(), mixnet_packet_receiver, - shutdown, ), request_receiver: RequestReceiver::new(received_buffer, query_receiver), } } + #[cfg(not(target_arch = "wasm32"))] + pub fn start_with_shutdown(self, shutdown: task::ShutdownListener) { + let mut fragmented_message_receiver = self.fragmented_message_receiver; + let mut request_receiver = self.request_receiver; + + let shutdown_handle = shutdown.clone(); + spawn_future(async move { + fragmented_message_receiver + .run_with_shutdown(shutdown_handle) + .await; + }); + spawn_future(async move { + request_receiver.run_with_shutdown(shutdown).await; + }); + } + + #[cfg(target_arch = "wasm32")] pub fn start(self) { - // TODO: should we do anything with JoinHandle(s) returned by start methods? - self.fragmented_message_receiver.start(); - self.request_receiver.start(); + let mut fragmented_message_receiver = self.fragmented_message_receiver; + let mut request_receiver = self.request_receiver; + spawn_future(async move { + fragmented_message_receiver.run().await; + }); + spawn_future(async move { + request_receiver.run().await; + }); } } diff --git a/clients/client-core/src/client/topology_control.rs b/clients/client-core/src/client/topology_control.rs index c276fdd49d..aa266d6022 100644 --- a/clients/client-core/src/client/topology_control.rs +++ b/clients/client-core/src/client/topology_control.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::spawn_future; use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::params::DEFAULT_NUM_MIX_HOPS; @@ -10,9 +11,7 @@ use std::ops::Deref; use std::sync::Arc; use std::time; use std::time::Duration; -use task::ShutdownListener; use tokio::sync::{RwLock, RwLockReadGuard}; -use tokio::task::JoinHandle; use topology::{nym_topology_from_detailed, NymTopology}; use url::Url; @@ -304,8 +303,11 @@ impl TopologyRefresher { self.topology_accessor.is_routable().await } - pub fn start(mut self, mut shutdown: ShutdownListener) -> JoinHandle<()> { - tokio::spawn(async move { + #[cfg(not(target_arch = "wasm32"))] + pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) { + spawn_future(async move { + debug!("Started TopologyRefresher with graceful shutdown support"); + while !shutdown.is_shutdown() { tokio::select! { _ = tokio::time::sleep(self.refresh_rate) => { @@ -320,4 +322,17 @@ impl TopologyRefresher { log::debug!("TopologyRefresher: Exiting"); }) } + + #[cfg(target_arch = "wasm32")] + pub fn start(mut self) { + use futures::StreamExt; + + spawn_future(async move { + let mut interval = + gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32); + while let Some(_) = interval.next().await { + self.refresh().await; + } + }) + } } diff --git a/clients/client-core/src/config/mod.rs b/clients/client-core/src/config/mod.rs index 0e545cf41a..be5bce902b 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -8,6 +8,9 @@ use std::path::PathBuf; use std::time::Duration; use url::Url; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::prelude::*; + pub mod persistence; pub const MISSING_VALUE: &str = "MISSING VALUE"; @@ -236,6 +239,18 @@ impl Config { self.debug.topology_resolution_timeout } + pub fn get_disabled_loop_cover_traffic_stream(&self) -> bool { + self.debug.disable_loop_cover_traffic_stream + } + + pub fn get_disabled_main_poisson_packet_distribution(&self) -> bool { + self.debug.disable_main_poisson_packet_distribution + } + + pub fn get_use_extended_packet_size(&self) -> bool { + self.debug.use_extended_packet_size + } + pub fn get_version(&self) -> &str { &self.client.version } @@ -252,6 +267,7 @@ impl Default for Config { } #[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter_with_clone))] pub struct GatewayEndpoint { /// gateway_id specifies ID of the gateway to which the client should send messages. /// If initially omitted, a random gateway will be chosen from the available topology. @@ -398,53 +414,64 @@ pub struct Debug { /// So for a packet going through three mix nodes, on average, it will take three times this value /// until the packet reaches its destination. #[serde(with = "humantime_serde")] - average_packet_delay: Duration, + pub average_packet_delay: Duration, /// The parameter of Poisson distribution determining how long, on average, /// sent acknowledgement is going to be delayed at any given mix node. /// So for an ack going through three mix nodes, on average, it will take three times this value /// until the packet reaches its destination. #[serde(with = "humantime_serde")] - average_ack_delay: Duration, + pub average_ack_delay: Duration, /// Value multiplied with the expected round trip time of an acknowledgement packet before /// it is assumed it was lost and retransmission of the data packet happens. /// In an ideal network with 0 latency, this value would have been 1. - ack_wait_multiplier: f64, + pub ack_wait_multiplier: f64, /// Value added to the expected round trip time of an acknowledgement packet before /// it is assumed it was lost and retransmission of the data packet happens. /// In an ideal network with 0 latency, this value would have been 0. #[serde(with = "humantime_serde")] - ack_wait_addition: Duration, + pub ack_wait_addition: Duration, /// The parameter of Poisson distribution determining how long, on average, /// it is going to take for another loop cover traffic message to be sent. #[serde(with = "humantime_serde")] - loop_cover_traffic_average_delay: Duration, + pub loop_cover_traffic_average_delay: Duration, /// The parameter of Poisson distribution determining how long, on average, /// it is going to take another 'real traffic stream' message to be sent. /// If no real packets are available and cover traffic is enabled, /// a loop cover message is sent instead in order to preserve the rate. #[serde(with = "humantime_serde")] - message_sending_average_delay: Duration, + pub message_sending_average_delay: Duration, /// How long we're willing to wait for a response to a message sent to the gateway, /// before giving up on it. #[serde(with = "humantime_serde")] - gateway_response_timeout: Duration, + pub gateway_response_timeout: Duration, /// The uniform delay every which clients are querying the directory server /// to try to obtain a compatible network topology to send sphinx packets through. #[serde(with = "humantime_serde")] - topology_refresh_rate: Duration, + pub topology_refresh_rate: Duration, /// During topology refresh, test packets are sent through every single possible network /// path. This timeout determines waiting period until it is decided that the packet /// did not reach its destination. #[serde(with = "humantime_serde")] - topology_resolution_timeout: Duration, + pub topology_resolution_timeout: Duration, + + /// Controls whether the dedicated loop cover traffic stream should be enabled. + /// (and sending packets, on average, every [Self::loop_cover_traffic_average_delay]) + pub disable_loop_cover_traffic_stream: bool, + + /// Controls whether the main packet stream constantly produces packets according to the predefined + /// poisson distribution. + pub disable_main_poisson_packet_distribution: bool, + + /// Controls whether the sent sphinx packet use the NON-DEFAULT bigger size. + pub use_extended_packet_size: bool, } impl Default for Debug { @@ -459,6 +486,9 @@ impl Default for Debug { gateway_response_timeout: DEFAULT_GATEWAY_RESPONSE_TIMEOUT, topology_refresh_rate: DEFAULT_TOPOLOGY_REFRESH_RATE, topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT, + disable_loop_cover_traffic_stream: false, + disable_main_poisson_packet_distribution: false, + use_extended_packet_size: false, } } } diff --git a/clients/client-core/src/init.rs b/clients/client-core/src/init.rs index a2e3ed22f7..98cc42df64 100644 --- a/clients/client-core/src/init.rs +++ b/clients/client-core/src/init.rs @@ -90,6 +90,7 @@ async fn register_with_gateway( gateway.owner.clone(), our_identity.clone(), timeout, + #[cfg(not(target_arch = "wasm32"))] None, ); gateway_client diff --git a/clients/client-core/src/lib.rs b/clients/client-core/src/lib.rs index e36cea9276..04e215daec 100644 --- a/clients/client-core/src/lib.rs +++ b/clients/client-core/src/lib.rs @@ -1,4 +1,23 @@ +use std::future::Future; + pub mod client; pub mod config; pub mod error; pub mod init; + +#[cfg(target_arch = "wasm32")] +pub(crate) fn spawn_future(future: F) +where + F: Future + 'static, +{ + wasm_bindgen_futures::spawn_local(future); +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn spawn_future(future: F) +where + F: Future + Send + 'static, + F::Output: Send + 'static, +{ + tokio::spawn(future); +} diff --git a/clients/credential/Cargo.toml b/clients/credential/Cargo.toml index 74311917e6..81fe361ad0 100644 --- a/clients/credential/Cargo.toml +++ b/clients/credential/Cargo.toml @@ -9,16 +9,17 @@ edition = "2021" async-trait = "0.1.52" bip39 = "1.0.1" cfg-if = "0.1" -clap = { version = "3.0.10", features = ["cargo", "derive"] } +clap = { version = "3.2", features = ["cargo", "derive"] } pickledb = "0.4.1" rand = "0.7.3" serde = { version = "1.0", features = ["derive"] } thiserror = "1.0" url = "2.2" -tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal", "macros"] } # async runtime +tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal", "macros"] } # async runtime coconut-interface = { path = "../../common/coconut-interface" } config = { path = "../../common/config" } +completions = { path = "../../common/completions" } credentials = { path = "../../common/credentials" } credential-storage = { path = "../../common/credential-storage" } crypto = { path = "../../common/crypto", features = ["rand", "asymmetric", "symmetric", "aes", "hashing"] } diff --git a/clients/credential/src/commands.rs b/clients/credential/src/commands.rs index c586b4a3e8..c281332810 100644 --- a/clients/credential/src/commands.rs +++ b/clients/credential/src/commands.rs @@ -3,6 +3,7 @@ use async_trait::async_trait; use clap::{Args, Subcommand}; +use completions::ArgShell; use pickledb::PickleDb; use rand::rngs::OsRng; use std::str::FromStr; @@ -28,6 +29,12 @@ pub(crate) enum Commands { ListDeposits(ListDeposits), /// Get a credential for a given deposit GetCredential(GetCredential), + + /// Generate shell completions + Completions(ArgShell), + + /// Generate Fig specification + GenerateFigSpec, } #[async_trait] diff --git a/clients/credential/src/main.rs b/clients/credential/src/main.rs index 88e20f3da2..a3d286f09f 100644 --- a/clients/credential/src/main.rs +++ b/clients/credential/src/main.rs @@ -12,6 +12,8 @@ cfg_if::cfg_if! { use commands::{Commands, Execute}; use error::Result; use network_defaults::setup_env; + use clap::CommandFactory; + use completions::fig_generate; use clap::Parser; use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod}; @@ -52,10 +54,14 @@ cfg_if::cfg_if! { ), }; + let bin_name = "nym-credential-client"; + match &args.command { Commands::Deposit(m) => m.execute(&mut db, shared_storage).await?, Commands::ListDeposits(m) => m.execute(&mut db, shared_storage).await?, Commands::GetCredential(m) => m.execute(&mut db, shared_storage).await?, + Commands::Completions(s) => s.generate(&mut crate::Cli::into_app(), bin_name), + Commands::GenerateFigSpec => fig_generate(&mut crate::Cli::into_app(), bin_name) } Ok(()) diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 0904d7ee01..f585de4e24 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -20,20 +20,21 @@ futures = "0.3" # bunch of futures stuff, however, now that I think about it, it # and the single instance of abortable we have should really be refactored anyway url = "2.2" -clap = { version = "3.2.8", features = ["cargo", "derive"] } +clap = { version = "3.2", features = ["cargo", "derive"] } dirs = "4.0" log = "0.4" # self explanatory pretty_env_logger = "0.4" # for formatting log messages rand = { version = "0.7.3", features = ["wasm-bindgen"] } # rng-related traits + some rng implementation to use serde = { version = "1.0.104", features = ["derive"] } # for config serialization/deserialization sled = "0.34" # for storage of replySURB decryption keys -tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal"] } # async runtime +tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] } # async runtime tokio-tungstenite = "0.14" # websocket ## internal client-core = { path = "../client-core" } coconut-interface = { path = "../../common/coconut-interface", optional = true } config = { path = "../../common/config" } +completions = { path = "../../common/completions" } credential-storage = { path = "../../common/credential-storage" } credentials = { path = "../../common/credentials", optional = true } crypto = { path = "../../common/crypto" } diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 43de8f2d0e..38f3d083a0 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -31,6 +31,7 @@ use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; use nymsphinx::anonymous_replies::ReplySurb; +use nymsphinx::params::PacketSize; use nymsphinx::receiver::ReconstructedMessage; use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; @@ -90,7 +91,7 @@ impl NymClient { ) { info!("Starting loop cover traffic stream..."); - LoopCoverTrafficStream::new( + let mut stream = LoopCoverTrafficStream::new( self.key_manager.ack_key(), self.config.get_base().get_average_ack_delay(), self.config.get_base().get_average_packet_delay(), @@ -100,9 +101,13 @@ impl NymClient { mix_tx, self.as_mix_recipient(), topology_accessor, - shutdown, - ) - .start(); + ); + + if self.config.get_base().get_use_extended_packet_size() { + stream.set_custom_packet_size(PacketSize::ExtendedPacket) + } + + stream.start_with_shutdown(shutdown); } fn start_real_traffic_controller( @@ -114,16 +119,23 @@ impl NymClient { mix_sender: BatchMixMessageSender, shutdown: ShutdownListener, ) { - let controller_config = real_messages_control::Config::new( + let mut controller_config = real_messages_control::Config::new( self.key_manager.ack_key(), self.config.get_base().get_ack_wait_multiplier(), self.config.get_base().get_ack_wait_addition(), self.config.get_base().get_average_ack_delay(), self.config.get_base().get_message_sending_average_delay(), self.config.get_base().get_average_packet_delay(), + self.config + .get_base() + .get_disabled_main_poisson_packet_distribution(), self.as_mix_recipient(), ); + if self.config.get_base().get_use_extended_packet_size() { + controller_config.set_custom_packet_size(PacketSize::ExtendedPacket) + } + info!("Starting real traffic stream..."); RealMessagesController::new( @@ -133,9 +145,8 @@ impl NymClient { mix_sender, topology_accessor, reply_key_storage, - shutdown, ) - .start(); + .start_with_shutdown(shutdown); } // buffer controlling all messages fetched from provider @@ -153,9 +164,8 @@ impl NymClient { query_receiver, mixnet_receiver, reply_key_storage, - shutdown, ) - .start() + .start_with_shutdown(shutdown) } async fn start_gateway_client( @@ -245,7 +255,7 @@ impl NymClient { } info!("Starting topology refresher..."); - topology_refresher.start(shutdown); + topology_refresher.start_with_shutdown(shutdown); } // controller for sending sphinx packets to mixnet (either real traffic or cover traffic) @@ -259,7 +269,7 @@ impl NymClient { shutdown: ShutdownListener, ) { info!("Starting mix traffic controller..."); - MixTrafficController::new(mix_rx, gateway_client, shutdown).start(); + MixTrafficController::new(mix_rx, gateway_client).start_with_shutdown(shutdown); } fn start_websocket_listener( @@ -401,11 +411,17 @@ impl NymClient { shutdown.subscribe(), ); - self.start_cover_traffic_stream( - shared_topology_accessor, - sphinx_message_sender, - shutdown.subscribe(), - ); + if !self + .config + .get_base() + .get_disabled_loop_cover_traffic_stream() + { + self.start_cover_traffic_stream( + shared_topology_accessor, + sphinx_message_sender, + shutdown.subscribe(), + ); + } match self.config.get_socket_type() { SocketType::WebSocket => { diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index 9f95b7945c..fdbf7ff5bf 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::client::config::{Config, SocketType}; +use clap::CommandFactory; use clap::{Parser, Subcommand}; +use completions::{fig_generate, ArgShell}; pub(crate) mod init; pub(crate) mod run; @@ -62,6 +64,12 @@ pub(crate) enum Commands { Run(run::Run), /// Try to upgrade the client Upgrade(upgrade::Upgrade), + + /// Generate shell completions + Completions(ArgShell), + + /// Generate Fig specification + GenerateFigSpec, } // Configuration that can be overridden. @@ -76,10 +84,14 @@ pub(crate) struct OverrideConfig { } pub(crate) async fn execute(args: &Cli) { + let bin_name = "nym-native-client"; + match &args.command { Commands::Init(m) => init::execute(m).await, Commands::Run(m) => run::execute(m).await, Commands::Upgrade(m) => upgrade::execute(m), + Commands::Completions(s) => s.generate(&mut Cli::into_app(), bin_name), + Commands::GenerateFigSpec => fig_generate(&mut Cli::into_app(), bin_name), } } diff --git a/clients/native/src/commands/upgrade.rs b/clients/native/src/commands/upgrade.rs index c925b10f08..c84073200c 100644 --- a/clients/native/src/commands/upgrade.rs +++ b/clients/native/src/commands/upgrade.rs @@ -103,7 +103,7 @@ fn minor_0_12_upgrade( Version::new(0, 12, 0) }; - print_start_upgrade(&config_version, &to_version); + print_start_upgrade(config_version, &to_version); config .get_base_mut() @@ -111,7 +111,7 @@ fn minor_0_12_upgrade( config.save_to_file(None).unwrap_or_else(|err| { eprintln!("failed to overwrite config file! - {:?}", err); - print_failed_upgrade(&config_version, &to_version); + print_failed_upgrade(config_version, &to_version); process::exit(1); }); diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index e9091ea4e8..c5729951c4 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -11,7 +11,7 @@ name = "nym_socks5" path = "src/lib.rs" [dependencies] -clap = { version = "3.2.8", features = ["cargo", "derive"] } +clap = { version = "3.2", features = ["cargo", "derive"] } dirs = "4.0" futures = "0.3" log = "0.4" @@ -20,13 +20,14 @@ pretty_env_logger = "0.4" rand = { version = "0.7.3", features = ["wasm-bindgen"] } serde = { version = "1.0", features = ["derive"] } # for config serialization/deserialization snafu = "0.6" -tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal"] } +tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] } url = "2.2" # internal client-core = { path = "../client-core" } coconut-interface = { path = "../../common/coconut-interface", optional = true } config = { path = "../../common/config" } +completions = { path = "../../common/completions" } credential-storage = { path = "../../common/credential-storage" } credentials = { path = "../../common/credentials", optional = true } crypto = { path = "../../common/crypto" } diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index f90191a087..2e45d59281 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -3,6 +3,11 @@ use std::sync::atomic::Ordering; +use crate::client::config::Config; +use crate::socks::{ + authentication::{AuthenticationMethods, Authenticator, User}, + server::SphinxSocksServer, +}; use client_core::client::cover_traffic_stream::LoopCoverTrafficStream; use client_core::client::inbound_messages::{ InputMessage, InputMessageReceiver, InputMessageSender, @@ -31,14 +36,9 @@ use gateway_client::{ use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; +use nymsphinx::params::PacketSize; use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; -use crate::client::config::Config; -use crate::socks::{ - authentication::{AuthenticationMethods, Authenticator, User}, - server::SphinxSocksServer, -}; - pub mod config; // Channels used to control the main task from outside @@ -91,7 +91,7 @@ impl NymClient { ) { info!("Starting loop cover traffic stream..."); - LoopCoverTrafficStream::new( + let mut stream = LoopCoverTrafficStream::new( self.key_manager.ack_key(), self.config.get_base().get_average_ack_delay(), self.config.get_base().get_average_packet_delay(), @@ -101,9 +101,13 @@ impl NymClient { mix_tx, self.as_mix_recipient(), topology_accessor, - shutdown, - ) - .start(); + ); + + if self.config.get_base().get_use_extended_packet_size() { + stream.set_custom_packet_size(PacketSize::ExtendedPacket) + } + + stream.start_with_shutdown(shutdown); } fn start_real_traffic_controller( @@ -115,16 +119,23 @@ impl NymClient { mix_sender: BatchMixMessageSender, shutdown: ShutdownListener, ) { - let controller_config = client_core::client::real_messages_control::Config::new( + let mut controller_config = client_core::client::real_messages_control::Config::new( self.key_manager.ack_key(), self.config.get_base().get_ack_wait_multiplier(), self.config.get_base().get_ack_wait_addition(), self.config.get_base().get_average_ack_delay(), self.config.get_base().get_message_sending_average_delay(), self.config.get_base().get_average_packet_delay(), + self.config + .get_base() + .get_disabled_main_poisson_packet_distribution(), self.as_mix_recipient(), ); + if self.config.get_base().get_use_extended_packet_size() { + controller_config.set_custom_packet_size(PacketSize::ExtendedPacket) + } + info!("Starting real traffic stream..."); RealMessagesController::new( @@ -134,9 +145,8 @@ impl NymClient { mix_sender, topology_accessor, reply_key_storage, - shutdown, ) - .start(); + .start_with_shutdown(shutdown); } // buffer controlling all messages fetched from provider @@ -154,9 +164,8 @@ impl NymClient { query_receiver, mixnet_receiver, reply_key_storage, - shutdown, ) - .start() + .start_with_shutdown(shutdown); } async fn start_gateway_client( @@ -246,7 +255,7 @@ impl NymClient { } info!("Starting topology refresher..."); - topology_refresher.start(shutdown); + topology_refresher.start_with_shutdown(shutdown); } // controller for sending sphinx packets to mixnet (either real traffic or cover traffic) @@ -260,7 +269,7 @@ impl NymClient { shutdown: ShutdownListener, ) { info!("Starting mix traffic controller..."); - MixTrafficController::new(mix_rx, gateway_client, shutdown).start(); + MixTrafficController::new(mix_rx, gateway_client).start_with_shutdown(shutdown); } fn start_socks5_listener( @@ -391,11 +400,18 @@ impl NymClient { shutdown.subscribe(), ); - self.start_cover_traffic_stream( - shared_topology_accessor, - sphinx_message_sender, - shutdown.subscribe(), - ); + if !self + .config + .get_base() + .get_disabled_loop_cover_traffic_stream() + { + self.start_cover_traffic_stream( + shared_topology_accessor, + sphinx_message_sender, + shutdown.subscribe(), + ); + } + self.start_socks5_listener( received_buffer_request_sender, input_sender, diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index 651f88bab2..dbed90777f 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::client::config::Config; +use clap::CommandFactory; use clap::{Parser, Subcommand}; +use completions::{fig_generate, ArgShell}; use config::parse_validators; pub mod init; @@ -63,6 +65,12 @@ pub(crate) enum Commands { Run(run::Run), /// Try to upgrade the client Upgrade(upgrade::Upgrade), + + /// Generate shell completions + Completions(ArgShell), + + /// Generate Fig specification + GenerateFigSpec, } // Configuration that can be overridden. @@ -76,10 +84,14 @@ pub(crate) struct OverrideConfig { } pub(crate) async fn execute(args: &Cli) { + let bin_name = "nym-socks5-client"; + match &args.command { Commands::Init(m) => init::execute(m).await, Commands::Run(m) => run::execute(m).await, Commands::Upgrade(m) => upgrade::execute(m), + Commands::Completions(s) => s.generate(&mut Cli::into_app(), bin_name), + Commands::GenerateFigSpec => fig_generate(&mut Cli::into_app(), bin_name), } } diff --git a/clients/socks5/src/commands/upgrade.rs b/clients/socks5/src/commands/upgrade.rs index 5e9517ac2e..ff41f3ce6e 100644 --- a/clients/socks5/src/commands/upgrade.rs +++ b/clients/socks5/src/commands/upgrade.rs @@ -102,7 +102,7 @@ fn minor_0_12_upgrade( Version::new(0, 12, 0) }; - print_start_upgrade(&config_version, &to_version); + print_start_upgrade(config_version, &to_version); config .get_base_mut() @@ -110,7 +110,7 @@ fn minor_0_12_upgrade( config.save_to_file(None).unwrap_or_else(|err| { eprintln!("failed to overwrite config file! - {:?}", err); - print_failed_upgrade(&config_version, &to_version); + print_failed_upgrade(config_version, &to_version); process::exit(1); }); diff --git a/clients/webassembly/Cargo.toml b/clients/webassembly/Cargo.toml index 2970b8cdf6..6f457bd2b9 100644 --- a/clients/webassembly/Cargo.toml +++ b/clients/webassembly/Cargo.toml @@ -20,13 +20,15 @@ coconut = ["coconut-interface", "credentials", "gateway-client/coconut"] [dependencies] futures = "0.3" serde = { version = "1.0", features = ["derive"] } -wasm-bindgen = { version = "=0.2.78", features = ["serde-serialize"] } +serde-wasm-bindgen = "0.4" +wasm-bindgen = { version = "=0.2.83", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4" js-sys = "0.3" rand = { version = "0.7.3", features = ["wasm-bindgen"] } url = "2.2" # internal +client-core = { path = "../client-core", default-features = false, features = ["wasm"] } coconut-interface = { path = "../../common/coconut-interface", optional = true } credentials = { path = "../../common/credentials", optional = true } crypto = { path = "../../common/crypto" } diff --git a/clients/webassembly/js-example/README.md b/clients/webassembly/js-example/README.md index 6af15a4618..f694466032 100644 --- a/clients/webassembly/js-example/README.md +++ b/clients/webassembly/js-example/README.md @@ -4,6 +4,16 @@ This example application demonstrates how to use WebAssembly to create Sphinx pa ## 🚴 Usage +Build the WASM package for bundling: + +``` +wasm-pack build --scope nymproject --target no-modules +``` + +in the `clients/webassembly` directory (one up). + +Start the webpack dev server: + ``` npm install # set up dependencies npm run start # starts a web server at http://localhost:8001 @@ -15,4 +25,4 @@ Check your dev console for output. Install `wasm-pack`. Instruction are at the [Rust WASM tutorial](https://rustwasm.github.io/docs/book/game-of-life/hello-world.html). -`wasm-pack build` in the `clients/webassembly` directory (one up) will rebuild the wasm package if you make changes to the Rust source. That will be automatically picked up (and reloaded, if need be) by the npm dev server. \ No newline at end of file +`wasm-pack build --scope nymproject --target no-modules` in the `clients/webassembly` directory (one up) will rebuild the wasm package if you make changes to the Rust source. That will be automatically picked up (and reloaded, if need be) by the npm dev server. \ No newline at end of file diff --git a/clients/webassembly/js-example/bootstrap.js b/clients/webassembly/js-example/bootstrap.js index 7934d627e8..f1e71036b6 100644 --- a/clients/webassembly/js-example/bootstrap.js +++ b/clients/webassembly/js-example/bootstrap.js @@ -1,5 +1,5 @@ // A dependency graph that contains any wasm must all be imported // asynchronously. This `bootstrap.js` file does the single async import, so // that no one else needs to worry about it again. -import("./index.js") - .catch(e => console.error("Error importing `index.js`:", e)); +import('./index.js') + .catch(e => console.error('Error importing `index.js`:', e)); diff --git a/clients/webassembly/js-example/index.js b/clients/webassembly/js-example/index.js index 22a42a09b3..4a28435219 100644 --- a/clients/webassembly/js-example/index.js +++ b/clients/webassembly/js-example/index.js @@ -1,4 +1,4 @@ -// Copyright 2020 Nym Technologies SA +// Copyright 2020-2022 Nym Technologies SA // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,55 +12,67 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { - NymClient, - set_panic_hook -} from "@nymproject/nym-client-wasm" +class WebWorkerClient { + worker = null; -// current limitation of rust-wasm for async stuff : ( -let client = null + constructor() { + this.worker = new Worker('./worker.js'); + + this.worker.onmessage = (ev) => { + if (ev.data && ev.data.kind) { + switch (ev.data.kind) { + case 'Ready': + const { selfAddress } = ev.data.args; + displaySenderAddress(selfAddress); + break; + case 'ReceiveMessage': + const { message } = ev.data.args; + displayReceived(message); + break; + } + } + }; + } + + sendMessage = (message, recipient) => { + if (!this.worker) { + console.error('Could not send message because worker does not exist'); + return; + } + + this.worker.postMessage({ + kind: 'SendMessage', + args: { + message, recipient, + }, + }); + }; +} + +let client = null; async function main() { - // sets up better stack traces in case of in-rust panics - set_panic_hook(); + client = new WebWorkerClient(); - // validator server we will use to get topology from - const validator = "https://validator.nymtech.net/api"; - - client = new NymClient(validator); - - const on_message = (msg) => displayReceived(msg); - const on_connect = () => console.log("Established (and authenticated) gateway connection!"); - - client.set_on_message(on_message); - client.set_on_gateway_connect(on_connect); - - // this is current limitation of wasm in rust - for async methods you can't take self my reference... - // I'm trying to figure out if I can somehow hack my way around it, but for time being you have to re-assign - // the object (it's the same one) - client = await client.initial_setup(); - - const self_address = client.self_address(); - displaySenderAddress(self_address); - - const sendButton = document.querySelector('#send-button'); - sendButton.onclick = function () { - sendMessageTo(); - } + const sendButton = document.querySelector('#send-button'); + sendButton.onclick = function() { + sendMessageTo(); + }; } /** * Create a Sphinx packet and send it to the mixnet through the gateway node. - * + * * Message and recipient are taken from the values in the user interface. * * @param {Client} nymClient the nym client to use for message sending */ async function sendMessageTo() { - const message = document.getElementById("message").value; - const recipient = document.getElementById("recipient").value; - client = await client.send_message(message, recipient); - displaySend(message); + const message = document.getElementById('message').value; + const recipient = document.getElementById('recipient').value; + + await client.sendMessage(message, recipient); + displaySend(message); } /** @@ -69,16 +81,16 @@ async function sendMessageTo() { * @param {string} message */ function displaySend(message) { - let timestamp = new Date().toISOString().slice(11, 21); + let timestamp = new Date().toISOString().substr(11, 12); - let sendDiv = document.createElement("div") - let paragraph = document.createElement("p") - paragraph.setAttribute('style', 'color: blue') - let paragraphContent = document.createTextNode(timestamp + " sent >>> " + message) - paragraph.appendChild(paragraphContent) + let sendDiv = document.createElement('div'); + let paragraph = document.createElement('p'); + paragraph.setAttribute('style', 'color: blue'); + let paragraphContent = document.createTextNode(timestamp + ' sent >>> ' + message); + paragraph.appendChild(paragraphContent); - sendDiv.appendChild(paragraph) - document.getElementById("output").appendChild(sendDiv) + sendDiv.appendChild(paragraph); + document.getElementById('output').appendChild(sendDiv); } /** @@ -87,17 +99,17 @@ function displaySend(message) { * @param {string} message */ function displayReceived(message) { - const content = message.message - const replySurb = message.replySurb + const content = message; - let timestamp = new Date().toISOString().slice(11, 21); - let receivedDiv = document.createElement("div") - let paragraph = document.createElement("p") - paragraph.setAttribute('style', 'color: green') - let paragraphContent = document.createTextNode(timestamp + " received >>> " + content) - paragraph.appendChild(paragraphContent) - receivedDiv.appendChild(paragraph) - document.getElementById("output").appendChild(receivedDiv) + let timestamp = new Date().toISOString().substr(11, 12); + let receivedDiv = document.createElement('div'); + let paragraph = document.createElement('p'); + paragraph.setAttribute('style', 'color: green'); + let paragraphContent = document.createTextNode(timestamp + ' received >>> ' + content); + // let paragraphContent = document.createTextNode(timestamp + " received >>> " + content + ((replySurb != null) ? "Reply SURB was attached here (but we can't do anything with it yet" : " (NO REPLY-SURB AVAILABLE)")) + paragraph.appendChild(paragraphContent); + receivedDiv.appendChild(paragraph); + document.getElementById('output').appendChild(receivedDiv); } @@ -107,7 +119,7 @@ function displayReceived(message) { * @param {Client} nymClient */ function displaySenderAddress(address) { - document.getElementById("sender").value = address; + document.getElementById('sender').value = address; } // Let's get started! diff --git a/clients/webassembly/js-example/package-lock.json b/clients/webassembly/js-example/package-lock.json index fe566e6f19..06359b2f03 100644 --- a/clients/webassembly/js-example/package-lock.json +++ b/clients/webassembly/js-example/package-lock.json @@ -24,7 +24,7 @@ }, "../pkg": { "name": "@nymproject/nym-client-wasm", - "version": "1.0.1", + "version": "1.0.0", "license": "Apache-2.0" }, "node_modules/@discoveryjs/json-ext": { @@ -32,73 +32,17 @@ "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", - "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -112,6 +56,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -121,6 +66,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -138,6 +84,7 @@ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", "dev": true, + "license": "MIT", "dependencies": { "@types/connect": "*", "@types/node": "*" @@ -148,6 +95,7 @@ "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -157,6 +105,7 @@ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -166,6 +115,7 @@ "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", "dev": true, + "license": "MIT", "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" @@ -173,9 +123,8 @@ }, "node_modules/@types/eslint": { "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", - "integrity": "sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -183,9 +132,8 @@ }, "node_modules/@types/eslint-scope": { "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", "dev": true, + "license": "MIT", "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -195,13 +143,13 @@ "version": "0.0.51", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/express": { "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", "dev": true, + "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.18", @@ -211,9 +159,8 @@ }, "node_modules/@types/express-serve-static-core": { "version": "4.17.28", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", - "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -222,63 +169,60 @@ }, "node_modules/@types/http-proxy": { "version": "1.17.8", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz", - "integrity": "sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/json-schema": { "version": "7.0.10", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz", - "integrity": "sha512-BLO9bBq59vW3fxCpD4o0N4U+DXsvwvIcl+jofw0frQo/GrBFC+/jRZj1E7kgp6dvTyNmA4y6JCV5Id/r3mNP5A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/mime": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { "version": "17.0.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/retry": { "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", - "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", "dev": true, + "license": "MIT", "dependencies": { "@types/express": "*" } }, "node_modules/@types/serve-static": { "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -289,15 +233,15 @@ "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@types/ws": { "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*" } @@ -307,6 +251,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/helper-numbers": "1.11.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.1" @@ -316,25 +261,29 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.11.1", "@webassemblyjs/helper-api-error": "1.11.1", @@ -345,13 +294,15 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-buffer": "1.11.1", @@ -364,6 +315,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", "dev": true, + "license": "MIT", "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -373,6 +325,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@xtuc/long": "4.2.2" } @@ -381,13 +334,15 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-buffer": "1.11.1", @@ -404,6 +359,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.1", @@ -417,6 +373,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-buffer": "1.11.1", @@ -429,6 +386,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.1", "@webassemblyjs/helper-api-error": "1.11.1", @@ -443,6 +401,7 @@ "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", "dev": true, + "license": "MIT", "dependencies": { "@webassemblyjs/ast": "1.11.1", "@xtuc/long": "4.2.2" @@ -450,9 +409,8 @@ }, "node_modules/@webpack-cli/configtest": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.1.tgz", - "integrity": "sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==", "dev": true, + "license": "MIT", "peerDependencies": { "webpack": "4.x.x || 5.x.x", "webpack-cli": "4.x.x" @@ -460,9 +418,8 @@ }, "node_modules/@webpack-cli/info": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.1.tgz", - "integrity": "sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA==", "dev": true, + "license": "MIT", "dependencies": { "envinfo": "^7.7.3" }, @@ -472,9 +429,8 @@ }, "node_modules/@webpack-cli/serve": { "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.1.tgz", - "integrity": "sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==", "dev": true, + "license": "MIT", "peerDependencies": { "webpack-cli": "4.x.x" }, @@ -488,19 +444,20 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/accepts": { "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", "dev": true, + "license": "MIT", "dependencies": { "mime-types": "~2.1.24", "negotiator": "0.6.2" @@ -511,9 +468,8 @@ }, "node_modules/acorn": { "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -526,15 +482,15 @@ "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^8" } }, "node_modules/aggregate-error": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, + "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -548,6 +504,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -564,6 +521,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -581,6 +539,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -596,15 +555,15 @@ "engines": [ "node >= 0.8.0" ], + "license": "Apache-2.0", "bin": { "ansi-html": "bin/ansi-html" } }, "node_modules/ansi-regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -617,6 +576,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -627,15 +587,15 @@ }, "node_modules/array-flatten": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/array-union": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -645,9 +605,8 @@ }, "node_modules/async": { "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, + "license": "MIT", "dependencies": { "lodash": "^4.17.14" } @@ -656,28 +615,30 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", - "dev": true + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true, + "license": "MIT" }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/body-parser": { "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", "dev": true, + "license": "MIT", "dependencies": { "bytes": "3.1.0", "content-type": "~1.0.4", @@ -696,18 +657,16 @@ }, "node_modules/body-parser/node_modules/bytes": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/bonjour": { "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", - "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", "dev": true, + "license": "MIT", "dependencies": { "array-flatten": "^2.1.0", "deep-equal": "^1.0.1", @@ -722,6 +681,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -732,6 +692,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.0.1" }, @@ -741,8 +702,6 @@ }, "node_modules/browserslist": { "version": "4.20.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", - "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", "dev": true, "funding": [ { @@ -754,6 +713,7 @@ "url": "https://tidelift.com/funding/github/npm/browserslist" } ], + "license": "MIT", "dependencies": { "caniuse-lite": "^1.0.30001317", "electron-to-chromium": "^1.4.84", @@ -772,19 +732,20 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/buffer-indexof": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -794,6 +755,7 @@ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.1", "get-intrinsic": "^1.0.2" @@ -804,8 +766,6 @@ }, "node_modules/caniuse-lite": { "version": "1.0.30001320", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001320.tgz", - "integrity": "sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA==", "dev": true, "funding": [ { @@ -816,7 +776,8 @@ "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/caniuse-lite" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chokidar": { "version": "3.5.3", @@ -829,6 +790,7 @@ "url": "https://paulmillr.com/funding/" } ], + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -850,6 +812,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -862,15 +825,15 @@ "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0" } }, "node_modules/clean-stack": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -880,6 +843,7 @@ "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, + "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", @@ -891,21 +855,22 @@ }, "node_modules/colorette": { "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/compressible": { "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, @@ -918,6 +883,7 @@ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.5", "bytes": "3.0.0", @@ -934,23 +900,22 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" }, "node_modules/connect-history-api-fallback": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8" } }, "node_modules/content-disposition": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "5.1.2" }, @@ -963,15 +928,15 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -979,14 +944,16 @@ "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" }, "node_modules/copy-webpack-plugin": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz", "integrity": "sha512-xFVltahqlsRcyyJqQbDY6EYTtyQZF9rf+JPjwHObLdPFMEISqkFkr7mFoVOC6BfYS/dNThyoQKvziugm+OnwBg==", "dev": true, + "license": "MIT", "dependencies": { "fast-glob": "^3.2.7", "glob-parent": "^6.0.1", @@ -1008,15 +975,15 @@ }, "node_modules/core-util-is": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1031,15 +998,15 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, "node_modules/deep-equal": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", "dev": true, + "license": "MIT", "dependencies": { "is-arguments": "^1.0.4", "is-date-object": "^1.0.1", @@ -1057,6 +1024,7 @@ "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "execa": "^5.0.0" }, @@ -1069,15 +1037,15 @@ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/define-properties": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, + "license": "MIT", "dependencies": { "object-keys": "^1.0.12" }, @@ -1087,9 +1055,8 @@ }, "node_modules/del": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", - "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", "dev": true, + "license": "MIT", "dependencies": { "globby": "^11.0.1", "graceful-fs": "^4.2.4", @@ -1109,18 +1076,16 @@ }, "node_modules/del/node_modules/array-union": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/del/node_modules/globby": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -1138,9 +1103,8 @@ }, "node_modules/del/node_modules/slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1148,29 +1112,31 @@ "node_modules/depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/destroy": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -1181,14 +1147,14 @@ "node_modules/dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", - "dev": true + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "dev": true, + "license": "MIT" }, "node_modules/dns-packet": { "version": "1.3.4", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", - "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", "dev": true, + "license": "MIT", "dependencies": { "ip": "^1.1.0", "safe-buffer": "^5.0.1" @@ -1196,9 +1162,8 @@ }, "node_modules/dns-txt": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", - "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", "dev": true, + "license": "MIT", "dependencies": { "buffer-indexof": "^1.0.0" } @@ -1206,29 +1171,29 @@ "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.4.92", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.92.tgz", - "integrity": "sha512-YAVbvQIcDE/IJ/vzDMjD484/hsRbFPW2qXJPaYTfOhtligmfYEYOep+5QojpaEU9kq6bMvNeC2aG7arYvTHYsA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/enhanced-resolve": { "version": "5.9.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", - "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -1242,6 +1207,7 @@ "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "dev": true, + "license": "MIT", "bin": { "envinfo": "dist/cli.js" }, @@ -1253,13 +1219,15 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1267,14 +1235,16 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -1288,6 +1258,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -1300,6 +1271,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -1309,6 +1281,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -1316,8 +1289,9 @@ "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1326,13 +1300,15 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.x" } @@ -1342,6 +1318,7 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -1362,9 +1339,8 @@ }, "node_modules/express": { "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.7", "array-flatten": "1.1.1", @@ -1404,20 +1380,21 @@ "node_modules/express/node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-glob": { "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -1434,6 +1411,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -1445,19 +1423,20 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fastest-levenshtein": { "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -1467,6 +1446,7 @@ "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", "dev": true, + "license": "Apache-2.0", "dependencies": { "websocket-driver": ">=0.5.1" }, @@ -1479,6 +1459,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1488,9 +1469,8 @@ }, "node_modules/finalhandler": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -1509,6 +1489,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -1519,8 +1500,6 @@ }, "node_modules/follow-redirects": { "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", "dev": true, "funding": [ { @@ -1528,6 +1507,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -1542,6 +1522,7 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1549,30 +1530,31 @@ "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/fs-monkey": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", - "dev": true + "dev": true, + "license": "Unlicense" }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, - "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1585,13 +1567,13 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/get-intrinsic": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -1606,6 +1588,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1615,9 +1598,8 @@ }, "node_modules/glob": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1638,6 +1620,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -1649,13 +1632,15 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/globby": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz", "integrity": "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^3.0.1", "dir-glob": "^3.0.1", @@ -1673,21 +1658,22 @@ }, "node_modules/graceful-fs": { "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.1" }, @@ -1700,15 +1686,15 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/has-symbols": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1725,8 +1711,9 @@ "node_modules/hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.1", "obuf": "^1.0.0", @@ -1736,21 +1723,20 @@ }, "node_modules/html-entities": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz", - "integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", - "dev": true + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "license": "MIT" }, "node_modules/http-errors": { "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", "dev": true, + "license": "MIT", "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -1765,20 +1751,21 @@ "node_modules/http-errors/node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" }, "node_modules/http-parser-js": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", - "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, + "license": "MIT", "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", @@ -1790,9 +1777,8 @@ }, "node_modules/http-proxy-middleware": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz", - "integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==", "dev": true, + "license": "MIT", "dependencies": { "@types/http-proxy": "^1.17.8", "http-proxy": "^1.18.1", @@ -1817,6 +1803,7 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } @@ -1826,6 +1813,7 @@ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -1838,6 +1826,7 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -1847,6 +1836,7 @@ "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", "dev": true, + "license": "MIT", "dependencies": { "pkg-dir": "^4.2.0", "resolve-cwd": "^3.0.0" @@ -1863,9 +1853,8 @@ }, "node_modules/indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1873,8 +1862,9 @@ "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1884,37 +1874,38 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/interpret": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/ip": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/is-arguments": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", - "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.0" }, @@ -1930,6 +1921,7 @@ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -1939,9 +1931,8 @@ }, "node_modules/is-core-module": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", "dev": true, + "license": "MIT", "dependencies": { "has": "^1.0.3" }, @@ -1951,9 +1942,8 @@ }, "node_modules/is-date-object": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1966,6 +1956,7 @@ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", "dev": true, + "license": "MIT", "bin": { "is-docker": "cli.js" }, @@ -1979,8 +1970,9 @@ "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1990,6 +1982,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2002,24 +1995,23 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/is-path-cwd": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/is-path-inside": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2029,6 +2021,7 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2041,6 +2034,7 @@ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, + "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, @@ -2050,9 +2044,8 @@ }, "node_modules/is-regex": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "has-symbols": "^1.0.2" @@ -2069,6 +2062,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -2081,6 +2075,7 @@ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", "dev": true, + "license": "MIT", "dependencies": { "is-docker": "^2.0.0" }, @@ -2091,20 +2086,23 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2114,6 +2112,7 @@ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", "dev": true, + "license": "MIT", "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -2125,30 +2124,30 @@ }, "node_modules/json-parse-better-errors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/loader-runner": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.11.5" } @@ -2158,6 +2157,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -2167,24 +2167,23 @@ }, "node_modules/lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/memfs": { "version": "3.4.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", - "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", "dev": true, + "license": "Unlicense", "dependencies": { "fs-monkey": "1.0.3" }, @@ -2195,20 +2194,23 @@ "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true, + "license": "MIT" }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -2216,17 +2218,17 @@ "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/micromatch": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.1", "picomatch": "^2.2.3" @@ -2240,6 +2242,7 @@ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -2252,6 +2255,7 @@ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2261,6 +2265,7 @@ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -2273,6 +2278,7 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2281,13 +2287,13 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2297,15 +2303,13 @@ }, "node_modules/minimist": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mkdirp": { "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.5" }, @@ -2316,14 +2320,14 @@ "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" }, "node_modules/multicast-dns": { "version": "6.2.3", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", "dev": true, + "license": "MIT", "dependencies": { "dns-packet": "^1.3.1", "thunky": "^1.0.2" @@ -2334,15 +2338,13 @@ }, "node_modules/multicast-dns-service-types": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/negotiator": { "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2351,28 +2353,28 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/node-forge": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", - "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==", "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { "node": ">= 6.13.0" } }, "node_modules/node-releases": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2382,6 +2384,7 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -2391,9 +2394,8 @@ }, "node_modules/object-is": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -2407,9 +2409,8 @@ }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2418,13 +2419,13 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/on-finished": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", "dev": true, + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -2437,6 +2438,7 @@ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -2444,8 +2446,9 @@ "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -2455,6 +2458,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -2470,6 +2474,7 @@ "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", "dev": true, + "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -2487,6 +2492,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -2502,6 +2508,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -2511,9 +2518,8 @@ }, "node_modules/p-map": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, + "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" }, @@ -2526,9 +2532,8 @@ }, "node_modules/p-retry": { "version": "4.6.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz", - "integrity": "sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==", "dev": true, + "license": "MIT", "dependencies": { "@types/retry": "^0.12.0", "retry": "^0.13.1" @@ -2542,6 +2547,7 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2551,6 +2557,7 @@ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -2560,6 +2567,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2567,8 +2575,9 @@ "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2578,6 +2587,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2586,19 +2596,22 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true, + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2607,13 +2620,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -2626,6 +2641,7 @@ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, + "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -2635,9 +2651,8 @@ }, "node_modules/portfinder": { "version": "1.0.28", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", - "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", "dev": true, + "license": "MIT", "dependencies": { "async": "^2.6.2", "debug": "^3.1.1", @@ -2649,30 +2664,28 @@ }, "node_modules/portfinder/node_modules/debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.1" } }, "node_modules/portfinder/node_modules/ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/proxy-addr": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -2686,15 +2699,15 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/qs": { "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.6" } @@ -2717,13 +2730,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -2733,15 +2748,15 @@ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/raw-body": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", "dev": true, + "license": "MIT", "dependencies": { "bytes": "3.1.0", "http-errors": "1.7.2", @@ -2754,9 +2769,8 @@ }, "node_modules/raw-body/node_modules/bytes": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -2766,6 +2780,7 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2781,6 +2796,7 @@ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -2793,6 +2809,7 @@ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", "dev": true, + "license": "MIT", "dependencies": { "resolve": "^1.9.0" }, @@ -2802,9 +2819,8 @@ }, "node_modules/regexp.prototype.flags": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", - "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.3" @@ -2821,6 +2837,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2828,14 +2845,14 @@ "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" }, "node_modules/resolve": { "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.8.1", "path-parse": "^1.0.7", @@ -2853,6 +2870,7 @@ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, + "license": "MIT", "dependencies": { "resolve-from": "^5.0.0" }, @@ -2865,6 +2883,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2874,6 +2893,7 @@ "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -2883,6 +2903,7 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -2893,6 +2914,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -2922,6 +2944,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -2930,19 +2953,22 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/schema-utils": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", "dev": true, + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.8.0", @@ -2960,14 +2986,14 @@ "node_modules/select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", - "dev": true + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "license": "MIT" }, "node_modules/selfsigned": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.0.tgz", - "integrity": "sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ==", "dev": true, + "license": "MIT", "dependencies": { "node-forge": "^1.2.0" }, @@ -2977,9 +3003,8 @@ }, "node_modules/send": { "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "2.6.9", "depd": "~1.1.2", @@ -3001,15 +3026,15 @@ }, "node_modules/send/node_modules/ms": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } @@ -3017,8 +3042,9 @@ "node_modules/serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", "dev": true, + "license": "MIT", "dependencies": { "accepts": "~1.3.4", "batch": "0.6.1", @@ -3035,8 +3061,9 @@ "node_modules/serve-index/node_modules/http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "dev": true, + "license": "MIT", "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -3050,20 +3077,21 @@ "node_modules/serve-index/node_modules/inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" }, "node_modules/serve-index/node_modules/setprototypeof": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/serve-static": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", "dev": true, + "license": "MIT", "dependencies": { "encodeurl": "~1.0.2", "escape-html": "~1.0.3", @@ -3076,15 +3104,15 @@ }, "node_modules/setprototypeof": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, + "license": "MIT", "dependencies": { "kind-of": "^6.0.2" }, @@ -3097,6 +3125,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -3109,6 +3138,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3117,13 +3147,15 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -3133,9 +3165,8 @@ }, "node_modules/sockjs": { "version": "0.3.21", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz", - "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==", "dev": true, + "license": "MIT", "dependencies": { "faye-websocket": "^0.11.3", "uuid": "^3.4.0", @@ -3147,6 +3178,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -3156,6 +3188,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -3166,6 +3199,7 @@ "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.0", "handle-thing": "^2.0.0", @@ -3182,6 +3216,7 @@ "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.0", "detect-node": "^2.0.4", @@ -3193,9 +3228,8 @@ }, "node_modules/spdy-transport/node_modules/debug": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.1.2" }, @@ -3212,13 +3246,15 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/spdy-transport/node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -3230,9 +3266,8 @@ }, "node_modules/spdy/node_modules/debug": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.1.2" }, @@ -3249,13 +3284,15 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -3265,15 +3302,15 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } }, "node_modules/strip-ansi": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -3289,6 +3326,7 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3298,6 +3336,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3313,6 +3352,7 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3325,19 +3365,19 @@ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/terser": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", - "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", + "version": "5.12.1", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", + "source-map": "~0.7.2", "source-map-support": "~0.5.20" }, "bin": { @@ -3349,9 +3389,8 @@ }, "node_modules/terser-webpack-plugin": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", "dev": true, + "license": "MIT", "dependencies": { "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", @@ -3386,6 +3425,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3402,6 +3442,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, + "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } @@ -3410,13 +3451,15 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/terser-webpack-plugin/node_modules/schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -3430,17 +3473,27 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/terser/node_modules/source-map": { + "version": "0.7.3", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -3450,9 +3503,8 @@ }, "node_modules/toidentifier": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6" } @@ -3462,6 +3514,7 @@ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", "dev": true, + "license": "MIT", "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -3473,8 +3526,9 @@ "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -3484,6 +3538,7 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -3491,24 +3546,24 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4.0" } }, "node_modules/uuid": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", "dev": true, + "license": "MIT", "bin": { "uuid": "bin/uuid" } @@ -3516,17 +3571,17 @@ "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8" } }, "node_modules/watchpack": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", - "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", "dev": true, + "license": "MIT", "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -3540,15 +3595,15 @@ "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", "dev": true, + "license": "MIT", "dependencies": { "minimalistic-assert": "^1.0.0" } }, "node_modules/webpack": { "version": "5.70.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz", - "integrity": "sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==", "dev": true, + "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^0.0.51", @@ -3593,9 +3648,8 @@ }, "node_modules/webpack-cli": { "version": "4.9.2", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.2.tgz", - "integrity": "sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==", "dev": true, + "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.1.1", @@ -3639,15 +3693,15 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10" } }, "node_modules/webpack-dev-middleware": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz", - "integrity": "sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==", "dev": true, + "license": "MIT", "dependencies": { "colorette": "^2.0.10", "memfs": "^3.4.1", @@ -3668,9 +3722,8 @@ }, "node_modules/webpack-dev-server": { "version": "4.7.4", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz", - "integrity": "sha512-nfdsb02Zi2qzkNmgtZjkrMOcXnYZ6FLKcQwpxT7MvmHKc+oTtDsBju8j+NMyAygZ9GW1jMEUpy3itHtqgEhe1A==", "dev": true, + "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -3723,6 +3776,7 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10" } @@ -3732,6 +3786,7 @@ "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", "dev": true, + "license": "MIT", "dependencies": { "clone-deep": "^4.0.1", "wildcard": "^2.0.0" @@ -3745,6 +3800,7 @@ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.13.0" } @@ -3754,6 +3810,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -3770,6 +3827,7 @@ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, + "license": "MIT", "peerDependencies": { "ajv": "^6.9.1" } @@ -3778,13 +3836,15 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/webpack/node_modules/schema-utils": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", "dev": true, + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -3803,6 +3863,7 @@ "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", @@ -3817,6 +3878,7 @@ "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=0.8.0" } @@ -3826,6 +3888,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -3840,19 +3903,20 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" }, "node_modules/ws": { "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -3877,55 +3941,6 @@ "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true }, - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, - "@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz", - "integrity": "sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -3995,8 +4010,6 @@ }, "@types/eslint": { "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", - "integrity": "sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==", "dev": true, "requires": { "@types/estree": "*", @@ -4005,8 +4018,6 @@ }, "@types/eslint-scope": { "version": "3.7.3", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", - "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", "dev": true, "requires": { "@types/eslint": "*", @@ -4021,8 +4032,6 @@ }, "@types/express": { "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", "dev": true, "requires": { "@types/body-parser": "*", @@ -4033,8 +4042,6 @@ }, "@types/express-serve-static-core": { "version": "4.17.28", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", - "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", "dev": true, "requires": { "@types/node": "*", @@ -4044,8 +4051,6 @@ }, "@types/http-proxy": { "version": "1.17.8", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz", - "integrity": "sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==", "dev": true, "requires": { "@types/node": "*" @@ -4053,20 +4058,14 @@ }, "@types/json-schema": { "version": "7.0.10", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.10.tgz", - "integrity": "sha512-BLO9bBq59vW3fxCpD4o0N4U+DXsvwvIcl+jofw0frQo/GrBFC+/jRZj1E7kgp6dvTyNmA4y6JCV5Id/r3mNP5A==", "dev": true }, "@types/mime": { "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", "dev": true }, "@types/node": { "version": "17.0.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", "dev": true }, "@types/qs": { @@ -4083,8 +4082,6 @@ }, "@types/retry": { "version": "0.12.1", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", - "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", "dev": true }, "@types/serve-index": { @@ -4098,8 +4095,6 @@ }, "@types/serve-static": { "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", "dev": true, "requires": { "@types/mime": "^1", @@ -4117,8 +4112,6 @@ }, "@types/ws": { "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", "dev": true, "requires": { "@types/node": "*" @@ -4272,15 +4265,11 @@ }, "@webpack-cli/configtest": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.1.tgz", - "integrity": "sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==", "dev": true, "requires": {} }, "@webpack-cli/info": { "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.1.tgz", - "integrity": "sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA==", "dev": true, "requires": { "envinfo": "^7.7.3" @@ -4288,8 +4277,6 @@ }, "@webpack-cli/serve": { "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.6.1.tgz", - "integrity": "sha512-gNGTiTrjEVQ0OcVnzsRSqTxaBSr+dmTfm+qJsCDluky8uhdLWep7Gcr62QsAKHTMxjCS/8nEITsmFAhfIx+QSw==", "dev": true, "requires": {} }, @@ -4307,8 +4294,6 @@ }, "accepts": { "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", "dev": true, "requires": { "mime-types": "~2.1.24", @@ -4317,8 +4302,6 @@ }, "acorn": { "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", "dev": true }, "acorn-import-assertions": { @@ -4330,8 +4313,6 @@ }, "aggregate-error": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, "requires": { "clean-stack": "^2.0.0", @@ -4376,8 +4357,6 @@ }, "ansi-regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true }, "anymatch": { @@ -4392,8 +4371,6 @@ }, "array-flatten": { "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", "dev": true }, "array-union": { @@ -4404,8 +4381,6 @@ }, "async": { "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, "requires": { "lodash": "^4.17.14" @@ -4420,7 +4395,7 @@ "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", "dev": true }, "binary-extensions": { @@ -4431,8 +4406,6 @@ }, "body-parser": { "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", "dev": true, "requires": { "bytes": "3.1.0", @@ -4449,16 +4422,12 @@ "dependencies": { "bytes": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "dev": true } } }, "bonjour": { "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", - "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", "dev": true, "requires": { "array-flatten": "^2.1.0", @@ -4490,8 +4459,6 @@ }, "browserslist": { "version": "4.20.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", - "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", "dev": true, "requires": { "caniuse-lite": "^1.0.30001317", @@ -4509,14 +4476,12 @@ }, "buffer-indexof": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", "dev": true }, "bytes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "dev": true }, "call-bind": { @@ -4531,8 +4496,6 @@ }, "caniuse-lite": { "version": "1.0.30001320", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001320.tgz", - "integrity": "sha512-MWPzG54AGdo3nWx7zHZTefseM5Y1ccM7hlQKHRqJkPozUaw3hNbBTMmLn16GG2FUzjR13Cr3NPfhIieX5PzXDA==", "dev": true }, "chokidar": { @@ -4570,8 +4533,6 @@ }, "clean-stack": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true }, "clone-deep": { @@ -4587,8 +4548,6 @@ }, "colorette": { "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", "dev": true }, "commander": { @@ -4624,19 +4583,15 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, "connect-history-api-fallback": { "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", "dev": true }, "content-disposition": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", "dev": true, "requires": { "safe-buffer": "5.1.2" @@ -4650,14 +4605,12 @@ }, "cookie": { "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", "dev": true }, "cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, "copy-webpack-plugin": { @@ -4676,8 +4629,6 @@ }, "core-util-is": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, "cross-spawn": { @@ -4702,8 +4653,6 @@ }, "deep-equal": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", "dev": true, "requires": { "is-arguments": "^1.0.4", @@ -4731,8 +4680,6 @@ }, "define-properties": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "dev": true, "requires": { "object-keys": "^1.0.12" @@ -4740,8 +4687,6 @@ }, "del": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", - "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", "dev": true, "requires": { "globby": "^11.0.1", @@ -4756,14 +4701,10 @@ "dependencies": { "array-union": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, "globby": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "requires": { "array-union": "^2.1.0", @@ -4776,8 +4717,6 @@ }, "slash": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true } } @@ -4785,13 +4724,11 @@ "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", "dev": true }, "destroy": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", "dev": true }, "detect-node": { @@ -4812,13 +4749,11 @@ "dns-equal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", "dev": true }, "dns-packet": { "version": "1.3.4", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", - "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", "dev": true, "requires": { "ip": "^1.1.0", @@ -4827,8 +4762,6 @@ }, "dns-txt": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", - "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", "dev": true, "requires": { "buffer-indexof": "^1.0.0" @@ -4837,25 +4770,21 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, "electron-to-chromium": { "version": "1.4.92", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.92.tgz", - "integrity": "sha512-YAVbvQIcDE/IJ/vzDMjD484/hsRbFPW2qXJPaYTfOhtligmfYEYOep+5QojpaEU9kq6bMvNeC2aG7arYvTHYsA==", "dev": true }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true }, "enhanced-resolve": { "version": "5.9.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", - "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -4883,7 +4812,7 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "dev": true }, "eslint-scope": { @@ -4922,7 +4851,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "dev": true }, "eventemitter3": { @@ -4956,8 +4885,6 @@ }, "express": { "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", "dev": true, "requires": { "accepts": "~1.3.7", @@ -4995,7 +4922,7 @@ "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true } } @@ -5008,8 +4935,6 @@ }, "fast-glob": { "version": "3.2.11", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", - "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", "dev": true, "requires": { "@nodelib/fs.stat": "^2.0.2", @@ -5038,8 +4963,6 @@ }, "fastest-levenshtein": { "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", "dev": true }, "fastq": { @@ -5071,8 +4994,6 @@ }, "finalhandler": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "dev": true, "requires": { "debug": "2.6.9", @@ -5096,8 +5017,6 @@ }, "follow-redirects": { "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", "dev": true }, "forwarded": { @@ -5109,19 +5028,17 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", "dev": true }, "fs-monkey": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", - "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", "dev": true }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, "fsevents": { @@ -5139,8 +5056,6 @@ }, "get-intrinsic": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "dev": true, "requires": { "function-bind": "^1.1.1", @@ -5156,8 +5071,6 @@ }, "glob": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -5199,8 +5112,6 @@ }, "graceful-fs": { "version": "4.2.9", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", "dev": true }, "handle-thing": { @@ -5226,8 +5137,6 @@ }, "has-symbols": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", "dev": true }, "hello-wasm-pack": { @@ -5239,7 +5148,7 @@ "hpack.js": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", "dev": true, "requires": { "inherits": "^2.0.1", @@ -5250,20 +5159,16 @@ }, "html-entities": { "version": "2.3.2", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz", - "integrity": "sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ==", "dev": true }, "http-deceiver": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", "dev": true }, "http-errors": { "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", "dev": true, "requires": { "depd": "~1.1.2", @@ -5276,15 +5181,13 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "dev": true } } }, "http-parser-js": { "version": "0.5.3", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", - "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", "dev": true }, "http-proxy": { @@ -5300,8 +5203,6 @@ }, "http-proxy-middleware": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz", - "integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==", "dev": true, "requires": { "@types/http-proxy": "^1.17.8", @@ -5344,14 +5245,12 @@ }, "indent-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, "requires": { "once": "^1.3.0", @@ -5372,8 +5271,6 @@ }, "ip": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", "dev": true }, "ipaddr.js": { @@ -5384,8 +5281,6 @@ }, "is-arguments": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", - "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", "dev": true, "requires": { "call-bind": "^1.0.0" @@ -5402,8 +5297,6 @@ }, "is-core-module": { "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", "dev": true, "requires": { "has": "^1.0.3" @@ -5411,8 +5304,6 @@ }, "is-date-object": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", "dev": true }, "is-docker": { @@ -5424,7 +5315,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true }, "is-glob": { @@ -5444,14 +5335,10 @@ }, "is-path-cwd": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", "dev": true }, "is-path-inside": { "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, "is-plain-obj": { @@ -5471,8 +5358,6 @@ }, "is-regex": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -5497,19 +5382,19 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, "isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true }, "jest-worker": { @@ -5525,8 +5410,6 @@ }, "json-parse-better-errors": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, "json-schema-traverse": { @@ -5543,8 +5426,6 @@ }, "loader-runner": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", "dev": true }, "locate-path": { @@ -5558,20 +5439,16 @@ }, "lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", "dev": true }, "memfs": { "version": "3.4.1", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", - "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", "dev": true, "requires": { "fs-monkey": "1.0.3" @@ -5580,7 +5457,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", "dev": true }, "merge-stream": { @@ -5598,13 +5475,11 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", "dev": true }, "micromatch": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", "dev": true, "requires": { "braces": "^3.0.1", @@ -5646,8 +5521,6 @@ }, "minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "requires": { "brace-expansion": "^1.1.7" @@ -5655,14 +5528,10 @@ }, "minimist": { "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "mkdirp": { "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, "requires": { "minimist": "^1.2.5" @@ -5671,13 +5540,11 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, "multicast-dns": { "version": "6.2.3", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", "dev": true, "requires": { "dns-packet": "^1.3.1", @@ -5686,14 +5553,10 @@ }, "multicast-dns-service-types": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", "dev": true }, "negotiator": { "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", "dev": true }, "neo-async": { @@ -5704,14 +5567,10 @@ }, "node-forge": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", - "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==", "dev": true }, "node-releases": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", - "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", "dev": true }, "normalize-path": { @@ -5731,8 +5590,6 @@ }, "object-is": { "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -5741,8 +5598,6 @@ }, "object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true }, "obuf": { @@ -5753,8 +5608,6 @@ }, "on-finished": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", "dev": true, "requires": { "ee-first": "1.1.1" @@ -5769,7 +5622,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "requires": { "wrappy": "1" @@ -5815,8 +5668,6 @@ }, "p-map": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, "requires": { "aggregate-error": "^3.0.0" @@ -5824,8 +5675,6 @@ }, "p-retry": { "version": "4.6.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz", - "integrity": "sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==", "dev": true, "requires": { "@types/retry": "^0.12.0", @@ -5853,7 +5702,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true }, "path-key": { @@ -5871,7 +5720,7 @@ "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", "dev": true }, "path-type": { @@ -5903,8 +5752,6 @@ }, "portfinder": { "version": "1.0.28", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", - "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", "dev": true, "requires": { "async": "^2.6.2", @@ -5914,8 +5761,6 @@ "dependencies": { "debug": { "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "requires": { "ms": "^2.1.1" @@ -5923,8 +5768,6 @@ }, "ms": { "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true } } @@ -5937,8 +5780,6 @@ }, "proxy-addr": { "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, "requires": { "forwarded": "0.2.0", @@ -5953,8 +5794,6 @@ }, "qs": { "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", "dev": true }, "queue-microtask": { @@ -5980,8 +5819,6 @@ }, "raw-body": { "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", "dev": true, "requires": { "bytes": "3.1.0", @@ -5992,8 +5829,6 @@ "dependencies": { "bytes": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", "dev": true } } @@ -6033,8 +5868,6 @@ }, "regexp.prototype.flags": { "version": "1.3.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", - "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", "dev": true, "requires": { "call-bind": "^1.0.2", @@ -6050,13 +5883,11 @@ "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, "resolve": { "version": "1.22.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", - "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", "dev": true, "requires": { "is-core-module": "^2.8.1", @@ -6136,13 +5967,11 @@ "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", "dev": true }, "selfsigned": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.0.tgz", - "integrity": "sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ==", "dev": true, "requires": { "node-forge": "^1.2.0" @@ -6150,8 +5979,6 @@ }, "send": { "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", "dev": true, "requires": { "debug": "2.6.9", @@ -6171,8 +5998,6 @@ "dependencies": { "ms": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true } } @@ -6189,7 +6014,7 @@ "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", "dev": true, "requires": { "accepts": "~1.3.4", @@ -6204,7 +6029,7 @@ "http-errors": { "version": "1.6.3", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", "dev": true, "requires": { "depd": "~1.1.2", @@ -6216,7 +6041,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "dev": true }, "setprototypeof": { @@ -6229,8 +6054,6 @@ }, "serve-static": { "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", "dev": true, "requires": { "encodeurl": "~1.0.2", @@ -6241,8 +6064,6 @@ }, "setprototypeof": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", "dev": true }, "shallow-clone": { @@ -6283,8 +6104,6 @@ }, "sockjs": { "version": "0.3.21", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz", - "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==", "dev": true, "requires": { "faye-websocket": "^0.11.3", @@ -6323,8 +6142,6 @@ "dependencies": { "debug": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -6354,8 +6171,6 @@ "dependencies": { "debug": { "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -6383,7 +6198,7 @@ "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true }, "string_decoder": { @@ -6397,8 +6212,6 @@ }, "strip-ansi": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", - "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", "dev": true, "requires": { "ansi-regex": "^6.0.1" @@ -6432,21 +6245,23 @@ "dev": true }, "terser": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", - "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", + "version": "5.12.1", "dev": true, "requires": { - "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", "commander": "^2.20.0", + "source-map": "~0.7.2", "source-map-support": "~0.5.20" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "dev": true + } } }, "terser-webpack-plugin": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", "dev": true, "requires": { "jest-worker": "^27.4.5", @@ -6511,8 +6326,6 @@ }, "toidentifier": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", "dev": true }, "type-is": { @@ -6528,7 +6341,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", "dev": true }, "uri-js": { @@ -6543,31 +6356,27 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "dev": true }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", "dev": true }, "uuid": { "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "dev": true }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", "dev": true }, "watchpack": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", - "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", "dev": true, "requires": { "glob-to-regexp": "^0.4.1", @@ -6585,8 +6394,6 @@ }, "webpack": { "version": "5.70.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz", - "integrity": "sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", @@ -6655,8 +6462,6 @@ }, "webpack-cli": { "version": "4.9.2", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.2.tgz", - "integrity": "sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==", "dev": true, "requires": { "@discoveryjs/json-ext": "^0.5.0", @@ -6683,8 +6488,6 @@ }, "webpack-dev-middleware": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.1.tgz", - "integrity": "sha512-81EujCKkyles2wphtdrnPg/QqegC/AtqNH//mQkBYSMqwFVCQrxM6ktB2O/SPlZy7LqeEfTbV3cZARGQz6umhg==", "dev": true, "requires": { "colorette": "^2.0.10", @@ -6696,8 +6499,6 @@ }, "webpack-dev-server": { "version": "4.7.4", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz", - "integrity": "sha512-nfdsb02Zi2qzkNmgtZjkrMOcXnYZ6FLKcQwpxT7MvmHKc+oTtDsBju8j+NMyAygZ9GW1jMEUpy3itHtqgEhe1A==", "dev": true, "requires": { "@types/bonjour": "^3.5.9", @@ -6791,13 +6592,11 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, "ws": { "version": "8.5.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", - "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", "dev": true, "requires": {} } diff --git a/clients/webassembly/js-example/package.json b/clients/webassembly/js-example/package.json index 361ae23c0e..a843e96700 100644 --- a/clients/webassembly/js-example/package.json +++ b/clients/webassembly/js-example/package.json @@ -36,4 +36,4 @@ "dependencies": { "@nymproject/nym-client-wasm": "file:../pkg" } -} +} \ No newline at end of file diff --git a/clients/webassembly/js-example/webpack.config.js b/clients/webassembly/js-example/webpack.config.js index e70c197a25..ddb11861db 100644 --- a/clients/webassembly/js-example/webpack.config.js +++ b/clients/webassembly/js-example/webpack.config.js @@ -1,15 +1,27 @@ -const CopyWebpackPlugin = require("copy-webpack-plugin"); +const CopyWebpackPlugin = require('copy-webpack-plugin'); const path = require('path'); module.exports = { - entry: "./bootstrap.js", - output: { - path: path.resolve(__dirname, "dist"), - filename: "bootstrap.js", + entry: { + bootstrap: './bootstrap.js', + worker: './worker.js', }, - mode: "development", + output: { + path: path.resolve(__dirname, 'dist'), + filename: '[name].js', + }, + // mode: 'development', + mode: 'production', plugins: [ - new CopyWebpackPlugin({ patterns: ['index.html'] }) + new CopyWebpackPlugin({ + patterns: [ + 'index.html', + { + from: 'node_modules/@nymproject/nym-client-wasm/*.(js|wasm)', + to: '[name][ext]', + }, + ], + }), ], - experiments: { syncWebAssembly: true } + experiments: { syncWebAssembly: true }, }; diff --git a/clients/webassembly/js-example/worker.js b/clients/webassembly/js-example/worker.js new file mode 100644 index 0000000000..78828f18c5 --- /dev/null +++ b/clients/webassembly/js-example/worker.js @@ -0,0 +1,131 @@ +// Copyright 2020-2022 Nym Technologies SA +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +importScripts('nym_client_wasm.js'); + +console.log('Initializing worker'); + +// wasm_bindgen creates a global variable (with the exports attached) that is in scope after `importScripts` +const { default_debug, get_gateway, NymClient, set_panic_hook, Config } = wasm_bindgen; + +class ClientWrapper { + constructor(config, onMessageHandler) { + this.rustClient = new NymClient(config); + this.rustClient.set_on_message(onMessageHandler); + this.rustClient.set_on_gateway_connect(this.onConnect); + } + + selfAddress = () => { + return this.rustClient.self_address(); + }; + + onConnect = () => { + console.log('Established (and authenticated) gateway connection!'); + }; + + start = async () => { + // this is current limitation of wasm in rust - for async methods you can't take self by reference... + // I'm trying to figure out if I can somehow hack my way around it, but for time being you have to re-assign + // the object (it's the same one) + this.rustClient = await this.rustClient.start(); + }; + + sendMessage = async (recipient, message) => { + this.rustClient = await this.rustClient.send_message(recipient, message); + }; + + sendBinaryMessage = async (recipient, message) => { + this.rustClient = await this.rustClient.send_binary_message(recipient, message); + }; +} + +let client = null; + +async function main() { + // load WASM package + await wasm_bindgen('nym_client_wasm_bg.wasm'); + + console.log('Loaded WASM'); + + // sets up better stack traces in case of in-rust panics + set_panic_hook(); + + console.error("the current mainnet is not compatible with v2! - either use the pre-merge branch or explicitly set the client to use one of V2 QA networks") + return + + // validator server we will use to get topology from + // MAINNET (V1): + const validator = 'https://validator.nymtech.net/api'; //"http://localhost:8081"; + const preferredGateway = 'E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM'; + // QA (V2): + // const validator = 'https://qa-validator-api.nymtech.net/api'; //"http://localhost:8081"; + // const preferredGateway = 'CgQrYP8etksSBf4nALNqp93SHPpgFwEUyTsjBNNLj5WM'; + + const gatewayEndpoint = await get_gateway(validator, preferredGateway); + + // only really useful if you want to adjust some settings like traffic rate + // (if not needed you can just pass a null) + const debug = default_debug(); + + debug.disable_main_poisson_packet_distribution = true; + debug.disable_loop_cover_traffic_stream = true; + debug.use_extended_packet_size = true; + // debug.average_packet_delay_ms = BigInt(10); + // debug.average_ack_delay_ms = BigInt(10); + // debug.ack_wait_addition_ms = BigInt(3000); + // debug.ack_wait_multiplier = 10; + + debug.topology_refresh_rate_ms = BigInt(60000) + + const config = new Config('my-awesome-wasm-client', validator, gatewayEndpoint, debug); + + const onMessageHandler = (message) => { + self.postMessage({ + kind: 'ReceiveMessage', + args: { + message, + }, + }); + }; + + console.log('Instantiating WASM client...'); + client = new ClientWrapper(config, onMessageHandler); + console.log('Web worker creating WASM client...'); + await client.start(); + console.log('WASM client running!'); + + const selfAddress = client.rustClient.self_address(); + console.log(`Client address is ${selfAddress}`); + self.postMessage({ + kind: 'Ready', + args: { + selfAddress, + }, + }); + + // Set callback to handle messages passed to the worker. + self.onmessage = async event => { + if (event.data && event.data.kind) { + switch (event.data.kind) { + case 'SendMessage': { + const { message, recipient } = event.data.args; + await client.sendMessage(message, recipient); + } + } + } + }; +} + +// Let's get started! +main(); \ No newline at end of file diff --git a/clients/webassembly/js-example/yarn.lock b/clients/webassembly/js-example/yarn.lock new file mode 100644 index 0000000000..ded755f84b --- /dev/null +++ b/clients/webassembly/js-example/yarn.lock @@ -0,0 +1,2163 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.0.3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.15" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.15.tgz#aba35c48a38d3fd84b37e66c9c0423f9744f9774" + integrity sha512-oWZNOULl+UbhsgB51uuZzglikfIKSUBO/M9W2OfEjn7cmqoAiCgmv9lyACTUacZwBz0ITnJ2NqjU8Tx0DHL88g== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" + integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@nymproject/nym-client-wasm@file:../pkg": + version "1.0.0" + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + version "3.5.10" + resolved "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz" + integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.3.5": + version "1.3.5" + resolved "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz" + integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.4.6" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.6.tgz#7976f054c1bccfcf514bff0564c0c41df5c08207" + integrity sha512-/fqTbjxyFUaYNO7VcW5g+4npmqVACz1bB7RTHYuLj+PRjw9hrCwrUXVQFpChUS0JsyEFvMZ7U/PfmvWgxJhI9g== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": + version "4.17.31" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz#a1139efeab4e7323834bb0226e62ac019f474b2f" + integrity sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@*", "@types/express@^4.17.13": + version "4.17.14" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.14.tgz#143ea0557249bc1b3b54f15db4c81c3d4eb3569c" + integrity sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.18" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-proxy@^1.17.8": + version "1.17.9" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.9.tgz#7f0e7931343761efde1e2bf48c40f02f3f75705a" + integrity sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw== + dependencies: + "@types/node" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + +"@types/node@*": + version "18.8.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.8.3.tgz#ce750ab4017effa51aed6a7230651778d54e327c" + integrity sha512-0os9vz6BpGwxGe9LOhgP/ncvYN5Tx1fNcd2TM3rD/aCGBkysb+ZWpXEocG24h6ZzOi13+VB8HndAQFezsSOw1w== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + +"@types/serve-index@^1.9.1": + version "1.9.1" + resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz" + integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.13.10": + version "1.15.0" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.0.tgz#c7930ff61afb334e121a9da780aac0d9b8f34155" + integrity sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg== + dependencies: + "@types/mime" "*" + "@types/node" "*" + +"@types/sockjs@^0.3.33": + version "0.3.33" + resolved "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz" + integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== + dependencies: + "@types/node" "*" + +"@types/ws@^8.5.1": + version "8.5.3" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.3.tgz#7d25a1ffbecd3c4f2d35068d0b283c037003274d" + integrity sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w== + dependencies: + "@types/node" "*" + +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== + +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== + +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== + +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz" + integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== + +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz" + integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz" + integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-wasm-section" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-opt" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/wast-printer" "1.11.1" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz" + integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz" + integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz" + integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" + integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== + +"@webpack-cli/info@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" + integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== + dependencies: + envinfo "^7.7.3" + +"@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== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + +acorn@^8.5.0, acorn@^8.7.1: + version "8.8.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8" + integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w== + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.8.0: + version "8.11.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz" + integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-flatten@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-union@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz" + integrity sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +body-parser@1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.0.tgz#3de69bd89011c11573d7bfee6a64f11b6bd27cc5" + integrity sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.10.3" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.0.11: + version "1.0.14" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.0.14.tgz#c346f5bc84e87802d08f8d5a60b93f758e514ee7" + integrity sha512-HIMbgLnk1Vqvs6B4Wq5ep7mxvj9sGz5d1JJyDNSGNIdA/w2MCz6GTjWTdjqOJV1bEPj+6IkxDvWNFKEBxNt4kQ== + dependencies: + array-flatten "^2.1.2" + dns-equal "^1.0.0" + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.14.5: + version "4.21.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" + integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== + dependencies: + caniuse-lite "^1.0.30001400" + electron-to-chromium "^1.4.251" + node-releases "^2.0.6" + update-browserslist-db "^1.0.9" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +caniuse-lite@^1.0.30001400: + version "1.0.30001418" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz#5f459215192a024c99e3e3a53aac310fc7cf24e6" + integrity sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg== + +chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +colorette@^2.0.10, colorette@^2.0.14: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^7.0.0: + version "7.2.0" + resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +copy-webpack-plugin@^10.2.4: + version "10.2.4" + resolved "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz" + integrity sha512-xFVltahqlsRcyyJqQbDY6EYTtyQZF9rf+JPjwHObLdPFMEISqkFkr7mFoVOC6BfYS/dNThyoQKvziugm+OnwBg== + dependencies: + fast-glob "^3.2.7" + glob-parent "^6.0.1" + globby "^12.0.2" + normalize-path "^3.0.0" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz" + integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg== + +dns-packet@^5.2.2: + version "5.4.0" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.4.0.tgz#1f88477cf9f27e78a213fb6d118ae38e759a879b" + integrity sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.251: + version "1.4.275" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.275.tgz#db25c8e39c9cc910a996d1ec9b73eee834cb0ac1" + integrity sha512-aJeQQ+Hl9Jyyzv4chBqYJwmVRY46N5i2BEX5Cuyk/5gFCUZ5F3i7Hnba6snZftWla7Gglwc5pIgcd+E7cW+rPg== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +enhanced-resolve@^5.10.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.10.0.tgz#0dc579c3bb2a1032e357ac45b8f3a6f3ad4fb1e6" + integrity sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +envinfo@^7.7.3: + version "7.8.1" + resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz" + integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +express@^4.17.3: + version "4.18.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.1.tgz#7797de8b9c72c857b9cd0e14a5eea80666267caf" + integrity sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.0" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.10.3" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.7: + version "3.2.12" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" + integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + 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-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +follow-redirects@^1.0.0: + version "1.15.2" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" + integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-monkey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.3.tgz#ae3ac92d53bb328efe0e9a1d9541f6ad8d48e2d3" + integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-intrinsic@^1.0.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.3.tgz#063c84329ad93e83893c7f4f243ef63ffa351385" + integrity sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globby@^12.0.2: + version "12.2.0" + resolved "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz" + integrity sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA== + dependencies: + array-union "^3.0.1" + dir-glob "^3.0.1" + fast-glob "^3.2.7" + ignore "^5.1.9" + merge2 "^1.4.1" + slash "^4.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hello-wasm-pack@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/hello-wasm-pack/-/hello-wasm-pack-0.1.0.tgz" + integrity sha512-3hx0GDkDLf/a9ThCMV2qG4mwza8N/MCtm8aeFFc/cdBCL2zMJ1kW1wjNl7xPqD1lz8Yl5+uhnc/cpui4dLwz/w== + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46" + integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-middleware@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore@^5.1.9: + version "5.2.0" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz" + integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.9.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.10.0.tgz#9012ede0a91c69587e647514e1d5277019e728ed" + integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== + dependencies: + has "^1.0.3" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.4.3: + version "3.4.7" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.4.7.tgz#e5252ad2242a724f938cb937e3c4f7ceb1f70e5a" + integrity sha512-ygaiUSNalBX85388uskeCyhSAoOSgzBbtVCr9jA2RROssFL9Q19/ZXFqS+2Th2sr1ewNIWgFdLzLC3Yl1Zv+lw== + dependencies: + fs-monkey "^1.0.3" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.2, micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-releases@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" + integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.0.9: + version "8.4.0" + resolved "https://registry.npmjs.org/open/-/open-8.4.0.tgz" + integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-retry@^4.5.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +qs@6.10.3: + version "6.10.3" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e" + integrity sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ== + dependencies: + side-channel "^1.0.4" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^2.0.1: + version "2.3.7" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6: + version "3.6.0" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.7.0: + version "0.7.1" + resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz" + integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== + dependencies: + resolve "^1.9.0" + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.9.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@5.1.2, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz" + integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.8.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.0.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.1.1.tgz#18a7613d714c0cd3385c48af0075abf3f266af61" + integrity sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ== + dependencies: + node-forge "^1" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +string_decoder@^1.1.1, string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.1.3: + version "5.3.6" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz#5590aec31aa3c6f771ce1b1acca60639eab3195c" + integrity sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.14" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + terser "^5.14.1" + +terser@^5.14.1: + version "5.15.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.15.1.tgz#8561af6e0fd6d839669c73b92bdd5777d870ed6c" + integrity sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw== + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webpack-cli@^4.9.2: + version "4.10.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31" + integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^1.2.0" + "@webpack-cli/info" "^1.5.0" + "@webpack-cli/serve" "^1.7.0" + colorette "^2.0.14" + commander "^7.0.0" + cross-spawn "^7.0.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^2.2.0" + rechoir "^0.7.0" + webpack-merge "^5.7.3" + +webpack-dev-middleware@^5.3.1: + version "5.3.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" + integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@^4.7.4: + version "4.11.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz#ae07f0d71ca0438cf88446f09029b92ce81380b5" + integrity sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw== + dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" + "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.5.1" + ansi-html-community "^0.0.8" + bonjour-service "^1.0.11" + chokidar "^3.5.3" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.0.1" + open "^8.0.9" + p-retry "^4.5.0" + rimraf "^3.0.2" + schema-utils "^4.0.0" + selfsigned "^2.1.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^5.3.1" + ws "^8.4.2" + +webpack-merge@^5.7.3: + version "5.8.0" + resolved "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz" + integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== + dependencies: + clone-deep "^4.0.1" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.70.0: + version "5.74.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.74.0.tgz#02a5dac19a17e0bb47093f2be67c695102a55980" + integrity sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.7.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.10.0" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz" + integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^8.4.2: + version "8.9.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.9.0.tgz#2a994bb67144be1b53fe2d23c53c028adeb7f45e" + integrity sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg== diff --git a/clients/webassembly/package-lock.json b/clients/webassembly/package-lock.json deleted file mode 100644 index 6e580e1b85..0000000000 --- a/clients/webassembly/package-lock.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "requires": true, - "lockfileVersion": 1, - "dependencies": { - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" - } - } -} diff --git a/clients/webassembly/src/client/config.rs b/clients/webassembly/src/client/config.rs new file mode 100644 index 0000000000..d11b7544e7 --- /dev/null +++ b/clients/webassembly/src/client/config.rs @@ -0,0 +1,159 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// due to expansion of #[wasm_bindgen] macro on `Debug` Config struct +#![allow(clippy::drop_non_drop)] + +use client_core::config::{Debug as ConfigDebug, GatewayEndpoint}; +use std::time::Duration; +use url::Url; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub struct Config { + /// ID specifies the human readable ID of this particular client. + pub(crate) id: String, + + pub(crate) validator_api_url: Url, + + pub(crate) disabled_credentials_mode: bool, + + /// Information regarding how the client should send data to gateway. + pub(crate) gateway_endpoint: GatewayEndpoint, + + pub(crate) debug: ConfigDebug, +} + +#[wasm_bindgen] +impl Config { + #[wasm_bindgen(constructor)] + pub fn new( + id: String, + validator_server: String, + gateway_endpoint: GatewayEndpoint, + debug: Option, + ) -> Self { + Config { + id, + validator_api_url: validator_server + .parse() + .expect("provided url was malformed"), + disabled_credentials_mode: true, + gateway_endpoint, + debug: debug.map(Into::into).unwrap_or_default(), + } + } +} + +// just a helper structure to more easily pass through the JS boundary +#[wasm_bindgen] +pub struct Debug { + /// The parameter of Poisson distribution determining how long, on average, + /// sent packet is going to be delayed at any given mix node. + /// So for a packet going through three mix nodes, on average, it will take three times this value + /// until the packet reaches its destination. + pub average_packet_delay_ms: u64, + + /// The parameter of Poisson distribution determining how long, on average, + /// sent acknowledgement is going to be delayed at any given mix node. + /// So for an ack going through three mix nodes, on average, it will take three times this value + /// until the packet reaches its destination. + pub average_ack_delay_ms: u64, + + /// Value multiplied with the expected round trip time of an acknowledgement packet before + /// it is assumed it was lost and retransmission of the data packet happens. + /// In an ideal network with 0 latency, this value would have been 1. + pub ack_wait_multiplier: f64, + + /// Value added to the expected round trip time of an acknowledgement packet before + /// it is assumed it was lost and retransmission of the data packet happens. + /// In an ideal network with 0 latency, this value would have been 0. + pub ack_wait_addition_ms: u64, + + /// The parameter of Poisson distribution determining how long, on average, + /// it is going to take for another loop cover traffic message to be sent. + pub loop_cover_traffic_average_delay_ms: u64, + + /// The parameter of Poisson distribution determining how long, on average, + /// it is going to take another 'real traffic stream' message to be sent. + /// If no real packets are available and cover traffic is enabled, + /// a loop cover message is sent instead in order to preserve the rate. + pub message_sending_average_delay_ms: u64, + + /// How long we're willing to wait for a response to a message sent to the gateway, + /// before giving up on it. + pub gateway_response_timeout_ms: u64, + + /// The uniform delay every which clients are querying the directory server + /// to try to obtain a compatible network topology to send sphinx packets through. + pub topology_refresh_rate_ms: u64, + + /// During topology refresh, test packets are sent through every single possible network + /// path. This timeout determines waiting period until it is decided that the packet + /// did not reach its destination. + pub topology_resolution_timeout_ms: u64, + + /// Controls whether the dedicated loop cover traffic stream should be enabled. + /// (and sending packets, on average, every [Self::loop_cover_traffic_average_delay_ms]) + pub disable_loop_cover_traffic_stream: bool, + + /// Controls whether the main packet stream constantly produces packets according to the predefined + /// poisson distribution. + pub disable_main_poisson_packet_distribution: bool, + + /// Controls whether the sent sphinx packet use the NON-DEFAULT bigger size. + pub use_extended_packet_size: bool, +} + +impl From for ConfigDebug { + fn from(debug: Debug) -> Self { + ConfigDebug { + average_packet_delay: Duration::from_millis(debug.average_packet_delay_ms), + average_ack_delay: Duration::from_millis(debug.average_ack_delay_ms), + ack_wait_multiplier: debug.ack_wait_multiplier, + ack_wait_addition: Duration::from_millis(debug.ack_wait_addition_ms), + loop_cover_traffic_average_delay: Duration::from_millis( + debug.loop_cover_traffic_average_delay_ms, + ), + message_sending_average_delay: Duration::from_millis( + debug.message_sending_average_delay_ms, + ), + gateway_response_timeout: Duration::from_millis(debug.gateway_response_timeout_ms), + topology_refresh_rate: Duration::from_millis(debug.topology_refresh_rate_ms), + topology_resolution_timeout: Duration::from_millis( + debug.topology_resolution_timeout_ms, + ), + disable_loop_cover_traffic_stream: debug.disable_loop_cover_traffic_stream, + disable_main_poisson_packet_distribution: debug + .disable_main_poisson_packet_distribution, + use_extended_packet_size: debug.use_extended_packet_size, + } + } +} + +impl From for Debug { + fn from(debug: ConfigDebug) -> Self { + Debug { + average_packet_delay_ms: debug.average_packet_delay.as_millis() as u64, + average_ack_delay_ms: debug.average_ack_delay.as_millis() as u64, + ack_wait_multiplier: debug.ack_wait_multiplier, + ack_wait_addition_ms: debug.ack_wait_addition.as_millis() as u64, + loop_cover_traffic_average_delay_ms: debug.loop_cover_traffic_average_delay.as_millis() + as u64, + message_sending_average_delay_ms: debug.message_sending_average_delay.as_millis() + as u64, + gateway_response_timeout_ms: debug.gateway_response_timeout.as_millis() as u64, + topology_refresh_rate_ms: debug.topology_refresh_rate.as_millis() as u64, + topology_resolution_timeout_ms: debug.topology_resolution_timeout.as_millis() as u64, + disable_loop_cover_traffic_stream: debug.disable_loop_cover_traffic_stream, + disable_main_poisson_packet_distribution: debug + .disable_main_poisson_packet_distribution, + use_extended_packet_size: debug.use_extended_packet_size, + } + } +} + +#[wasm_bindgen] +pub fn default_debug() -> Debug { + ConfigDebug::default().into() +} diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index 848010516b..544447e9dc 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -1,47 +1,47 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crypto::asymmetric::{encryption, identity}; +use self::config::Config; +use client_core::client::{ + cover_traffic_stream::LoopCoverTrafficStream, + inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}, + key_manager::KeyManager, + mix_traffic::{BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController}, + real_messages_control::{self, RealMessagesController}, + received_buffer::{ + ReceivedBufferMessage, ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, + ReceivedMessagesBufferController, + }, + topology_control::{TopologyAccessor, TopologyRefresher, TopologyRefresherConfig}, +}; +use crypto::asymmetric::identity; use futures::channel::mpsc; -use gateway_client::GatewayClient; -use nymsphinx::acknowledgements::AckKey; +use futures::StreamExt; +use gateway_client::{ + AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, + MixnetMessageSender, +}; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::preparer::MessagePreparer; +use nymsphinx::params::PacketSize; use rand::rngs::OsRng; -use received_processor::ReceivedMessagesProcessor; -use std::sync::Arc; -use std::time::Duration; -use topology::{gateway, nym_topology_from_detailed, NymTopology}; -use url::Url; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::spawn_local; use wasm_utils::{console_log, console_warn}; -pub(crate) mod received_processor; - -const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(200); -const DEFAULT_AVERAGE_ACK_DELAY: Duration = Duration::from_millis(200); -const DEFAULT_GATEWAY_RESPONSE_TIMEOUT: Duration = Duration::from_millis(1_500); +pub mod config; #[wasm_bindgen] pub struct NymClient { - validator_server: Url, - disabled_credentials_mode: bool, + config: Config, - // TODO: technically this doesn't need to be an Arc since wasm is run on a single thread - // however, once we eventually combine this code with the native-client's, it will make things - // easier. - identity: Arc, - encryption_keys: Arc, - ack_key: Arc, - - message_preparer: Option>, - // message_receiver: MessageReceiver, + /// KeyManager object containing smart pointers to all relevant keys used by the client. + key_manager: KeyManager, // TODO: this should be stored somewhere persistently // received_keys: HashSet, - topology: Option, - gateway_client: Option, + /// Channel used for transforming 'raw' messages into sphinx packets and sending them + /// through the mix network. + input_tx: Option, // callbacks on_message: Option, @@ -51,31 +51,24 @@ pub struct NymClient { #[wasm_bindgen] impl NymClient { #[wasm_bindgen(constructor)] - pub fn new(validator_server: String) -> Self { - let mut rng = OsRng; - // for time being generate new keys each time... - let identity = identity::KeyPair::new(&mut rng); - let encryption_keys = encryption::KeyPair::new(&mut rng); - let ack_key = AckKey::new(&mut rng); - + pub fn new(config: Config) -> Self { Self { - identity: Arc::new(identity), - encryption_keys: Arc::new(encryption_keys), - ack_key: Arc::new(ack_key), - validator_server: validator_server - .parse() - .expect("malformed validator server url provided"), - message_preparer: None, - // received_keys: Default::default(), - topology: None, - gateway_client: None, - + config, + key_manager: Self::setup_key_manager(), on_message: None, on_gateway_connect: None, - disabled_credentials_mode: true, + input_tx: None, } } + // perhaps this should be public? + fn setup_key_manager() -> KeyManager { + let mut rng = OsRng; + // for time being generate new keys each time... + console_log!("generated new set of keys"); + KeyManager::new(&mut rng) + } + pub fn set_on_message(&mut self, on_message: js_sys::Function) { self.on_message = Some(on_message); } @@ -85,62 +78,137 @@ impl NymClient { self.on_gateway_connect = Some(on_connect) } - pub fn set_disabled_credentials_mode(&mut self, disabled_credentials_mode: bool) { - console_log!( - "Setting disabled credentials mode to {}", - disabled_credentials_mode - ); - self.disabled_credentials_mode = disabled_credentials_mode; - } - - fn self_recipient(&self) -> Recipient { + fn as_mix_recipient(&self) -> Recipient { Recipient::new( - *self.identity.public_key(), - *self.encryption_keys.public_key(), - self.gateway_client - .as_ref() - .expect("gateway connection was not established!") - .gateway_identity(), + *self.key_manager.identity_keypair().public_key(), + *self.key_manager.encryption_keypair().public_key(), + identity::PublicKey::from_base58_string(&self.config.gateway_endpoint.gateway_id) + .expect("no gateway has been selected"), ) } pub fn self_address(&self) -> String { - self.self_recipient().to_string() + self.as_mix_recipient().to_string() } - // Right now it's impossible to have async exported functions to take `&self` rather than self - pub async fn initial_setup(self) -> Self { - let disabled_credentials_mode = self.disabled_credentials_mode; + // future constantly pumping loop cover traffic at some specified average rate + // the pumped traffic goes to the MixTrafficController + fn start_cover_traffic_stream( + &self, + topology_accessor: TopologyAccessor, + mix_tx: BatchMixMessageSender, + ) { + console_log!("Starting loop cover traffic stream..."); - let bandwidth_controller = None; - - let mut client = self.get_and_update_topology().await; - let gateway = client.choose_gateway(); - - let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded(); - let (ack_sender, ack_receiver) = mpsc::unbounded(); - - let mut gateway_client = GatewayClient::new( - gateway.clients_address(), - Arc::clone(&client.identity), - gateway.identity_key, - gateway.owner.clone(), - None, - mixnet_messages_sender, - ack_sender, - DEFAULT_GATEWAY_RESPONSE_TIMEOUT, - bandwidth_controller, + let mut stream = LoopCoverTrafficStream::new( + self.key_manager.ack_key(), + self.config.debug.average_ack_delay, + self.config.debug.average_packet_delay, + self.config.debug.loop_cover_traffic_average_delay, + mix_tx, + self.as_mix_recipient(), + topology_accessor, ); - gateway_client.set_disabled_credentials_mode(disabled_credentials_mode); + if self.config.debug.use_extended_packet_size { + stream.set_custom_packet_size(PacketSize::ExtendedPacket) + } - gateway_client + stream.start(); + } + + fn start_real_traffic_controller( + &self, + topology_accessor: TopologyAccessor, + ack_receiver: AcknowledgementReceiver, + input_receiver: InputMessageReceiver, + mix_sender: BatchMixMessageSender, + ) { + let mut controller_config = real_messages_control::Config::new( + self.key_manager.ack_key(), + self.config.debug.ack_wait_multiplier, + self.config.debug.ack_wait_addition, + self.config.debug.average_ack_delay, + self.config.debug.message_sending_average_delay, + self.config.debug.average_packet_delay, + self.config.debug.disable_main_poisson_packet_distribution, + self.as_mix_recipient(), + ); + + if self.config.debug.use_extended_packet_size { + controller_config.set_custom_packet_size(PacketSize::ExtendedPacket) + } + + console_log!("Starting real traffic stream..."); + + RealMessagesController::new( + controller_config, + ack_receiver, + input_receiver, + mix_sender, + topology_accessor, + ) + .start(); + } + + // buffer controlling all messages fetched from provider + // required so that other components would be able to use them (say the websocket) + fn start_received_messages_buffer_controller( + &self, + query_receiver: ReceivedBufferRequestReceiver, + mixnet_receiver: MixnetMessageReceiver, + ) { + console_log!("Starting received messages buffer controller..."); + ReceivedMessagesBufferController::new( + self.key_manager.encryption_keypair(), + query_receiver, + mixnet_receiver, + ) + .start() + } + + async fn start_gateway_client( + &mut self, + mixnet_message_sender: MixnetMessageSender, + ack_sender: AcknowledgementSender, + ) -> GatewayClient { + let gateway_id = self.config.gateway_endpoint.gateway_id.clone(); + if gateway_id.is_empty() { + panic!("The identity of the gateway is unknown - did you run `get_gateway()`?") + } + let gateway_owner = self.config.gateway_endpoint.gateway_owner.clone(); + if gateway_owner.is_empty() { + panic!("The owner of the gateway is unknown - did you run `get_gateway()`?") + } + let gateway_address = self.config.gateway_endpoint.gateway_listener.clone(); + if gateway_address.is_empty() { + panic!("The address of the gateway is unknown - did you run `get_gateway()`?") + } + + let gateway_identity = identity::PublicKey::from_base58_string(gateway_id) + .expect("provided gateway id is invalid!"); + + let mut gateway_client = GatewayClient::new( + gateway_address, + self.key_manager.identity_keypair(), + gateway_identity, + gateway_owner, + None, + mixnet_message_sender, + ack_sender, + self.config.debug.gateway_response_timeout, + None, + ); + + gateway_client.set_disabled_credentials_mode(self.config.disabled_credentials_mode); + + let shared_keys = gateway_client .authenticate_and_start() .await .expect("could not authenticate and start up the gateway connection"); + self.key_manager.insert_gateway_shared_key(shared_keys); - client.gateway_client = Some(gateway_client); - match client.on_gateway_connect.as_ref() { + match self.on_gateway_connect.as_ref() { Some(callback) => { callback .call0(&JsValue::null()) @@ -149,124 +217,168 @@ impl NymClient { None => console_log!("Gateway connection established - no callback specified"), }; - let rng = rand::rngs::OsRng; - let message_preparer = MessagePreparer::new( - rng, - client.self_recipient(), - DEFAULT_AVERAGE_PACKET_DELAY, - DEFAULT_AVERAGE_ACK_DELAY, + gateway_client + } + + // future responsible for periodically polling directory server and updating + // the current global view of topology + async fn start_topology_refresher(&mut self, topology_accessor: TopologyAccessor) { + let topology_refresher_config = TopologyRefresherConfig::new( + vec![self.config.validator_api_url.clone()], + self.config.debug.topology_refresh_rate, + env!("CARGO_PKG_VERSION").to_string(), ); + let mut topology_refresher = + TopologyRefresher::new(topology_refresher_config, topology_accessor); + // before returning, block entire runtime to refresh the current network view so that any + // components depending on topology would see a non-empty view + console_log!("Obtaining initial network topology"); + topology_refresher.refresh().await; - let received_processor = ReceivedMessagesProcessor::new( - Arc::clone(&client.encryption_keys), - Arc::clone(&client.ack_key), - ); + // TODO: a slightly more graceful termination here + if !topology_refresher.is_topology_routable().await { + panic!( + "The current network topology seem to be insufficient to route any packets through\ + - check if enough nodes and a gateway are online" + ); + } - client.message_preparer = Some(message_preparer); + console_log!("Starting topology refresher..."); - spawn_local(received_processor.start_processing( + // TODO: re-enable + topology_refresher.start(); + } + + // controller for sending sphinx packets to mixnet (either real traffic or cover traffic) + // TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership + // over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for + // requests? + fn start_mix_traffic_controller( + &mut self, + mix_rx: BatchMixMessageReceiver, + gateway_client: GatewayClient, + ) { + console_log!("Starting mix traffic controller..."); + MixTrafficController::new(mix_rx, gateway_client).start(); + } + + // TODO: this procedure is extremely overcomplicated, because it's based off native client's behaviour + // which doesn't fully apply in this case + fn start_reconstructed_pusher( + &mut self, + received_buffer_request_sender: ReceivedBufferRequestSender, + ) { + let on_message = self.on_message.take(); + + spawn_local(async move { + let (reconstructed_sender, mut reconstructed_receiver) = mpsc::unbounded(); + + // tell the buffer to start sending stuff to us + received_buffer_request_sender + .unbounded_send(ReceivedBufferMessage::ReceiverAnnounce( + reconstructed_sender, + )) + .expect("the buffer request failed!"); + + let this = JsValue::null(); + + while let Some(reconstructed) = reconstructed_receiver.next().await { + if let Some(ref callback) = on_message { + for msg in reconstructed { + if msg.reply_surb.is_some() { + console_log!("the received message contained a reply-surb that we do not know how to handle (yet)") + } + let stringified = String::from_utf8_lossy(&msg.message).into_owned(); + let arg1 = serde_wasm_bindgen::to_value(&stringified).unwrap(); + callback.call1(&this, &arg1).expect("on message failed!"); + } + } else { + console_warn!("no on_message callback was specified. the received message content is getting dropped"); + console_log!("the raw messages: {:?}", reconstructed) + } + } + }); + } + + pub async fn start(mut self) -> NymClient { + console_log!("Starting wasm client '{}'", self.config.id); + // channels for inter-component communication + // TODO: make the channels be internally created by the relevant components + // rather than creating them here, so say for example the buffer controller would create the request channels + // and would allow anyone to clone the sender channel + + // sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet + // they are used by cover traffic stream and real traffic stream + // sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic + let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded(); + + // unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway + // unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer + let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded(); + + // used for announcing connection or disconnection of a channel for pushing re-assembled messages to + let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); + + // channels responsible for controlling real messages + let (input_sender, input_receiver) = mpsc::unbounded::(); + + // channels responsible for controlling ack messages + let (ack_sender, ack_receiver) = mpsc::unbounded(); + let shared_topology_accessor = TopologyAccessor::new(); + + // the components are started in very specific order. Unless you know what you are doing, + // do not change that. + self.start_topology_refresher(shared_topology_accessor.clone()) + .await; + self.start_received_messages_buffer_controller( + received_buffer_request_receiver, mixnet_messages_receiver, - ack_receiver, - client.on_message.take().expect("on_message was not set!"), - )); + ); - client + let gateway_client = self + .start_gateway_client(mixnet_messages_sender, ack_sender) + .await; + + self.start_mix_traffic_controller(sphinx_message_receiver, gateway_client); + self.start_real_traffic_controller( + shared_topology_accessor.clone(), + ack_receiver, + input_receiver, + sphinx_message_sender.clone(), + ); + + if !self.config.debug.disable_loop_cover_traffic_stream { + self.start_cover_traffic_stream(shared_topology_accessor, sphinx_message_sender); + } + + self.start_reconstructed_pusher(received_buffer_request_sender); + self.input_tx = Some(input_sender); + + self } // Right now it's impossible to have async exported functions to take `&mut self` rather than mut self // TODO: try Rc> approach? - pub async fn send_message(mut self, message: String, recipient: String) -> Self { + pub async fn send_message(self, message: String, recipient: String) -> Self { console_log!("Sending {} to {}", message, recipient); let message_bytes = message.into_bytes(); + self.send_binary_message(message_bytes, recipient).await + } + + pub async fn send_binary_message(self, message: Vec, recipient: String) -> Self { + console_log!("Sending {} bytes to {}", message.len(), recipient); + let recipient = Recipient::try_from_base58_string(recipient).unwrap(); - let topology = self - .topology + let input_msg = InputMessage::new_fresh(recipient, message, false); + + self.input_tx .as_ref() - .expect("did not obtain topology before"); - - let message_preparer = self.message_preparer.as_mut().unwrap(); - - let (split_message, _reply_keys) = message_preparer - .prepare_and_split_message(message_bytes, false, topology) - .expect("failed to split the message"); - - let mut mix_packets = Vec::with_capacity(split_message.len()); - for message_chunk in split_message { - // don't bother with acks etc. for time being - let prepared_fragment = message_preparer - .prepare_chunk_for_sending(message_chunk, topology, &self.ack_key, &recipient) - .unwrap(); - - console_warn!("packet is going to have round trip time of {:?}, but we're not going to do anything for acks anyway ", prepared_fragment.total_delay); - mix_packets.push(prepared_fragment.mix_packet); - } - self.gateway_client - .as_mut() - .unwrap() - .batch_send_mix_packets(mix_packets) - .await + .expect("start method was not called before!") + .unbounded_send(input_msg) .unwrap(); + self } - - pub(crate) fn choose_gateway(&self) -> &gateway::Node { - let topology = self - .topology - .as_ref() - .expect("did not obtain topology before"); - - // choose the first one available - assert!(!topology.gateways().is_empty()); - topology.gateways().first().unwrap() - } - - // Right now it's impossible to have async exported functions to take `&mut self` rather than mut self - // self: Rc - // or this: Rc> - pub async fn get_and_update_topology(mut self) -> Self { - let new_topology = self.get_nym_topology().await; - self.update_topology(new_topology); - self - } - - pub(crate) fn update_topology(&mut self, topology: NymTopology) { - self.topology = Some(topology) - } - - // when updated to 0.10.0, to prevent headache later on, this function requires those two imports: - // use js_sys::Promise; - // use wasm_bindgen_futures::future_to_promise; - // - // pub fn get_full_topology_json(&self) -> Promise { - // let validator_client_config = validator_client::Config::new( - // vec![self.validator_server.clone()], - // &self.mixnet_contract_address, - // ); - // let validator_client = validator_client::Client::new(validator_client_config); - // - // future_to_promise(async move { - // let topology = &validator_client.get_active_topology().await.unwrap(); - // Ok(JsValue::from_serde(&topology).unwrap()) - // }) - // } - - pub(crate) async fn get_nym_topology(&self) -> NymTopology { - let validator_client = validator_client::ApiClient::new(self.validator_server.clone()); - - let mixnodes = match validator_client.get_cached_active_mixnodes().await { - Err(err) => panic!("{:?}", err), - Ok(mixes) => mixes, - }; - - let gateways = match validator_client.get_cached_gateways().await { - Err(err) => panic!("{}", err), - Ok(gateways) => gateways, - }; - - let topology = nym_topology_from_detailed(mixnodes, gateways); - let version = env!("CARGO_PKG_VERSION"); - topology.filter_system_version(version) - } } diff --git a/clients/webassembly/src/client/received_processor.rs b/clients/webassembly/src/client/received_processor.rs deleted file mode 100644 index a59a0dfa59..0000000000 --- a/clients/webassembly/src/client/received_processor.rs +++ /dev/null @@ -1,183 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crypto::asymmetric::encryption; -use futures::StreamExt; -use gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver}; -use nymsphinx::acknowledgements::identifier::recover_identifier; -use nymsphinx::acknowledgements::AckKey; -use nymsphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}; -use nymsphinx::receiver::{MessageReceiver, MessageRecoveryError, ReconstructedMessage}; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::sync::Arc; -use wasm_bindgen::JsValue; -use wasm_utils::{console_error, console_log, console_warn}; - -#[derive(Serialize, Deserialize)] -pub struct ProcessedMessage { - pub message: String, - pub reply_surb: Option, -} - -impl From for ProcessedMessage { - fn from(reconstructed: ReconstructedMessage) -> Self { - ProcessedMessage { - message: String::from_utf8_lossy(&reconstructed.message).into_owned(), - reply_surb: reconstructed - .reply_surb - .map(|reply_surb| reply_surb.to_base58_string()), - } - } -} - -pub(crate) struct ReceivedMessagesProcessor { - local_encryption_keypair: Arc, - ack_key: Arc, - message_receiver: MessageReceiver, - - recently_reconstructed: HashSet, -} - -impl ReceivedMessagesProcessor { - pub(crate) fn new( - local_encryption_keypair: Arc, - ack_key: Arc, - ) -> Self { - ReceivedMessagesProcessor { - local_encryption_keypair, - ack_key, - message_receiver: MessageReceiver::new(), - recently_reconstructed: HashSet::new(), - } - } - - // TODO: duplicate code from received_buffer.rs in client-core.... - fn process_received_fragment(&mut self, raw_fragment: Vec) -> Option { - let fragment_data = match self - .message_receiver - .recover_plaintext(self.local_encryption_keypair.private_key(), raw_fragment) - { - Err(e) => { - console_warn!("failed to recover fragment data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e); - return None; - } - Ok(frag_data) => frag_data, - }; - - if nymsphinx::cover::is_cover(&fragment_data) { - // currently won't be the case for a loooong time - console_log!("The message was a loop cover message! Skipping it"); - return None; - } - - let fragment = match self.message_receiver.recover_fragment(&fragment_data) { - Err(e) => { - console_warn!("failed to recover fragment from raw data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e); - return None; - } - Ok(frag) => frag, - }; - - if self.recently_reconstructed.contains(&fragment.id()) { - console_warn!("Received a chunk of already re-assembled message ({:?})! It probably got here because the ack got lost", fragment.id()); - return None; - } - - // if we returned an error the underlying message is malformed in some way - match self.message_receiver.insert_new_fragment(fragment) { - Err(err) => match err { - MessageRecoveryError::MalformedReconstructedMessage(message_sets) => { - // TODO: should we really insert reconstructed sets? could this be abused for some attack? - for set_id in message_sets { - if !self.recently_reconstructed.insert(set_id) { - // or perhaps we should even panic at this point? - console_error!( - "Reconstructed another message containing already used set id!" - ) - } - } - None - } - _ => unreachable!( - "no other error kind should have been returned here! If so, it's a bug!" - ), - }, - Ok(reconstruction_result) => match reconstruction_result { - Some((reconstructed_message, used_sets)) => { - for set_id in used_sets { - if !self.recently_reconstructed.insert(set_id) { - // or perhaps we should even panic at this point? - console_error!( - "Reconstructed another message containing already used set id!" - ) - } - } - Some(reconstructed_message.into()) - } - None => None, - }, - } - } - - // TODO: duplicate code from acknowledgement listener... - fn process_received_ack(&self, ack_content: Vec) { - let frag_id = match recover_identifier(&self.ack_key, &ack_content) - .map(FragmentIdentifier::try_from_bytes) - { - Some(Ok(frag_id)) => frag_id, - _ => { - console_warn!("Received invalid ACK!"); // should we do anything else about that? - return; - } - }; - - // if we received an ack for cover message or a reply there will be nothing to remove, - // because nothing was inserted in the first place - if frag_id == COVER_FRAG_ID { - return; - } else if frag_id.is_reply() { - console_warn!("Received an ack for a reply message - no need to do anything! (don't know what to do!)"); - // TODO: probably there will need to be some extra procedure here, something to notify - // user that his reply reached the recipient (since we got an ack) - return; - } - - console_log!( - "Received an ack for fragment {:?} but can't do anything more about it just yet...", - frag_id - ); - - // here be ack handling... - } - - // TODO: this needs to have a shutdown signal! - pub(crate) async fn start_processing( - mut self, - mixnet_messages_receiver: MixnetMessageReceiver, - ack_receiver: AcknowledgementReceiver, - on_message: js_sys::Function, - ) { - let mut fused_mixnet_messages_receiver = mixnet_messages_receiver.fuse(); - let mut fused_ack_receiver = ack_receiver.fuse(); - let this = JsValue::null(); - - loop { - futures::select! { - mix_msgs = fused_mixnet_messages_receiver.next() => { - for mix_msg in mix_msgs.unwrap() { - if let Some(processed) = self.process_received_fragment(mix_msg) { - let arg1 = JsValue::from_serde(&processed).unwrap(); - on_message.call1(&this, &arg1).expect("on message failed!"); - } - } - } - acks = fused_ack_receiver.next() => { - for ack in acks.unwrap() { - self.process_received_ack(ack); - } - } - } - } - } -} diff --git a/clients/webassembly/src/gateway_selector.rs b/clients/webassembly/src/gateway_selector.rs new file mode 100644 index 0000000000..f20ca90fbb --- /dev/null +++ b/clients/webassembly/src/gateway_selector.rs @@ -0,0 +1,44 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use client_core::config::GatewayEndpoint; +use wasm_bindgen::prelude::*; + +#[wasm_bindgen] +pub async fn get_gateway(api_server: String, preferred: Option) -> GatewayEndpoint { + let validator_client = validator_client::ApiClient::new(api_server.parse().unwrap()); + + let gateways = match validator_client.get_cached_gateways().await { + Err(err) => panic!("failed to obtain list of all gateways - {}", err), + Ok(gateways) => gateways, + }; + + if let Some(preferred) = preferred { + if let Some(details) = gateways + .iter() + .find(|g| g.gateway.identity_key == preferred) + { + return GatewayEndpoint { + gateway_id: details.gateway.identity_key.clone(), + gateway_owner: details.owner.to_string(), + gateway_listener: format!( + "ws://{}:{}", + details.gateway.host, details.gateway.clients_port + ), + }; + } + } + + let details = gateways + .first() + .expect("current topology holds no gateways"); + + GatewayEndpoint { + gateway_id: details.gateway.identity_key.clone(), + gateway_owner: details.owner.to_string(), + gateway_listener: format!( + "ws://{}:{}", + details.gateway.host, details.gateway.clients_port + ), + } +} diff --git a/clients/webassembly/src/lib.rs b/clients/webassembly/src/lib.rs index bedff27424..fd18bec107 100644 --- a/clients/webassembly/src/lib.rs +++ b/clients/webassembly/src/lib.rs @@ -5,6 +5,8 @@ use wasm_bindgen::prelude::*; #[cfg(target_arch = "wasm32")] mod client; +#[cfg(target_arch = "wasm32")] +pub mod gateway_selector; #[wasm_bindgen] pub fn set_panic_hook() { diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index d2ae078e07..01557331fa 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -14,9 +14,8 @@ log = "0.4" thiserror = "1.0" url = "2.2" rand = { version = "0.7.3", features = ["wasm-bindgen"] } -secp256k1 = { version = "0.20.3", optional = true } -web3 = { version = "0.17.0", default-features = false, optional = true } async-trait = { version = "0.1.51" } +tokio = { version = "1.21.2", features = ["macros"] } # internal coconut-interface = { path = "../../coconut-interface", optional = true } @@ -28,13 +27,14 @@ nymsphinx = { path = "../../nymsphinx" } pemstore = { path = "../../pemstore" } validator-client = { path = "../validator-client", optional = true } + [dependencies.tungstenite] version = "0.13" default-features = false # non-wasm-only dependencies [target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] -version = "1.19.1" +version = "1.21.2" features = ["macros", "rt", "net", "sync", "time"] [target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-stream] @@ -61,8 +61,9 @@ version = "0.4" path = "../../wasm-utils" # only import it in wasm. Prefer proper tokio timer in non-wasm -[target."cfg(target_arch = \"wasm32\")".dependencies.fluvio-wasm-timer] -version = "0.2.5" +[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-timer] +git = "https://github.com/mmsinclair/wasm-timer" +rev = "b9d1a54ad514c2f230a026afe0dde341e98cd7b6" # this is due to tungstenite using `rand` 0.8 and associated changes in `getrandom` crate # which now does not support wasm32-unknown-unknown target by default. @@ -80,4 +81,3 @@ features = ["js"] [features] coconut = ["gateway-requests/coconut", "coconut-interface", "validator-client", "credentials/coconut"] wasm = [] -default = ["web3/default", "secp256k1"] diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index 13f13d6119..9e17f55ff8 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -36,7 +36,7 @@ use tungstenite::protocol::Message; use tokio_tungstenite::connect_async; #[cfg(target_arch = "wasm32")] -use fluvio_wasm_timer as wasm_timer; +use wasm_timer; #[cfg(target_arch = "wasm32")] use wasm_utils::websocket::JSWebsocket; diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index bda9352a3b..bb6f71f564 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -11,8 +11,6 @@ use thiserror::Error; use tungstenite::Error as WsError; #[cfg(target_arch = "wasm32")] use wasm_bindgen::JsValue; -#[cfg(not(feature = "coconut"))] -use web3::{contract::Error as Web3ContractError, Error as Web3Error}; #[derive(Debug, Error)] pub enum GatewayClientError { @@ -37,18 +35,6 @@ pub enum GatewayClientError { #[error("There was a network error")] NetworkErrorWasm(JsValue), - #[cfg(not(feature = "coconut"))] - #[error("Could not burn ERC20 token in Ethereum smart contract - {0}")] - BurnTokenError(#[from] Web3Error), - - #[cfg(not(feature = "coconut"))] - #[error("Could not run web3 contract - {0}")] - Web3ContractError(#[from] Web3ContractError), - - #[cfg(not(feature = "coconut"))] - #[error("Invalid Ethereum private key")] - InvalidEthereumPrivateKey, - #[error("Invalid URL - {0}")] InvalidURL(String), diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index 81e682c019..1becf01b4a 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -25,3 +25,15 @@ pub(crate) fn cleanup_socket_message( None => Err(GatewayClientError::ConnectionAbruptlyClosed), } } + +pub(crate) fn cleanup_socket_messages( + msgs: Option>>, +) -> Result, GatewayClientError> { + match msgs { + Some(msgs) => msgs + .into_iter() + .map(|msg| msg.map_err(GatewayClientError::NetworkError)) + .collect(), + None => Err(GatewayClientError::ConnectionAbruptlyClosed), + } +} diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs index 13977efdc7..ed39c36984 100644 --- a/common/client-libs/gateway-client/src/packet_router.rs +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -59,11 +59,12 @@ impl PacketRouter { } else if received_packet.len() == PacketSize::RegularPacket.plaintext_size() - ack_overhead { + trace!("routing regular packet"); received_messages.push(received_packet); } else if received_packet.len() == PacketSize::ExtendedPacket.plaintext_size() - ack_overhead { - warn!("received extended packet? Did not expect this..."); + trace!("routing extended packet"); received_messages.push(received_packet); } else { // this can happen if other clients are not padding their messages diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index bc1111ab09..df1b95c0ae 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -1,12 +1,12 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::cleanup_socket_message; +use crate::cleanup_socket_messages; use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; use futures::channel::oneshot; use futures::stream::{SplitSink, SplitStream}; -use futures::{FutureExt, SinkExt, StreamExt}; +use futures::{SinkExt, StreamExt}; use gateway_requests::registration::handshake::SharedKeys; use gateway_requests::BinaryResponse; use log::*; @@ -44,16 +44,15 @@ pub(crate) struct PartiallyDelegated { } impl PartiallyDelegated { - fn route_socket_message( - ws_msg: Message, - packet_router: &mut PacketRouter, - shared_key: &SharedKeys, - ) -> Result<(), GatewayClientError> { - match ws_msg { - Message::Binary(bin_msg) => { - // this function decrypts the request and checks the MAC - let plaintext = - match BinaryResponse::try_from_encrypted_tagged_bytes(bin_msg, shared_key) { + fn recover_received_plaintexts(ws_msgs: Vec, shared_key: &SharedKeys) -> Vec> { + let mut plaintexts = Vec::with_capacity(ws_msgs.len()); + for ws_msg in ws_msgs { + match ws_msg { + Message::Binary(bin_msg) => { + // this function decrypts the request and checks the MAC + let plaintext = match BinaryResponse::try_from_encrypted_tagged_bytes( + bin_msg, shared_key, + ) { Ok(bin_response) => match bin_response { BinaryResponse::PushedMixMessage(plaintext) => plaintext, }, @@ -62,29 +61,37 @@ impl PartiallyDelegated { "message received from the gateway was malformed! - {:?}", err ); - return Ok(()); + continue; } }; + plaintexts.push(plaintext) + } + // I think that in the future we should perhaps have some sequence number system, i.e. + // so each request/response pair can be easily identified, so that if messages are + // not ordered (for some peculiar reason) we wouldn't lose anything. + // This would also require NOT discarding any text responses here. - // TODO: some batching mechanism to allow reading and sending more than - // one packet at the time, because the receiver can easily handle it - packet_router.route_received(vec![plaintext]) - } - // I think that in the future we should perhaps have some sequence number system, i.e. - // so each request/response pair can be easily identified, so that if messages are - // not ordered (for some peculiar reason) we wouldn't lose anything. - // This would also require NOT discarding any text responses here. - - // TODO: those can return the "send confirmations" - perhaps it should be somehow worked around? - Message::Text(text) => { - trace!( + // TODO: those can return the "send confirmations" - perhaps it should be somehow worked around? + Message::Text(text) => { + trace!( "received a text message - probably a response to some previous query! - {}", text ); - Ok(()) + continue; + } + _ => continue, } - _ => Ok(()), } + plaintexts + } + + fn route_socket_messages( + ws_msgs: Vec, + packet_router: &mut PacketRouter, + shared_key: &SharedKeys, + ) -> Result<(), GatewayClientError> { + let plaintexts = Self::recover_received_plaintexts(ws_msgs, shared_key); + packet_router.route_received(plaintexts) } pub(crate) fn split_and_listen_for_mixnet_messages( @@ -101,47 +108,46 @@ impl PartiallyDelegated { let (sink, mut stream) = conn.split(); let mixnet_receiver_future = async move { - let mut fused_receiver = notify_receiver.fuse(); - let mut fused_stream = (&mut stream).fuse(); + let mut notify_receiver = notify_receiver; + let mut chunk_stream = (&mut stream).ready_chunks(8); let mut packet_router = packet_router; // Bit of an ugly workaround for selecting on an `Option` without having access to // `tokio::select` #[cfg(not(target_arch = "wasm32"))] let shutdown = { - let m_shutdown = shutdown.clone(); async { - if let Some(mut s) = m_shutdown { + if let Some(mut s) = shutdown { s.recv().await } else { std::future::pending::<()>().await } } - .fuse() }; #[cfg(not(target_arch = "wasm32"))] tokio::pin!(shutdown); #[cfg(target_arch = "wasm32")] - let mut shutdown = std::future::pending::<()>().fuse(); + let mut shutdown = std::future::pending::<()>(); let ret_err = loop { - futures::select! { - _ = shutdown => { + tokio::select! { + _ = &mut shutdown => { log::trace!("GatewayClient listener: Received shutdown"); log::debug!("GatewayClient listener: Exiting"); return; } - _ = &mut fused_receiver => { + _ = &mut notify_receiver => { break Ok(()); } - msg = fused_stream.next() => { - let ws_msg = match cleanup_socket_message(msg) { + msgs = chunk_stream.next() => { + let ws_msgs = match cleanup_socket_messages(msgs) { Err(err) => break Err(err), - Ok(msg) => msg + Ok(msgs) => msgs }; - if let Err(err) = Self::route_socket_message(ws_msg, &mut packet_router, shared_key.as_ref()) { - log::warn!("Route socket message failed: {:?}", err); + + if let Err(err) = Self::route_socket_messages(ws_msgs, &mut packet_router, shared_key.as_ref()) { + log::warn!("Route socket messages failed: {:?}", err); } } }; diff --git a/common/client-libs/mixnet-client/Cargo.toml b/common/client-libs/mixnet-client/Cargo.toml index c99ddabbbb..5267ecb366 100644 --- a/common/client-libs/mixnet-client/Cargo.toml +++ b/common/client-libs/mixnet-client/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" [dependencies] futures = "0.3" log = "0.4.8" -tokio = { version = "1.19.1", features = ["time", "net", "rt"] } +tokio = { version = "1.21.2", features = ["time", "net", "rt"] } tokio-util = { version = "0.7.3", features = ["codec"] } # internal diff --git a/common/client-libs/mixnet-client/src/client.rs b/common/client-libs/mixnet-client/src/client.rs index 4e089a30c3..181eac65ec 100644 --- a/common/client-libs/mixnet-client/src/client.rs +++ b/common/client-libs/mixnet-client/src/client.rs @@ -186,7 +186,7 @@ impl Client { address.into(), receiver, initial_connection_timeout, - &*current_reconnection_attempt, + ¤t_reconnection_attempt, ) .await }); diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 67c472710c..0beeefe809 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -22,7 +22,7 @@ reqwest = { version = "0.11", features = ["json"] } thiserror = "1" log = "0.4" url = { version = "2.2", features = ["serde"] } -tokio = { version = "1.19.1", features = ["sync", "time"] } +tokio = { version = "1.21.2", features = ["sync", "time"] } futures = "0.3" coconut-interface = { path = "../../coconut-interface" } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 30ddb307b2..a4d0a22f05 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -3,7 +3,7 @@ use crate::{validator_api, ValidatorClientError}; use mixnet_contract_common::mixnode::MixNodeDetails; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; use url::Url; use validator_api_requests::coconut::{ @@ -11,11 +11,9 @@ use validator_api_requests::coconut::{ VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ - DeprecatedRewardEstimationResponse, DeprecatedUptimeResponse, GatewayCoreStatusResponse, - MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, - StakeSaturationResponse, + GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, + RewardEstimationResponse, StakeSaturationResponse, }; -use validator_api_requests::Deprecated; #[cfg(feature = "nymd-client")] use crate::nymd::traits::MixnetQueryClient; @@ -208,7 +206,7 @@ impl Client { // basically handles paging for us pub async fn get_all_nymd_rewarded_set_mixnodes( &self, - ) -> Result, ValidatorClientError> + ) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync + Send, { @@ -282,7 +280,7 @@ impl Client { pub async fn get_all_nymd_unbonded_mixnodes( &self, - ) -> Result, ValidatorClientError> + ) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync + Send, { @@ -308,7 +306,7 @@ impl Client { pub async fn get_all_nymd_unbonded_mixnodes_by_owner( &self, owner: &cosmrs::AccountId, - ) -> Result, ValidatorClientError> + ) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync + Send, { @@ -334,7 +332,7 @@ impl Client { pub async fn get_all_nymd_unbonded_mixnodes_by_identity( &self, identity_key: String, - ) -> Result, ValidatorClientError> + ) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync + Send, { @@ -386,7 +384,7 @@ impl Client { pub async fn get_all_nymd_single_mixnode_delegations( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync + Send, @@ -634,7 +632,7 @@ impl ApiClient { pub async fn get_mixnode_core_status_count( &self, - mix_id: NodeId, + mix_id: MixId, since: Option, ) -> Result { Ok(self @@ -645,14 +643,14 @@ impl ApiClient { pub async fn get_mixnode_status( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result { Ok(self.validator_api.get_mixnode_status(mix_id).await?) } pub async fn get_mixnode_reward_estimation( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result { Ok(self .validator_api @@ -662,7 +660,7 @@ impl ApiClient { pub async fn get_mixnode_stake_saturation( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result { Ok(self .validator_api @@ -706,69 +704,4 @@ impl ApiClient { .verify_bandwidth_credential(request_body) .await?) } - - // ================================================= - // DEPRECATED ROUTES - // TO REMOVE ONCE OTHER PARTS OF THE SYSTEM MIGRATED - // ================================================= - - pub async fn deprecated_get_mixnode_core_status_count_by_identity( - &self, - identity: IdentityKeyRef<'_>, - since: Option, - ) -> Result, ValidatorClientError> { - Ok(self - .validator_api - .deprecated_get_mixnode_core_status_count_by_identity(identity, since) - .await?) - } - - pub async fn deprecated_get_mixnode_status_by_identity( - &self, - identity: IdentityKeyRef<'_>, - ) -> Result, ValidatorClientError> { - Ok(self - .validator_api - .deprecated_get_mixnode_status_by_identity(identity) - .await?) - } - - pub async fn deprecated_get_mixnode_reward_estimation_by_identity( - &self, - identity: IdentityKeyRef<'_>, - ) -> Result { - Ok(self - .validator_api - .deprecated_get_mixnode_reward_estimation_by_identity(identity) - .await?) - } - - pub async fn deprecated_get_mixnode_stake_saturation_by_identity( - &self, - identity: IdentityKeyRef<'_>, - ) -> Result, ValidatorClientError> { - Ok(self - .validator_api - .deprecated_get_mixnode_stake_saturation_by_identity(identity) - .await?) - } - - pub async fn deprecated_get_mixnode_avg_uptime_by_identity( - &self, - identity: IdentityKeyRef<'_>, - ) -> Result { - Ok(self - .validator_api - .deprecated_get_mixnode_avg_uptime_by_identity(identity) - .await?) - } - - pub async fn deprecated_get_mixnode_avg_uptimes_by_identity( - &self, - ) -> Result, ValidatorClientError> { - Ok(self - .validator_api - .deprecated_get_mixnode_avg_uptimes_by_identity() - .await?) - } } diff --git a/common/client-libs/validator-client/src/connection_tester.rs b/common/client-libs/validator-client/src/connection_tester.rs index 3193fc32e0..dc1acb8cd9 100644 --- a/common/client-libs/validator-client/src/connection_tester.rs +++ b/common/client-libs/validator-client/src/connection_tester.rs @@ -1,12 +1,12 @@ use crate::nymd::error::NymdError; use crate::nymd::{Config as ClientConfig, NymdClient, QueryNymdClient}; use crate::ApiClient; -use network_defaults::all::Network; use crate::nymd::traits::MixnetQueryClient; use colored::Colorize; use core::fmt; use itertools::Itertools; +use network_defaults::NymNetworkDetails; use std::collections::HashMap; use std::hash::BuildHasher; use std::time::Duration; @@ -18,12 +18,12 @@ const CONNECTION_TEST_TIMEOUT_SEC: u64 = 2; // Run connection tests for all specified nymd and api urls. These are all run concurrently. pub async fn run_validator_connection_test( - nymd_urls: impl Iterator, - api_urls: impl Iterator, - mixnet_contract_address: HashMap, + nymd_urls: impl Iterator, + api_urls: impl Iterator, + mixnet_contract_address: HashMap, ) -> ( - HashMap>, - HashMap>, + HashMap>, + HashMap>, ) { // Setup all the clients for the connection tests let connection_test_clients = @@ -46,16 +46,16 @@ pub async fn run_validator_connection_test( } fn setup_connection_tests( - nymd_urls: impl Iterator, - api_urls: impl Iterator, - mixnet_contract_address: HashMap, + nymd_urls: impl Iterator, + api_urls: impl Iterator, + mixnet_contract_address: HashMap, ) -> impl Iterator { let nymd_connection_test_clients = nymd_urls.filter_map(move |(network, url)| { let address = mixnet_contract_address .get(&network) .expect("No configured contract address") .clone(); - let config = ClientConfig::try_from_nym_network_details(&network.details()) + let config = ClientConfig::try_from_nym_network_details(&network) .expect("failed to create valid nymd client config"); if let Ok(mut client) = NymdClient::::connect(config, url.as_str()) { @@ -81,7 +81,7 @@ fn setup_connection_tests( fn extract_and_collect_results_into_map( connection_results: &[ConnectionResult], url_type: &UrlType, -) -> HashMap> { +) -> HashMap> { connection_results .iter() .filter(|c| &c.url_type() == url_type) @@ -93,7 +93,7 @@ fn extract_and_collect_results_into_map( } async fn test_nymd_connection( - network: Network, + network: NymNetworkDetails, url: &Url, client: &NymdClient, ) -> ConnectionResult { @@ -105,56 +105,47 @@ async fn test_nymd_connection( { Ok(Err(NymdError::TendermintError(e))) => { // If we get a tendermint-rpc error, we classify the node as not contactable - log::debug!( - "Checking: nymd_url: {network}: {url}: {}: {}", - "failed".red(), - e - ); + log::debug!("Checking: nymd_url: {url}: {}: {}", "failed".red(), e); false } Ok(Err(NymdError::AbciError(code, log))) => { // We accept the mixnet contract not found as ok from a connection standpoint. This happens // for example on a pre-launch network. log::debug!( - "Checking: nymd_url: {network}: {url}: {}, but with abci error: {code}: {log}", + "Checking: nymd_url: {url}: {}, but with abci error: {code}: {log}", "success".green() ); code == 18 } Ok(Err(error @ NymdError::NoContractAddressAvailable)) => { - log::debug!( - "Checking: nymd_url: {network}: {url}: {}: {error}", - "failed".red() - ); + log::debug!("Checking: nymd_url: {url}: {}: {error}", "failed".red()); false } Ok(Err(e)) => { // For any other error, we're optimistic and just try anyway. log::debug!( - "Checking: nymd_url: {network}: {url}: {}, but with error: {e}", + "Checking: nymd_url: {url}: {}, but with error: {e}", "success".green() ); true } Ok(Ok(_)) => { - log::debug!( - "Checking: nymd_url: {network}: {url}: {}", - "success".green() - ); + log::debug!("Checking: nymd_url: {url}: {}", "success".green()); true } Err(e) => { - log::debug!( - "Checking: nymd_url: {network}: {url}: {}: {e}", - "failed".red() - ); + log::debug!("Checking: nymd_url: {url}: {}: {e}", "failed".red()); false } }; ConnectionResult::Nymd(network, url.clone(), result) } -async fn test_api_connection(network: Network, url: &Url, client: &ApiClient) -> ConnectionResult { +async fn test_api_connection( + network: NymNetworkDetails, + url: &Url, + client: &ApiClient, +) -> ConnectionResult { let result = match timeout( Duration::from_secs(CONNECTION_TEST_TIMEOUT_SEC), client.get_cached_mixnodes(), @@ -162,21 +153,15 @@ async fn test_api_connection(network: Network, url: &Url, client: &ApiClient) -> .await { Ok(Ok(_)) => { - log::debug!("Checking: api_url: {network}: {url}: {}", "success".green()); + log::debug!("Checking: api_url: {url}: {}", "success".green()); true } Ok(Err(e)) => { - log::debug!( - "Checking: api_url: {network}: {url}: {}: {e}", - "failed".red() - ); + log::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); false } Err(e) => { - log::debug!( - "Checking: api_url: {network}: {url}: {}: {e}", - "failed".red() - ); + log::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); false } }; @@ -184,8 +169,8 @@ async fn test_api_connection(network: Network, url: &Url, client: &ApiClient) -> } enum ClientForConnectionTest { - Nymd(Network, Url, Box>), - Api(Network, Url, ApiClient), + Nymd(NymNetworkDetails, Url, Box>), + Api(NymNetworkDetails, Url, ApiClient), } impl ClientForConnectionTest { @@ -218,12 +203,12 @@ impl fmt::Display for UrlType { #[derive(Debug)] enum ConnectionResult { - Nymd(Network, Url, bool), - Api(Network, Url, bool), + Nymd(NymNetworkDetails, Url, bool), + Api(NymNetworkDetails, Url, bool), } impl ConnectionResult { - fn result(&self) -> (&Network, &Url, &bool) { + fn result(&self) -> (&NymNetworkDetails, &Url, &bool) { match self { ConnectionResult::Nymd(network, url, result) | ConnectionResult::Api(network, url, result) => (network, url, result), @@ -240,11 +225,8 @@ impl ConnectionResult { impl fmt::Display for ConnectionResult { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let (network, url, result) = self.result(); + let (_network, url, result) = self.result(); let url_type = self.url_type(); - write!( - f, - "{network}: {url}: {url_type}: connection is successful: {result}" - ) + write!(f, "{url}: {url_type}: connection is successful: {result}") } } diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 1a508aaa2f..481cb021be 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -15,14 +15,12 @@ use cosmrs::rpc::query::Query; use cosmrs::rpc::Error as TendermintRpcError; use cosmrs::rpc::HttpClientUrl; use cosmrs::tx::Msg; -use cosmwasm_std::Uint128; use execute::execute; use network_defaults::{ChainDetails, NymNetworkDetails}; use serde::{Deserialize, Serialize}; use std::convert::TryInto; use std::time::SystemTime; use vesting_contract_common::ExecuteMsg as VestingExecuteMsg; -use vesting_contract_common::QueryMsg as VestingQueryMsg; pub use crate::nymd::cosmwasm_client::client::CosmWasmClient; pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient; @@ -44,9 +42,10 @@ pub use cosmrs::Coin as CosmosCoin; pub use cosmrs::{bip32, AccountId, Decimal, Denom}; pub use cosmwasm_std::Coin as CosmWasmCoin; pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment}; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; pub use signing_client::Client as SigningNymdClient; pub use traits::{VestingQueryClient, VestingSigningClient}; +use vesting_contract_common::PledgeCap; pub mod coin; pub mod cosmwasm_client; @@ -149,7 +148,7 @@ impl NymdClient { // maybe the wallet could be made into a generic, but for now, let's just have this one implementation pub fn connect_with_signer( config: Config, - network: config::defaults::all::Network, + network: config::defaults::NymNetworkDetails, endpoint: U, signer: DirectSecp256k1HdWallet, gas_price: Option, @@ -157,7 +156,7 @@ impl NymdClient { where U: TryInto, { - let denom = network.base_mix_denom(); + let denom = network.chain_details.mix_denom.base; let client_address = signer .try_derive_accounts()? .into_iter() @@ -482,16 +481,6 @@ impl NymdClient { self.client.get_total_supply().await } - pub async fn vesting_get_locked_pledge_cap(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = VestingQueryMsg::GetLockedPledgeCap {}; - self.client - .query_contract_smart(self.vesting_contract_address(), &request) - .await - } - pub async fn simulate(&self, messages: I) -> Result where C: SigningCosmWasmClient + Sync, @@ -725,7 +714,7 @@ impl NymdClient { #[execute("vesting")] fn _vesting_withdraw_delegator_reward( &self, - mix_id: NodeId, + mix_id: MixId, fee: Option, ) -> (VestingExecuteMsg, Option) where @@ -737,12 +726,16 @@ impl NymdClient { #[execute("vesting")] fn _vesting_update_locked_pledge_cap( &self, - amount: Uint128, + address: String, + cap: PledgeCap, fee: Option, ) -> (VestingExecuteMsg, Option) where C: SigningCosmWasmClient + Sync, { - (VestingExecuteMsg::UpdateLockedPledgeCap { amount }, fee) + ( + VestingExecuteMsg::UpdateLockedPledgeCap { address, cap }, + fee, + ) } } diff --git a/common/client-libs/validator-client/src/nymd/traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/mixnet_query_client.rs index 4622f2d638..675c15c5d1 100644 --- a/common/client-libs/validator-client/src/nymd/traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/mixnet_query_client.rs @@ -18,10 +18,11 @@ use mixnet_contract_common::rewarding::{ use mixnet_contract_common::{ delegation, ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse, EpochEventId, GatewayBondResponse, GatewayOwnershipResponse, - IdentityKey, IntervalEventId, LayerDistribution, MixOwnershipResponse, MixnodeDetailsResponse, - NodeId, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse, - PagedMixNodeDelegationsResponse, PagedMixnodeBondsResponse, PagedRewardedSetResponse, - PendingEpochEventsResponse, PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg, + IdentityKey, IntervalEventId, LayerDistribution, MixId, MixOwnershipResponse, + MixnodeDetailsResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, + PagedGatewayResponse, PagedMixNodeDelegationsResponse, PagedMixnodeBondsResponse, + PagedRewardedSetResponse, PendingEpochEventsResponse, PendingIntervalEventsResponse, + QueryMsg as MixnetQueryMsg, }; use serde::Deserialize; @@ -65,7 +66,7 @@ pub trait MixnetQueryClient { async fn get_rewarded_set_paged( &self, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetRewardedSet { limit, start_after }) @@ -77,7 +78,7 @@ pub trait MixnetQueryClient { async fn get_mixnode_bonds_paged( &self, limit: Option, - start_after: Option, + start_after: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixNodeBonds { limit, start_after }) .await @@ -86,7 +87,7 @@ pub trait MixnetQueryClient { async fn get_mixnodes_detailed_paged( &self, limit: Option, - start_after: Option, + start_after: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixNodesDetailed { limit, start_after }) .await @@ -95,7 +96,7 @@ pub trait MixnetQueryClient { async fn get_unbonded_paged( &self, limit: Option, - start_after: Option, + start_after: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodes { limit, start_after }) .await @@ -105,7 +106,7 @@ pub trait MixnetQueryClient { &self, owner: &AccountId, limit: Option, - start_after: Option, + start_after: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodesByOwner { owner: owner.to_string(), @@ -119,7 +120,7 @@ pub trait MixnetQueryClient { &self, identity_key: String, limit: Option, - start_after: Option, + start_after: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodesByIdentityKey { identity_key, @@ -141,7 +142,7 @@ pub trait MixnetQueryClient { async fn get_mixnode_details( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeDetails { mix_id }) .await @@ -149,7 +150,7 @@ pub trait MixnetQueryClient { async fn get_mixnode_rewarding_details( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeRewardingDetails { mix_id }) .await @@ -157,7 +158,7 @@ pub trait MixnetQueryClient { async fn get_mixnode_stake_saturation( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetStakeSaturation { mix_id }) .await @@ -165,7 +166,7 @@ pub trait MixnetQueryClient { async fn get_unbonded_mixnode_information( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodeInformation { mix_id }) .await @@ -212,7 +213,7 @@ pub trait MixnetQueryClient { /// Gets list of all delegations towards particular mixnode on particular page. async fn get_mixnode_delegations_paged( &self, - mix_id: NodeId, + mix_id: MixId, start_after: Option, limit: Option, ) -> Result { @@ -228,7 +229,7 @@ pub trait MixnetQueryClient { async fn get_delegator_delegations_paged( &self, delegator: String, - start_after: Option<(NodeId, OwnerProxySubKey)>, + start_after: Option<(MixId, OwnerProxySubKey)>, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetDelegatorDelegations { @@ -242,7 +243,7 @@ pub trait MixnetQueryClient { /// Checks value of delegation of given client towards particular mixnode. async fn get_delegation_details( &self, - mix_id: NodeId, + mix_id: MixId, delegator: &AccountId, proxy: Option, ) -> Result { @@ -277,7 +278,7 @@ pub trait MixnetQueryClient { async fn get_pending_mixnode_operator_reward( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetPendingMixNodeOperatorReward { mix_id }) .await @@ -286,7 +287,7 @@ pub trait MixnetQueryClient { async fn get_pending_delegator_reward( &self, delegator: &AccountId, - mix_id: NodeId, + mix_id: MixId, proxy: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetPendingDelegatorReward { @@ -300,7 +301,7 @@ pub trait MixnetQueryClient { // given the provided performance, estimate the reward at the end of the current epoch async fn get_estimated_current_epoch_operator_reward( &self, - mix_id: NodeId, + mix_id: MixId, estimated_performance: Performance, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetEstimatedCurrentEpochOperatorReward { @@ -314,7 +315,7 @@ pub trait MixnetQueryClient { async fn get_estimated_current_epoch_delegator_reward( &self, delegator: &AccountId, - mix_id: NodeId, + mix_id: MixId, proxy: Option, estimated_performance: Performance, ) -> Result { diff --git a/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs index c1e8c8cce6..41fa7818a8 100644 --- a/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs @@ -11,7 +11,7 @@ use cosmrs::AccountId; use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; use mixnet_contract_common::reward_params::{IntervalRewardingParamsUpdate, Performance}; use mixnet_contract_common::{ - ContractStateParams, ExecuteMsg as MixnetExecuteMsg, Gateway, MixNode, NodeId, + ContractStateParams, ExecuteMsg as MixnetExecuteMsg, Gateway, MixId, MixNode, }; #[async_trait] @@ -108,7 +108,7 @@ pub trait MixnetSigningClient { async fn advance_current_epoch( &self, - new_rewarded_set: Vec, + new_rewarded_set: Vec, expected_active_set_size: u32, fee: Option, ) -> Result { @@ -324,7 +324,7 @@ pub trait MixnetSigningClient { async fn delegate_to_mixnode( &self, - mix_id: NodeId, + mix_id: MixId, amount: Coin, fee: Option, ) -> Result { @@ -339,7 +339,7 @@ pub trait MixnetSigningClient { async fn delegate_to_mixnode_on_behalf( &self, delegate: AccountId, - mix_id: NodeId, + mix_id: MixId, amount: Coin, fee: Option, ) -> Result { @@ -356,7 +356,7 @@ pub trait MixnetSigningClient { async fn undelegate_from_mixnode( &self, - mix_id: NodeId, + mix_id: MixId, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -370,7 +370,7 @@ pub trait MixnetSigningClient { async fn undelegate_to_mixnode_on_behalf( &self, delegate: AccountId, - mix_id: NodeId, + mix_id: MixId, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -388,7 +388,7 @@ pub trait MixnetSigningClient { async fn reward_mixnode( &self, - mix_id: NodeId, + mix_id: MixId, performance: Performance, fee: Option, ) -> Result { @@ -425,7 +425,7 @@ pub trait MixnetSigningClient { async fn withdraw_delegator_reward( &self, - mix_id: NodeId, + mix_id: MixId, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -439,7 +439,7 @@ pub trait MixnetSigningClient { async fn withdraw_delegator_reward_on_behalf( &self, owner: AccountId, - mix_id: NodeId, + mix_id: MixId, fee: Option, ) -> Result { self.execute_mixnet_contract( diff --git a/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs index 66ceaaa41b..22dd42952c 100644 --- a/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/vesting_query_client.rs @@ -7,7 +7,7 @@ use crate::nymd::error::NymdError; use crate::nymd::NymdClient; use async_trait::async_trait; use cosmwasm_std::{Coin as CosmWasmCoin, Timestamp}; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use vesting_contract::vesting::Account; use vesting_contract_common::{ messages::QueryMsg as VestingQueryMsg, AllDelegationsResponse, DelegationTimesResponse, @@ -76,12 +76,12 @@ pub trait VestingQueryClient { async fn get_delegation_timestamps( &self, address: &str, - mix_id: NodeId, + mix_id: MixId, ) -> Result; async fn get_all_vesting_delegations_paged( &self, - start_after: Option<(u32, NodeId, u64)>, + start_after: Option<(u32, MixId, u64)>, limit: Option, ) -> Result; @@ -269,7 +269,7 @@ impl VestingQueryClient for NymdClient { async fn get_delegation_timestamps( &self, address: &str, - mix_id: NodeId, + mix_id: MixId, ) -> Result { let request = VestingQueryMsg::GetDelegationTimes { address: address.to_string(), @@ -282,7 +282,7 @@ impl VestingQueryClient for NymdClient { async fn get_all_vesting_delegations_paged( &self, - start_after: Option<(u32, NodeId, u64)>, + start_after: Option<(u32, MixId, u64)>, limit: Option, ) -> Result { let request = VestingQueryMsg::GetAllDelegations { start_after, limit }; diff --git a/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs index 7325b17bc8..eebf0adfa3 100644 --- a/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/vesting_signing_client.rs @@ -7,8 +7,9 @@ use crate::nymd::error::NymdError; use crate::nymd::{Coin, Fee, NymdClient}; use async_trait::async_trait; use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use mixnet_contract_common::{Gateway, MixNode, NodeId}; +use mixnet_contract_common::{Gateway, MixId, MixNode}; use vesting_contract_common::messages::{ExecuteMsg as VestingExecuteMsg, VestingSpecification}; +use vesting_contract_common::PledgeCap; #[async_trait] pub trait VestingSigningClient { @@ -81,21 +82,21 @@ pub trait VestingSigningClient { async fn vesting_track_undelegation( &self, address: &str, - mix_id: NodeId, + mix_id: MixId, amount: Coin, fee: Option, ) -> Result; async fn vesting_delegate_to_mixnode( &self, - mix_id: NodeId, + mix_id: MixId, amount: Coin, fee: Option, ) -> Result; async fn vesting_undelegate_from_mixnode( &self, - mix_id: NodeId, + mix_id: MixId, fee: Option, ) -> Result; @@ -105,6 +106,7 @@ pub trait VestingSigningClient { staking_address: Option, vesting_spec: Option, amount: Coin, + cap: Option, fee: Option, ) -> Result; } @@ -330,7 +332,7 @@ impl VestingSigningClient for NymdClient async fn vesting_track_undelegation( &self, address: &str, - mix_id: NodeId, + mix_id: MixId, amount: Coin, fee: Option, ) -> Result { @@ -348,7 +350,7 @@ impl VestingSigningClient for NymdClient async fn vesting_delegate_to_mixnode( &self, - mix_id: NodeId, + mix_id: MixId, amount: Coin, fee: Option, ) -> Result { @@ -365,7 +367,7 @@ impl VestingSigningClient for NymdClient async fn vesting_undelegate_from_mixnode( &self, - mix_id: NodeId, + mix_id: MixId, fee: Option, ) -> Result { self.execute_vesting_contract( @@ -382,6 +384,7 @@ impl VestingSigningClient for NymdClient staking_address: Option, vesting_spec: Option, amount: Coin, + cap: Option, fee: Option, ) -> Result { let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); @@ -389,6 +392,7 @@ impl VestingSigningClient for NymdClient owner_address: owner_address.to_string(), staking_address, vesting_spec, + cap, }; self.client .execute( diff --git a/common/client-libs/validator-client/src/nymd/wallet/mod.rs b/common/client-libs/validator-client/src/nymd/wallet/mod.rs index e071d48853..c78aacde70 100644 --- a/common/client-libs/validator-client/src/nymd/wallet/mod.rs +++ b/common/client-libs/validator-client/src/nymd/wallet/mod.rs @@ -72,7 +72,7 @@ impl DirectSecp256k1HdWallet { } fn derive_keypair(&self, hd_path: &DerivationPath) -> Result { - let extended_private_key = XPrv::derive_from_path(&self.seed, hd_path)?; + let extended_private_key = XPrv::derive_from_path(self.seed, hd_path)?; let private_key: SigningKey = extended_private_key.into(); let public_key = private_key.public_key(); @@ -207,8 +207,9 @@ impl DirectSecp256k1HdWalletBuilder { #[cfg(test)] mod tests { + use network_defaults::NymNetworkDetails; + use super::*; - use network_defaults::all::Network::*; #[test] fn generating_account_addresses() { @@ -218,7 +219,9 @@ mod tests { "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" ]; - let prefix = MAINNET.bech32_prefix(); + let prefix = NymNetworkDetails::new_mainnet() + .chain_details + .bech32_account_prefix; let addrs = vec![ "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", diff --git a/common/client-libs/validator-client/src/validator_api/mod.rs b/common/client-libs/validator-client/src/validator_api/mod.rs index a63e714066..ee501a19e0 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -4,7 +4,7 @@ use crate::validator_api::error::ValidatorAPIError; use crate::validator_api::routes::{CORE_STATUS_COUNT, SINCE_ARG}; use mixnet_contract_common::mixnode::MixNodeDetails; -use mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId}; +use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; use serde::{Deserialize, Serialize}; use url::Url; use validator_api_requests::coconut::{ @@ -12,11 +12,10 @@ use validator_api_requests::coconut::{ VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ - DeprecatedRewardEstimationResponse, DeprecatedUptimeResponse, GatewayCoreStatusResponse, - InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeCoreStatusResponse, - MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, + GatewayCoreStatusResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, + MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, + StakeSaturationResponse, UptimeResponse, }; -use validator_api_requests::Deprecated; pub mod error; pub mod routes; @@ -59,7 +58,6 @@ impl Client { V: AsRef, { let url = create_api_url(&self.url, path, params); - log::trace!("url: {:?}", url.as_str()); Ok(self.reqwest_client.get(url).send().await?.json().await?) } @@ -185,7 +183,7 @@ impl Client { pub async fn get_mixnode_core_status_count( &self, - mix_id: NodeId, + mix_id: MixId, since: Option, ) -> Result { if let Some(since) = since { @@ -217,7 +215,7 @@ impl Client { pub async fn get_mixnode_status( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result { self.query_validator_api( &[ @@ -234,7 +232,7 @@ impl Client { pub async fn get_mixnode_reward_estimation( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result { self.query_validator_api( &[ @@ -251,7 +249,7 @@ impl Client { pub async fn get_mixnode_stake_saturation( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result { self.query_validator_api( &[ @@ -268,7 +266,7 @@ impl Client { pub async fn get_mixnode_inclusion_probability( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result { self.query_validator_api( &[ @@ -285,7 +283,7 @@ impl Client { pub async fn get_mixnode_avg_uptime( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Result { self.query_validator_api( &[ @@ -378,151 +376,6 @@ impl Client { ) .await } - - // ================================================= - // DEPRECATED ROUTES - // TO REMOVE ONCE OTHER PARTS OF THE SYSTEM MIGRATED - // ================================================= - - pub async fn deprecated_get_mixnode_core_status_count_by_identity( - &self, - identity: IdentityKeyRef<'_>, - since: Option, - ) -> Result, ValidatorAPIError> { - if let Some(since) = since { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODE, - routes::DEPRECATED, - identity, - CORE_STATUS_COUNT, - ], - &[(SINCE_ARG, since.to_string())], - ) - .await - } else { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODE, - routes::DEPRECATED, - identity, - CORE_STATUS_COUNT, - ], - NO_PARAMS, - ) - .await - } - } - - pub async fn deprecated_get_mixnode_status_by_identity( - &self, - identity: IdentityKeyRef<'_>, - ) -> Result, ValidatorAPIError> { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODE, - routes::DEPRECATED, - identity, - routes::STATUS, - ], - NO_PARAMS, - ) - .await - } - - pub async fn deprecated_get_mixnode_reward_estimation_by_identity( - &self, - identity: IdentityKeyRef<'_>, - ) -> Result { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODE, - routes::DEPRECATED, - identity, - routes::REWARD_ESTIMATION, - ], - NO_PARAMS, - ) - .await - } - - pub async fn deprecated_get_mixnode_stake_saturation_by_identity( - &self, - identity: IdentityKeyRef<'_>, - ) -> Result, ValidatorAPIError> { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODE, - routes::DEPRECATED, - identity, - routes::STAKE_SATURATION, - ], - NO_PARAMS, - ) - .await - } - - pub async fn deprecated_get_mixnode_inclusion_probability_by_identity( - &self, - identity: IdentityKeyRef<'_>, - ) -> Result, ValidatorAPIError> { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODE, - routes::DEPRECATED, - identity, - routes::INCLUSION_CHANCE, - ], - NO_PARAMS, - ) - .await - } - - pub async fn deprecated_get_mixnode_avg_uptime_by_identity( - &self, - identity: IdentityKeyRef<'_>, - ) -> Result { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODE, - routes::DEPRECATED, - identity, - routes::AVG_UPTIME, - ], - NO_PARAMS, - ) - .await - } - - pub async fn deprecated_get_mixnode_avg_uptimes_by_identity( - &self, - ) -> Result, ValidatorAPIError> { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODES, - routes::DEPRECATED, - routes::AVG_UPTIME, - ], - NO_PARAMS, - ) - .await - } } // utility function that should solve the double slash problem in validator API forever. diff --git a/common/client-libs/validator-client/src/validator_api/routes.rs b/common/client-libs/validator-client/src/validator_api/routes.rs index 04e657bb46..8f1a125c80 100644 --- a/common/client-libs/validator-client/src/validator_api/routes.rs +++ b/common/client-libs/validator-client/src/validator_api/routes.rs @@ -6,7 +6,6 @@ use network_defaults::VALIDATOR_API_VERSION; pub const API_VERSION: &str = VALIDATOR_API_VERSION; pub const MIXNODES: &str = "mixnodes"; pub const GATEWAYS: &str = "gateways"; -pub const DEPRECATED: &str = "deprecated"; pub const DETAILED: &str = "detailed"; pub const ACTIVE: &str = "active"; diff --git a/common/commands/src/validator/account/balance.rs b/common/commands/src/validator/account/balance.rs index 8ac24b273f..eb8e2dfa8f 100644 --- a/common/commands/src/validator/account/balance.rs +++ b/common/commands/src/validator/account/balance.rs @@ -51,7 +51,7 @@ pub async fn query_balance( return; } - let denom = args.denom.unwrap_or_else(|| "".to_string()); + let denom = args.denom.unwrap_or_default(); for coin in coins { if denom.is_empty() || denom.eq_ignore_ascii_case(&coin.denom) { diff --git a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs index c2bd86c7ee..50b4880c6e 100644 --- a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use mixnet_contract_common::{Coin, NodeId}; +use mixnet_contract_common::{Coin, MixId}; use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs index cdf0ef4140..d816e65a0c 100644 --- a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs +++ b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs @@ -9,7 +9,7 @@ use crate::utils::{pretty_cosmwasm_coin, show_error_passthrough}; use comfy_table::Table; use cosmwasm_std::Addr; -use mixnet_contract_common::{Delegation, PendingEpochEvent, PendingEpochEventData}; +use mixnet_contract_common::{Delegation, PendingEpochEvent, PendingEpochEventKind}; #[derive(Debug, Parser)] pub struct Args {} @@ -63,7 +63,7 @@ async fn print_delegations(delegations: Vec, client: &SigningClientW for delegation in delegations { table.add_row(vec![ to_iso_timestamp(delegation.height as u32, client).await, - delegation.node_id.to_string(), + delegation.mix_id.to_string(), pretty_cosmwasm_coin(&delegation.amount), delegation .proxy @@ -90,8 +90,8 @@ async fn print_delegation_events( ]); for event in events { - match event.event { - PendingEpochEventData::Delegate { + match event.event.kind { + PendingEpochEventKind::Delegate { owner, mix_id, amount, @@ -107,7 +107,7 @@ async fn print_delegation_events( ]); } } - PendingEpochEventData::Undelegate { + PendingEpochEventKind::Undelegate { owner, mix_id, proxy, diff --git a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs index ab4497b18d..1858b7cebb 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs index 23f6d7ecc2..6eca5cbf7a 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs index f97e949d8c..063d4a523f 100644 --- a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs index 3c6ce441e3..ba33a49037 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs @@ -4,7 +4,7 @@ use clap::Parser; use log::info; -use mixnet_contract_common::{Coin, NodeId}; +use mixnet_contract_common::{Coin, MixId}; use validator_client::nymd::traits::MixnetQueryClient; use validator_client::nymd::VestingSigningClient; @@ -13,7 +13,7 @@ use crate::context::SigningClient; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs index 1149639583..59fb6b6c3d 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs @@ -3,7 +3,7 @@ use clap::Parser; use log::info; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use validator_client::nymd::traits::MixnetQueryClient; use validator_client::nymd::VestingSigningClient; @@ -12,7 +12,7 @@ use crate::context::SigningClient; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs index d82a01c0c5..6c26b09eeb 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs @@ -68,7 +68,7 @@ pub async fn vesting_bond_gateway(client: SigningClient, args: Args, denom: &str let coin = Coin::new(args.amount, denom); let res = client - .vesting_bond_gateway(gateway, &*args.signature, coin.into(), None) + .vesting_bond_gateway(gateway, &args.signature, coin.into(), None) .await .expect("failed to bond gateway!"); diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs index 870676782d..6f20996460 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs @@ -95,7 +95,7 @@ pub async fn vesting_bond_mixnode(client: SigningClient, args: Args, denom: &str }; let res = client - .vesting_bond_mixnode(mixnode, cost_params, &*args.signature, coin.into(), None) + .vesting_bond_mixnode(mixnode, cost_params, &args.signature, coin.into(), None) .await .expect("failed to bond vesting mixnode!"); diff --git a/common/commands/src/validator/signature/errors.rs b/common/commands/src/validator/signature/errors.rs index 82aae7e08c..2369e06d66 100644 --- a/common/commands/src/validator/signature/errors.rs +++ b/common/commands/src/validator/signature/errors.rs @@ -5,6 +5,9 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum Errors { + #[error("account id does not match")] + AccountIdError, + #[error("signature error - {0}")] SignatureError(#[from] k256::ecdsa::signature::Error), diff --git a/common/commands/src/validator/signature/helpers.rs b/common/commands/src/validator/signature/helpers.rs index 817a880b64..1c2d05c9e1 100644 --- a/common/commands/src/validator/signature/helpers.rs +++ b/common/commands/src/validator/signature/helpers.rs @@ -21,12 +21,22 @@ pub fn secp256k1_verify_with_public_key_json( public_key_as_json: String, signature_as_hex: String, message: String, + account_id: String, + account_prefix: &str, ) -> Result<(), Errors> { let public_key = PublicKey::from_json(&public_key_as_json)?; - let verifying_key = VerifyingKey::from_sec1_bytes(&public_key.to_bytes())?; - let signature = Signature::from_str(&signature_as_hex)?; - let message_as_bytes = message.into_bytes(); - Ok(verifying_key.verify(&message_as_bytes, &signature)?) + match public_key.account_id(account_prefix) { + Ok(derived_account_id) => { + if derived_account_id.to_string() != account_id { + return Err(Errors::AccountIdError); + } + let verifying_key = VerifyingKey::from_sec1_bytes(&public_key.to_bytes())?; + let signature = Signature::from_str(&signature_as_hex)?; + let message_as_bytes = message.into_bytes(); + Ok(verifying_key.verify(&message_as_bytes, &signature)?) + } + Err(e) => Err(Errors::CosmrsError(e)), + } } #[cfg(test)] @@ -69,9 +79,16 @@ mod test_secp256k1 { let json_public_key = r#"{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A4FdhUMasPmNhRZjtpKlmjNbq7EEUgPxfdI+E3vSajvc"}"#.to_string(); let signature_as_hex = "E3AA5AC0DA1B7DEBB7808000F719D8ACB9A0BE10AFA2756A788516268EB246A1257EC1097C5E364EF916145B01641DEDFE955994CB340BDAFA99A65BCA3F6F28".to_string(); let message = "test 1234".to_string(); + let account_id = "n1lntkptzz8grf2w4yht4szxktzwsucgv4s7vv9g".to_string(); + let account_prefix = "n"; - let result = - secp256k1_verify_with_public_key_json(json_public_key, signature_as_hex, message); + let result = secp256k1_verify_with_public_key_json( + json_public_key, + signature_as_hex, + message, + account_id, + account_prefix, + ); assert!(result.is_ok()); } @@ -81,9 +98,16 @@ mod test_secp256k1 { let bad_json_public_key = r#"This is not JSON ☠️"#.to_string(); let signature_as_hex = "E3AA5AC0DA1B7DEBB7808000F719D8ACB9A0BE10AFA2756A788516268EB246A1257EC1097C5E364EF916145B01641DEDFE955994CB340BDAFA99A65BCA3F6F28".to_string(); let message = "abcdef".to_string(); + let account_id = "".to_string(); + let account_prefix = "n"; - let result = - secp256k1_verify_with_public_key_json(bad_json_public_key, signature_as_hex, message); + let result = secp256k1_verify_with_public_key_json( + bad_json_public_key, + signature_as_hex, + message, + account_id, + account_prefix, + ); assert!(result.is_err()); } diff --git a/common/commands/src/validator/vesting/create_vesting_schedule.rs b/common/commands/src/validator/vesting/create_vesting_schedule.rs index 60f69e6f2b..c7c133673d 100644 --- a/common/commands/src/validator/vesting/create_vesting_schedule.rs +++ b/common/commands/src/validator/vesting/create_vesting_schedule.rs @@ -12,6 +12,7 @@ use validator_client::nymd::AccountId; use validator_client::nymd::VestingSigningClient; use validator_client::nymd::{CosmosCoin, Denom}; use vesting_contract_common::messages::VestingSpecification; +use vesting_contract_common::PledgeCap; use crate::context::SigningClient; @@ -34,6 +35,12 @@ pub struct Args { #[clap(long)] pub staking_address: Option, + + #[clap( + long, + help = "Pledge cap as either absolute uNYM value or percentage, floats need to be in the 0.0 to 1.0 range and will be parsed as percentages, integers will be parsed as uNYM" + )] + pub pledge_cap: Option, } pub async fn create(args: Args, client: SigningClient, network_details: &NymNetworkDetails) { @@ -51,10 +58,11 @@ pub async fn create(args: Args, client: SigningClient, network_details: &NymNetw let res = client .create_periodic_vesting_account( - &*args.address, + &args.address, args.staking_address, Some(vesting), coin.into(), + args.pledge_cap, None, ) .await @@ -70,7 +78,7 @@ pub async fn create(args: Args, client: SigningClient, network_details: &NymNetw let send_coin_response = client .send( - &AccountId::from_str(&*args.address).unwrap(), + &AccountId::from_str(&args.address).unwrap(), vec![coin.into()], "payment made :)", None, diff --git a/common/completions/Cargo.toml b/common/completions/Cargo.toml new file mode 100644 index 0000000000..ace1f76e7f --- /dev/null +++ b/common/completions/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "completions" +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 = "3.2", features = ["derive"] } +clap_complete = "3.2" +clap_complete_fig = "3.2" \ No newline at end of file diff --git a/common/completions/src/lib.rs b/common/completions/src/lib.rs new file mode 100644 index 0000000000..8526c18d4f --- /dev/null +++ b/common/completions/src/lib.rs @@ -0,0 +1,58 @@ +use clap::builder::Command; +use clap::clap_derive::ArgEnum; +use clap::Args; +use clap_complete::generator::generate; +use clap_complete::Shell as ClapShell; +use std::io; + +pub fn fig_generate(command: &mut Command, name: &str) { + clap_complete::generate( + clap_complete_fig::Fig, + command, + name, + &mut std::io::stdout(), + ) +} + +#[derive(ArgEnum, Copy, Clone)] +pub enum Shell { + Bash, + Elvish, + Fish, + PowerShell, + Zsh, +} + +#[derive(Args, Copy, Clone)] +pub struct ArgShell { + #[clap(arg_enum, value_name = "SHELL")] + shell: Shell, +} + +impl ArgShell { + pub fn generate(&self, command: &mut Command, name: &str) { + self.shell.generate(command, name) + } +} + +impl Shell { + pub fn generate(&self, command: &mut Command, name: &str) { + match &self { + Self::Bash => { + generate(ClapShell::Bash, command, name, &mut io::stdout()); + } + Self::Elvish => { + generate(ClapShell::Elvish, command, name, &mut io::stdout()); + } + Self::Fish => { + generate(ClapShell::Fish, command, name, &mut io::stdout()); + } + Self::PowerShell => { + generate(ClapShell::PowerShell, command, name, &mut io::stdout()); + } + Self::Zsh => { + generate(ClapShell::Zsh, command, name, &mut io::stdout()); + } + } + } +} diff --git a/common/config/src/lib.rs b/common/config/src/lib.rs index a1ec8ff1ac..1f72896d9c 100644 --- a/common/config/src/lib.rs +++ b/common/config/src/lib.rs @@ -88,6 +88,7 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned { let location = custom_location .unwrap_or_else(|| self.config_directory().join(Self::config_file_name())); + log::info!("Configuration file will be saved to {:?}", location); cfg_if::cfg_if! { if #[cfg(unix)] { diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml index 2145acc513..1c1ec8ebee 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -9,3 +9,5 @@ edition = "2021" [dependencies] cosmwasm-std = "1.0.0" serde = { version = "1.0", features = ["derive"] } +schemars = "0.8" +thiserror = "1" diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index 6c25d95552..320212143d 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -1,7 +1,120 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use serde::{Deserialize, Serialize}; +use cosmwasm_std::Decimal; +use cosmwasm_std::Uint128; +use schemars::JsonSchema; +use serde::de::Error; +use serde::{Deserialize, Deserializer, Serialize}; +use std::fmt::{self, Display, Formatter}; +use std::ops::Mul; +use std::str::FromStr; +use thiserror::Error; + +pub fn truncate_decimal(amount: Decimal) -> Uint128 { + amount * Uint128::new(1) +} + +#[derive(Error, Debug)] +pub enum ContractsCommonError { + #[error("Provided percent value ({0}) is greater than 100%")] + InvalidPercent(Decimal), + + #[error("{source}")] + StdErr { + #[from] + source: cosmwasm_std::StdError, + }, +} + +/// Percent represents a value between 0 and 100% +/// (i.e. between 0.0 and 1.0) +#[derive( + Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema, +)] +pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); + +impl Percent { + pub fn new(value: Decimal) -> Result { + if value > Decimal::one() { + Err(ContractsCommonError::InvalidPercent(value)) + } else { + Ok(Percent(value)) + } + } + + pub fn is_zero(&self) -> bool { + self.0 == Decimal::zero() + } + + pub fn from_percentage_value(value: u64) -> Result { + Percent::new(Decimal::percent(value)) + } + + pub fn value(&self) -> Decimal { + self.0 + } + + pub fn round_to_integer(&self) -> u8 { + let hundred = Decimal::from_ratio(100u32, 1u32); + // we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range + truncate_decimal(hundred * self.0).u128() as u8 + } +} + +impl Display for Percent { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0; + write!(f, "{}%", adjusted) + } +} + +impl FromStr for Percent { + type Err = ContractsCommonError; + + fn from_str(s: &str) -> Result { + Percent::new(Decimal::from_str(s)?) + } +} + +impl Mul for Percent { + type Output = Decimal; + + fn mul(self, rhs: Decimal) -> Self::Output { + self.0 * rhs + } +} + +impl Mul for Decimal { + type Output = Decimal; + + fn mul(self, rhs: Percent) -> Self::Output { + rhs * self + } +} + +impl Mul for Percent { + type Output = Uint128; + + fn mul(self, rhs: Uint128) -> Self::Output { + self.0 * rhs + } +} + +// implement custom Deserialize because we want to validate Percent has the correct range +fn de_decimal_percent<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let v = Decimal::deserialize(deserializer)?; + if v > Decimal::one() { + Err(D::Error::custom( + "provided decimal percent is larger than 100%", + )) + } else { + Ok(v) + } +} // TODO: there's no reason this couldn't be used for proper binaries, but in that case // perhaps the struct should get renamed and moved to a "more" common crate diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index bd5a6c2085..1f3e72af8a 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -16,6 +16,7 @@ schemars = "0.8" thiserror = "1.0" contracts-common = { path = "../contracts-common" } serde_json = "1.0.0" +humantime-serde = "1.1.1" # TO CHECK WHETHER STILL NEEDED: log = "0.4.14" @@ -28,4 +29,5 @@ time = { version = "0.3.5", features = ["serde", "macros"] } [features] default = [] +contract-testing = [] generate-ts = ['ts-rs'] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs index 7fbff39f98..ce144ebad9 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs @@ -4,7 +4,7 @@ // due to code generated by JsonSchema #![allow(clippy::field_reassign_with_default)] -use crate::{Addr, NodeId}; +use crate::{Addr, MixId}; use cosmwasm_std::{Coin, Decimal}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize}; // just use a string representation of those so that we wouldn't need to bother with decoding bytes // and trying to figure out whether they're valid, etc pub type OwnerProxySubKey = String; -pub type StorageKey = (NodeId, OwnerProxySubKey); +pub type StorageKey = (MixId, OwnerProxySubKey); pub fn generate_owner_storage_subkey(address: &Addr, proxy: Option<&Addr>) -> String { if let Some(proxy) = &proxy { @@ -34,12 +34,11 @@ pub struct Delegation { pub owner: Addr, /// Id of the MixNode that this delegation was performed against. - pub node_id: NodeId, + pub mix_id: MixId, // Note to UI/UX devs: there's absolutely no point in displaying this value to the users, // it would serve them no purpose. It's only used for calculating rewards /// Value of the "unit delegation" associated with the mixnode at the time of delegation. - #[serde(rename = "crr")] pub cumulative_reward_ratio: Decimal, /// Original delegation amount. Note that it is never mutated as delegation accumulates rewards. @@ -55,7 +54,7 @@ pub struct Delegation { impl Delegation { pub fn new( owner: Addr, - node_id: NodeId, + mix_id: MixId, cumulative_reward_ratio: Decimal, amount: Coin, height: u64, @@ -63,7 +62,7 @@ impl Delegation { ) -> Self { Delegation { owner, - node_id, + mix_id, cumulative_reward_ratio, amount, height, @@ -72,20 +71,20 @@ impl Delegation { } pub fn generate_storage_key( - node_id: NodeId, + mix_id: MixId, owner_address: &Addr, proxy: Option<&Addr>, ) -> StorageKey { - (node_id, generate_owner_storage_subkey(owner_address, proxy)) + (mix_id, generate_owner_storage_subkey(owner_address, proxy)) } // this function might seem a bit redundant, but I'd rather explicitly keep it around in case // some types change in the future pub fn generate_storage_key_with_subkey( - node_id: NodeId, + mix_id: MixId, owner_proxy_subkey: OwnerProxySubKey, ) -> StorageKey { - (node_id, owner_proxy_subkey) + (mix_id, owner_proxy_subkey) } pub fn dec_amount(&self) -> Decimal { @@ -99,7 +98,7 @@ impl Delegation { } pub fn storage_key(&self) -> StorageKey { - Self::generate_storage_key(self.node_id, &self.owner, self.proxy.as_ref()) + Self::generate_storage_key(self.mix_id, &self.owner, self.proxy.as_ref()) } } @@ -121,13 +120,13 @@ impl PagedMixNodeDelegationsResponse { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct PagedDelegatorDelegationsResponse { pub delegations: Vec, - pub start_next_after: Option<(NodeId, OwnerProxySubKey)>, + pub start_next_after: Option<(MixId, OwnerProxySubKey)>, } impl PagedDelegatorDelegationsResponse { pub fn new( delegations: Vec, - start_next_after: Option<(NodeId, OwnerProxySubKey)>, + start_next_after: Option<(MixId, OwnerProxySubKey)>, ) -> Self { PagedDelegatorDelegationsResponse { delegations, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index 3808df55d8..e0d369f933 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -1,7 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::NodeId; +use crate::MixId; use cosmwasm_std::{Addr, Coin, Decimal}; use thiserror::Error; @@ -13,9 +13,6 @@ pub enum MixnetContractError { source: cosmwasm_std::StdError, }, - #[error("Provided percent value is greater than 100%")] - InvalidPercent, - #[error("Attempted to subtract decimals with overflow ({minuend}.sub({subtrahend}))")] OverflowDecimalSubtraction { minuend: Decimal, @@ -31,8 +28,8 @@ pub enum MixnetContractError { #[error("Not enough funds sent for node delegation. (received {received}, minimum {minimum})")] InsufficientDelegation { received: Coin, minimum: Coin }, - #[error("Mixnode ({id}) does not exist")] - MixNodeBondNotFound { id: NodeId }, + #[error("Mixnode ({mix_id}) does not exist")] + MixNodeBondNotFound { mix_id: MixId }, #[error("{owner} does not seem to own any mixnodes")] NoAssociatedMixNodeBond { owner: Addr }, @@ -83,23 +80,23 @@ pub enum MixnetContractError { epoch_end: i64, }, - #[error("Mixnode {node_id} has already been rewarded during the current rewarding epoch ({absolute_epoch_id})")] + #[error("Mixnode {mix_id} has already been rewarded during the current rewarding epoch ({absolute_epoch_id})")] MixnodeAlreadyRewarded { - node_id: NodeId, + mix_id: MixId, absolute_epoch_id: u32, }, - #[error("Mixnode {node_id} hasn't been selected to the rewarding set in this epoch ({absolute_epoch_id})")] + #[error("Mixnode {mix_id} hasn't been selected to the rewarding set in this epoch ({absolute_epoch_id})")] MixnodeNotInRewardedSet { - node_id: NodeId, + mix_id: MixId, absolute_epoch_id: u32, }, - #[error("Mixnode {node_id} is currently in the process of unbonding")] - MixnodeIsUnbonding { node_id: NodeId }, + #[error("Mixnode {mix_id} is currently in the process of unbonding")] + MixnodeIsUnbonding { mix_id: MixId }, - #[error("Mixnode {node_id} has already unbonded")] - MixnodeHasUnbonded { node_id: NodeId }, + #[error("Mixnode {mix_id} has already unbonded")] + MixnodeHasUnbonded { mix_id: MixId }, #[error("The contract has ended up in a state that was deemed impossible: {comment}")] InconsistentState { comment: String }, @@ -108,7 +105,7 @@ pub enum MixnetContractError { "Could not find any delegation information associated with mixnode {mix_id} for {address} (proxy: {proxy:?})" )] NoMixnodeDelegationFound { - mix_id: NodeId, + mix_id: MixId, address: String, proxy: Option, }, @@ -134,6 +131,6 @@ pub enum MixnetContractError { #[error("Received unexpected value for the rewarded set. Got: {received}, expected at most: {expected}")] UnexpectedRewardedSetSize { received: u32, expected: u32 }, - #[error("Mixnode {node_id} appears multiple times in the provided rewarded set update!")] - DuplicateRewardedSetNode { node_id: NodeId }, + #[error("Mixnode {mix_id} appears multiple times in the provided rewarded set update!")] + DuplicateRewardedSetNode { mix_id: MixId }, } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index 2804f8660f..bba85e3ccd 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -4,9 +4,11 @@ use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; use crate::reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate}; use crate::rewarding::RewardDistribution; -use crate::{ContractStateParams, IdentityKeyRef, Interval, Layer, NodeId}; +use crate::{BlockHeight, ContractStateParams, IdentityKeyRef, Interval, Layer, MixId}; pub use contracts_common::events::*; -use cosmwasm_std::{Addr, Coin, Event}; +use cosmwasm_std::{Addr, Coin, Decimal, Event}; + +pub const EVENT_VERSION_PREFIX: &str = "v2_"; pub enum MixnetEventType { MixnodeBonding, @@ -47,7 +49,7 @@ impl From for String { impl ToString for MixnetEventType { fn to_string(&self) -> String { - match self { + let event_name = match self { MixnetEventType::MixnodeBonding => "mixnode_bonding", MixnetEventType::GatewayBonding => "gateway_bonding", MixnetEventType::GatewayUnbonding => "gateway_unbonding", @@ -78,8 +80,9 @@ impl ToString for MixnetEventType { MixnetEventType::PendingIntervalConfigUpdate => "pending_interval_config_update", MixnetEventType::IntervalConfigUpdate => "interval_config_update", MixnetEventType::DelegationOnUnbonding => "delegation_on_unbonding_node", - } - .into() + }; + + format!("{}{}", EVENT_VERSION_PREFIX, event_name) } } @@ -93,10 +96,10 @@ pub const PROXY_KEY: &str = "proxy"; // delegation/undelegation pub const DELEGATOR_KEY: &str = "delegator"; pub const DELEGATION_TARGET_KEY: &str = "delegation_target"; -pub const DELEGATION_HEIGHT_KEY: &str = "delegation_latest_block_height"; +pub const UNIT_REWARD_KEY: &str = "unit_reward"; // bonding/unbonding -pub const NODE_ID_KEY: &str = "node_id"; +pub const MIX_ID_KEY: &str = "mix_id"; pub const NODE_IDENTITY_KEY: &str = "identity"; pub const ASSIGNED_LAYER_KEY: &str = "assigned_layer"; @@ -117,56 +120,51 @@ pub const UPDATED_MIXNODE_COST_PARAMS_KEY: &str = "updated_mixnode_cost_params"; // rewarding pub const INTERVAL_KEY: &str = "interval_details"; -pub const TOTAL_MIXNODE_REWARD_KEY: &str = "total_node_reward"; -pub const TOTAL_PLEDGE_KEY: &str = "pledge"; -pub const TOTAL_DELEGATIONS_KEY: &str = "delegated"; pub const OPERATOR_REWARD_KEY: &str = "operator_reward"; pub const DELEGATES_REWARD_KEY: &str = "delegates_reward"; pub const APPROXIMATE_TIME_LEFT_SECS_KEY: &str = "approximate_time_left_secs"; pub const INTERVAL_REWARDING_PARAMS_UPDATE_KEY: &str = "interval_rewarding_params_update"; pub const UPDATED_INTERVAL_REWARDING_PARAMS_KEY: &str = "updated_interval_rewarding_params"; +pub const PRIOR_DELEGATES_KEY: &str = "prior_delegates"; +pub const PRIOR_UNIT_REWARD_KEY: &str = "prior_unit_reward"; -pub const DISTRIBUTED_DELEGATION_REWARDS_KEY: &str = "distributed_delegation_rewards"; -pub const FURTHER_DELEGATIONS_TO_REWARD_KEY: &str = "further_delegations"; pub const NO_REWARD_REASON_KEY: &str = "no_reward_reason"; pub const BOND_NOT_FOUND_VALUE: &str = "bond_not_found"; -pub const BOND_TOO_FRESH_VALUE: &str = "bond_too_fresh"; pub const ZERO_PERFORMANCE_VALUE: &str = "zero_performance"; // rewarded set update pub const ACTIVE_SET_SIZE_KEY: &str = "active_set_size"; -pub const REWARDED_SET_SIZE_KEY: &str = "rewarded_set_size"; -pub const NODES_IN_REWARDED_SET_KEY: &str = "nodes_in_rewarded_set"; -pub const CURRENT_INTERVAL_ID_KEY: &str = "current_interval"; -pub const NEW_CURRENT_INTERVAL_KEY: &str = "new_current_interval"; pub const NEW_CURRENT_EPOCH_KEY: &str = "new_current_epoch"; -pub const BLOCK_HEIGHT_KEY: &str = "block_height"; -pub const RECONCILIATION_ERROR_EVENT: &str = "reconciliation_error"; // interval pub const EVENTS_EXECUTED_KEY: &str = "number_of_events_executed"; +pub const EVENT_CREATION_HEIGHT_KEY: &str = "created_at"; pub const REWARDED_SET_NODES_KEY: &str = "rewarded_set_nodes"; pub const NEW_EPOCHS_DURATION_SECS_KEY: &str = "new_epoch_durations_secs"; pub const NEW_EPOCHS_IN_INTERVAL: &str = "new_epochs_in_interval"; pub fn new_delegation_event( + created_at: BlockHeight, delegator: &Addr, proxy: &Option, amount: &Coin, - mix_id: NodeId, + mix_id: MixId, + unit_reward: Decimal, ) -> Event { Event::new(MixnetEventType::Delegation) + .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(DELEGATOR_KEY, delegator) .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) + .add_attribute(UNIT_REWARD_KEY, unit_reward.to_string()) } pub fn new_delegation_on_unbonded_node_event( delegator: &Addr, proxy: &Option, - mix_id: NodeId, + mix_id: MixId, ) -> Event { Event::new(MixnetEventType::Delegation) .add_attribute(DELEGATOR_KEY, delegator) @@ -178,7 +176,7 @@ pub fn new_pending_delegation_event( delegator: &Addr, proxy: &Option, amount: &Coin, - mix_id: NodeId, + mix_id: MixId, ) -> Event { Event::new(MixnetEventType::PendingDelegation) .add_attribute(DELEGATOR_KEY, delegator) @@ -191,20 +189,20 @@ pub fn new_withdraw_operator_reward_event( owner: &Addr, proxy: &Option, amount: Coin, - mix_id: NodeId, + mix_id: MixId, ) -> Event { Event::new(MixnetEventType::WithdrawOperatorReward) .add_attribute(OWNER_KEY, owner.as_str()) .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(AMOUNT_KEY, amount.to_string()) - .add_attribute(NODE_ID_KEY, mix_id.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) } pub fn new_withdraw_delegator_reward_event( delegator: &Addr, proxy: &Option, amount: Coin, - mix_id: NodeId, + mix_id: MixId, ) -> Event { Event::new(MixnetEventType::WithdrawDelegatorReward) .add_attribute(DELEGATOR_KEY, delegator) @@ -213,8 +211,9 @@ pub fn new_withdraw_delegator_reward_event( .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_active_set_update_event(new_size: u32) -> Event { +pub fn new_active_set_update_event(created_at: BlockHeight, new_size: u32) -> Event { Event::new(MixnetEventType::ActiveSetUpdate) + .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(ACTIVE_SET_SIZE_KEY, new_size.to_string()) } @@ -231,10 +230,13 @@ pub fn new_pending_active_set_update_event( } pub fn new_rewarding_params_update_event( + created_at: BlockHeight, + update: IntervalRewardingParamsUpdate, updated: IntervalRewardParams, ) -> Event { Event::new(MixnetEventType::IntervalRewardingParamsUpdate) + .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute( INTERVAL_REWARDING_PARAMS_UPDATE_KEY, update.to_inline_json(), @@ -260,22 +262,28 @@ pub fn new_pending_rewarding_params_update_event( ) } -pub fn new_undelegation_event(delegator: &Addr, proxy: &Option, mix_id: NodeId) -> Event { +pub fn new_undelegation_event( + created_at: BlockHeight, + delegator: &Addr, + proxy: &Option, + mix_id: MixId, +) -> Event { Event::new(MixnetEventType::Undelegation) + .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(DELEGATOR_KEY, delegator) .add_optional_attribute(PROXY_KEY, proxy.as_ref()) - .add_attribute(NODE_ID_KEY, mix_id.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) } pub fn new_pending_undelegation_event( delegator: &Addr, proxy: &Option, - mix_id: NodeId, + mix_id: MixId, ) -> Event { Event::new(MixnetEventType::PendingUndelegation) .add_attribute(DELEGATOR_KEY, delegator) .add_optional_attribute(PROXY_KEY, proxy.as_ref()) - .add_attribute(NODE_ID_KEY, mix_id.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) } pub fn new_gateway_bonding_event( @@ -309,12 +317,12 @@ pub fn new_mixnode_bonding_event( proxy: &Option, amount: &Coin, identity: IdentityKeyRef<'_>, - node_id: NodeId, + mix_id: MixId, assigned_layer: Layer, ) -> Event { // coin implements Display trait and we use that implementation here Event::new(MixnetEventType::MixnodeBonding) - .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(OWNER_KEY, owner) .add_optional_attribute(PROXY_KEY, proxy.as_ref()) @@ -322,55 +330,59 @@ pub fn new_mixnode_bonding_event( .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_mixnode_unbonding_event(node_id: NodeId) -> Event { - Event::new(MixnetEventType::MixnodeUnbonding).add_attribute(NODE_ID_KEY, node_id.to_string()) +pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Event { + Event::new(MixnetEventType::MixnodeUnbonding) + .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) } pub fn new_pending_mixnode_unbonding_event( owner: &Addr, proxy: &Option, identity: IdentityKeyRef<'_>, - node_id: NodeId, + mix_id: MixId, ) -> Event { Event::new(MixnetEventType::PendingMixnodeUnbonding) - .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(OWNER_KEY, owner) .add_optional_attribute(PROXY_KEY, proxy.as_ref()) } pub fn new_mixnode_config_update_event( - node_id: NodeId, + mix_id: MixId, owner: &Addr, proxy: &Option, update: &MixNodeConfigUpdate, ) -> Event { Event::new(MixnetEventType::MixnodeConfigUpdate) - .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(OWNER_KEY, owner) .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(UPDATED_MIXNODE_CONFIG_KEY, update.to_inline_json()) } pub fn new_mixnode_pending_cost_params_update_event( - node_id: NodeId, + mix_id: MixId, owner: &Addr, proxy: &Option, new_costs: &MixNodeCostParams, ) -> Event { Event::new(MixnetEventType::PendingMixnodeCostParamsUpdate) - .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(OWNER_KEY, owner) .add_optional_attribute(PROXY_KEY, proxy.as_ref()) .add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json()) } pub fn new_mixnode_cost_params_update_event( - node_id: NodeId, + created_at: BlockHeight, + mix_id: MixId, new_costs: &MixNodeCostParams, ) -> Event { Event::new(MixnetEventType::MixnodeCostParamsUpdate) - .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json()) } @@ -426,41 +438,41 @@ pub fn new_settings_update_event( event } -pub fn new_not_found_mix_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event { +pub fn new_not_found_mix_operator_rewarding_event(interval: Interval, mix_id: MixId) -> Event { Event::new(MixnetEventType::MixnodeRewarding) .add_attribute( INTERVAL_KEY, interval.current_epoch_absolute_id().to_string(), ) - .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(NO_REWARD_REASON_KEY, BOND_NOT_FOUND_VALUE) } -pub fn new_zero_uptime_mix_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event { +pub fn new_zero_uptime_mix_operator_rewarding_event(interval: Interval, mix_id: MixId) -> Event { Event::new(MixnetEventType::MixnodeRewarding) .add_attribute( INTERVAL_KEY, interval.current_epoch_absolute_id().to_string(), ) - .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(NO_REWARD_REASON_KEY, ZERO_PERFORMANCE_VALUE) } pub fn new_mix_rewarding_event( interval: Interval, - node_id: NodeId, + mix_id: MixId, reward_distribution: RewardDistribution, + prior_delegates: Decimal, + prior_unit_reward: Decimal, ) -> Event { Event::new(MixnetEventType::MixnodeRewarding) - // TODO: to calculate Timmy's (delegator) reward at this time - // emit: - // - unit delegation BEFORE rewarding (to determine Timmy's state before rewarding happened) - // - total delegation BEFORE rewarding .add_attribute( INTERVAL_KEY, interval.current_epoch_absolute_id().to_string(), ) - .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(PRIOR_DELEGATES_KEY, prior_delegates.to_string()) + .add_attribute(PRIOR_UNIT_REWARD_KEY, prior_unit_reward.to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute( OPERATOR_REWARD_KEY, reward_distribution.operator.to_string(), @@ -495,11 +507,13 @@ pub fn new_reconcile_pending_events() -> Event { } pub fn new_interval_config_update_event( + created_at: BlockHeight, epochs_in_interval: u32, epoch_duration_secs: u64, updated_rewarding_params: IntervalRewardParams, ) -> Event { Event::new(MixnetEventType::IntervalConfigUpdate) + .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute( NEW_EPOCHS_DURATION_SECS_KEY, epoch_duration_secs.to_string(), diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs new file mode 100644 index 0000000000..00f4c4b12c --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs @@ -0,0 +1,13 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::Decimal; + +pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option) { + let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128)); + if a > b { + assert!(a - b < epsilon, "{} != {}", a, b) + } else { + assert!(b - a < epsilon, "{} != {}", a, b) + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs index 515c120439..560f1582b7 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -60,14 +60,22 @@ pub(crate) mod string_rfc3339_offset_date_time { } } +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/Interval.ts") +)] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)] pub struct Interval { id: IntervalId, epochs_in_interval: u32, + // TODO add a better TS type generation + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] #[serde(with = "string_rfc3339_offset_date_time")] current_epoch_start: OffsetDateTime, current_epoch_id: EpochId, + #[cfg_attr(feature = "generate-ts", ts(type = "{ secs: number; nanos: number; }"))] epoch_length: Duration, total_elapsed_epochs: EpochId, } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs index 5e0bcb34d1..200b4c5a00 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs @@ -6,6 +6,7 @@ pub mod delegation; pub mod error; pub mod events; pub mod gateway; +pub mod helpers; mod interval; pub mod mixnode; mod msg; @@ -33,7 +34,8 @@ pub use mixnode::{ }; pub use msg::*; pub use pending_events::{ - PendingEpochEvent, PendingEpochEventData, PendingIntervalEvent, PendingIntervalEventData, + PendingEpochEvent, PendingEpochEventData, PendingEpochEventKind, PendingIntervalEvent, + PendingIntervalEventData, PendingIntervalEventKind, }; pub use reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate, RewardingParams}; pub use types::*; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 898cba50cf..5eb98bcbb3 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -9,7 +9,7 @@ use crate::error::MixnetContractError; use crate::reward_params::{NodeRewardParams, RewardingParams}; use crate::rewarding::helpers::truncate_reward; use crate::rewarding::RewardDistribution; -use crate::{Delegation, EpochId, IdentityKey, NodeId, Percent, SphinxKey}; +use crate::{Delegation, EpochId, IdentityKey, MixId, Percent, SphinxKey}; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -47,8 +47,8 @@ impl MixNodeDetails { } } - pub fn mix_id(&self) -> NodeId { - self.bond_information.id + pub fn mix_id(&self) -> MixId { + self.bond_information.mix_id } pub fn is_unbonding(&self) -> bool { @@ -78,34 +78,27 @@ impl MixNodeDetails { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeRewarding { /// Information provided by the operator that influence the cost function. - #[serde(rename = "cp")] pub cost_params: MixNodeCostParams, /// Total pledge and compounded reward earned by the node operator. - #[serde(rename = "op")] pub operator: Decimal, /// Total delegation and compounded reward earned by all node delegators. - #[serde(rename = "dg")] pub delegates: Decimal, /// Cumulative reward earned by the "unit delegation" since the block 0. - #[serde(rename = "tur")] pub total_unit_reward: Decimal, /// Value of the theoretical "unit delegation" that has delegated to this mixnode at block 0. - #[serde(rename = "ud")] pub unit_delegation: Decimal, /// Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt /// to reward it multiple times in the same epoch. - #[serde(rename = "le")] pub last_rewarded_epoch: EpochId, // technically we don't need that field to determine reward magnitude or anything // but it saves on extra queries to determine if we're removing the final delegation // (so that we could zero the field correctly) - #[serde(rename = "uqd")] pub unique_delegations: u32, } @@ -311,7 +304,7 @@ impl MixNodeRewarding { pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal { let starting_ratio = delegation.cumulative_reward_ratio; let ending_ratio = self.full_reward_ratio(); - let adjust = starting_ratio + UNIT_DELEGATION_BASE; + let adjust = starting_ratio + self.unit_delegation; (ending_ratio - starting_ratio) * delegation.dec_amount() / adjust } @@ -428,7 +421,7 @@ impl MixNodeRewarding { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeBond { /// Unique id assigned to the bonded mixnode. - pub id: NodeId, + pub mix_id: MixId, /// Address of the owner of this mixnode. pub owner: Addr, @@ -456,7 +449,7 @@ pub struct MixNodeBond { impl MixNodeBond { pub fn new( - id: NodeId, + mix_id: MixId, owner: Addr, original_pledge: Coin, layer: Layer, @@ -465,7 +458,7 @@ impl MixNodeBond { bonding_height: u64, ) -> Self { MixNodeBond { - id, + mix_id, owner, original_pledge, layer, @@ -580,6 +573,7 @@ pub struct UnbondedMixnode { #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] pub proxy: Option, + #[cfg_attr(feature = "generate-ts", ts(type = "number"))] pub unbonding_height: u64, } @@ -607,11 +601,11 @@ impl MixNodeConfigUpdate { pub struct PagedMixnodeBondsResponse { pub nodes: Vec, pub per_page: usize, - pub start_next_after: Option, + pub start_next_after: Option, } impl PagedMixnodeBondsResponse { - pub fn new(nodes: Vec, per_page: usize, start_next_after: Option) -> Self { + pub fn new(nodes: Vec, per_page: usize, start_next_after: Option) -> Self { PagedMixnodeBondsResponse { nodes, per_page, @@ -624,14 +618,14 @@ impl PagedMixnodeBondsResponse { pub struct PagedMixnodesDetailsResponse { pub nodes: Vec, pub per_page: usize, - pub start_next_after: Option, + pub start_next_after: Option, } impl PagedMixnodesDetailsResponse { pub fn new( nodes: Vec, per_page: usize, - start_next_after: Option, + start_next_after: Option, ) -> Self { PagedMixnodesDetailsResponse { nodes, @@ -643,16 +637,16 @@ impl PagedMixnodesDetailsResponse { #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] pub struct PagedUnbondedMixnodesResponse { - pub nodes: Vec<(NodeId, UnbondedMixnode)>, + pub nodes: Vec<(MixId, UnbondedMixnode)>, pub per_page: usize, - pub start_next_after: Option, + pub start_next_after: Option, } impl PagedUnbondedMixnodesResponse { pub fn new( - nodes: Vec<(NodeId, UnbondedMixnode)>, + nodes: Vec<(MixId, UnbondedMixnode)>, per_page: usize, - start_next_after: Option, + start_next_after: Option, ) -> Self { PagedUnbondedMixnodesResponse { nodes, @@ -670,25 +664,25 @@ pub struct MixOwnershipResponse { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixnodeDetailsResponse { - pub mix_id: NodeId, + pub mix_id: MixId, pub mixnode_details: Option, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixnodeRewardingDetailsResponse { - pub mix_id: NodeId, + pub mix_id: MixId, pub rewarding_details: Option, } #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] pub struct UnbondedMixnodeResponse { - pub mix_id: NodeId, + pub mix_id: MixId, pub unbonded_info: Option, } #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] pub struct StakeSaturationResponse { - pub mix_id: NodeId, + pub mix_id: MixId, pub current_saturation: Option, pub uncapped_saturation: Option, } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 0e7a5dea62..ee172a4c20 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -6,7 +6,7 @@ use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; use crate::reward_params::{ IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams, }; -use crate::{delegation, ContractStateParams, NodeId, Percent}; +use crate::{delegation, ContractStateParams, MixId, Percent}; use crate::{Gateway, IdentityKey, MixNode}; use cosmwasm_std::Decimal; use schemars::JsonSchema; @@ -87,7 +87,7 @@ pub enum ExecuteMsg { force_immediately: bool, }, AdvanceCurrentEpoch { - new_rewarded_set: Vec, + new_rewarded_set: Vec, expected_active_set_size: u32, }, ReconcileEpochEvents { @@ -142,23 +142,23 @@ pub enum ExecuteMsg { // delegation-related: DelegateToMixnode { - mix_id: NodeId, + mix_id: MixId, }, DelegateToMixnodeOnBehalf { - mix_id: NodeId, + mix_id: MixId, delegate: String, }, UndelegateFromMixnode { - mix_id: NodeId, + mix_id: MixId, }, UndelegateFromMixnodeOnBehalf { - mix_id: NodeId, + mix_id: MixId, delegate: String, }, // reward-related RewardMixnode { - mix_id: NodeId, + mix_id: MixId, performance: Performance, }, WithdrawOperatorReward {}, @@ -166,12 +166,18 @@ pub enum ExecuteMsg { owner: String, }, WithdrawDelegatorReward { - mix_id: NodeId, + mix_id: MixId, }, WithdrawDelegatorRewardOnBehalf { - mix_id: NodeId, + mix_id: MixId, owner: String, }, + + // testing-only + #[cfg(feature = "contract-testing")] + TestingResolveAllPendingEvents { + limit: Option, + }, } impl ExecuteMsg { @@ -256,6 +262,10 @@ impl ExecuteMsg { "withdrawing delegator reward from mixnode {} on behalf", mix_id ), + #[cfg(feature = "contract-testing")] + ExecuteMsg::TestingResolveAllPendingEvents { .. } => { + "resolving all pending events".into() + } } } } @@ -272,46 +282,46 @@ pub enum QueryMsg { GetCurrentIntervalDetails {}, GetRewardedSet { limit: Option, - start_after: Option, + start_after: Option, }, // mixnode-related: GetMixNodeBonds { limit: Option, - start_after: Option, + start_after: Option, }, GetMixNodesDetailed { limit: Option, - start_after: Option, + start_after: Option, }, GetUnbondedMixNodes { limit: Option, - start_after: Option, + start_after: Option, }, GetUnbondedMixNodesByOwner { owner: String, limit: Option, - start_after: Option, + start_after: Option, }, GetUnbondedMixNodesByIdentityKey { identity_key: String, limit: Option, - start_after: Option, + start_after: Option, }, GetOwnedMixnode { address: String, }, GetMixnodeDetails { - mix_id: NodeId, + mix_id: MixId, }, GetMixnodeRewardingDetails { - mix_id: NodeId, + mix_id: MixId, }, GetStakeSaturation { - mix_id: NodeId, + mix_id: MixId, }, GetUnbondedMixNodeInformation { - mix_id: NodeId, + mix_id: MixId, }, GetBondedMixnodeDetailsByIdentity { mix_identity: IdentityKey, @@ -333,7 +343,7 @@ pub enum QueryMsg { // delegation-related: // gets all [paged] delegations associated with particular mixnode GetMixnodeDelegations { - mix_id: NodeId, + mix_id: MixId, // since `start_after` is user-provided input, we can't use `Addr` as we // can't guarantee it's validated. start_after: Option, @@ -344,12 +354,12 @@ pub enum QueryMsg { // since `delegator` is user-provided input, we can't use `Addr` as we // can't guarantee it's validated. delegator: String, - start_after: Option<(NodeId, OwnerProxySubKey)>, + start_after: Option<(MixId, OwnerProxySubKey)>, limit: Option, }, // gets delegation associated with particular mixnode, delegator pair GetDelegationDetails { - mix_id: NodeId, + mix_id: MixId, delegator: String, proxy: Option, }, @@ -364,21 +374,21 @@ pub enum QueryMsg { address: String, }, GetPendingMixNodeOperatorReward { - mix_id: NodeId, + mix_id: MixId, }, GetPendingDelegatorReward { address: String, - mix_id: NodeId, + mix_id: MixId, proxy: Option, }, // given the provided performance, estimate the reward at the end of the current epoch GetEstimatedCurrentEpochOperatorReward { - mix_id: NodeId, + mix_id: MixId, estimated_performance: Performance, }, GetEstimatedCurrentEpochDelegatorReward { address: String, - mix_id: NodeId, + mix_id: MixId, proxy: Option, estimated_performance: Performance, }, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs index a2f749f068..fce8bd8cf0 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs @@ -3,7 +3,7 @@ use crate::mixnode::MixNodeCostParams; use crate::reward_params::IntervalRewardingParamsUpdate; -use crate::{EpochEventId, IntervalEventId, NodeId}; +use crate::{BlockHeight, EpochEventId, IntervalEventId, MixId}; use cosmwasm_std::{Addr, Coin}; use serde::{Deserialize, Serialize}; @@ -14,28 +14,43 @@ pub struct PendingEpochEvent { } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub enum PendingEpochEventData { +pub struct PendingEpochEventData { + pub created_at: BlockHeight, + pub kind: PendingEpochEventKind, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum PendingEpochEventKind { // can't just pass the `Delegation` struct here as it's impossible to determine // `cumulative_reward_ratio` ahead of time Delegate { owner: Addr, - mix_id: NodeId, + mix_id: MixId, amount: Coin, proxy: Option, }, Undelegate { owner: Addr, - mix_id: NodeId, + mix_id: MixId, proxy: Option, }, UnbondMixnode { - mix_id: NodeId, + mix_id: MixId, }, UpdateActiveSetSize { new_size: u32, }, } +impl PendingEpochEventKind { + pub fn attach_source_height(self, created_at: BlockHeight) -> PendingEpochEventData { + PendingEpochEventData { + created_at, + kind: self, + } + } +} + impl From<(EpochEventId, PendingEpochEventData)> for PendingEpochEvent { fn from(data: (EpochEventId, PendingEpochEventData)) -> Self { PendingEpochEvent { @@ -52,9 +67,15 @@ pub struct PendingIntervalEvent { } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub enum PendingIntervalEventData { +pub struct PendingIntervalEventData { + pub created_at: BlockHeight, + pub kind: PendingIntervalEventKind, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum PendingIntervalEventKind { ChangeMixCostParams { - mix_id: NodeId, + mix_id: MixId, new_costs: MixNodeCostParams, }, @@ -67,6 +88,15 @@ pub enum PendingIntervalEventData { }, } +impl PendingIntervalEventKind { + pub fn attach_source_height(self, created_at: BlockHeight) -> PendingIntervalEventData { + PendingIntervalEventData { + created_at, + kind: self, + } + } +} + impl From<(IntervalEventId, PendingIntervalEventData)> for PendingIntervalEvent { fn from(data: (IntervalEventId, PendingIntervalEventData)) -> Self { PendingIntervalEvent { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs index 8a6a8f3366..4d9ca75459 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs @@ -1,6 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use contracts_common::truncate_decimal; use cosmwasm_std::{Coin, Decimal, Uint128}; /// Truncates all decimal points so that the reward would fit in a `Coin` and so that we would @@ -17,7 +18,3 @@ pub fn truncate_reward(reward: Decimal, denom: impl Into) -> Coin { pub fn truncate_reward_amount(reward: Decimal) -> Uint128 { truncate_decimal(reward) } - -pub fn truncate_decimal(amount: Decimal) -> Uint128 { - amount * Uint128::new(1) -} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs index 9aaaa8a1a5..e50df76434 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs @@ -8,15 +8,24 @@ use serde::{Deserialize, Serialize}; pub mod helpers; pub mod simulator; +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/RewardEstimate.ts") +)] #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)] pub struct RewardEstimate { + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub total_node_reward: Decimal, // note that operator reward includes the operating_cost, // i.e. say total_node_reward was `1nym` and operating_cost was `2nym` // in that case the operator reward would still be `1nym` as opposed to 0 + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub operator: Decimal, + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub delegates: Decimal, + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub operating_cost: Decimal, } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator.rs deleted file mode 100644 index 9fa4fc743b..0000000000 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator.rs +++ /dev/null @@ -1,465 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::error::MixnetContractError; -use crate::mixnode::{MixNodeCostParams, MixNodeRewarding}; -use crate::reward_params::{IntervalRewardParams, NodeRewardParams, RewardingParams}; -use crate::rewarding::helpers::truncate_reward; -use crate::rewarding::RewardDistribution; -use crate::{Delegation, Interval, Percent}; -use cosmwasm_std::{Addr, Coin, Decimal}; - -pub struct Simulator { - pub node_rewarding_details: MixNodeRewarding, - pub node_delegations: Vec, - pub system_rewarding_params: RewardingParams, - - pub interval: Interval, -} - -impl Simulator { - pub fn new( - profit_margin_percent: Percent, - interval_operating_cost: Coin, - system_rewarding_params: RewardingParams, - initial_pledge: Coin, - interval: Interval, - ) -> Self { - let cost_params = MixNodeCostParams { - profit_margin_percent, - interval_operating_cost, - }; - - Simulator { - node_rewarding_details: MixNodeRewarding::initialise_new( - cost_params, - &initial_pledge, - Default::default(), - ), - node_delegations: vec![], - system_rewarding_params, - interval, - } - } - - pub fn delegate(&mut self, amount: Coin) { - let cumulative_reward_ratio = self.node_rewarding_details.total_unit_reward; - // let record = self.node_rewarding_details.increment_period(); - // self.node_historical_records.insert(period, record); - self.node_rewarding_details - .add_base_delegation(amount.amount); - - // we don't care about the owner/node details here - self.node_delegations.push(Delegation::new( - Addr::unchecked("bob"), - 42, - cumulative_reward_ratio, - amount, - 123, - None, - )) - } - - pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal { - self.node_rewarding_details - .determine_delegation_reward(delegation) - } - - // since this is a simulator only, not something to be used in the production code, the unwraps are fine - // if user inputs are invalid - pub fn undelegate( - &mut self, - delegation_index: usize, - ) -> Result<(Coin, Coin), MixnetContractError> { - let delegation = self.node_delegations.remove(delegation_index); - let reward = self.determine_delegation_reward(&delegation); - self.node_rewarding_details - .decrease_delegates_decimal(delegation.dec_amount() + reward)?; - self.node_rewarding_details.unique_delegations -= 1; - - let reward_denom = &delegation.amount.denom; - let truncated_reward = truncate_reward(reward, reward_denom); - - // if this was last delegation, move all leftover decimal tokens to the operator - // (this is literally in the order of a millionth of a micronym) - if self.node_delegations.is_empty() { - assert_eq!(self.node_rewarding_details.unique_delegations, 0); - self.node_rewarding_details.operator += self.node_rewarding_details.delegates; - self.node_rewarding_details.delegates = Decimal::zero(); - } - - Ok((delegation.amount, truncated_reward)) - } - - pub fn determine_total_delegation_reward(&self) -> Decimal { - let mut total = Decimal::zero(); - for delegation in &self.node_delegations { - total += self.determine_delegation_reward(delegation) - } - total - } - - pub fn simulate_epoch(&mut self, node_params: NodeRewardParams) -> RewardDistribution { - let reward_distribution = self.node_rewarding_details.calculate_epoch_reward( - &self.system_rewarding_params, - node_params, - self.interval.epochs_in_interval(), - ); - self.node_rewarding_details.distribute_rewards( - reward_distribution, - self.interval.current_epoch_absolute_id(), - ); - self.interval = self.interval.advance_epoch(); - - reward_distribution - } - - // assume node state doesn't change in the interval (kinda unrealistic) - pub fn simulate_interval(&mut self, node_params: NodeRewardParams) { - assert_eq!(self.interval.current_epoch_id(), 0); - let id = self.interval.current_interval_id(); - let mut distributed = Decimal::zero(); - for _ in 0..self.interval.epochs_in_interval() { - let distr = self.simulate_epoch(node_params); - distributed += distr.operator; - distributed += distr.delegates; - } - assert_eq!(id + 1, self.interval.current_interval_id()); - - // update reward pool and all of that - let old = self.system_rewarding_params.interval; - let reward_pool = old.reward_pool - distributed; - let staking_supply = old.staking_supply + distributed; - let epoch_reward_budget = reward_pool - / Decimal::from_atomics(self.interval.epochs_in_interval(), 0).unwrap() - * old.interval_pool_emission.value(); - let stake_saturation_point = staking_supply - / Decimal::from_atomics(self.system_rewarding_params.rewarded_set_size, 0).unwrap(); - - let updated_params = RewardingParams { - interval: IntervalRewardParams { - reward_pool, - staking_supply, - epoch_reward_budget, - stake_saturation_point, - sybil_resistance: old.sybil_resistance, - active_set_work_factor: old.active_set_work_factor, - interval_pool_emission: old.interval_pool_emission, - }, - rewarded_set_size: self.system_rewarding_params.rewarded_set_size, - active_set_size: self.system_rewarding_params.active_set_size, - }; - - self.system_rewarding_params = updated_params; - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::reward_params::IntervalRewardParams; - use crate::rewarding::helpers::truncate_reward_amount; - use cosmwasm_std::testing::mock_env; - use cosmwasm_std::{coin, Uint128}; - use std::time::Duration; - - fn base_simulator(initial_pledge: u128) -> Simulator { - let profit_margin = Percent::from_percentage_value(10).unwrap(); - let interval_operating_cost = Coin::new(40_000_000, "unym"); - let epochs_in_interval = 720u32; - let rewarded_set_size = 240; - let active_set_size = 100; - let interval_pool_emission = Percent::from_percentage_value(2).unwrap(); - - let reward_pool = 250_000_000_000_000u128; - let staking_supply = 100_000_000_000_000u128; - let epoch_reward_budget = - interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval); - let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size); - - let rewarding_params = RewardingParams { - interval: IntervalRewardParams { - reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) - staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M - epoch_reward_budget, - stake_saturation_point, - sybil_resistance: Percent::from_percentage_value(30).unwrap(), - active_set_work_factor: Decimal::percent(1000), // value '10' - interval_pool_emission, - }, - rewarded_set_size, - active_set_size, - }; - - let interval = Interval::init_interval( - epochs_in_interval, - Duration::from_secs(60 * 60), - &mock_env(), - ); - let initial_pledge = Coin::new(initial_pledge, "unym"); - Simulator::new( - profit_margin, - interval_operating_cost, - rewarding_params, - initial_pledge, - interval, - ) - } - - fn compare_decimals(a: Decimal, b: Decimal) { - let epsilon = Decimal::from_ratio(1u128, 100_000_000u128); - if a > b { - assert!(a - b < epsilon, "{} != {}", a, b) - } else { - assert!(b - a < epsilon, "{} != {}", a, b) - } - } - - // essentially our delegations + estimated rewards HAVE TO equal to what we actually determined - fn check_rewarding_invariant(simulator: &Simulator) { - let delegation_sum: Decimal = simulator - .node_delegations - .iter() - .map(|d| d.dec_amount()) - .sum(); - let reward_sum = simulator.determine_total_delegation_reward(); - compare_decimals( - delegation_sum + reward_sum, - simulator.node_rewarding_details.delegates, - ) - } - - #[test] - fn simulator_returns_expected_values_for_base_case() { - let mut simulator = base_simulator(10000_000000); - - let epoch_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); - let rewards = simulator.simulate_epoch(epoch_params); - - assert_eq!(rewards.delegates, Decimal::zero()); - compare_decimals(rewards.operator, "1128452.5416104363".parse().unwrap()); - } - - #[test] - fn single_delegation_at_genesis() { - let mut simulator = base_simulator(10000_000000); - simulator.delegate(Coin::new(18000_000000, "unym")); - - let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); - let rewards = simulator.simulate_epoch(node_params); - - compare_decimals(rewards.delegates, "1795950.2602660495".parse().unwrap()); - compare_decimals(rewards.operator, "1363716.856243172".parse().unwrap()); - - compare_decimals( - rewards.delegates, - simulator.determine_total_delegation_reward(), - ); - assert_eq!( - simulator.node_rewarding_details.operator, - rewards.operator + Decimal::from_atomics(10000_000000u128, 0).unwrap() - ); - assert_eq!( - simulator.node_rewarding_details.delegates, - rewards.delegates + Decimal::from_atomics(18000_000000u128, 0).unwrap() - ); - } - - #[test] - fn delegation_and_undelegation() { - let mut simulator = base_simulator(10000_000000); - let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); - - let rewards1 = simulator.simulate_epoch(node_params); - let expected_operator1 = "1128452.5416104363".parse().unwrap(); - assert_eq!(rewards1.delegates, Decimal::zero()); - compare_decimals(rewards1.operator, expected_operator1); - - simulator.delegate(Coin::new(18000_000000, "unym")); - - let rewards2 = simulator.simulate_epoch(node_params); - let expected_operator2 = "1363843.413584609".parse().unwrap(); - let expected_delegator_reward1 = "1795952.25874404".parse().unwrap(); - compare_decimals(rewards2.delegates, expected_delegator_reward1); - compare_decimals(rewards2.operator, expected_operator2); - - let rewards3 = simulator.simulate_epoch(node_params); - let expected_operator3 = "1364017.7824440491".parse().unwrap(); - let expected_delegator_reward2 = "1796135.9269468693".parse().unwrap(); - compare_decimals(rewards3.delegates, expected_delegator_reward2); - compare_decimals(rewards3.operator, expected_operator3); - - let (delegation, reward) = simulator.undelegate(0).unwrap(); - assert_eq!(delegation.amount.u128(), 18000_000000); - assert_eq!( - reward.amount, - (expected_delegator_reward1 + expected_delegator_reward2) * Uint128::new(1) - ); - - let base_op = Decimal::from_atomics(10000_000000u128, 0).unwrap(); - compare_decimals( - simulator.node_rewarding_details.operator, - base_op + expected_operator1 + expected_operator2 + expected_operator3, - ); - assert_eq!(Decimal::zero(), simulator.node_rewarding_details.delegates); - } - - #[test] - fn withdrawing_operator_reward() { - // essentially all delegators' rewards (and the operator itself) are still correctly computed - let original_pledge = coin(10000_000000, "unym"); - let mut simulator = base_simulator(original_pledge.amount.u128()); - let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); - - // add 2 delegations at genesis (because it makes things easier and as shown with previous tests - // delegating at different times still work) - simulator.delegate(Coin::new(18000_000000, "unym")); - simulator.delegate(Coin::new(4000_000000, "unym")); - - // "normal", sanity check rewarding - let rewards1 = simulator.simulate_epoch(node_params); - let expected_operator1 = "1411087.1007647323".parse().unwrap(); - let expected_delegator_reward1 = "2199961.032388664".parse().unwrap(); - compare_decimals(rewards1.delegates, expected_delegator_reward1); - compare_decimals(rewards1.operator, expected_operator1); - check_rewarding_invariant(&simulator); - - let reward = simulator - .node_rewarding_details - .withdraw_operator_reward(&original_pledge); - assert_eq!(reward.amount, truncate_reward_amount(expected_operator1)); - assert_eq!( - simulator.node_rewarding_details.operator, - Decimal::from_atomics(original_pledge.amount, 0).unwrap() - ); - - let rewards2 = simulator.simulate_epoch(node_params); - let expected_operator2 = "1411113.0004067947".parse().unwrap(); - let expected_delegator_reward2 = "2200183.3879084454".parse().unwrap(); - compare_decimals(rewards2.delegates, expected_delegator_reward2); - compare_decimals(rewards2.operator, expected_operator2); - check_rewarding_invariant(&simulator); - } - - #[test] - fn withdrawing_delegator_reward() { - // essentially all delegators' rewards (and the operator itself) are still correctly computed - let mut simulator = base_simulator(10000_000000); - let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); - - // add 2 delegations at genesis (because it makes things easier and as shown with previous tests - // delegating at different times still work) - simulator.delegate(Coin::new(18000_000000, "unym")); - simulator.delegate(Coin::new(4000_000000, "unym")); - - // "normal", sanity check rewarding - let rewards1 = simulator.simulate_epoch(node_params); - let expected_operator1 = "1411087.1007647323".parse().unwrap(); - let expected_delegator_reward1 = "2199961.032388664".parse().unwrap(); - compare_decimals(rewards1.delegates, expected_delegator_reward1); - compare_decimals(rewards1.operator, expected_operator1); - check_rewarding_invariant(&simulator); - - // reference to our `18000_000000` delegation - let delegation1 = &mut simulator.node_delegations[0]; - let reward = simulator - .node_rewarding_details - .withdraw_delegator_reward(delegation1) - .unwrap(); - let expected_del1_reward = "1799968.1174089068".parse().unwrap(); - assert_eq!(reward.amount, truncate_reward_amount(expected_del1_reward)); - - // new reward after withdrawal - let rewards2 = simulator.simulate_epoch(node_params); - let expected_operator2 = "1411250.1907492676".parse().unwrap(); - let expected_delegator_reward2 = "2200004.051009689".parse().unwrap(); - compare_decimals(rewards2.delegates, expected_delegator_reward2); - compare_decimals(rewards2.operator, expected_operator2); - check_rewarding_invariant(&simulator); - - // check final values - let reward_del1 = simulator - .node_rewarding_details - .withdraw_delegator_reward(&mut simulator.node_delegations[0]) - .unwrap(); - let expected_del1_reward = "1799970.5883041779".parse().unwrap(); - assert_eq!( - reward_del1.amount, - truncate_reward_amount(expected_del1_reward) - ); - - let reward_del2 = simulator - .node_rewarding_details - .withdraw_delegator_reward(&mut simulator.node_delegations[1]) - .unwrap(); - let first: Decimal = "399992.91497975704".parse().unwrap(); - let second: Decimal = "400033.4627055114".parse().unwrap(); - let expected_del2_reward = first + second; - assert_eq!( - reward_del2.amount, - truncate_reward_amount(expected_del2_reward) - ); - } - - #[test] - fn simulating_multiple_epochs() { - let mut simulator = base_simulator(10000_000000); - - let mut is_active = true; - let mut performance = Percent::from_percentage_value(100).unwrap(); - for epoch in 0..720 { - if epoch == 0 { - simulator.delegate(Coin::new(18000_000000, "unym")) - } - if epoch == 42 { - simulator.delegate(Coin::new(2000_000000, "unym")) - } - if epoch == 89 { - is_active = false; - } - if epoch == 123 { - simulator.delegate(Coin::new(6666_000000, "unym")) - } - if epoch == 167 { - performance = Percent::from_percentage_value(90).unwrap(); - } - if epoch == 245 { - simulator.delegate(Coin::new(2050_000000, "unym")) - } - if epoch == 264 { - let (delegation, _reward) = simulator.undelegate(1).unwrap(); - // sanity check to make sure we undelegated what we wanted to undelegate : ) - assert_eq!(delegation.amount.u128(), 2000_000000); - // TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be - } - if epoch == 345 { - is_active = true; - } - if epoch == 358 { - performance = Percent::from_percentage_value(100).unwrap(); - } - if epoch == 458 { - let (delegation, _reward) = simulator.undelegate(0).unwrap(); - // sanity check to make sure we undelegated what we wanted to undelegate : ) - assert_eq!(delegation.amount.u128(), 18000_000000); - // TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be - } - if epoch == 545 { - simulator.delegate(Coin::new(5000_000000, "unym")) - } - - // this has to always hold - check_rewarding_invariant(&simulator); - let node_params = NodeRewardParams::new(performance, is_active); - simulator.simulate_epoch(node_params); - } - - // after everyone undelegates, there should be nothing left in the delegates pool - let delegations = simulator.node_delegations.len(); - for _ in 0..delegations { - simulator.undelegate(0).unwrap(); - } - assert_eq!(Decimal::zero(), simulator.node_rewarding_details.delegates); - } -} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs new file mode 100644 index 0000000000..bd4fc8d8d3 --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs @@ -0,0 +1,749 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::MixnetContractError; +use crate::reward_params::NodeRewardParams; +use crate::rewarding::simulator::simulated_node::SimulatedNode; +use crate::rewarding::RewardDistribution; +use crate::{ + Delegation, Interval, IntervalRewardParams, MixId, MixNodeCostParams, RewardingParams, +}; +use cosmwasm_std::{Coin, Decimal}; +use std::collections::BTreeMap; + +pub mod simulated_node; + +pub struct Simulator { + pub nodes: BTreeMap, + pub system_rewarding_params: RewardingParams, + pub interval: Interval, + + next_mix_id: MixId, + pending_reward_pool_emission: Decimal, +} + +impl Simulator { + pub fn new(system_rewarding_params: RewardingParams, interval: Interval) -> Self { + Simulator { + nodes: Default::default(), + system_rewarding_params, + interval, + next_mix_id: 0, + pending_reward_pool_emission: Default::default(), + } + } + + fn advance_epoch(&mut self) { + let updated = self.interval.advance_epoch(); + + // we rolled over an interval + if self.interval.current_interval_id() + 1 == updated.current_interval_id() { + let old = self.system_rewarding_params.interval; + let reward_pool = old.reward_pool - self.pending_reward_pool_emission; + let staking_supply = old.staking_supply + self.pending_reward_pool_emission; + let epoch_reward_budget = reward_pool + / Decimal::from_atomics(self.interval.epochs_in_interval(), 0).unwrap() + * old.interval_pool_emission.value(); + let stake_saturation_point = staking_supply + / Decimal::from_atomics(self.system_rewarding_params.rewarded_set_size, 0).unwrap(); + + let updated_params = RewardingParams { + interval: IntervalRewardParams { + reward_pool, + staking_supply, + epoch_reward_budget, + stake_saturation_point, + sybil_resistance: old.sybil_resistance, + active_set_work_factor: old.active_set_work_factor, + interval_pool_emission: old.interval_pool_emission, + }, + rewarded_set_size: self.system_rewarding_params.rewarded_set_size, + active_set_size: self.system_rewarding_params.active_set_size, + }; + + self.system_rewarding_params = updated_params; + self.pending_reward_pool_emission = Decimal::zero(); + } + self.interval = updated; + } + + pub fn bond(&mut self, pledge: Coin, cost_params: MixNodeCostParams) -> MixId { + let mix_id = self.next_mix_id; + + self.nodes.insert( + mix_id, + SimulatedNode::new( + mix_id, + cost_params, + &pledge, + self.interval.current_epoch_absolute_id(), + ), + ); + + self.next_mix_id += 1; + + mix_id + } + + pub fn delegate>(&mut self, delegator: S, delegation: Coin, mix_id: MixId) { + let node = self.nodes.get_mut(&mix_id).expect("node doesn't exist"); + node.delegate(delegator, delegation) + } + + // since this is a simulator only, not something to be used in the production code, the unwraps are fine + // if user inputs are invalid + pub fn undelegate>( + &mut self, + delegator: S, + mix_id: MixId, + ) -> Result<(Coin, Coin), MixnetContractError> { + let node = self.nodes.get_mut(&mix_id).expect("node not found"); + node.undelegate(delegator) + } + + pub fn simulate_epoch_single_node(&mut self, params: NodeRewardParams) -> RewardDistribution { + assert_eq!(self.nodes.len(), 1); + + let id = *self.nodes.keys().next().unwrap(); + let mut params_map = BTreeMap::new(); + params_map.insert(id, params); + self.simulate_epoch(¶ms_map).remove(&id).unwrap() + } + + pub fn simulate_epoch( + &mut self, + node_params: &BTreeMap, + ) -> BTreeMap { + let mut params_keys = node_params.keys().copied().collect::>(); + params_keys.sort_unstable(); + let mut node_keys = self.nodes.keys().copied().collect::>(); + node_keys.sort_unstable(); + + if params_keys != node_keys { + panic!("invalid node rewarding params provided"); + } + + let mut dist = BTreeMap::new(); + + for (mix_id, node) in self.nodes.iter_mut() { + let reward_distribution = node.rewarding_details.calculate_epoch_reward( + &self.system_rewarding_params, + node_params[mix_id], + self.interval.epochs_in_interval(), + ); + node.rewarding_details.distribute_rewards( + reward_distribution, + self.interval.current_epoch_absolute_id(), + ); + self.pending_reward_pool_emission += reward_distribution.operator; + self.pending_reward_pool_emission += reward_distribution.delegates; + + dist.insert(*mix_id, reward_distribution); + } + + self.advance_epoch(); + dist + } + + pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal { + self.nodes[&delegation.mix_id] + .rewarding_details + .determine_delegation_reward(delegation) + } + + pub fn determine_total_delegation_reward(&self) -> Decimal { + let mut total = Decimal::zero(); + + for node in self.nodes.values() { + for delegation in node.delegations.values() { + total += node + .rewarding_details + .determine_delegation_reward(delegation) + } + } + total + } + + // assume node state doesn't change in the interval (kinda unrealistic) + pub fn simulate_full_interval(&mut self, node_params: &BTreeMap) { + for _ in 0..self.interval.epochs_in_interval() { + self.simulate_epoch(node_params); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::helpers::compare_decimals; + use crate::Percent; + use cosmwasm_std::testing::mock_env; + use std::time::Duration; + + #[cfg(test)] + mod single_node_case { + use super::*; + use crate::helpers::compare_decimals; + use crate::reward_params::IntervalRewardParams; + use crate::rewarding::helpers::truncate_reward_amount; + use crate::Percent; + use cosmwasm_std::coin; + use cosmwasm_std::testing::mock_env; + use std::time::Duration; + + fn base_simulator(initial_pledge: u128) -> Simulator { + let profit_margin = Percent::from_percentage_value(10).unwrap(); + let interval_operating_cost = Coin::new(40_000_000, "unym"); + let epochs_in_interval = 720u32; + let rewarded_set_size = 240; + let active_set_size = 100; + let interval_pool_emission = Percent::from_percentage_value(2).unwrap(); + + let reward_pool = 250_000_000_000_000u128; + let staking_supply = 100_000_000_000_000u128; + let epoch_reward_budget = + interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval); + let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size); + + let rewarding_params = RewardingParams { + interval: IntervalRewardParams { + reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) + staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M + epoch_reward_budget, + stake_saturation_point, + sybil_resistance: Percent::from_percentage_value(30).unwrap(), + active_set_work_factor: Decimal::percent(1000), // value '10' + interval_pool_emission, + }, + rewarded_set_size, + active_set_size, + }; + + let interval = Interval::init_interval( + epochs_in_interval, + Duration::from_secs(60 * 60), + &mock_env(), + ); + let initial_pledge = Coin::new(initial_pledge, "unym"); + let mut simulator = Simulator::new(rewarding_params, interval); + + let cost_params = MixNodeCostParams { + profit_margin_percent: profit_margin, + interval_operating_cost, + }; + simulator.bond(initial_pledge, cost_params); + simulator + } + + // essentially our delegations + estimated rewards HAVE TO equal to what we actually determined + fn check_rewarding_invariant(simulator: &Simulator) { + for node in simulator.nodes.values() { + let delegation_sum: Decimal = + node.delegations.values().map(|d| d.dec_amount()).sum(); + + let reward_sum: Decimal = node + .delegations + .values() + .map(|d| node.rewarding_details.determine_delegation_reward(d)) + .sum(); + + // let reward_sum = simulator.determine_total_delegation_reward(); + compare_decimals( + delegation_sum + reward_sum, + node.rewarding_details.delegates, + None, + ) + } + } + + #[test] + fn simulator_returns_expected_values_for_base_case() { + let mut simulator = base_simulator(10000_000000); + + let epoch_params = + NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let rewards = simulator.simulate_epoch_single_node(epoch_params); + + assert_eq!(rewards.delegates, Decimal::zero()); + compare_decimals( + rewards.operator, + "1128452.5416104363".parse().unwrap(), + None, + ); + } + + #[test] + fn single_delegation_at_genesis() { + let mut simulator = base_simulator(10000_000000); + simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0); + + let node_params = + NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let rewards = simulator.simulate_epoch_single_node(node_params); + + compare_decimals( + rewards.delegates, + "1795950.2602660495".parse().unwrap(), + None, + ); + compare_decimals(rewards.operator, "1363716.856243172".parse().unwrap(), None); + + compare_decimals( + rewards.delegates, + simulator.determine_total_delegation_reward(), + None, + ); + let node = &simulator.nodes[&0]; + assert_eq!( + node.rewarding_details.operator, + rewards.operator + Decimal::from_atomics(10000_000000u128, 0).unwrap() + ); + assert_eq!( + node.rewarding_details.delegates, + rewards.delegates + Decimal::from_atomics(18000_000000u128, 0).unwrap() + ); + } + + #[test] + fn delegation_and_undelegation() { + let mut simulator = base_simulator(10000_000000); + let node_params = + NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + + let rewards1 = simulator.simulate_epoch_single_node(node_params); + let expected_operator1 = "1128452.5416104363".parse().unwrap(); + assert_eq!(rewards1.delegates, Decimal::zero()); + compare_decimals(rewards1.operator, expected_operator1, None); + + simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0); + + let rewards2 = simulator.simulate_epoch_single_node(node_params); + let expected_operator2 = "1363843.413584609".parse().unwrap(); + let expected_delegator_reward1 = "1795952.25874404".parse().unwrap(); + compare_decimals(rewards2.delegates, expected_delegator_reward1, None); + compare_decimals(rewards2.operator, expected_operator2, None); + + let rewards3 = simulator.simulate_epoch_single_node(node_params); + let expected_operator3 = "1364017.7824440491".parse().unwrap(); + let expected_delegator_reward2 = "1796135.9269468693".parse().unwrap(); + compare_decimals(rewards3.delegates, expected_delegator_reward2, None); + compare_decimals(rewards3.operator, expected_operator3, None); + + let (delegation, reward) = simulator.undelegate("alice", 0).unwrap(); + assert_eq!(delegation.amount.u128(), 18000_000000); + assert_eq!( + reward.amount, + truncate_reward_amount(expected_delegator_reward1 + expected_delegator_reward2) + ); + + let base_op = Decimal::from_atomics(10000_000000u128, 0).unwrap(); + + let node = &simulator.nodes[&0]; + compare_decimals( + node.rewarding_details.operator, + base_op + expected_operator1 + expected_operator2 + expected_operator3, + None, + ); + assert_eq!(Decimal::zero(), node.rewarding_details.delegates); + } + + #[test] + fn withdrawing_operator_reward() { + // essentially all delegators' rewards (and the operator itself) are still correctly computed + let original_pledge = coin(10000_000000, "unym"); + let mut simulator = base_simulator(original_pledge.amount.u128()); + let node_params = + NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + + // add 2 delegations at genesis (because it makes things easier and as shown with previous tests + // delegating at different times still work) + simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0); + simulator.delegate("bob", Coin::new(4000_000000, "unym"), 0); + + // "normal", sanity check rewarding + let rewards1 = simulator.simulate_epoch_single_node(node_params); + let expected_operator1 = "1411087.1007647323".parse().unwrap(); + let expected_delegator_reward1 = "2199961.032388664".parse().unwrap(); + compare_decimals(rewards1.delegates, expected_delegator_reward1, None); + compare_decimals(rewards1.operator, expected_operator1, None); + check_rewarding_invariant(&simulator); + + let node = simulator.nodes.get_mut(&0).unwrap(); + let reward = node + .rewarding_details + .withdraw_operator_reward(&original_pledge); + assert_eq!(reward.amount, truncate_reward_amount(expected_operator1)); + assert_eq!( + node.rewarding_details.operator, + Decimal::from_atomics(original_pledge.amount, 0).unwrap() + ); + + let rewards2 = simulator.simulate_epoch_single_node(node_params); + let expected_operator2 = "1411113.0004067947".parse().unwrap(); + let expected_delegator_reward2 = "2200183.3879084454".parse().unwrap(); + compare_decimals(rewards2.delegates, expected_delegator_reward2, None); + compare_decimals(rewards2.operator, expected_operator2, None); + check_rewarding_invariant(&simulator); + } + + #[test] + fn withdrawing_delegator_reward() { + // essentially all delegators' rewards (and the operator itself) are still correctly computed + let mut simulator = base_simulator(10000_000000); + let node_params = + NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + + // add 2 delegations at genesis (because it makes things easier and as shown with previous tests + // delegating at different times still work) + simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0); + simulator.delegate("bob", Coin::new(4000_000000, "unym"), 0); + + // "normal", sanity check rewarding + let rewards1 = simulator.simulate_epoch_single_node(node_params); + let expected_operator1 = "1411087.1007647323".parse().unwrap(); + let expected_delegator_reward1 = "2199961.032388664".parse().unwrap(); + compare_decimals(rewards1.delegates, expected_delegator_reward1, None); + compare_decimals(rewards1.operator, expected_operator1, None); + check_rewarding_invariant(&simulator); + + // reference to our `18000_000000` delegation + let node = simulator.nodes.get_mut(&0).unwrap(); + let delegation1 = node.delegations.get_mut("alice").unwrap(); + let reward = node + .rewarding_details + .withdraw_delegator_reward(delegation1) + .unwrap(); + let expected_del1_reward = "1799968.1174089068".parse().unwrap(); + assert_eq!(reward.amount, truncate_reward_amount(expected_del1_reward)); + + // new reward after withdrawal + let rewards2 = simulator.simulate_epoch_single_node(node_params); + let expected_operator2 = "1411250.1907492676".parse().unwrap(); + let expected_delegator_reward2 = "2200004.051009689".parse().unwrap(); + compare_decimals(rewards2.delegates, expected_delegator_reward2, None); + compare_decimals(rewards2.operator, expected_operator2, None); + check_rewarding_invariant(&simulator); + + // check final values + let node = simulator.nodes.get_mut(&0).unwrap(); + let delegation1 = node.delegations.get_mut("alice").unwrap(); + + let reward_del1 = node + .rewarding_details + .withdraw_delegator_reward(delegation1) + .unwrap(); + let expected_del1_reward = "1799970.5883041779".parse().unwrap(); + assert_eq!( + reward_del1.amount, + truncate_reward_amount(expected_del1_reward) + ); + + let delegation2 = node.delegations.get_mut("bob").unwrap(); + let reward_del2 = node + .rewarding_details + .withdraw_delegator_reward(delegation2) + .unwrap(); + let first: Decimal = "399992.91497975704".parse().unwrap(); + let second: Decimal = "400033.4627055114".parse().unwrap(); + let expected_del2_reward = first + second; + assert_eq!( + reward_del2.amount, + truncate_reward_amount(expected_del2_reward) + ); + } + + #[test] + fn simulating_multiple_epochs() { + let mut simulator = base_simulator(10000_000000); + + let mut is_active = true; + let mut performance = Percent::from_percentage_value(100).unwrap(); + for epoch in 0..720 { + if epoch == 0 { + simulator.delegate("a", Coin::new(18000_000000, "unym"), 0) + } + if epoch == 42 { + simulator.delegate("b", Coin::new(2000_000000, "unym"), 0) + } + if epoch == 89 { + is_active = false; + } + if epoch == 123 { + simulator.delegate("c", Coin::new(6666_000000, "unym"), 0) + } + if epoch == 167 { + performance = Percent::from_percentage_value(90).unwrap(); + } + if epoch == 245 { + simulator.delegate("d", Coin::new(2050_000000, "unym"), 0) + } + if epoch == 264 { + let (delegation, _reward) = simulator.undelegate("b", 0).unwrap(); + // sanity check to make sure we undelegated what we wanted to undelegate : ) + assert_eq!(delegation.amount.u128(), 2000_000000); + // TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be + } + if epoch == 345 { + is_active = true; + } + if epoch == 358 { + performance = Percent::from_percentage_value(100).unwrap(); + } + if epoch == 458 { + let (delegation, _reward) = simulator.undelegate("a", 0).unwrap(); + // sanity check to make sure we undelegated what we wanted to undelegate : ) + assert_eq!(delegation.amount.u128(), 18000_000000); + // TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be + } + if epoch == 545 { + simulator.delegate("e", Coin::new(5000_000000, "unym"), 0) + } + + // this has to always hold + check_rewarding_invariant(&simulator); + let node_params = NodeRewardParams::new(performance, is_active); + simulator.simulate_epoch_single_node(node_params); + } + + // after everyone undelegates, there should be nothing left in the delegates pool + simulator.undelegate("c", 0).unwrap(); + simulator.undelegate("d", 0).unwrap(); + simulator.undelegate("e", 0).unwrap(); + + let node = &simulator.nodes[&0]; + assert_eq!(Decimal::zero(), node.rewarding_details.delegates); + } + } + + #[test] + fn multiple_nodes_against_known_values() { + #![allow(clippy::inconsistent_digit_grouping)] + + // TODO: this test can be further improved by checking values after EVERY interval + // rather than just checking the final results + + let epochs_in_interval = 1u32; + let rewarded_set_size = 10; + let active_set_size = 6; + let interval_pool_emission = Percent::from_percentage_value(2).unwrap(); + + let reward_pool = 250_000_000_000_000u128; + let staking_supply = 100_000_000_000_000u128; + let epoch_reward_budget = + interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval); + let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size); + + let rewarding_params = RewardingParams { + interval: IntervalRewardParams { + reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), + staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), + epoch_reward_budget, + stake_saturation_point, + sybil_resistance: Percent::from_percentage_value(30).unwrap(), + active_set_work_factor: Decimal::percent(1000), // value '10' + interval_pool_emission, + }, + rewarded_set_size, + active_set_size, + }; + + let interval = Interval::init_interval( + epochs_in_interval, + Duration::from_secs(60 * 60), + &mock_env(), + ); + + let mut simulator = Simulator::new(rewarding_params, interval); + + let n0 = simulator.bond( + Coin::new(11_000_000_000000, "unym"), + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: Coin::new(40_000_000, "unym"), + }, + ); + simulator.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n0); + + let n1 = simulator.bond( + Coin::new(1_000_000_000000, "unym"), + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: Coin::new(40_000_000, "unym"), + }, + ); + simulator.delegate("delegator", Coin::new(11_000_000_000000, "unym"), n1); + + let n2 = simulator.bond( + Coin::new(1_000_000_000000, "unym"), + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: Coin::new(40_000_000, "unym"), + }, + ); + simulator.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n2); + + let n3 = simulator.bond( + Coin::new(1_000_000_000000, "unym"), + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(0).unwrap(), + interval_operating_cost: Coin::new(500_000_000, "unym"), + }, + ); + simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n3); + + let n4 = simulator.bond( + Coin::new(1000_000000, "unym"), + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: Coin::new(40_000_000, "unym"), + }, + ); + simulator.delegate("delegator", Coin::new(7_999_000_000000, "unym"), n4); + + let n5 = simulator.bond( + Coin::new(1_000_000_000000, "unym"), + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: Coin::new(40_000_000, "unym"), + }, + ); + simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n5); + + let n6 = simulator.bond( + Coin::new(11_000_000_000000, "unym"), + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: Coin::new(40_000_000, "unym"), + }, + ); + simulator.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n6); + + let n7 = simulator.bond( + Coin::new(1_000_000_000000, "unym"), + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: Coin::new(40_000_000, "unym"), + }, + ); + simulator.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n7); + + let n8 = simulator.bond( + Coin::new(1_000_000_000000, "unym"), + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(0).unwrap(), + interval_operating_cost: Coin::new(500_000_000, "unym"), + }, + ); + simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n8); + + let n9 = simulator.bond( + Coin::new(1_000_000_000000, "unym"), + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: Coin::new(40_000_000, "unym"), + }, + ); + simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n9); + + let uptime_1 = Percent::from_percentage_value(100).unwrap(); + let uptime_09 = Percent::from_percentage_value(90).unwrap(); + let uptime_0 = Percent::from_percentage_value(0).unwrap(); + + let node_params = [ + (n0, NodeRewardParams::new(uptime_1, true)), + (n1, NodeRewardParams::new(uptime_1, true)), + (n2, NodeRewardParams::new(uptime_1, true)), + (n3, NodeRewardParams::new(uptime_09, true)), + (n4, NodeRewardParams::new(uptime_09, true)), + (n5, NodeRewardParams::new(uptime_0, true)), + (n6, NodeRewardParams::new(uptime_1, false)), + (n7, NodeRewardParams::new(uptime_1, false)), + (n8, NodeRewardParams::new(uptime_09, false)), + (n9, NodeRewardParams::new(uptime_0, false)), + ] + .into_iter() + .collect::>(); + + for _ in 0..23 { + simulator.simulate_full_interval(&node_params); + } + + // we allow the delta to be within 0.1unym, + // because the expected values, especially for the reward pool, + // do not provide us with any higher precision + let epsilon = Some(Decimal::from_ratio(1u32, 10u32)); + + let expected_reward_pool = "184876811322111.7".parse().unwrap(); + let expected_staking_supply = "165123188677888.3".parse().unwrap(); + compare_decimals( + expected_reward_pool, + simulator.system_rewarding_params.interval.reward_pool, + epsilon, + ); + compare_decimals( + expected_staking_supply, + simulator.system_rewarding_params.interval.staking_supply, + epsilon, + ); + + let expected_n0_pledge = "24307061704726.808".parse().unwrap(); + let expected_n0_delegated = "2031528592775.6752".parse().unwrap(); + let node = &simulator.nodes[&0].rewarding_details; + compare_decimals(node.operator, expected_n0_pledge, epsilon); + compare_decimals(node.delegates, expected_n0_delegated, epsilon); + + let expected_n1_pledge = "3544171010629.92".parse().unwrap(); + let expected_n1_delegated = "20854154351479.96".parse().unwrap(); + let node = &simulator.nodes[&1].rewarding_details; + compare_decimals(node.operator, expected_n1_pledge, epsilon); + compare_decimals(node.delegates, expected_n1_delegated, epsilon); + + let expected_n2_pledge = "3781120900089.8865".parse().unwrap(); + let expected_n2_delegated = "18634530734287.53".parse().unwrap(); + let node = &simulator.nodes[&2].rewarding_details; + compare_decimals(node.operator, expected_n2_pledge, epsilon); + compare_decimals(node.delegates, expected_n2_delegated, epsilon); + + let expected_n3_pledge = "2313562111772.3165".parse().unwrap(); + let expected_n3_delegated = "16090515100131.858".parse().unwrap(); + let node = &simulator.nodes[&3].rewarding_details; + compare_decimals(node.operator, expected_n3_pledge, epsilon); + compare_decimals(node.delegates, expected_n3_delegated, epsilon); + + let expected_n4_pledge = "1419679306492.7962".parse().unwrap(); + let expected_n4_delegated = "16802494863659.93".parse().unwrap(); + let node = &simulator.nodes[&4].rewarding_details; + compare_decimals(node.operator, expected_n4_pledge, epsilon); + compare_decimals(node.delegates, expected_n4_delegated, epsilon); + + let expected_n5_pledge = "1000000000000".parse().unwrap(); + let expected_n5_delegated = "7000000000000".parse().unwrap(); + let node = &simulator.nodes[&5].rewarding_details; + compare_decimals(node.operator, expected_n5_pledge, epsilon); + compare_decimals(node.delegates, expected_n5_delegated, epsilon); + + let expected_n6_pledge = "14114996375922.574".parse().unwrap(); + let expected_n6_delegated = "1249173915284.053".parse().unwrap(); + let node = &simulator.nodes[&6].rewarding_details; + compare_decimals(node.operator, expected_n6_pledge, epsilon); + compare_decimals(node.delegates, expected_n6_delegated, epsilon); + + let expected_n7_pledge = "1225564192694.3525".parse().unwrap(); + let expected_n7_delegated = "9931461332688.53".parse().unwrap(); + let node = &simulator.nodes[&7].rewarding_details; + compare_decimals(node.operator, expected_n7_pledge, epsilon); + compare_decimals(node.delegates, expected_n7_delegated, epsilon); + + let expected_n8_pledge = "1112319106593.8608".parse().unwrap(); + let expected_n8_delegated = "7710855078658.264".parse().unwrap(); + let node = &simulator.nodes[&8].rewarding_details; + compare_decimals(node.operator, expected_n8_pledge, epsilon); + compare_decimals(node.delegates, expected_n8_delegated, epsilon); + + let expected_n9_pledge = "1000000000000".parse().unwrap(); + let expected_n9_delegated = "7000000000000".parse().unwrap(); + let node = &simulator.nodes[&9].rewarding_details; + compare_decimals(node.operator, expected_n9_pledge, epsilon); + compare_decimals(node.delegates, expected_n9_delegated, epsilon); + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs new file mode 100644 index 0000000000..c7e1417a17 --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs @@ -0,0 +1,73 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{Delegation, EpochId, MixId, MixNodeCostParams, MixNodeRewarding}; +use cosmwasm_std::{Addr, Coin}; +use std::collections::HashMap; + +use crate::error::MixnetContractError; +use crate::rewarding::helpers::truncate_reward; + +pub struct SimulatedNode { + pub mix_id: MixId, + pub rewarding_details: MixNodeRewarding, + pub delegations: HashMap, +} + +impl SimulatedNode { + pub fn new( + mix_id: MixId, + cost_params: MixNodeCostParams, + initial_pledge: &Coin, + current_epoch: EpochId, + ) -> Self { + SimulatedNode { + mix_id, + rewarding_details: MixNodeRewarding::initialise_new( + cost_params, + initial_pledge, + current_epoch, + ), + delegations: HashMap::new(), + } + } + + pub fn delegate>(&mut self, delegator: S, delegation: Coin) { + self.rewarding_details + .add_base_delegation(delegation.amount); + + let delegator = delegator.into(); + let delegation = Delegation::new( + Addr::unchecked(&delegator), + self.mix_id, + self.rewarding_details.total_unit_reward, + delegation, + 42, + None, + ); + + self.delegations.insert(delegator, delegation); + } + + pub fn undelegate>( + &mut self, + delegator: S, + ) -> Result<(Coin, Coin), MixnetContractError> { + let delegator = delegator.into(); + let delegation = self + .delegations + .remove(&delegator) + .expect("delegation not found"); + + let reward = self + .rewarding_details + .determine_delegation_reward(&delegation); + self.rewarding_details + .remove_delegation_decimal(delegation.dec_amount() + reward)?; + + let reward_denom = &delegation.amount.denom; + let truncated_reward = truncate_reward(reward, reward_denom); + + Ok((delegation.amount, truncated_reward)) + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index 71faa991a6..f4af0362db 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -2,15 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::MixnetContractError; -use crate::rewarding::helpers::truncate_decimal; use crate::{Layer, RewardedSetNodeStatus}; -use cosmwasm_std::{Addr, Uint128}; -use cosmwasm_std::{Coin, Decimal}; +use cosmwasm_std::Addr; +use cosmwasm_std::Coin; use schemars::JsonSchema; -use serde::de::Error; -use serde::{Deserialize, Deserializer, Serialize}; -use std::fmt::{self, Display, Formatter}; -use std::ops::{Index, Mul}; +use serde::{Deserialize, Serialize}; +use std::ops::Index; // type aliases for better reasoning about available data pub type IdentityKey = String; @@ -19,91 +16,11 @@ pub type SphinxKey = String; pub type SphinxKeyRef<'a> = &'a str; pub type EpochId = u32; pub type IntervalId = u32; -pub type NodeId = u32; +pub type MixId = u32; +pub type BlockHeight = u64; pub type EpochEventId = u32; pub type IntervalEventId = u32; -/// Percent represents a value between 0 and 100% -/// (i.e. between 0.0 and 1.0) -#[derive( - Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema, -)] -pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); - -impl Percent { - pub fn new(value: Decimal) -> Result { - if value > Decimal::one() { - Err(MixnetContractError::InvalidPercent) - } else { - Ok(Percent(value)) - } - } - - pub fn is_zero(&self) -> bool { - self.0 == Decimal::zero() - } - - pub fn from_percentage_value(value: u64) -> Result { - Percent::new(Decimal::percent(value)) - } - - pub fn value(&self) -> Decimal { - self.0 - } - - pub fn round_to_integer(&self) -> u8 { - let hundred = Decimal::from_ratio(100u32, 1u32); - // we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range - truncate_decimal(hundred * self.0).u128() as u8 - } -} - -impl Display for Percent { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0; - write!(f, "{}%", adjusted) - } -} - -impl Mul for Percent { - type Output = Decimal; - - fn mul(self, rhs: Decimal) -> Self::Output { - self.0 * rhs - } -} - -impl Mul for Decimal { - type Output = Decimal; - - fn mul(self, rhs: Percent) -> Self::Output { - rhs * self - } -} - -impl Mul for Percent { - type Output = Uint128; - - fn mul(self, rhs: Uint128) -> Self::Output { - self.0 * rhs - } -} - -// implement custom Deserialize because we want to validate Percent has the correct range -fn de_decimal_percent<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let v = Decimal::deserialize(deserializer)?; - if v > Decimal::one() { - Err(D::Error::custom( - "provided decimal percent is larger than 100%", - )) - } else { - Ok(v) - } -} - #[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] pub struct LayerDistribution { pub layer1: u64, @@ -202,13 +119,13 @@ pub struct ContractStateParams { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct PagedRewardedSetResponse { - pub nodes: Vec<(NodeId, RewardedSetNodeStatus)>, - pub start_next_after: Option, + pub nodes: Vec<(MixId, RewardedSetNodeStatus)>, + pub start_next_after: Option, } #[cfg(test)] mod tests { - use super::*; + use contracts_common::Percent; #[test] fn percent_serde() { diff --git a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml index 4bb2c9a123..c2fa50a159 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml @@ -6,8 +6,10 @@ edition = "2021" [dependencies] cosmwasm-std = "1.0.0" mixnet-contract-common = { path = "../mixnet-contract" } +contracts-common = { path = "../contracts-common" } serde = { version = "1.0", features = ["derive"] } schemars = "0.8" +log = "0.4" ts-rs = {version = "6.1.2", optional = true} [features] diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs index e81a8068fa..4d494510b5 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs @@ -1,7 +1,11 @@ +use std::str::FromStr; + // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use contracts_common::Percent; use cosmwasm_std::{Addr, Coin, Timestamp, Uint128}; -use mixnet_contract_common::NodeId; +use log::warn; +use mixnet_contract_common::MixId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -42,6 +46,46 @@ impl PledgeData { } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub enum PledgeCap { + Percent(Percent), + Absolute(Uint128), // This has to be in unym +} + +impl FromStr for PledgeCap { + type Err = String; + + fn from_str(cap: &str) -> Result { + let cap = cap.replace('_', "").replace(',', "."); + match Percent::from_str(&cap) { + Ok(p) => { + if p.is_zero() { + warn!("Pledge cap set to 0%, are you sure this is right?") + } + Ok(PledgeCap::Percent(p)) + } + Err(_) => { + match cap.parse::() { + Ok(i) => { + if i < 100_000_000_000 { + warn!("PledgeCap set to less then 100_000 NYM, are you sure this is right?"); + } + Ok(PledgeCap::Absolute(Uint128::from(i))) + } + Err(_e) => Err(format!("Could not parse {} as Percent or Uint128", cap)), + } + } + } + } +} + +impl Default for PledgeCap { + fn default() -> Self { + PledgeCap::Absolute(Uint128::from(100_000_000_000u128)) + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct OriginalVestingResponse { pub amount: Coin, @@ -74,13 +118,13 @@ impl OriginalVestingResponse { #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, JsonSchema)] pub struct VestingDelegation { pub account_id: u32, - pub mix_id: NodeId, + pub mix_id: MixId, pub block_timestamp: u64, pub amount: Uint128, } impl VestingDelegation { - pub fn storage_key(&self) -> (u32, NodeId, u64) { + pub fn storage_key(&self) -> (u32, MixId, u64) { (self.account_id, self.mix_id, self.block_timestamp) } } @@ -89,12 +133,41 @@ impl VestingDelegation { pub struct DelegationTimesResponse { pub owner: Addr, pub account_id: u32, - pub mix_id: NodeId, + pub mix_id: MixId, pub delegation_timestamps: Vec, } #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, JsonSchema)] pub struct AllDelegationsResponse { pub delegations: Vec, - pub start_next_after: Option<(u32, NodeId, u64)>, + pub start_next_after: Option<(u32, MixId, u64)>, +} + +#[cfg(test)] +mod test { + use contracts_common::Percent; + use cosmwasm_std::Uint128; + use std::str::FromStr; + + use crate::PledgeCap; + + #[test] + fn test_pledge_cap_from_str() { + assert_eq!( + PledgeCap::from_str("0.1").unwrap(), + PledgeCap::Percent(Percent::from_percentage_value(10).unwrap()) + ); + assert_eq!( + PledgeCap::from_str("0,1").unwrap(), + PledgeCap::Percent(Percent::from_percentage_value(10).unwrap()) + ); + assert_eq!( + PledgeCap::from_str("100_000_000_000").unwrap(), + PledgeCap::Absolute(Uint128::new(100_000_000_000)) + ); + assert_eq!( + PledgeCap::from_str("100000000000").unwrap(), + PledgeCap::Absolute(Uint128::new(100_000_000_000)) + ); + } } diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index aa4f26afdd..98f8024dbc 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -1,11 +1,13 @@ -use cosmwasm_std::{Coin, Timestamp, Uint128}; +use cosmwasm_std::{Coin, Timestamp}; use mixnet_contract_common::{ mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, - Gateway, MixNode, NodeId, + Gateway, MixId, MixNode, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::PledgeCap; + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct InitMsg { @@ -61,7 +63,7 @@ pub enum ExecuteMsg { }, ClaimOperatorReward {}, ClaimDelegatorReward { - mix_id: NodeId, + mix_id: MixId, }, UpdateMixnodeCostParams { new_costs: MixNodeCostParams, @@ -73,23 +75,24 @@ pub enum ExecuteMsg { address: String, }, DelegateToMixnode { - mix_id: NodeId, + mix_id: MixId, amount: Coin, }, UndelegateFromMixnode { - mix_id: NodeId, + mix_id: MixId, }, CreateAccount { owner_address: String, staking_address: Option, vesting_spec: Option, + cap: Option, }, WithdrawVestedCoins { amount: Coin, }, TrackUndelegation { owner: String, - mix_id: NodeId, + mix_id: MixId, amount: Coin, }, BondMixnode { @@ -120,7 +123,8 @@ pub enum ExecuteMsg { to_address: Option, }, UpdateLockedPledgeCap { - amount: Uint128, + address: String, + cap: PledgeCap, }, } @@ -201,13 +205,12 @@ pub enum QueryMsg { GetCurrentVestingPeriod { address: String, }, - GetLockedPledgeCap {}, GetDelegationTimes { address: String, - mix_id: NodeId, + mix_id: MixId, }, GetAllDelegations { - start_after: Option<(u32, NodeId, u64)>, + start_after: Option<(u32, MixId, u64)>, limit: Option, }, } diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index c0c290e974..4eacccd531 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -11,9 +11,9 @@ async-trait = { version = "0.1.51" } log = "0.4" sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]} thiserror = "1.0" -tokio = { version = "1.19.1", features = [ "rt-multi-thread", "net", "signal", "fs" ] } +tokio = { version = "1.21.2", features = [ "rt-multi-thread", "net", "signal", "fs" ] } [build-dependencies] sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } -tokio = { version = "1.19.1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1.21.2", features = ["rt-multi-thread", "macros"] } diff --git a/common/credential-storage/build.rs b/common/credential-storage/build.rs index 0640039329..ad3cfe4bff 100644 --- a/common/credential-storage/build.rs +++ b/common/credential-storage/build.rs @@ -11,7 +11,7 @@ async fn main() { let out_dir = env::var("OUT_DIR").unwrap(); let database_path = format!("{}/coconut-credential-example.sqlite", out_dir); - let mut conn = SqliteConnection::connect(&*format!("sqlite://{}?mode=rwc", database_path)) + let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path)) .await .expect("Failed to create SQLx database connection"); diff --git a/common/crypto/dkg/src/bte/mod.rs b/common/crypto/dkg/src/bte/mod.rs index f3b4445c54..cb3dd07fa4 100644 --- a/common/crypto/dkg/src/bte/mod.rs +++ b/common/crypto/dkg/src/bte/mod.rs @@ -144,7 +144,7 @@ impl Tau { let tau_mem = self.0.as_raw_slice(); assert_eq!(tau_mem.len(), 1, "tau length invariant was broken"); - random_oracle_builder.update(&tau_mem[0].to_be_bytes()); + random_oracle_builder.update(tau_mem[0].to_be_bytes()); let oracle_output = random_oracle_builder.finalize(); debug_assert_eq!(oracle_output.len() * 8, HASH_SECURITY_PARAM); diff --git a/common/ledger/src/lib.rs b/common/ledger/src/lib.rs index ec8b8c8082..2ff4a18956 100644 --- a/common/ledger/src/lib.rs +++ b/common/ledger/src/lib.rs @@ -74,7 +74,7 @@ impl CosmosLedger { /// Get the SECP265K1 address of the device. pub fn get_addr_secp265k1(&self, display: bool) -> Result { - let display = if display { 1 } else { 0 }; + let display = u8::from(display); let components = path_bytes(self.path.clone())?; let data: Vec = vec![ [self.prefix.len() as u8].as_slice(), diff --git a/common/mixnode-common/Cargo.toml b/common/mixnode-common/Cargo.toml index 21bad877b8..53676023af 100644 --- a/common/mixnode-common/Cargo.toml +++ b/common/mixnode-common/Cargo.toml @@ -13,7 +13,7 @@ humantime-serde = "1.0" log = "0.4" rand = "0.8" serde = { version = "1.0", features = ["derive"] } -tokio = { version = "1.19.1", features = ["time", "macros", "rt", "net", "io-util"] } +tokio = { version = "1.21.2", features = ["time", "macros", "rt", "net", "io-util"] } tokio-util = { version = "0.7.3", features = ["codec"] } url = "2.2" diff --git a/common/network-defaults/src/all.rs b/common/network-defaults/src/all.rs deleted file mode 100644 index 91188326a0..0000000000 --- a/common/network-defaults/src/all.rs +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright 2021-2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::{ - DefaultNetworkDetails, DenomDetailsOwned, NymNetworkDetails, ValidatorDetails, - MAINNET_DEFAULTS, QA_DEFAULTS, SANDBOX_DEFAULTS, -}; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt, str::FromStr}; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum NetworkDefaultsError { - #[error("The provided network was invalid")] - MalformedNetworkProvided(String), -} - -// the reason for allowing it is that this is just a temporary solution -#[allow(clippy::large_enum_variant)] -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] -pub enum Network { - QA, - SANDBOX, - MAINNET, - CUSTOM { details: NymNetworkDetails }, -} - -impl Network { - pub fn new_custom(details: NymNetworkDetails) -> Self { - Network::CUSTOM { details } - } - - pub fn details(&self) -> NymNetworkDetails { - match self { - Self::QA => (&*QA_DEFAULTS).into(), - Self::SANDBOX => (&*SANDBOX_DEFAULTS).into(), - Self::MAINNET => (&*MAINNET_DEFAULTS).into(), - // I dislike the clone here, but for compatibility reasons we cannot define other networks with `NymNetworkDetails` directly yet - Self::CUSTOM { details } => details.clone(), - } - } - - pub fn bech32_prefix(&self) -> String { - self.details().chain_details.bech32_account_prefix - } - - pub fn mix_denom(&self) -> DenomDetailsOwned { - self.details().chain_details.mix_denom - } - - pub fn stake_denom(&self) -> DenomDetailsOwned { - self.details().chain_details.stake_denom - } - - pub fn base_mix_denom(&self) -> String { - self.details().chain_details.mix_denom.base - } - - pub fn base_stake_denom(&self) -> String { - self.details().chain_details.stake_denom.base - } - - pub fn mixnet_contract_address(&self) -> Option { - self.details().contracts.mixnet_contract_address - } - - pub fn vesting_contract_address(&self) -> Option { - self.details().contracts.vesting_contract_address - } - - pub fn bandwidth_claim_contract_address(&self) -> Option { - self.details().contracts.bandwidth_claim_contract_address - } - - pub fn coconut_bandwidth_contract_address(&self) -> Option { - self.details().contracts.coconut_bandwidth_contract_address - } - - pub fn multisig_contract_address(&self) -> Option { - self.details().contracts.multisig_contract_address - } - - pub fn validators(&self) -> Vec { - self.details().endpoints - } - - // only used in mixnet contract tests, but I don't want to be messing with that code now - pub fn rewarding_validator_address(&self) -> &str { - match self { - Network::QA => crate::qa::REWARDING_VALIDATOR_ADDRESS, - Network::SANDBOX => crate::sandbox::REWARDING_VALIDATOR_ADDRESS, - Network::MAINNET => crate::mainnet::REWARDING_VALIDATOR_ADDRESS, - Network::CUSTOM { .. } => { - panic!("rewarding validator address is unavailable for a custom network") - } - } - } - - // this should be handled differently, but I don't want to break compatibility - pub fn statistics_service_url(&self) -> &str { - match self { - Network::MAINNET => crate::mainnet::STATISTICS_SERVICE_DOMAIN_ADDRESS, - Network::SANDBOX => crate::mainnet::STATISTICS_SERVICE_DOMAIN_ADDRESS, - Network::QA => crate::mainnet::STATISTICS_SERVICE_DOMAIN_ADDRESS, - Network::CUSTOM { .. } => { - panic!("statistics service url is unavailable for a custom network") - } - } - } -} - -impl FromStr for Network { - type Err = NetworkDefaultsError; - - fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { - "qa" => Ok(Network::QA), - "sandbox" => Ok(Network::SANDBOX), - "mainnet" => Ok(Network::MAINNET), - _ => Err(NetworkDefaultsError::MalformedNetworkProvided( - s.to_string(), - )), - } - } -} - -impl fmt::Display for Network { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - Network::QA => f.write_str("QA"), - Network::SANDBOX => f.write_str("Sandbox"), - Network::MAINNET => f.write_str("Mainnet"), - Network::CUSTOM { .. } => f.write_str("Custom"), - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct NetworkDetails { - bech32_prefix: String, - mix_denom: DenomDetailsOwned, - stake_denom: DenomDetailsOwned, - mixnet_contract_address: String, - vesting_contract_address: String, - bandwidth_claim_contract_address: String, - statistics_service_url: String, - validators: Vec, -} - -impl From<&DefaultNetworkDetails> for NetworkDetails { - fn from(details: &DefaultNetworkDetails) -> Self { - NetworkDetails { - bech32_prefix: details.bech32_prefix.into(), - mix_denom: details.mix_denom.into(), - stake_denom: details.stake_denom.into(), - mixnet_contract_address: details.mixnet_contract_address.into(), - vesting_contract_address: details.vesting_contract_address.into(), - bandwidth_claim_contract_address: details.bandwidth_claim_contract_address.into(), - statistics_service_url: details.statistics_service_url.into(), - validators: details.validators.clone(), - } - } -} - -// this also has to exist for compatibility reasons since I don't want to be touching the wallet now -impl From for NetworkDetails { - fn from(details: NymNetworkDetails) -> Self { - NetworkDetails { - bech32_prefix: details.chain_details.bech32_account_prefix, - mix_denom: details.chain_details.mix_denom, - stake_denom: details.chain_details.stake_denom, - mixnet_contract_address: details - .contracts - .mixnet_contract_address - .unwrap_or_default(), - vesting_contract_address: details - .contracts - .vesting_contract_address - .unwrap_or_default(), - bandwidth_claim_contract_address: details - .contracts - .bandwidth_claim_contract_address - .unwrap_or_default(), - statistics_service_url: "".to_string(), - validators: details.endpoints, - } - } -} - -impl NetworkDetails { - pub fn base_mix_denom(&self) -> &str { - &self.mix_denom.base - } -} - -#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] -pub struct SupportedNetworks { - networks: HashMap, -} - -impl SupportedNetworks { - pub fn new(support: Vec) -> Self { - SupportedNetworks { - networks: support - .into_iter() - .map(|n| { - let details = n.details().into(); - (n, details) - }) - .collect(), - } - } - - pub fn bech32_prefix(&self, network: Network) -> Option<&str> { - self.networks - .get(&network) - .map(|network_details| network_details.bech32_prefix.as_str()) - } - - pub fn base_mix_denom(&self, network: Network) -> Option<&str> { - self.networks - .get(&network) - .map(|network_details| network_details.base_mix_denom()) - } - - pub fn mixnet_contract_address(&self, network: Network) -> Option<&str> { - self.networks - .get(&network) - .map(|network_details| network_details.mixnet_contract_address.as_str()) - } - - pub fn vesting_contract_address(&self, network: Network) -> Option<&str> { - self.networks - .get(&network) - .map(|network_details| network_details.vesting_contract_address.as_str()) - } - - pub fn bandwidth_claim_contract_address(&self, network: Network) -> Option<&str> { - self.networks - .get(&network) - .map(|network_details| network_details.bandwidth_claim_contract_address.as_str()) - } - - pub fn validators(&self, network: Network) -> impl Iterator { - self.networks - .get(&network) - .map(|network_details| &network_details.validators) - .into_iter() - .flatten() - } -} diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index ddd4a9dabb..c600199140 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -1,15 +1,11 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; -use std::{env::var, path::PathBuf}; +use std::{env::var, ops::Not, path::PathBuf}; use url::Url; -pub mod all; pub mod mainnet; -pub mod qa; -pub mod sandbox; pub mod var_names; pub const ETH_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_CONTRACT_ADDRESS; @@ -103,159 +99,99 @@ impl NymNetworkDetails { } pub fn new_mainnet() -> Self { - (&*MAINNET_DEFAULTS).into() + fn parse_optional_str(raw: &str) -> Option { + raw.is_empty().not().then(|| raw.into()) + } + + // Consider caching this process (lazy static) + NymNetworkDetails { + chain_details: ChainDetails { + bech32_account_prefix: mainnet::BECH32_PREFIX.into(), + mix_denom: mainnet::MIX_DENOM.into(), + stake_denom: mainnet::STAKE_DENOM.into(), + }, + endpoints: mainnet::validators(), + contracts: NymContracts { + mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS), + vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS), + bandwidth_claim_contract_address: parse_optional_str( + mainnet::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, + ), + coconut_bandwidth_contract_address: parse_optional_str( + mainnet::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, + ), + multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS), + }, + } } + #[must_use] pub fn with_bech32_account_prefix>(mut self, prefix: S) -> Self { self.chain_details.bech32_account_prefix = prefix.into(); self } + #[must_use] pub fn with_mix_denom(mut self, mix_denom: DenomDetailsOwned) -> Self { self.chain_details.mix_denom = mix_denom; self } + #[must_use] pub fn with_stake_denom(mut self, stake_denom: DenomDetailsOwned) -> Self { self.chain_details.stake_denom = stake_denom; self } + #[must_use] pub fn with_base_mix_denom>(mut self, base_mix_denom: S) -> Self { self.chain_details.mix_denom = DenomDetailsOwned::base_only(base_mix_denom.into()); self } + #[must_use] pub fn with_base_stake_denom>(mut self, base_stake_denom: S) -> Self { self.chain_details.stake_denom = DenomDetailsOwned::base_only(base_stake_denom.into()); self } + #[must_use] pub fn with_validator_endpoint(mut self, endpoint: ValidatorDetails) -> Self { self.endpoints.push(endpoint); self } + #[must_use] pub fn with_mixnet_contract>(mut self, contract: Option) -> Self { self.contracts.mixnet_contract_address = contract.map(Into::into); self } + #[must_use] pub fn with_vesting_contract>(mut self, contract: Option) -> Self { self.contracts.vesting_contract_address = contract.map(Into::into); self } + #[must_use] pub fn with_bandwidth_claim_contract>(mut self, contract: Option) -> Self { self.contracts.bandwidth_claim_contract_address = contract.map(Into::into); self } + #[must_use] pub fn with_coconut_bandwidth_contract>(mut self, contract: Option) -> Self { self.contracts.coconut_bandwidth_contract_address = contract.map(Into::into); self } + #[must_use] pub fn with_multisig_contract>(mut self, contract: Option) -> Self { self.contracts.multisig_contract_address = contract.map(Into::into); self } } -// This conversion only exists for convenience reasons until -// we can completely phase out `DefaultNetworkDetails` -impl<'a> From<&'a DefaultNetworkDetails> for NymNetworkDetails { - fn from(details: &'a DefaultNetworkDetails) -> Self { - fn parse_optional_str(raw: &str) -> Option { - if raw.is_empty() { - None - } else { - Some(raw.into()) - } - } - - NymNetworkDetails { - chain_details: ChainDetails { - bech32_account_prefix: details.bech32_prefix.into(), - mix_denom: details.mix_denom.into(), - stake_denom: details.stake_denom.into(), - }, - endpoints: details.validators.clone(), - contracts: NymContracts { - mixnet_contract_address: parse_optional_str(details.mixnet_contract_address), - vesting_contract_address: parse_optional_str(details.vesting_contract_address), - bandwidth_claim_contract_address: parse_optional_str( - details.bandwidth_claim_contract_address, - ), - coconut_bandwidth_contract_address: parse_optional_str( - details.coconut_bandwidth_contract_address, - ), - multisig_contract_address: parse_optional_str(details.multisig_contract_address), - }, - } - } -} - -// Since these are lazily constructed, we can afford to switch some of them to stronger types in the -// future. If we do this, and also get rid of the references we could potentially unify with -// `NetworkDetails`. -pub struct DefaultNetworkDetails { - bech32_prefix: &'static str, - mix_denom: DenomDetails, - stake_denom: DenomDetails, - mixnet_contract_address: &'static str, - vesting_contract_address: &'static str, - bandwidth_claim_contract_address: &'static str, - coconut_bandwidth_contract_address: &'static str, - multisig_contract_address: &'static str, - #[allow(dead_code)] - rewarding_validator_address: &'static str, - statistics_service_url: &'static str, - validators: Vec, -} - -static MAINNET_DEFAULTS: Lazy = Lazy::new(|| DefaultNetworkDetails { - bech32_prefix: mainnet::BECH32_PREFIX, - mix_denom: mainnet::MIX_DENOM, - stake_denom: mainnet::STAKE_DENOM, - mixnet_contract_address: mainnet::MIXNET_CONTRACT_ADDRESS, - vesting_contract_address: mainnet::VESTING_CONTRACT_ADDRESS, - bandwidth_claim_contract_address: mainnet::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, - coconut_bandwidth_contract_address: mainnet::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - multisig_contract_address: mainnet::MULTISIG_CONTRACT_ADDRESS, - rewarding_validator_address: mainnet::REWARDING_VALIDATOR_ADDRESS, - statistics_service_url: mainnet::STATISTICS_SERVICE_DOMAIN_ADDRESS, - validators: mainnet::validators(), -}); - -static SANDBOX_DEFAULTS: Lazy = Lazy::new(|| DefaultNetworkDetails { - bech32_prefix: sandbox::BECH32_PREFIX, - mix_denom: sandbox::MIX_DENOM, - stake_denom: sandbox::STAKE_DENOM, - mixnet_contract_address: sandbox::MIXNET_CONTRACT_ADDRESS, - vesting_contract_address: sandbox::VESTING_CONTRACT_ADDRESS, - bandwidth_claim_contract_address: sandbox::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, - coconut_bandwidth_contract_address: sandbox::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - multisig_contract_address: sandbox::MULTISIG_CONTRACT_ADDRESS, - rewarding_validator_address: sandbox::REWARDING_VALIDATOR_ADDRESS, - statistics_service_url: sandbox::STATISTICS_SERVICE_DOMAIN_ADDRESS, - validators: sandbox::validators(), -}); - -static QA_DEFAULTS: Lazy = Lazy::new(|| DefaultNetworkDetails { - bech32_prefix: qa::BECH32_PREFIX, - mix_denom: qa::MIX_DENOM, - stake_denom: qa::STAKE_DENOM, - mixnet_contract_address: qa::MIXNET_CONTRACT_ADDRESS, - vesting_contract_address: qa::VESTING_CONTRACT_ADDRESS, - bandwidth_claim_contract_address: qa::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, - coconut_bandwidth_contract_address: qa::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, - multisig_contract_address: qa::MULTISIG_CONTRACT_ADDRESS, - rewarding_validator_address: qa::REWARDING_VALIDATOR_ADDRESS, - statistics_service_url: qa::STATISTICS_SERVICE_DOMAIN_ADDRESS, - validators: qa::validators(), -}); - #[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct DenomDetails { pub base: &'static str, @@ -349,12 +285,17 @@ pub fn setup_env(config_env_file: Option) { .expect("Invalid path to environment configuration file"); } else { // if nothing is set, the use mainnet defaults + // if the user has not set `CONFIGURED`, then even if they set any of the env variables, + // overwrite them crate::mainnet::export_to_env(); } } Err(_) => crate::mainnet::export_to_env(), _ => {} } + + // if we haven't explicitly defined any of the constants, fallback to defaults + crate::mainnet::export_to_env_if_not_set() } // Name of the event triggered by the eth contract. If the event name is changed, diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index 53e85d8d89..ed415e2622 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -31,42 +31,107 @@ pub(crate) fn validators() -> Vec { vec![ValidatorDetails::new(NYMD_VALIDATOR, Some(API_VALIDATOR))] } +const DEFAULT_SUFFIX: &str = "_MAINNET_DEFAULT"; + +fn set_var_to_default(var: &str, value: &str) { + std::env::set_var(var, value); + std::env::set_var(format!("{}{}", var, DEFAULT_SUFFIX), "1") +} + +fn set_var_conditionally_to_default(var: &str, value: &str) { + if std::env::var(var).is_err() { + set_var_to_default(var, value) + } +} + +pub fn uses_default(var: &str) -> bool { + std::env::var(format!("{}{}", var, DEFAULT_SUFFIX)).is_ok() +} + +pub fn read_var_if_not_default(var: &str) -> Option { + if uses_default(var) { + None + } else { + std::env::var(var).ok() + } +} + pub fn export_to_env() { - std::env::set_var(var_names::CONFIGURED, "true"); - std::env::set_var(var_names::BECH32_PREFIX, BECH32_PREFIX); - std::env::set_var(var_names::MIX_DENOM, MIX_DENOM.base); - std::env::set_var(var_names::MIX_DENOM_DISPLAY, MIX_DENOM.display); - std::env::set_var(var_names::STAKE_DENOM, STAKE_DENOM.base); - std::env::set_var(var_names::STAKE_DENOM_DISPLAY, STAKE_DENOM.display); - std::env::set_var( + set_var_to_default(var_names::CONFIGURED, "true"); + set_var_to_default(var_names::BECH32_PREFIX, BECH32_PREFIX); + set_var_to_default(var_names::MIX_DENOM, MIX_DENOM.base); + set_var_to_default(var_names::MIX_DENOM_DISPLAY, MIX_DENOM.display); + set_var_to_default(var_names::STAKE_DENOM, STAKE_DENOM.base); + set_var_to_default(var_names::STAKE_DENOM_DISPLAY, STAKE_DENOM.display); + set_var_to_default( var_names::DENOMS_EXPONENT, - STAKE_DENOM.display_exponent.to_string(), + &STAKE_DENOM.display_exponent.to_string(), ); - std::env::set_var(var_names::MIXNET_CONTRACT_ADDRESS, MIXNET_CONTRACT_ADDRESS); - std::env::set_var( + set_var_to_default(var_names::MIXNET_CONTRACT_ADDRESS, MIXNET_CONTRACT_ADDRESS); + set_var_to_default( var_names::VESTING_CONTRACT_ADDRESS, VESTING_CONTRACT_ADDRESS, ); - std::env::set_var( + set_var_to_default( var_names::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, BANDWIDTH_CLAIM_CONTRACT_ADDRESS, ); - std::env::set_var( + set_var_to_default( var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, COCONUT_BANDWIDTH_CONTRACT_ADDRESS, ); - std::env::set_var( + set_var_to_default( var_names::MULTISIG_CONTRACT_ADDRESS, MULTISIG_CONTRACT_ADDRESS, ); - std::env::set_var( + set_var_to_default( var_names::REWARDING_VALIDATOR_ADDRESS, REWARDING_VALIDATOR_ADDRESS, ); - std::env::set_var( + set_var_to_default( var_names::STATISTICS_SERVICE_DOMAIN_ADDRESS, STATISTICS_SERVICE_DOMAIN_ADDRESS, ); - std::env::set_var(var_names::NYMD_VALIDATOR, NYMD_VALIDATOR); - std::env::set_var(var_names::API_VALIDATOR, API_VALIDATOR); + set_var_to_default(var_names::NYMD_VALIDATOR, NYMD_VALIDATOR); + set_var_to_default(var_names::API_VALIDATOR, API_VALIDATOR); +} + +pub fn export_to_env_if_not_set() { + set_var_conditionally_to_default(var_names::CONFIGURED, "true"); + set_var_conditionally_to_default(var_names::BECH32_PREFIX, BECH32_PREFIX); + set_var_conditionally_to_default(var_names::MIX_DENOM, MIX_DENOM.base); + set_var_conditionally_to_default(var_names::MIX_DENOM_DISPLAY, MIX_DENOM.display); + set_var_conditionally_to_default(var_names::STAKE_DENOM, STAKE_DENOM.base); + set_var_conditionally_to_default(var_names::STAKE_DENOM_DISPLAY, STAKE_DENOM.display); + set_var_conditionally_to_default( + var_names::DENOMS_EXPONENT, + &STAKE_DENOM.display_exponent.to_string(), + ); + set_var_conditionally_to_default(var_names::MIXNET_CONTRACT_ADDRESS, MIXNET_CONTRACT_ADDRESS); + set_var_conditionally_to_default( + var_names::VESTING_CONTRACT_ADDRESS, + VESTING_CONTRACT_ADDRESS, + ); + set_var_conditionally_to_default( + var_names::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, + BANDWIDTH_CLAIM_CONTRACT_ADDRESS, + ); + set_var_conditionally_to_default( + var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, + COCONUT_BANDWIDTH_CONTRACT_ADDRESS, + ); + set_var_conditionally_to_default( + var_names::MULTISIG_CONTRACT_ADDRESS, + MULTISIG_CONTRACT_ADDRESS, + ); + set_var_conditionally_to_default( + var_names::REWARDING_VALIDATOR_ADDRESS, + REWARDING_VALIDATOR_ADDRESS, + ); + set_var_conditionally_to_default( + var_names::STATISTICS_SERVICE_DOMAIN_ADDRESS, + STATISTICS_SERVICE_DOMAIN_ADDRESS, + ); + set_var_conditionally_to_default(var_names::NYMD_VALIDATOR, NYMD_VALIDATOR); + set_var_conditionally_to_default(var_names::API_VALIDATOR, API_VALIDATOR); } diff --git a/common/network-defaults/src/qa.rs b/common/network-defaults/src/qa.rs deleted file mode 100644 index 3b5faa4da7..0000000000 --- a/common/network-defaults/src/qa.rs +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::{DenomDetails, ValidatorDetails}; - -pub(crate) const BECH32_PREFIX: &str = "n"; - -pub const MIX_DENOM: DenomDetails = DenomDetails::new("unym", "nym", 6); -pub const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6); - -pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = - "n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep"; -pub(crate) const VESTING_CONTRACT_ADDRESS: &str = - "n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav"; -pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = - "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; -pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = - "n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw"; -pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs"; -pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] = - hex_literal::hex!("0000000000000000000000000000000000000000"); -pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = - hex_literal::hex!("0000000000000000000000000000000000000000"); -pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq"; - -pub(crate) const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "http://0.0.0.0"; -pub(crate) fn validators() -> Vec { - vec![ValidatorDetails::new( - "https://qa-validator.nymtech.net", - Some("https://qa-validator-api.nymtech.net/api"), - )] -} diff --git a/common/network-defaults/src/sandbox.rs b/common/network-defaults/src/sandbox.rs deleted file mode 100644 index e758b1717a..0000000000 --- a/common/network-defaults/src/sandbox.rs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::{DenomDetails, ValidatorDetails}; - -pub(crate) const BECH32_PREFIX: &str = "nymt"; - -pub const MIX_DENOM: DenomDetails = DenomDetails::new("unymt", "nymt", 6); -pub const STAKE_DENOM: DenomDetails = DenomDetails::new("unyxt", "nyxt", 6); - -pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt1ghd753shjuwexxywmgs4xz7x2q732vcnstz02j"; -pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt14ejqjyq8um4p3xfqj74yld5waqljf88fn549lh"; -pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = - "nymt17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9f8xzkv"; -pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = - "nymt1nz0r0au8aj6dc00wmm3ufy4g4k86rjzlgq608r"; -pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "nymt1k8re7jwz6rnnwrktnejdwkwnncte7ek7kk6fvg"; -pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] = - hex_literal::hex!("8e0DcFF7F3085235C32E845f3667aEB3f1e83133"); -pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = - hex_literal::hex!("E8883BAeF3869e14E4823F46662e81D4F7d2A81F"); -pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "nymt1jh0s6qu6tuw9ut438836mmn7f3f2wencrnmdj4"; - -pub(crate) const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "http://0.0.0.0"; -pub(crate) fn validators() -> Vec { - vec![ValidatorDetails::new( - "https://sandbox-validator.nymtech.net", - Some("https://sandbox-validator.nymtech.net/api"), - )] -} diff --git a/common/nonexhaustive-delayqueue/Cargo.toml b/common/nonexhaustive-delayqueue/Cargo.toml index 778342e578..74c1776e95 100644 --- a/common/nonexhaustive-delayqueue/Cargo.toml +++ b/common/nonexhaustive-delayqueue/Cargo.toml @@ -7,6 +7,16 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tokio = { version = "1.19.1", features = [] } +tokio = { version = "1.21.2", features = [] } tokio-stream = "0.1.9" # this one seems to be a thing until `Stream` trait is stabilised in stdlib tokio-util = { version = "0.7.3", features = ["time"] } + +[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-timer] +git = "https://github.com/mmsinclair/wasm-timer" +rev = "b9d1a54ad514c2f230a026afe0dde341e98cd7b6" + +[target."cfg(target_arch = \"wasm32\")".dependencies.slab] +version = "0.4.4" + +[target."cfg(target_arch = \"wasm32\")".dependencies.futures-core] +version = "0.3.0" \ No newline at end of file diff --git a/common/nonexhaustive-delayqueue/src/lib.rs b/common/nonexhaustive-delayqueue/src/lib.rs index 11c1d608c3..4e2329db2b 100644 --- a/common/nonexhaustive-delayqueue/src/lib.rs +++ b/common/nonexhaustive-delayqueue/src/lib.rs @@ -4,13 +4,32 @@ use std::pin::Pin; use std::task::{Context, Poll, Waker}; use std::time::Duration; -use tokio::time::Instant; use tokio_stream::Stream; -use tokio_util::time::{delay_queue, DelayQueue}; pub use tokio::time::error::Error as TimerError; + +// this is a copy of tokio-util delay_queue with `Sleep` and `Instant` being replaced with +// `wasm_timer` equivalents +#[cfg(target_arch = "wasm32")] +mod wasm_delay_queue; + +#[cfg(not(target_arch = "wasm32"))] +type DelayQueue = tokio_util::time::DelayQueue; +#[cfg(not(target_arch = "wasm32"))] pub use tokio_util::time::delay_queue::Expired; -pub type QueueKey = delay_queue::Key; +#[cfg(not(target_arch = "wasm32"))] +pub type QueueKey = tokio_util::time::delay_queue::Key; +#[cfg(not(target_arch = "wasm32"))] +use tokio::time::Instant; + +#[cfg(target_arch = "wasm32")] +type DelayQueue = crate::wasm_delay_queue::DelayQueue; +#[cfg(target_arch = "wasm32")] +pub use crate::wasm_delay_queue::delay_queue::Expired; +#[cfg(target_arch = "wasm32")] +pub type QueueKey = crate::wasm_delay_queue::delay_queue::Key; +#[cfg(target_arch = "wasm32")] +use wasm_timer::Instant; /// A variant of tokio's `DelayQueue`, such that its `Stream` implementation will never return a 'None'. pub struct NonExhaustiveDelayQueue { diff --git a/common/nonexhaustive-delayqueue/src/wasm_delay_queue/delay_queue.rs b/common/nonexhaustive-delayqueue/src/wasm_delay_queue/delay_queue.rs new file mode 100644 index 0000000000..6d18b7b12d --- /dev/null +++ b/common/nonexhaustive-delayqueue/src/wasm_delay_queue/delay_queue.rs @@ -0,0 +1,1234 @@ +//! A queue of delayed elements. +//! +//! See [`DelayQueue`] for more details. +//! +//! [`DelayQueue`]: struct@DelayQueue + +use crate::wasm_delay_queue::sleep_until; +use crate::wasm_delay_queue::wheel::{self, Wheel}; +use core::ops::{Index, IndexMut}; +use futures_core::{ready, Future}; +use slab::Slab; +use std::cmp; +use std::collections::HashMap; +use std::convert::From; +use std::fmt; +use std::fmt::Debug; +use std::marker::PhantomData; +use std::pin::Pin; +use std::task::{self, Poll, Waker}; +use std::time::Duration; +use wasm_timer::{Delay, Instant}; + +/// A queue of delayed elements. +/// +/// Once an element is inserted into the `DelayQueue`, it is yielded once the +/// specified deadline has been reached. +/// +/// # Usage +/// +/// Elements are inserted into `DelayQueue` using the [`insert`] or +/// [`insert_at`] methods. A deadline is provided with the item and a [`Key`] is +/// returned. The key is used to remove the entry or to change the deadline at +/// which it should be yielded back. +/// +/// Once delays have been configured, the `DelayQueue` is used via its +/// [`Stream`] implementation. [`poll_expired`] is called. If an entry has reached its +/// deadline, it is returned. If not, `Poll::Pending` is returned indicating that the +/// current task will be notified once the deadline has been reached. +/// +/// # `Stream` implementation +/// +/// Items are retrieved from the queue via [`DelayQueue::poll_expired`]. If no delays have +/// expired, no items are returned. In this case, `Poll::Pending` is returned and the +/// current task is registered to be notified once the next item's delay has +/// expired. +/// +/// If no items are in the queue, i.e. `is_empty()` returns `true`, then `poll` +/// returns `Poll::Ready(None)`. This indicates that the stream has reached an end. +/// However, if a new item is inserted *after*, `poll` will once again start +/// returning items or `Poll::Pending`. +/// +/// Items are returned ordered by their expirations. Items that are configured +/// to expire first will be returned first. There are no ordering guarantees +/// for items configured to expire at the same instant. Also note that delays are +/// rounded to the closest millisecond. +/// +/// # Implementation +/// +/// The [`DelayQueue`] is backed by a separate instance of a timer wheel similar to that used internally +/// by Tokio's standalone timer utilities such as [`sleep`]. Because of this, it offers the same +/// performance and scalability benefits. +/// +/// State associated with each entry is stored in a [`slab`]. This amortizes the cost of allocation, +/// and allows reuse of the memory allocated for expired entires. +/// +/// Capacity can be checked using [`capacity`] and allocated preemptively by using +/// the [`reserve`] method. +/// +/// # Usage +/// +/// Using `DelayQueue` to manage cache entries. +/// +/// ```rust,no_run +/// use tokio_util::time::{DelayQueue, delay_queue}; +/// +/// use futures::ready; +/// use std::collections::HashMap; +/// use std::task::{Context, Poll}; +/// use std::time::Duration; +/// # type CacheKey = String; +/// # type Value = String; +/// +/// struct Cache { +/// entries: HashMap, +/// expirations: DelayQueue, +/// } +/// +/// const TTL_SECS: u64 = 30; +/// +/// impl Cache { +/// fn insert(&mut self, key: CacheKey, value: Value) { +/// let delay = self.expirations +/// .insert(key.clone(), Duration::from_secs(TTL_SECS)); +/// +/// self.entries.insert(key, (value, delay)); +/// } +/// +/// fn get(&self, key: &CacheKey) -> Option<&Value> { +/// self.entries.get(key) +/// .map(|&(ref v, _)| v) +/// } +/// +/// fn remove(&mut self, key: &CacheKey) { +/// if let Some((_, cache_key)) = self.entries.remove(key) { +/// self.expirations.remove(&cache_key); +/// } +/// } +/// +/// fn poll_purge(&mut self, cx: &mut Context<'_>) -> Poll<()> { +/// while let Some(entry) = ready!(self.expirations.poll_expired(cx)) { +/// self.entries.remove(entry.get_ref()); +/// } +/// +/// Poll::Ready(()) +/// } +/// } +/// ``` +/// +/// [`insert`]: method@Self::insert +/// [`insert_at`]: method@Self::insert_at +/// [`Key`]: struct@Key +/// [`Stream`]: https://docs.rs/futures/0.1/futures/stream/trait.Stream.html +/// [`poll_expired`]: method@Self::poll_expired +/// [`Stream::poll_expired`]: method@Self::poll_expired +/// [`DelayQueue`]: struct@DelayQueue +/// [`sleep`]: fn@tokio::time::sleep +/// [`slab`]: slab +/// [`capacity`]: method@Self::capacity +/// [`reserve`]: method@Self::reserve +#[derive(Debug)] +pub struct DelayQueue { + /// Stores data associated with entries + slab: SlabStorage, + + /// Lookup structure tracking all delays in the queue + wheel: Wheel>, + + /// Delays that were inserted when already expired. These cannot be stored + /// in the wheel + expired: Stack, + + /// Delay expiring when the *first* item in the queue expires + delay: Option>>, + + /// Wheel polling state + wheel_now: u64, + + /// Instant at which the timer starts + start: Instant, + + /// Waker that is invoked when we potentially need to reset the timer. + /// Because we lazily create the timer when the first entry is created, we + /// need to awaken any poller that polled us before that point. + waker: Option, +} + +#[derive(Default)] +struct SlabStorage { + inner: Slab>, + + // A `compact` call requires a re-mapping of the `Key`s that were changed + // during the `compact` call of the `slab`. Since the keys that were given out + // cannot be changed retroactively we need to keep track of these re-mappings. + // The keys of `key_map` correspond to the old keys that were given out and + // the values to the `Key`s that were re-mapped by the `compact` call. + key_map: HashMap, + + // Index used to create new keys to hand out. + next_key_index: usize, + + // Whether `compact` has been called, necessary in order to decide whether + // to include keys in `key_map`. + compact_called: bool, +} + +impl SlabStorage { + pub(crate) fn with_capacity(capacity: usize) -> SlabStorage { + SlabStorage { + inner: Slab::with_capacity(capacity), + key_map: HashMap::new(), + next_key_index: 0, + compact_called: false, + } + } + + // Inserts data into the inner slab and re-maps keys if necessary + pub(crate) fn insert(&mut self, val: Data) -> Key { + let mut key = KeyInternal::new(self.inner.insert(val)); + let key_contained = self.key_map.contains_key(&key.into()); + + if key_contained { + // It's possible that a `compact` call creates capacity in `self.inner` in + // such a way that a `self.inner.insert` call creates a `key` which was + // previously given out during an `insert` call prior to the `compact` call. + // If `key` is contained in `self.key_map`, we have encountered this exact situation, + // We need to create a new key `key_to_give_out` and include the relation + // `key_to_give_out` -> `key` in `self.key_map`. + let key_to_give_out = self.create_new_key(); + assert!(!self.key_map.contains_key(&key_to_give_out.into())); + self.key_map.insert(key_to_give_out.into(), key); + key = key_to_give_out; + } else if self.compact_called { + // Include an identity mapping in `self.key_map` in order to allow us to + // panic if a key that was handed out is removed more than once. + self.key_map.insert(key.into(), key); + } + + key.into() + } + + // Re-map the key in case compact was previously called. + // Note: Since we include identity mappings in key_map after compact was called, + // we have information about all keys that were handed out. In the case in which + // compact was called and we try to remove a Key that was previously removed + // we can detect invalid keys if no key is found in `key_map`. This is necessary + // in order to prevent situations in which a previously removed key + // corresponds to a re-mapped key internally and which would then be incorrectly + // removed from the slab. + // + // Example to illuminate this problem: + // + // Let's assume our `key_map` is {1 -> 2, 2 -> 1} and we call remove(1). If we + // were to remove 1 again, we would not find it inside `key_map` anymore. + // If we were to imply from this that no re-mapping was necessary, we would + // incorrectly remove 1 from `self.slab.inner`, which corresponds to the + // handed-out key 2. + pub(crate) fn remove(&mut self, key: &Key) -> Data { + let remapped_key = if self.compact_called { + match self.key_map.remove(key) { + Some(key_internal) => key_internal, + None => panic!("invalid key"), + } + } else { + (*key).into() + }; + + self.inner.remove(remapped_key.index) + } + + pub(crate) fn shrink_to_fit(&mut self) { + self.inner.shrink_to_fit(); + self.key_map.shrink_to_fit(); + } + + pub(crate) fn compact(&mut self) { + if !self.compact_called { + for (key, _) in self.inner.iter() { + self.key_map.insert(Key::new(key), KeyInternal::new(key)); + } + } + + let mut remapping = HashMap::new(); + self.inner.compact(|_, from, to| { + remapping.insert(from, to); + true + }); + + // At this point `key_map` contains a mapping for every element. + for internal_key in self.key_map.values_mut() { + if let Some(new_internal_key) = remapping.get(&internal_key.index) { + *internal_key = KeyInternal::new(*new_internal_key); + } + } + + if self.key_map.capacity() > 2 * self.key_map.len() { + self.key_map.shrink_to_fit(); + } + + self.compact_called = true; + } + + // Tries to re-map a `Key` that was given out to the user to its + // corresponding internal key. + fn remap_key(&self, key: &Key) -> Option { + let key_map = &self.key_map; + if self.compact_called { + key_map.get(&*key).copied() + } else { + Some((*key).into()) + } + } + + fn create_new_key(&mut self) -> KeyInternal { + while self.key_map.contains_key(&Key::new(self.next_key_index)) { + self.next_key_index = self.next_key_index.wrapping_add(1); + } + + KeyInternal::new(self.next_key_index) + } + + pub(crate) fn len(&self) -> usize { + self.inner.len() + } + + pub(crate) fn capacity(&self) -> usize { + self.inner.capacity() + } + + pub(crate) fn clear(&mut self) { + self.inner.clear(); + self.key_map.clear(); + self.compact_called = false; + } + + pub(crate) fn reserve(&mut self, additional: usize) { + self.inner.reserve(additional); + + if self.compact_called { + self.key_map.reserve(additional); + } + } + + pub(crate) fn is_empty(&self) -> bool { + self.inner.is_empty() + } + + pub(crate) fn contains(&self, key: &Key) -> bool { + let remapped_key = self.remap_key(key); + + match remapped_key { + Some(internal_key) => self.inner.contains(internal_key.index), + None => false, + } + } +} + +impl fmt::Debug for SlabStorage +where + T: fmt::Debug, +{ + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + if fmt.alternate() { + fmt.debug_map().entries(self.inner.iter()).finish() + } else { + fmt.debug_struct("Slab") + .field("len", &self.len()) + .field("cap", &self.capacity()) + .finish() + } + } +} + +impl Index for SlabStorage { + type Output = Data; + + fn index(&self, key: Key) -> &Self::Output { + let remapped_key = self.remap_key(&key); + + match remapped_key { + Some(internal_key) => &self.inner[internal_key.index], + None => panic!("Invalid index {}", key.index), + } + } +} + +impl IndexMut for SlabStorage { + fn index_mut(&mut self, key: Key) -> &mut Data { + let remapped_key = self.remap_key(&key); + + match remapped_key { + Some(internal_key) => &mut self.inner[internal_key.index], + None => panic!("Invalid index {}", key.index), + } + } +} + +/// An entry in `DelayQueue` that has expired and been removed. +/// +/// Values are returned by [`DelayQueue::poll_expired`]. +/// +/// [`DelayQueue::poll_expired`]: method@DelayQueue::poll_expired +#[derive(Debug)] +pub struct Expired { + /// The data stored in the queue + data: T, + + /// The expiration time + deadline: Instant, + + /// The key associated with the entry + key: Key, +} + +/// Token to a value stored in a `DelayQueue`. +/// +/// Instances of `Key` are returned by [`DelayQueue::insert`]. See [`DelayQueue`] +/// documentation for more details. +/// +/// [`DelayQueue`]: struct@DelayQueue +/// [`DelayQueue::insert`]: method@DelayQueue::insert +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Key { + index: usize, +} + +// Whereas `Key` is given out to users that use `DelayQueue`, internally we use +// `KeyInternal` as the key type in order to make the logic of mapping between keys +// as a result of `compact` calls clearer. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct KeyInternal { + index: usize, +} + +#[derive(Debug)] +struct Stack { + /// Head of the stack + head: Option, + _p: PhantomData T>, +} + +#[derive(Debug)] +struct Data { + /// The data being stored in the queue and will be returned at the requested + /// instant. + inner: T, + + /// The instant at which the item is returned. + when: u64, + + /// Set to true when stored in the `expired` queue + expired: bool, + + /// Next entry in the stack + next: Option, + + /// Previous entry in the stack + prev: Option, +} + +/// Maximum number of entries the queue can handle +const MAX_ENTRIES: usize = (1 << 30) - 1; + +impl DelayQueue { + /// Creates a new, empty, `DelayQueue`. + /// + /// The queue will not allocate storage until items are inserted into it. + /// + /// # Examples + /// + /// ```rust + /// # use tokio_util::time::DelayQueue; + /// let delay_queue: DelayQueue = DelayQueue::new(); + /// ``` + pub fn new() -> DelayQueue { + DelayQueue::with_capacity(0) + } + + /// Creates a new, empty, `DelayQueue` with the specified capacity. + /// + /// The queue will be able to hold at least `capacity` elements without + /// reallocating. If `capacity` is 0, the queue will not allocate for + /// storage. + /// + /// # Examples + /// + /// ```rust + /// # use tokio_util::time::DelayQueue; + /// # use std::time::Duration; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let mut delay_queue = DelayQueue::with_capacity(10); + /// + /// // These insertions are done without further allocation + /// for i in 0..10 { + /// delay_queue.insert(i, Duration::from_secs(i)); + /// } + /// + /// // This will make the queue allocate additional storage + /// delay_queue.insert(11, Duration::from_secs(11)); + /// # } + /// ``` + pub fn with_capacity(capacity: usize) -> DelayQueue { + DelayQueue { + wheel: Wheel::new(), + slab: SlabStorage::with_capacity(capacity), + expired: Stack::default(), + delay: None, + wheel_now: 0, + start: Instant::now(), + waker: None, + } + } + + /// Inserts `value` into the queue set to expire at a specific instant in + /// time. + /// + /// This function is identical to `insert`, but takes an `Instant` instead + /// of a `Duration`. + /// + /// `value` is stored in the queue until `when` is reached. At which point, + /// `value` will be returned from [`poll_expired`]. If `when` has already been + /// reached, then `value` is immediately made available to poll. + /// + /// The return value represents the insertion and is used as an argument to + /// [`remove`] and [`reset`]. Note that [`Key`] is a token and is reused once + /// `value` is removed from the queue either by calling [`poll_expired`] after + /// `when` is reached or by calling [`remove`]. At this point, the caller + /// must take care to not use the returned [`Key`] again as it may reference + /// a different item in the queue. + /// + /// See [type] level documentation for more details. + /// + /// # Panics + /// + /// This function panics if `when` is too far in the future. + /// + /// # Examples + /// + /// Basic usage + /// + /// ```rust + /// use tokio::time::{Duration, Instant}; + /// use tokio_util::time::DelayQueue; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let mut delay_queue = DelayQueue::new(); + /// let key = delay_queue.insert_at( + /// "foo", Instant::now() + Duration::from_secs(5)); + /// + /// // Remove the entry + /// let item = delay_queue.remove(&key); + /// assert_eq!(*item.get_ref(), "foo"); + /// # } + /// ``` + /// + /// [`poll_expired`]: method@Self::poll_expired + /// [`remove`]: method@Self::remove + /// [`reset`]: method@Self::reset + /// [`Key`]: struct@Key + /// [type]: # + #[track_caller] + pub fn insert_at(&mut self, value: T, when: Instant) -> Key { + assert!(self.slab.len() < MAX_ENTRIES, "max entries exceeded"); + + // Normalize the deadline. Values cannot be set to expire in the past. + let when = self.normalize_deadline(when); + + // Insert the value in the store + let key = self.slab.insert(Data { + inner: value, + when, + expired: false, + next: None, + prev: None, + }); + + self.insert_idx(when, key); + + // Set a new delay if the current's deadline is later than the one of the new item + let should_set_delay = if let Some(ref delay) = self.delay { + let current_exp = self.normalize_deadline(delay.deadline()); + current_exp > when + } else { + true + }; + + if should_set_delay { + if let Some(waker) = self.waker.take() { + waker.wake(); + } + + let delay_time = self.start + Duration::from_millis(when); + if let Some(ref mut delay) = &mut self.delay { + delay.as_mut().reset_at(delay_time); + } else { + self.delay = Some(Box::pin(sleep_until(delay_time))); + } + } + + key + } + + /// Attempts to pull out the next value of the delay queue, registering the + /// current task for wakeup if the value is not yet available, and returning + /// `None` if the queue is exhausted. + pub fn poll_expired(&mut self, cx: &mut task::Context<'_>) -> Poll>> { + if !self + .waker + .as_ref() + .map(|w| w.will_wake(cx.waker())) + .unwrap_or(false) + { + self.waker = Some(cx.waker().clone()); + } + + let item = ready!(self.poll_idx(cx)); + Poll::Ready(item.map(|key| { + let data = self.slab.remove(&key); + debug_assert!(data.next.is_none()); + debug_assert!(data.prev.is_none()); + + Expired { + key, + data: data.inner, + deadline: self.start + Duration::from_millis(data.when), + } + })) + } + + /// Inserts `value` into the queue set to expire after the requested duration + /// elapses. + /// + /// This function is identical to `insert_at`, but takes a `Duration` + /// instead of an `Instant`. + /// + /// `value` is stored in the queue until `timeout` duration has + /// elapsed after `insert` was called. At that point, `value` will + /// be returned from [`poll_expired`]. If `timeout` is a `Duration` of + /// zero, then `value` is immediately made available to poll. + /// + /// The return value represents the insertion and is used as an + /// argument to [`remove`] and [`reset`]. Note that [`Key`] is a + /// token and is reused once `value` is removed from the queue + /// either by calling [`poll_expired`] after `timeout` has elapsed + /// or by calling [`remove`]. At this point, the caller must not + /// use the returned [`Key`] again as it may reference a different + /// item in the queue. + /// + /// See [type] level documentation for more details. + /// + /// # Panics + /// + /// This function panics if `timeout` is greater than the maximum + /// duration supported by the timer in the current `Runtime`. + /// + /// # Examples + /// + /// Basic usage + /// + /// ```rust + /// use tokio_util::time::DelayQueue; + /// use std::time::Duration; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let mut delay_queue = DelayQueue::new(); + /// let key = delay_queue.insert("foo", Duration::from_secs(5)); + /// + /// // Remove the entry + /// let item = delay_queue.remove(&key); + /// assert_eq!(*item.get_ref(), "foo"); + /// # } + /// ``` + /// + /// [`poll_expired`]: method@Self::poll_expired + /// [`remove`]: method@Self::remove + /// [`reset`]: method@Self::reset + /// [`Key`]: struct@Key + /// [type]: # + #[track_caller] + pub fn insert(&mut self, value: T, timeout: Duration) -> Key { + self.insert_at(value, Instant::now() + timeout) + } + + #[track_caller] + fn insert_idx(&mut self, when: u64, key: Key) { + use self::wheel::{InsertError, Stack}; + + // Register the deadline with the timer wheel + match self.wheel.insert(when, key, &mut self.slab) { + Ok(_) => {} + Err((_, InsertError::Elapsed)) => { + self.slab[key].expired = true; + // The delay is already expired, store it in the expired queue + self.expired.push(key, &mut self.slab); + } + Err((_, err)) => panic!("invalid deadline; err={:?}", err), + } + } + + /// Removes the key from the expired queue or the timer wheel + /// depending on its expiration status. + /// + /// # Panics + /// + /// Panics if the key is not contained in the expired queue or the wheel. + #[track_caller] + fn remove_key(&mut self, key: &Key) { + use crate::wasm_delay_queue::wheel::Stack; + + // Special case the `expired` queue + if self.slab[*key].expired { + self.expired.remove(key, &mut self.slab); + } else { + self.wheel.remove(key, &mut self.slab); + } + } + + /// Removes the item associated with `key` from the queue. + /// + /// There must be an item associated with `key`. The function returns the + /// removed item as well as the `Instant` at which it will the delay will + /// have expired. + /// + /// # Panics + /// + /// The function panics if `key` is not contained by the queue. + /// + /// # Examples + /// + /// Basic usage + /// + /// ```rust + /// use tokio_util::time::DelayQueue; + /// use std::time::Duration; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let mut delay_queue = DelayQueue::new(); + /// let key = delay_queue.insert("foo", Duration::from_secs(5)); + /// + /// // Remove the entry + /// let item = delay_queue.remove(&key); + /// assert_eq!(*item.get_ref(), "foo"); + /// # } + /// ``` + #[track_caller] + pub fn remove(&mut self, key: &Key) -> Expired { + let prev_deadline = self.next_deadline(); + + self.remove_key(key); + let data = self.slab.remove(key); + + let next_deadline = self.next_deadline(); + if prev_deadline != next_deadline { + match (next_deadline, &mut self.delay) { + (None, _) => self.delay = None, + (Some(deadline), Some(delay)) => delay.as_mut().reset_at(deadline), + (Some(deadline), None) => self.delay = Some(Box::pin(sleep_until(deadline))), + } + } + + Expired { + key: Key::new(key.index), + data: data.inner, + deadline: self.start + Duration::from_millis(data.when), + } + } + + /// Sets the delay of the item associated with `key` to expire at `when`. + /// + /// This function is identical to `reset` but takes an `Instant` instead of + /// a `Duration`. + /// + /// The item remains in the queue but the delay is set to expire at `when`. + /// If `when` is in the past, then the item is immediately made available to + /// the caller. + /// + /// # Panics + /// + /// This function panics if `when` is too far in the future or if `key` is + /// not contained by the queue. + /// + /// # Examples + /// + /// Basic usage + /// + /// ```rust + /// use tokio::time::{Duration, Instant}; + /// use tokio_util::time::DelayQueue; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let mut delay_queue = DelayQueue::new(); + /// let key = delay_queue.insert("foo", Duration::from_secs(5)); + /// + /// // "foo" is scheduled to be returned in 5 seconds + /// + /// delay_queue.reset_at(&key, Instant::now() + Duration::from_secs(10)); + /// + /// // "foo" is now scheduled to be returned in 10 seconds + /// # } + /// ``` + #[track_caller] + pub fn reset_at(&mut self, key: &Key, when: Instant) { + self.remove_key(key); + + // Normalize the deadline. Values cannot be set to expire in the past. + let when = self.normalize_deadline(when); + + self.slab[*key].when = when; + self.slab[*key].expired = false; + + self.insert_idx(when, *key); + + let next_deadline = self.next_deadline(); + if let (Some(ref mut delay), Some(deadline)) = (&mut self.delay, next_deadline) { + // This should awaken us if necessary (ie, if already expired) + delay.as_mut().reset_at(deadline); + } + } + + /// Shrink the capacity of the slab, which `DelayQueue` uses internally for storage allocation. + /// This function is not guaranteed to, and in most cases, won't decrease the capacity of the slab + /// to the number of elements still contained in it, because elements cannot be moved to a different + /// index. To decrease the capacity to the size of the slab use [`compact`]. + /// + /// This function can take O(n) time even when the capacity cannot be reduced or the allocation is + /// shrunk in place. Repeated calls run in O(1) though. + /// + /// [`compact`]: method@Self::compact + pub fn shrink_to_fit(&mut self) { + self.slab.shrink_to_fit(); + } + + /// Shrink the capacity of the slab, which `DelayQueue` uses internally for storage allocation, + /// to the number of elements that are contained in it. + /// + /// This methods runs in O(n). + /// + /// # Examples + /// + /// Basic usage + /// + /// ```rust + /// use tokio_util::time::DelayQueue; + /// use std::time::Duration; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let mut delay_queue = DelayQueue::with_capacity(10); + /// + /// let key1 = delay_queue.insert(5, Duration::from_secs(5)); + /// let key2 = delay_queue.insert(10, Duration::from_secs(10)); + /// let key3 = delay_queue.insert(15, Duration::from_secs(15)); + /// + /// delay_queue.remove(&key2); + /// + /// delay_queue.compact(); + /// assert_eq!(delay_queue.capacity(), 2); + /// # } + /// ``` + pub fn compact(&mut self) { + self.slab.compact(); + } + + /// Returns the next time to poll as determined by the wheel + fn next_deadline(&mut self) -> Option { + self.wheel + .poll_at() + .map(|poll_at| self.start + Duration::from_millis(poll_at)) + } + + /// Sets the delay of the item associated with `key` to expire after + /// `timeout`. + /// + /// This function is identical to `reset_at` but takes a `Duration` instead + /// of an `Instant`. + /// + /// The item remains in the queue but the delay is set to expire after + /// `timeout`. If `timeout` is zero, then the item is immediately made + /// available to the caller. + /// + /// # Panics + /// + /// This function panics if `timeout` is greater than the maximum supported + /// duration or if `key` is not contained by the queue. + /// + /// # Examples + /// + /// Basic usage + /// + /// ```rust + /// use tokio_util::time::DelayQueue; + /// use std::time::Duration; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let mut delay_queue = DelayQueue::new(); + /// let key = delay_queue.insert("foo", Duration::from_secs(5)); + /// + /// // "foo" is scheduled to be returned in 5 seconds + /// + /// delay_queue.reset(&key, Duration::from_secs(10)); + /// + /// // "foo"is now scheduled to be returned in 10 seconds + /// # } + /// ``` + #[track_caller] + pub fn reset(&mut self, key: &Key, timeout: Duration) { + self.reset_at(key, Instant::now() + timeout); + } + + /// Clears the queue, removing all items. + /// + /// After calling `clear`, [`poll_expired`] will return `Ok(Ready(None))`. + /// + /// Note that this method has no effect on the allocated capacity. + /// + /// [`poll_expired`]: method@Self::poll_expired + /// + /// # Examples + /// + /// ```rust + /// use tokio_util::time::DelayQueue; + /// use std::time::Duration; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let mut delay_queue = DelayQueue::new(); + /// + /// delay_queue.insert("foo", Duration::from_secs(5)); + /// + /// assert!(!delay_queue.is_empty()); + /// + /// delay_queue.clear(); + /// + /// assert!(delay_queue.is_empty()); + /// # } + /// ``` + pub fn clear(&mut self) { + self.slab.clear(); + self.expired = Stack::default(); + self.wheel = Wheel::new(); + self.delay = None; + } + + /// Returns the number of elements the queue can hold without reallocating. + /// + /// # Examples + /// + /// ```rust + /// use tokio_util::time::DelayQueue; + /// + /// let delay_queue: DelayQueue = DelayQueue::with_capacity(10); + /// assert_eq!(delay_queue.capacity(), 10); + /// ``` + pub fn capacity(&self) -> usize { + self.slab.capacity() + } + + /// Returns the number of elements currently in the queue. + /// + /// # Examples + /// + /// ```rust + /// use tokio_util::time::DelayQueue; + /// use std::time::Duration; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let mut delay_queue: DelayQueue = DelayQueue::with_capacity(10); + /// assert_eq!(delay_queue.len(), 0); + /// delay_queue.insert(3, Duration::from_secs(5)); + /// assert_eq!(delay_queue.len(), 1); + /// # } + /// ``` + pub fn len(&self) -> usize { + self.slab.len() + } + + /// Reserves capacity for at least `additional` more items to be queued + /// without allocating. + /// + /// `reserve` does nothing if the queue already has sufficient capacity for + /// `additional` more values. If more capacity is required, a new segment of + /// memory will be allocated and all existing values will be copied into it. + /// As such, if the queue is already very large, a call to `reserve` can end + /// up being expensive. + /// + /// The queue may reserve more than `additional` extra space in order to + /// avoid frequent reallocations. + /// + /// # Panics + /// + /// Panics if the new capacity exceeds the maximum number of entries the + /// queue can contain. + /// + /// # Examples + /// + /// ``` + /// use tokio_util::time::DelayQueue; + /// use std::time::Duration; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let mut delay_queue = DelayQueue::new(); + /// + /// delay_queue.insert("hello", Duration::from_secs(10)); + /// delay_queue.reserve(10); + /// + /// assert!(delay_queue.capacity() >= 11); + /// # } + /// ``` + #[track_caller] + pub fn reserve(&mut self, additional: usize) { + assert!( + self.slab.capacity() + additional <= MAX_ENTRIES, + "max queue capacity exceeded" + ); + self.slab.reserve(additional); + } + + /// Returns `true` if there are no items in the queue. + /// + /// Note that this function returns `false` even if all items have not yet + /// expired and a call to `poll` will return `Poll::Pending`. + /// + /// # Examples + /// + /// ``` + /// use tokio_util::time::DelayQueue; + /// use std::time::Duration; + /// + /// # #[tokio::main] + /// # async fn main() { + /// let mut delay_queue = DelayQueue::new(); + /// assert!(delay_queue.is_empty()); + /// + /// delay_queue.insert("hello", Duration::from_secs(5)); + /// assert!(!delay_queue.is_empty()); + /// # } + /// ``` + pub fn is_empty(&self) -> bool { + self.slab.is_empty() + } + + /// Polls the queue, returning the index of the next slot in the slab that + /// should be returned. + /// + /// A slot should be returned when the associated deadline has been reached. + fn poll_idx(&mut self, cx: &mut task::Context<'_>) -> Poll> { + use self::wheel::Stack; + + let expired = self.expired.pop(&mut self.slab); + + if expired.is_some() { + return Poll::Ready(expired); + } + + loop { + if let Some(ref mut delay) = self.delay { + ready!(Pin::new(&mut *delay).poll(cx)).ok(); + + let now = crate::wasm_delay_queue::ms( + delay.deadline() - self.start, + crate::wasm_delay_queue::Round::Down, + ); + + self.wheel_now = now; + } + + // We poll the wheel to get the next value out before finding the next deadline. + let wheel_idx = self.wheel.poll(self.wheel_now, &mut self.slab); + + self.delay = self.next_deadline().map(|when| Box::pin(sleep_until(when))); + + if let Some(idx) = wheel_idx { + return Poll::Ready(Some(idx)); + } + + if self.delay.is_none() { + return Poll::Ready(None); + } + } + } + + fn normalize_deadline(&self, when: Instant) -> u64 { + let when = if when < self.start { + 0 + } else { + crate::wasm_delay_queue::ms(when - self.start, crate::wasm_delay_queue::Round::Up) + }; + + cmp::max(when, self.wheel.elapsed()) + } +} + +// We never put `T` in a `Pin`... +impl Unpin for DelayQueue {} + +impl Default for DelayQueue { + fn default() -> DelayQueue { + DelayQueue::new() + } +} + +impl futures_core::Stream for DelayQueue { + // DelayQueue seems much more specific, where a user may care that it + // has reached capacity, so return those errors instead of panicking. + type Item = Expired; + + fn poll_next(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { + DelayQueue::poll_expired(self.get_mut(), cx) + } +} + +impl wheel::Stack for Stack { + type Owned = Key; + type Borrowed = Key; + type Store = SlabStorage; + + fn is_empty(&self) -> bool { + self.head.is_none() + } + + fn push(&mut self, item: Self::Owned, store: &mut Self::Store) { + // Ensure the entry is not already in a stack. + debug_assert!(store[item].next.is_none()); + debug_assert!(store[item].prev.is_none()); + + // Remove the old head entry + let old = self.head.take(); + + if let Some(idx) = old { + store[idx].prev = Some(item); + } + + store[item].next = old; + self.head = Some(item); + } + + fn pop(&mut self, store: &mut Self::Store) -> Option { + if let Some(key) = self.head { + self.head = store[key].next; + + if let Some(idx) = self.head { + store[idx].prev = None; + } + + store[key].next = None; + debug_assert!(store[key].prev.is_none()); + + Some(key) + } else { + None + } + } + + #[track_caller] + fn remove(&mut self, item: &Self::Borrowed, store: &mut Self::Store) { + let key = *item; + assert!(store.contains(item)); + + // Ensure that the entry is in fact contained by the stack + debug_assert!({ + // This walks the full linked list even if an entry is found. + let mut next = self.head; + let mut contains = false; + + while let Some(idx) = next { + let data = &store[idx]; + + if idx == *item { + debug_assert!(!contains); + contains = true; + } + + next = data.next; + } + + contains + }); + + if let Some(next) = store[key].next { + store[next].prev = store[key].prev; + } + + if let Some(prev) = store[key].prev { + store[prev].next = store[key].next; + } else { + self.head = store[key].next; + } + + store[key].next = None; + store[key].prev = None; + } + + fn when(item: &Self::Borrowed, store: &Self::Store) -> u64 { + store[*item].when + } +} + +impl Default for Stack { + fn default() -> Stack { + Stack { + head: None, + _p: PhantomData, + } + } +} + +impl Key { + pub(crate) fn new(index: usize) -> Key { + Key { index } + } +} + +impl KeyInternal { + pub(crate) fn new(index: usize) -> KeyInternal { + KeyInternal { index } + } +} + +impl From for KeyInternal { + fn from(item: Key) -> Self { + KeyInternal::new(item.index) + } +} + +impl From for Key { + fn from(item: KeyInternal) -> Self { + Key::new(item.index) + } +} + +impl Expired { + /// Returns a reference to the inner value. + pub fn get_ref(&self) -> &T { + &self.data + } + + /// Returns a mutable reference to the inner value. + pub fn get_mut(&mut self) -> &mut T { + &mut self.data + } + + /// Consumes `self` and returns the inner value. + pub fn into_inner(self) -> T { + self.data + } + + /// Returns the deadline that the expiration was set to. + pub fn deadline(&self) -> Instant { + self.deadline + } + + /// Returns the key that the expiration is indexed by. + pub fn key(&self) -> Key { + self.key + } +} diff --git a/common/nonexhaustive-delayqueue/src/wasm_delay_queue/mod.rs b/common/nonexhaustive-delayqueue/src/wasm_delay_queue/mod.rs new file mode 100644 index 0000000000..fa8b62f561 --- /dev/null +++ b/common/nonexhaustive-delayqueue/src/wasm_delay_queue/mod.rs @@ -0,0 +1,42 @@ +use std::time::Duration; + +mod wheel; + +pub mod delay_queue; + +#[doc(inline)] +pub use delay_queue::DelayQueue; + +// ===== Internal utils ===== + +enum Round { + Up, + Down, +} + +/// Convert a `Duration` to milliseconds, rounding up and saturating at +/// `u64::MAX`. +/// +/// The saturating is fine because `u64::MAX` milliseconds are still many +/// million years. +#[inline] +fn ms(duration: Duration, round: Round) -> u64 { + const NANOS_PER_MILLI: u32 = 1_000_000; + const MILLIS_PER_SEC: u64 = 1_000; + + // Round up. + let millis = match round { + Round::Up => (duration.subsec_nanos() + NANOS_PER_MILLI - 1) / NANOS_PER_MILLI, + Round::Down => duration.subsec_millis(), + }; + + duration + .as_secs() + .saturating_mul(MILLIS_PER_SEC) + .saturating_add(u64::from(millis)) +} + +#[inline] +fn sleep_until(deadline: wasm_timer::Instant) -> wasm_timer::Delay { + wasm_timer::Delay::new_at(deadline) +} diff --git a/common/nonexhaustive-delayqueue/src/wasm_delay_queue/wheel/level.rs b/common/nonexhaustive-delayqueue/src/wasm_delay_queue/wheel/level.rs new file mode 100644 index 0000000000..f84d80e265 --- /dev/null +++ b/common/nonexhaustive-delayqueue/src/wasm_delay_queue/wheel/level.rs @@ -0,0 +1,253 @@ +use crate::wasm_delay_queue::wheel::Stack; + +use std::fmt; + +/// Wheel for a single level in the timer. This wheel contains 64 slots. +pub(crate) struct Level { + level: usize, + + /// Bit field tracking which slots currently contain entries. + /// + /// Using a bit field to track slots that contain entries allows avoiding a + /// scan to find entries. This field is updated when entries are added or + /// removed from a slot. + /// + /// The least-significant bit represents slot zero. + occupied: u64, + + /// Slots + slot: [T; LEVEL_MULT], +} + +/// Indicates when a slot must be processed next. +#[derive(Debug)] +pub(crate) struct Expiration { + /// The level containing the slot. + pub(crate) level: usize, + + /// The slot index. + pub(crate) slot: usize, + + /// The instant at which the slot needs to be processed. + pub(crate) deadline: u64, +} + +/// Level multiplier. +/// +/// Being a power of 2 is very important. +const LEVEL_MULT: usize = 64; + +impl Level { + pub(crate) fn new(level: usize) -> Level { + // Rust's derived implementations for arrays require that the value + // contained by the array be `Copy`. So, here we have to manually + // initialize every single slot. + macro_rules! s { + () => { + T::default() + }; + } + + Level { + level, + occupied: 0, + slot: [ + // It does not look like the necessary traits are + // derived for [T; 64]. + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + s!(), + ], + } + } + + /// Finds the slot that needs to be processed next and returns the slot and + /// `Instant` at which this slot must be processed. + pub(crate) fn next_expiration(&self, now: u64) -> Option { + // Use the `occupied` bit field to get the index of the next slot that + // needs to be processed. + let slot = match self.next_occupied_slot(now) { + Some(slot) => slot, + None => return None, + }; + + // From the slot index, calculate the `Instant` at which it needs to be + // processed. This value *must* be in the future with respect to `now`. + + let level_range = level_range(self.level); + let slot_range = slot_range(self.level); + + // TODO: This can probably be simplified w/ power of 2 math + let level_start = now - (now % level_range); + let deadline = level_start + slot as u64 * slot_range; + + debug_assert!( + deadline >= now, + "deadline={}; now={}; level={}; slot={}; occupied={:b}", + deadline, + now, + self.level, + slot, + self.occupied + ); + + Some(Expiration { + level: self.level, + slot, + deadline, + }) + } + + fn next_occupied_slot(&self, now: u64) -> Option { + if self.occupied == 0 { + return None; + } + + // Get the slot for now using Maths + let now_slot = (now / slot_range(self.level)) as usize; + let occupied = self.occupied.rotate_right(now_slot as u32); + let zeros = occupied.trailing_zeros() as usize; + let slot = (zeros + now_slot) % 64; + + Some(slot) + } + + pub(crate) fn add_entry(&mut self, when: u64, item: T::Owned, store: &mut T::Store) { + let slot = slot_for(when, self.level); + + self.slot[slot].push(item, store); + self.occupied |= occupied_bit(slot); + } + + pub(crate) fn remove_entry(&mut self, when: u64, item: &T::Borrowed, store: &mut T::Store) { + let slot = slot_for(when, self.level); + + self.slot[slot].remove(item, store); + + if self.slot[slot].is_empty() { + // The bit is currently set + debug_assert!(self.occupied & occupied_bit(slot) != 0); + + // Unset the bit + self.occupied ^= occupied_bit(slot); + } + } + + pub(crate) fn pop_entry_slot(&mut self, slot: usize, store: &mut T::Store) -> Option { + let ret = self.slot[slot].pop(store); + + if ret.is_some() && self.slot[slot].is_empty() { + // The bit is currently set + debug_assert!(self.occupied & occupied_bit(slot) != 0); + + self.occupied ^= occupied_bit(slot); + } + + ret + } +} + +impl fmt::Debug for Level { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_struct("Level") + .field("occupied", &self.occupied) + .finish() + } +} + +fn occupied_bit(slot: usize) -> u64 { + 1 << slot +} + +fn slot_range(level: usize) -> u64 { + LEVEL_MULT.pow(level as u32) as u64 +} + +fn level_range(level: usize) -> u64 { + LEVEL_MULT as u64 * slot_range(level) +} + +/// Convert a duration (milliseconds) and a level to a slot position +fn slot_for(duration: u64, level: usize) -> usize { + ((duration >> (level * 6)) % LEVEL_MULT as u64) as usize +} + +#[cfg(all(test, not(loom)))] +mod test { + use super::*; + + #[test] + fn test_slot_for() { + for pos in 0..64 { + assert_eq!(pos as usize, slot_for(pos, 0)); + } + + for level in 1..5 { + for pos in level..64 { + let a = pos * 64_usize.pow(level as u32); + assert_eq!(pos as usize, slot_for(a as u64, level)); + } + } + } +} diff --git a/common/nonexhaustive-delayqueue/src/wasm_delay_queue/wheel/mod.rs b/common/nonexhaustive-delayqueue/src/wasm_delay_queue/wheel/mod.rs new file mode 100644 index 0000000000..ffa05ab71b --- /dev/null +++ b/common/nonexhaustive-delayqueue/src/wasm_delay_queue/wheel/mod.rs @@ -0,0 +1,315 @@ +mod level; +pub(crate) use self::level::Expiration; +use self::level::Level; + +mod stack; +pub(crate) use self::stack::Stack; + +use std::borrow::Borrow; +use std::fmt::Debug; +use std::usize; + +/// Timing wheel implementation. +/// +/// This type provides the hashed timing wheel implementation that backs `Timer` +/// and `DelayQueue`. +/// +/// The structure is generic over `T: Stack`. This allows handling timeout data +/// being stored on the heap or in a slab. In order to support the latter case, +/// the slab must be passed into each function allowing the implementation to +/// lookup timer entries. +/// +/// See `Timer` documentation for some implementation notes. +#[derive(Debug)] +pub(crate) struct Wheel { + /// The number of milliseconds elapsed since the wheel started. + elapsed: u64, + + /// Timer wheel. + /// + /// Levels: + /// + /// * 1 ms slots / 64 ms range + /// * 64 ms slots / ~ 4 sec range + /// * ~ 4 sec slots / ~ 4 min range + /// * ~ 4 min slots / ~ 4 hr range + /// * ~ 4 hr slots / ~ 12 day range + /// * ~ 12 day slots / ~ 2 yr range + levels: Vec>, +} + +/// Number of levels. Each level has 64 slots. By using 6 levels with 64 slots +/// each, the timer is able to track time up to 2 years into the future with a +/// precision of 1 millisecond. +const NUM_LEVELS: usize = 6; + +/// The maximum duration of a delay +const MAX_DURATION: u64 = (1 << (6 * NUM_LEVELS)) - 1; + +#[derive(Debug)] +pub(crate) enum InsertError { + Elapsed, + Invalid, +} + +impl Wheel +where + T: Stack, +{ + /// Create a new timing wheel + pub(crate) fn new() -> Wheel { + let levels = (0..NUM_LEVELS).map(Level::new).collect(); + + Wheel { elapsed: 0, levels } + } + + /// Return the number of milliseconds that have elapsed since the timing + /// wheel's creation. + pub(crate) fn elapsed(&self) -> u64 { + self.elapsed + } + + /// Insert an entry into the timing wheel. + /// + /// # Arguments + /// + /// * `when`: is the instant at which the entry should be fired. It is + /// represented as the number of milliseconds since the creation + /// of the timing wheel. + /// + /// * `item`: The item to insert into the wheel. + /// + /// * `store`: The slab or `()` when using heap storage. + /// + /// # Return + /// + /// Returns `Ok` when the item is successfully inserted, `Err` otherwise. + /// + /// `Err(Elapsed)` indicates that `when` represents an instant that has + /// already passed. In this case, the caller should fire the timeout + /// immediately. + /// + /// `Err(Invalid)` indicates an invalid `when` argument as been supplied. + pub(crate) fn insert( + &mut self, + when: u64, + item: T::Owned, + store: &mut T::Store, + ) -> Result<(), (T::Owned, InsertError)> { + if when <= self.elapsed { + return Err((item, InsertError::Elapsed)); + } else if when - self.elapsed > MAX_DURATION { + return Err((item, InsertError::Invalid)); + } + + // Get the level at which the entry should be stored + let level = self.level_for(when); + + self.levels[level].add_entry(when, item, store); + + debug_assert!({ + self.levels[level] + .next_expiration(self.elapsed) + .map(|e| e.deadline >= self.elapsed) + .unwrap_or(true) + }); + + Ok(()) + } + + /// Remove `item` from the timing wheel. + #[track_caller] + pub(crate) fn remove(&mut self, item: &T::Borrowed, store: &mut T::Store) { + let when = T::when(item, store); + + assert!( + self.elapsed <= when, + "elapsed={}; when={}", + self.elapsed, + when + ); + + let level = self.level_for(when); + + self.levels[level].remove_entry(when, item, store); + } + + /// Instant at which to poll + pub(crate) fn poll_at(&self) -> Option { + self.next_expiration().map(|expiration| expiration.deadline) + } + + /// Advances the timer up to the instant represented by `now`. + pub(crate) fn poll(&mut self, now: u64, store: &mut T::Store) -> Option { + loop { + let expiration = self.next_expiration().and_then(|expiration| { + if expiration.deadline > now { + None + } else { + Some(expiration) + } + }); + + match expiration { + Some(ref expiration) => { + if let Some(item) = self.poll_expiration(expiration, store) { + return Some(item); + } + + self.set_elapsed(expiration.deadline); + } + None => { + // in this case the poll did not indicate an expiration + // _and_ we were not able to find a next expiration in + // the current list of timers. advance to the poll's + // current time and do nothing else. + self.set_elapsed(now); + return None; + } + } + } + } + + /// Returns the instant at which the next timeout expires. + fn next_expiration(&self) -> Option { + // Check all levels + for level in 0..NUM_LEVELS { + if let Some(expiration) = self.levels[level].next_expiration(self.elapsed) { + // There cannot be any expirations at a higher level that happen + // before this one. + debug_assert!(self.no_expirations_before(level + 1, expiration.deadline)); + + return Some(expiration); + } + } + + None + } + + /// Used for debug assertions + fn no_expirations_before(&self, start_level: usize, before: u64) -> bool { + let mut res = true; + + for l2 in start_level..NUM_LEVELS { + if let Some(e2) = self.levels[l2].next_expiration(self.elapsed) { + if e2.deadline < before { + res = false; + } + } + } + + res + } + + /// iteratively find entries that are between the wheel's current + /// time and the expiration time. for each in that population either + /// return it for notification (in the case of the last level) or tier + /// it down to the next level (in all other cases). + pub(crate) fn poll_expiration( + &mut self, + expiration: &Expiration, + store: &mut T::Store, + ) -> Option { + while let Some(item) = self.pop_entry(expiration, store) { + if expiration.level == 0 { + debug_assert_eq!(T::when(item.borrow(), store), expiration.deadline); + + return Some(item); + } else { + let when = T::when(item.borrow(), store); + + let next_level = expiration.level - 1; + + self.levels[next_level].add_entry(when, item, store); + } + } + + None + } + + fn set_elapsed(&mut self, when: u64) { + assert!( + self.elapsed <= when, + "elapsed={:?}; when={:?}", + self.elapsed, + when + ); + + if when > self.elapsed { + self.elapsed = when; + } + } + + fn pop_entry(&mut self, expiration: &Expiration, store: &mut T::Store) -> Option { + self.levels[expiration.level].pop_entry_slot(expiration.slot, store) + } + + fn level_for(&self, when: u64) -> usize { + level_for(self.elapsed, when) + } +} + +fn level_for(elapsed: u64, when: u64) -> usize { + const SLOT_MASK: u64 = (1 << 6) - 1; + + // Mask in the trailing bits ignored by the level calculation in order to cap + // the possible leading zeros + let masked = elapsed ^ when | SLOT_MASK; + + let leading_zeros = masked.leading_zeros() as usize; + let significant = 63 - leading_zeros; + significant / 6 +} + +#[cfg(all(test, not(loom)))] +mod test { + use super::*; + + #[test] + fn test_level_for() { + for pos in 0..64 { + assert_eq!( + 0, + level_for(0, pos), + "level_for({}) -- binary = {:b}", + pos, + pos + ); + } + + for level in 1..5 { + for pos in level..64 { + let a = pos * 64_usize.pow(level as u32); + assert_eq!( + level, + level_for(0, a as u64), + "level_for({}) -- binary = {:b}", + a, + a + ); + + if pos > level { + let a = a - 1; + assert_eq!( + level, + level_for(0, a as u64), + "level_for({}) -- binary = {:b}", + a, + a + ); + } + + if pos < 64 { + let a = a + 1; + assert_eq!( + level, + level_for(0, a as u64), + "level_for({}) -- binary = {:b}", + a, + a + ); + } + } + } + } +} diff --git a/common/nonexhaustive-delayqueue/src/wasm_delay_queue/wheel/stack.rs b/common/nonexhaustive-delayqueue/src/wasm_delay_queue/wheel/stack.rs new file mode 100644 index 0000000000..c87adcafda --- /dev/null +++ b/common/nonexhaustive-delayqueue/src/wasm_delay_queue/wheel/stack.rs @@ -0,0 +1,28 @@ +use std::borrow::Borrow; +use std::cmp::Eq; +use std::hash::Hash; + +/// Abstracts the stack operations needed to track timeouts. +pub(crate) trait Stack: Default { + /// Type of the item stored in the stack + type Owned: Borrow; + + /// Borrowed item + type Borrowed: Eq + Hash; + + /// Item storage, this allows a slab to be used instead of just the heap + type Store; + + /// Returns `true` if the stack is empty + fn is_empty(&self) -> bool; + + /// Push an item onto the stack + fn push(&mut self, item: Self::Owned, store: &mut Self::Store); + + /// Pop an item from the stack + fn pop(&mut self, store: &mut Self::Store) -> Option; + + fn remove(&mut self, item: &Self::Borrowed, store: &mut Self::Store); + + fn when(item: &Self::Borrowed, store: &Self::Store) -> u64; +} diff --git a/common/nymcoconut/README.md b/common/nymcoconut/README.md new file mode 100644 index 0000000000..2433e80e34 --- /dev/null +++ b/common/nymcoconut/README.md @@ -0,0 +1 @@ +This project was partially funded through the NGI0 PET Fund, a fund established by NL.net with financial support from the European Commission's NGI programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825310. \ No newline at end of file diff --git a/common/nymsphinx/Cargo.toml b/common/nymsphinx/Cargo.toml index b697700ec1..f0f24ac665 100644 --- a/common/nymsphinx/Cargo.toml +++ b/common/nymsphinx/Cargo.toml @@ -33,5 +33,5 @@ mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" path = "framing" [target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio] -version = "1.19.1" +version = "1.21.2" features = ["sync"] diff --git a/common/nymsphinx/cover/src/lib.rs b/common/nymsphinx/cover/src/lib.rs index 3a545dbaa0..aaaff5a9b5 100644 --- a/common/nymsphinx/cover/src/lib.rs +++ b/common/nymsphinx/cover/src/lib.rs @@ -76,6 +76,7 @@ pub fn generate_loop_cover_packet( full_address: &Recipient, average_ack_delay: time::Duration, average_packet_delay: time::Duration, + packet_size: PacketSize, ) -> Result where R: RngCore + CryptoRng, @@ -95,8 +96,7 @@ where >(rng, full_address.encryption_key()); let public_key_bytes = ephemeral_keypair.public_key().to_bytes(); - let cover_size = - PacketSize::default().plaintext_size() - public_key_bytes.len() - ack_bytes.len(); + let cover_size = packet_size.plaintext_size() - public_key_bytes.len() - ack_bytes.len(); let mut cover_content: Vec<_> = LOOP_COVER_MESSAGE_PAYLOAD .iter() @@ -129,7 +129,7 @@ where // once merged, that's an easy rng injection point for sphinx packets : ) let packet = SphinxPacketBuilder::new() - .with_payload_size(PacketSize::default().payload_size()) + .with_payload_size(packet_size.payload_size()) .build_packet(packet_payload, &route, &destination, &delays) .unwrap(); diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index a51075290e..d91a7c85a5 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -53,7 +53,8 @@ impl From for PreparationError { /// Prepares the message that is to be sent through the mix network by attaching /// an optional reply-SURB, padding it to appropriate length, encrypting its content, /// and chunking into appropriate size [`Fragment`]s. -#[cfg_attr(not(target_arch = "wasm32"), derive(Clone))] +// #[cfg_attr(not(target_arch = "wasm32"), derive(Clone))] +#[derive(Clone)] #[must_use] pub struct MessagePreparer { /// Instance of a cryptographically secure random number generator. @@ -104,7 +105,7 @@ where } /// Allows setting non-default size of the sphinx packets sent out. - pub fn with_packet_size(mut self, packet_size: PacketSize) -> Self { + pub fn with_custom_real_message_packet_size(mut self, packet_size: PacketSize) -> Self { self.packet_size = packet_size; self } diff --git a/common/socks5/proxy-helpers/Cargo.toml b/common/socks5/proxy-helpers/Cargo.toml index 99ce5f3559..fd49c094ef 100644 --- a/common/socks5/proxy-helpers/Cargo.toml +++ b/common/socks5/proxy-helpers/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" [dependencies] bytes = "1.0" -tokio = { version = "1.19.1", features = [ "net", "io-util", "sync", "macros", "time", "rt-multi-thread" ] } +tokio = { version = "1.21.2", features = [ "net", "io-util", "sync", "macros", "time", "rt-multi-thread" ] } tokio-util = { version = "0.7.3", features = [ "io" ] } # reason for getting this guy is to to able to port to tokio 1.X more quickly by being able to use # their `read_buf` [from the util crate] replacement rather than having to rethink/reimplement `AvailableReader` with the new AsyncRead trait definition. # In the long run, the dependency should probably get removed in favour of pure-tokio implementation, but for time being it's fine. diff --git a/common/statistics/Cargo.toml b/common/statistics/Cargo.toml index 9514edd29b..4b0a27dcae 100644 --- a/common/statistics/Cargo.toml +++ b/common/statistics/Cargo.toml @@ -16,4 +16,4 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1" sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "chrono"]} thiserror = "1" -tokio = { version = "1.19.1", features = [ "time" ] } +tokio = { version = "1.21.2", features = [ "time" ] } diff --git a/common/task/Cargo.toml b/common/task/Cargo.toml index 7ce3ae62c9..4c0c088d67 100644 --- a/common/task/Cargo.toml +++ b/common/task/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] log = "0.4" -tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal"] } +tokio = { version = "1.21.2", features = ["macros", "signal", "time", "sync"] } [dev-dependencies] -tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal", "test-util", "macros"] } +tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal", "test-util", "macros"] } diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index e21e074d34..ed6b7c71fb 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -242,7 +242,7 @@ pub fn nym_topology_from_detailed( ); continue; } - let mix_id = bond.id; + let mix_id = bond.mix_id; let mix_identity = bond.mix_node.identity_key.clone(); let layer_entry = mixes.entry(layer).or_insert_with(Vec::new); diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index aa071c4fe2..81e0308b2f 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -3,7 +3,7 @@ use crate::{filter, NetworkAddress}; use crypto::asymmetric::{encryption, identity}; -use mixnet_contract_common::{Layer, MixNodeBond, NodeId}; +use mixnet_contract_common::{Layer, MixId, MixNodeBond}; use nymsphinx_addressing::nodes::NymNodeRoutingAddress; use nymsphinx_types::Node as SphinxNode; use std::convert::{TryFrom, TryInto}; @@ -68,7 +68,7 @@ impl Display for MixnodeConversionError { #[derive(Debug, Clone)] pub struct Node { - pub mix_id: NodeId, + pub mix_id: MixId, pub owner: String, pub host: NetworkAddress, // we're keeping this as separate resolved field since we do not want to be resolving the potential @@ -113,7 +113,7 @@ impl<'a> TryFrom<&'a MixNodeBond> for Node { })?[0]; Ok(Node { - mix_id: bond.id, + mix_id: bond.mix_id, owner: bond.owner.as_str().to_owned(), host, mix_host, diff --git a/common/types/src/currency.rs b/common/types/src/currency.rs index e6927a1721..38292d11d3 100644 --- a/common/types/src/currency.rs +++ b/common/types/src/currency.rs @@ -1,6 +1,5 @@ use crate::error::TypesError; -use config::defaults::all::Network; -use config::defaults::{DenomDetails, DenomDetailsOwned}; +use config::defaults::{DenomDetails, DenomDetailsOwned, NymNetworkDetails}; use cosmwasm_std::Fraction; use cosmwasm_std::{Decimal, Uint128}; use schemars::JsonSchema; @@ -56,10 +55,16 @@ pub type Denom = String; pub struct RegisteredCoins(HashMap); impl RegisteredCoins { - pub fn default_denoms(network: Network) -> Self { + pub fn default_denoms(network: &NymNetworkDetails) -> Self { let mut network_coins = HashMap::new(); - network_coins.insert(network.mix_denom().base, network.mix_denom().into()); - network_coins.insert(network.stake_denom().base, network.stake_denom().into()); + network_coins.insert( + network.chain_details.mix_denom.base.clone(), + network.chain_details.mix_denom.clone().into(), + ); + network_coins.insert( + network.chain_details.stake_denom.base.clone(), + network.chain_details.stake_denom.clone().into(), + ); RegisteredCoins(network_coins) } @@ -513,7 +518,7 @@ mod test { #[test] fn converting_to_display() { - let reg = RegisteredCoins::default_denoms(Network::MAINNET); + let reg = RegisteredCoins::default_denoms(&NymNetworkDetails::new_mainnet()); let values = vec![ (1u128, "0.000001"), (10u128, "0.00001"), @@ -527,16 +532,29 @@ mod test { ]; for (raw, expected) in values { - let coin = Coin::new(raw, Network::MAINNET.mix_denom().base); + let coin = Coin::new( + raw, + NymNetworkDetails::new_mainnet() + .chain_details + .mix_denom + .base + .clone(), + ); let display = reg.attempt_convert_to_display_dec_coin(coin).unwrap(); - assert_eq!(Network::MAINNET.mix_denom().display, display.denom); + assert_eq!( + NymNetworkDetails::new_mainnet() + .chain_details + .mix_denom + .display, + display.denom + ); assert_eq!(expected, display.amount.to_string()); } } #[test] fn converting_to_base() { - let reg = RegisteredCoins::default_denoms(Network::MAINNET); + let reg = RegisteredCoins::default_denoms(&NymNetworkDetails::new_mainnet()); let values = vec![ (1u128, "0.000001"), (10u128, "0.00001"), @@ -551,11 +569,21 @@ mod test { for (expected, raw_display) in values { let coin = DecCoin { - denom: Network::MAINNET.mix_denom().display, + denom: NymNetworkDetails::new_mainnet() + .chain_details + .mix_denom + .display + .clone(), amount: raw_display.parse().unwrap(), }; let base = reg.attempt_convert_to_base_coin(coin).unwrap(); - assert_eq!(Network::MAINNET.mix_denom().base, base.denom); + assert_eq!( + NymNetworkDetails::new_mainnet() + .chain_details + .mix_denom + .base, + base.denom + ); assert_eq!(expected, base.amount); } } diff --git a/common/types/src/delegation.rs b/common/types/src/delegation.rs index e2a60b002c..8538d0559b 100644 --- a/common/types/src/delegation.rs +++ b/common/types/src/delegation.rs @@ -3,7 +3,7 @@ use crate::deprecated::DelegationEvent; use crate::error::TypesError; use crate::mixnode::MixNodeCostParams; use cosmwasm_std::Decimal; -use mixnet_contract_common::{Delegation as MixnetContractDelegation, NodeId}; +use mixnet_contract_common::{Delegation as MixnetContractDelegation, MixId}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -15,7 +15,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct Delegation { pub owner: String, - pub mix_id: NodeId, + pub mix_id: MixId, pub amount: DecCoin, pub height: u64, pub proxy: Option, // proxy address used to delegate the funds on behalf of another address @@ -28,7 +28,7 @@ impl Delegation { ) -> Result { Ok(Delegation { owner: delegation.owner.to_string(), - mix_id: delegation.node_id, + mix_id: delegation.mix_id, amount: reg.attempt_convert_to_display_dec_coin(delegation.amount.into())?, height: delegation.height, proxy: delegation.proxy.map(|d| d.to_string()), @@ -44,7 +44,7 @@ impl Delegation { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct DelegationWithEverything { pub owner: String, - pub mix_id: NodeId, + pub mix_id: MixId, pub node_identity: String, pub amount: DecCoin, pub accumulated_by_delegates: Option, diff --git a/common/types/src/deprecated.rs b/common/types/src/deprecated.rs index a40dea8c4c..62c7193c9e 100644 --- a/common/types/src/deprecated.rs +++ b/common/types/src/deprecated.rs @@ -4,7 +4,7 @@ use crate::currency::DecCoin; use crate::error::TypesError; use crate::pending_events::{PendingEpochEvent, PendingEpochEventData}; -use mixnet_contract_common::{IdentityKey, NodeId}; +use mixnet_contract_common::{IdentityKey, MixId}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -27,7 +27,7 @@ pub enum DelegationEventKind { #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] pub struct DelegationEvent { pub kind: DelegationEventKind, - pub mix_id: NodeId, + pub mix_id: MixId, pub address: String, pub amount: Option, pub proxy: Option, diff --git a/common/types/src/error.rs b/common/types/src/error.rs index a49f7587e2..b0874ba7aa 100644 --- a/common/types/src/error.rs +++ b/common/types/src/error.rs @@ -72,6 +72,8 @@ pub enum TypesError { UnknownCoinDenom(String), #[error("Provided event is not a delegation event")] NotADelegationEvent, + #[error("Unknown network - {0}")] + UnknownNetwork(String), } impl Serialize for TypesError { diff --git a/common/types/src/mixnode.rs b/common/types/src/mixnode.rs index 5535ec97f8..e1c3bf4940 100644 --- a/common/types/src/mixnode.rs +++ b/common/types/src/mixnode.rs @@ -5,10 +5,10 @@ use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; use cosmwasm_std::Decimal; use mixnet_contract_common::{ - EpochId, MixNode, MixNodeBond as MixnetContractMixNodeBond, + EpochId, MixId, MixNode, MixNodeBond as MixnetContractMixNodeBond, MixNodeCostParams as MixnetContractMixNodeCostParams, MixNodeDetails as MixnetContractMixNodeDetails, - MixNodeRewarding as MixnetContractMixNodeRewarding, NodeId, Percent, + MixNodeRewarding as MixnetContractMixNodeRewarding, Percent, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -49,7 +49,7 @@ impl MixNodeDetails { )] #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] pub struct MixNodeBond { - pub id: NodeId, + pub mix_id: MixId, pub owner: String, pub original_pledge: DecCoin, pub layer: String, @@ -65,7 +65,7 @@ impl MixNodeBond { reg: &RegisteredCoins, ) -> Result { Ok(MixNodeBond { - id: bond.id, + mix_id: bond.mix_id, owner: bond.owner.into_string(), original_pledge: reg .attempt_convert_to_display_dec_coin(bond.original_pledge.into())?, diff --git a/common/types/src/pending_events.rs b/common/types/src/pending_events.rs index 65f157abd7..afe5a54d9e 100644 --- a/common/types/src/pending_events.rs +++ b/common/types/src/pending_events.rs @@ -5,11 +5,11 @@ use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; use crate::mixnode::MixNodeCostParams; use mixnet_contract_common::{ - EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, NodeId, + BlockHeight, EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, MixId, PendingEpochEvent as MixnetContractPendingEpochEvent, - PendingEpochEventData as MixnetContractPendingEpochEventData, + PendingEpochEventKind as MixnetContractPendingEpochEventKind, PendingIntervalEvent as MixnetContractPendingIntervalEvent, - PendingIntervalEventData as MixnetContractPendingIntervalEventData, + PendingIntervalEventKind as MixnetContractPendingIntervalEventKind, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -22,6 +22,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct PendingEpochEvent { pub id: EpochEventId, + pub created_at: BlockHeight, pub event: PendingEpochEventData, } @@ -32,7 +33,8 @@ impl PendingEpochEvent { ) -> Result { Ok(PendingEpochEvent { id: pending_event.id, - event: PendingEpochEventData::try_from_mixnet_contract(pending_event.event, reg)?, + created_at: pending_event.event.created_at, + event: PendingEpochEventData::try_from_mixnet_contract(pending_event.event.kind, reg)?, }) } } @@ -46,17 +48,17 @@ impl PendingEpochEvent { pub enum PendingEpochEventData { Delegate { owner: String, - mix_id: NodeId, + mix_id: MixId, amount: DecCoin, proxy: Option, }, Undelegate { owner: String, - mix_id: NodeId, + mix_id: MixId, proxy: Option, }, UnbondMixnode { - mix_id: NodeId, + mix_id: MixId, }, UpdateActiveSetSize { new_size: u32, @@ -65,11 +67,11 @@ pub enum PendingEpochEventData { impl PendingEpochEventData { pub fn try_from_mixnet_contract( - pending_event: MixnetContractPendingEpochEventData, + pending_event: MixnetContractPendingEpochEventKind, reg: &RegisteredCoins, ) -> Result { match pending_event { - MixnetContractPendingEpochEventData::Delegate { + MixnetContractPendingEpochEventKind::Delegate { owner, mix_id, amount, @@ -80,7 +82,7 @@ impl PendingEpochEventData { amount: reg.attempt_convert_to_display_dec_coin(amount.into())?, proxy: proxy.map(|p| p.into_string()), }), - MixnetContractPendingEpochEventData::Undelegate { + MixnetContractPendingEpochEventKind::Undelegate { owner, mix_id, proxy, @@ -89,10 +91,10 @@ impl PendingEpochEventData { mix_id, proxy: proxy.map(|p| p.into_string()), }), - MixnetContractPendingEpochEventData::UnbondMixnode { mix_id } => { + MixnetContractPendingEpochEventKind::UnbondMixnode { mix_id } => { Ok(PendingEpochEventData::UnbondMixnode { mix_id }) } - MixnetContractPendingEpochEventData::UpdateActiveSetSize { new_size } => { + MixnetContractPendingEpochEventKind::UpdateActiveSetSize { new_size } => { Ok(PendingEpochEventData::UpdateActiveSetSize { new_size }) } } @@ -107,6 +109,7 @@ impl PendingEpochEventData { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct PendingIntervalEvent { pub id: IntervalEventId, + pub created_at: BlockHeight, pub event: PendingIntervalEventData, } @@ -117,7 +120,11 @@ impl PendingIntervalEvent { ) -> Result { Ok(PendingIntervalEvent { id: pending_event.id, - event: PendingIntervalEventData::try_from_mixnet_contract(pending_event.event, reg)?, + created_at: pending_event.event.created_at, + event: PendingIntervalEventData::try_from_mixnet_contract( + pending_event.event.kind, + reg, + )?, }) } } @@ -130,7 +137,7 @@ impl PendingIntervalEvent { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub enum PendingIntervalEventData { ChangeMixCostParams { - mix_id: NodeId, + mix_id: MixId, new_costs: MixNodeCostParams, }, @@ -145,11 +152,11 @@ pub enum PendingIntervalEventData { impl PendingIntervalEventData { pub fn try_from_mixnet_contract( - pending_event: MixnetContractPendingIntervalEventData, + pending_event: MixnetContractPendingIntervalEventKind, reg: &RegisteredCoins, ) -> Result { match pending_event { - MixnetContractPendingIntervalEventData::ChangeMixCostParams { mix_id, new_costs } => { + MixnetContractPendingIntervalEventKind::ChangeMixCostParams { mix_id, new_costs } => { Ok(PendingIntervalEventData::ChangeMixCostParams { mix_id, new_costs: MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( @@ -157,10 +164,10 @@ impl PendingIntervalEventData { )?, }) } - MixnetContractPendingIntervalEventData::UpdateRewardingParams { update } => { + MixnetContractPendingIntervalEventKind::UpdateRewardingParams { update } => { Ok(PendingIntervalEventData::UpdateRewardingParams { update }) } - MixnetContractPendingIntervalEventData::UpdateIntervalConfig { + MixnetContractPendingIntervalEventKind::UpdateIntervalConfig { epochs_in_interval, epoch_duration_secs, } => Ok(PendingIntervalEventData::UpdateIntervalConfig { diff --git a/common/wasm-utils/Cargo.toml b/common/wasm-utils/Cargo.toml index 1a19af2a8b..75021ddad9 100644 --- a/common/wasm-utils/Cargo.toml +++ b/common/wasm-utils/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" [dependencies] futures = "0.3" js-sys = "^0.3.51" -wasm-bindgen = "=0.2.78" +wasm-bindgen = "=0.2.83" wasm-bindgen-futures = "0.4" # we don't want entire tokio-tungstenite, tungstenite itself is just fine - we just want message and error enums diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 38fbe32341..0050b844a1 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -3,10 +3,12 @@ ### Added - vesting-contract: added queries for delegation timestamps and paged query for all vesting delegations in the contract ([#1569]) +- all binaries: added shell completion and [Fig](fig.io) spec generation ([#1638]) ### Changed - mixnet-contract: compounding delegator rewards now happens instantaneously as opposed to having to wait for the current epoch to finish ([#1571]) +- network-requester: updated CLI to use `clap` macros ([#1638]) ### Fixed @@ -18,6 +20,7 @@ [#1569]: https://github.com/nymtech/nym/pull/1569 [#1569]: https://github.com/nymtech/nym/pull/1571 [#1613]: https://github.com/nymtech/nym/pull/1613 +[#1638]: https://github.com/nymtech/nym/pull/1638 ## [nym-contracts-v1.0.1](https://github.com/nymtech/nym/tree/nym-contracts-v1.0.1) (2022-06-22) diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index c0609e5ea2..a62c529faf 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -221,7 +221,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]] @@ -778,6 +780,22 @@ dependencies = [ "digest 0.9.0", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime", + "serde", +] + [[package]] name = "idna" version = "0.2.3" @@ -937,6 +955,7 @@ dependencies = [ "bs58", "contracts-common", "cosmwasm-std", + "humantime-serde", "log", "schemars", "serde", @@ -1589,6 +1608,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", @@ -1602,7 +1622,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", + "log", "mixnet-contract-common", "schemars", "serde", diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 237e9c38ca..f8e0780d87 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -38,3 +38,7 @@ crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } [build-dependencies] vergen = { version = "5", default-features = false, features = ["build", "git", "rustc"] } + +[features] +default = [] +contract-testing = ["mixnet-contract-common/contract-testing"] diff --git a/contracts/mixnet/Makefile b/contracts/mixnet/Makefile new file mode 100644 index 0000000000..93032c5c3e --- /dev/null +++ b/contracts/mixnet/Makefile @@ -0,0 +1,5 @@ +opt: wasm + wasm-opt -Os ../target/wasm32-unknown-unknown/release/mixnet_contract.wasm -o ../target/wasm32-unknown-unknown/release/mixnet_contract.wasm + +wasm: + RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown diff --git a/contracts/mixnet/README.md b/contracts/mixnet/README.md index a4ace81c19..e685426dc7 100644 --- a/contracts/mixnet/README.md +++ b/contracts/mixnet/README.md @@ -8,6 +8,16 @@ This is the [cosmwasm](https://www.cosmwasm.com) smart contract which runs the N RUSTFLAGS='-C link-arg=-s' cargo wasm ``` +## Production build + +Install wasm-opt: + +``` +npm i wasm-opt -g +``` + +Run `make mixnet-opt` from the top level Nym directory + ## CI Support We have template configurations for both [GitHub Actions](.github/workflows/Basic.yml) diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 60c51aa603..1e91323c6c 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -138,7 +138,7 @@ pub fn execute( expected_active_set_size, ), ExecuteMsg::ReconcileEpochEvents { limit } => { - crate::interval::transactions::try_reconcile_epoch_events(deps, env, limit) + crate::interval::transactions::try_reconcile_epoch_events(deps, env, info, limit) } // mixnode-related: @@ -169,17 +169,19 @@ pub fn execute( owner_signature, ), ExecuteMsg::UnbondMixnode {} => { - crate::mixnodes::transactions::try_remove_mixnode(deps, info) + crate::mixnodes::transactions::try_remove_mixnode(deps, env, info) } ExecuteMsg::UnbondMixnodeOnBehalf { owner } => { - crate::mixnodes::transactions::try_remove_mixnode_on_behalf(deps, info, owner) + crate::mixnodes::transactions::try_remove_mixnode_on_behalf(deps, env, info, owner) } ExecuteMsg::UpdateMixnodeCostParams { new_costs } => { - crate::mixnodes::transactions::try_update_mixnode_cost_params(deps, info, new_costs) + crate::mixnodes::transactions::try_update_mixnode_cost_params( + deps, env, info, new_costs, + ) } ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { new_costs, owner } => { crate::mixnodes::transactions::try_update_mixnode_cost_params_on_behalf( - deps, info, new_costs, owner, + deps, env, info, new_costs, owner, ) } ExecuteMsg::UpdateMixnodeConfig { new_config } => { @@ -223,19 +225,21 @@ pub fn execute( // delegation-related: ExecuteMsg::DelegateToMixnode { mix_id } => { - crate::delegations::transactions::try_delegate_to_mixnode(deps, info, mix_id) + crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_id) } ExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, delegate } => { crate::delegations::transactions::try_delegate_to_mixnode_on_behalf( - deps, info, mix_id, delegate, + deps, env, info, mix_id, delegate, ) } ExecuteMsg::UndelegateFromMixnode { mix_id } => { - crate::delegations::transactions::try_remove_delegation_from_mixnode(deps, info, mix_id) + crate::delegations::transactions::try_remove_delegation_from_mixnode( + deps, env, info, mix_id, + ) } ExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, delegate } => { crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf( - deps, info, mix_id, delegate, + deps, env, info, mix_id, delegate, ) } @@ -259,6 +263,12 @@ pub fn execute( deps, info, mix_id, owner, ) } + + // testing-only + #[cfg(feature = "contract-testing")] + ExecuteMsg::TestingResolveAllPendingEvents { limit } => { + crate::testing::transactions::try_resolve_all_pending_events(deps, env, limit) + } } } diff --git a/contracts/mixnet/src/delegations/helpers.rs b/contracts/mixnet/src/delegations/helpers.rs index 5a639d6c05..b21e0d9ba4 100644 --- a/contracts/mixnet/src/delegations/helpers.rs +++ b/contracts/mixnet/src/delegations/helpers.rs @@ -15,7 +15,7 @@ pub(crate) fn undelegate( ) -> Result { let tokens = mix_rewarding.undelegate(&delegation)?; - rewards_storage::MIXNODE_REWARDING.save(store, delegation.node_id, &mix_rewarding)?; + rewards_storage::MIXNODE_REWARDING.save(store, delegation.mix_id, &mix_rewarding)?; storage::delegations().replace(store, delegation.storage_key(), None, Some(&delegation))?; Ok(tokens) diff --git a/contracts/mixnet/src/delegations/queries.rs b/contracts/mixnet/src/delegations/queries.rs index d921c22b26..ff913c030b 100644 --- a/contracts/mixnet/src/delegations/queries.rs +++ b/contracts/mixnet/src/delegations/queries.rs @@ -12,13 +12,13 @@ use cosmwasm_std::StdResult; use cw_storage_plus::Bound; use mixnet_contract_common::delegation::{MixNodeDelegationResponse, OwnerProxySubKey}; use mixnet_contract_common::{ - delegation, Delegation, NodeId, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, + delegation, Delegation, MixId, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedMixNodeDelegationsResponse, }; pub(crate) fn query_mixnode_delegations_paged( deps: Deps<'_>, - mix_id: NodeId, + mix_id: MixId, start_after: Option, limit: Option, ) -> StdResult { @@ -50,7 +50,7 @@ pub(crate) fn query_mixnode_delegations_paged( pub(crate) fn query_delegator_delegations_paged( deps: Deps<'_>, delegation_owner: String, - start_after: Option<(NodeId, OwnerProxySubKey)>, + start_after: Option<(MixId, OwnerProxySubKey)>, limit: Option, ) -> StdResult { let validated_owner = deps.api.addr_validate(&delegation_owner)?; @@ -74,7 +74,7 @@ pub(crate) fn query_delegator_delegations_paged( let start_next_after = delegations .last() - .map(|del| (del.node_id, del.proxy_storage_key())); + .map(|del| (del.mix_id, del.proxy_storage_key())); Ok(PagedDelegatorDelegationsResponse::new( delegations, @@ -85,7 +85,7 @@ pub(crate) fn query_delegator_delegations_paged( // queries for delegation value of given address for particular node pub(crate) fn query_mixnode_delegation( deps: Deps<'_>, - mix_id: NodeId, + mix_id: MixId, delegation_owner: String, proxy: Option, ) -> StdResult { @@ -284,19 +284,19 @@ mod tests { let res1 = query_mixnode_delegations_paged(test.deps(), mix_id1, None, None).unwrap(); assert_eq!(res1.delegations.len(), 10); - assert!(res1.delegations.into_iter().all(|d| d.node_id == mix_id1)); + assert!(res1.delegations.into_iter().all(|d| d.mix_id == mix_id1)); let res2 = query_mixnode_delegations_paged(test.deps(), mix_id2, None, None).unwrap(); assert_eq!(res2.delegations.len(), 14); - assert!(res2.delegations.into_iter().all(|d| d.node_id == mix_id2)); + assert!(res2.delegations.into_iter().all(|d| d.mix_id == mix_id2)); let res3 = query_mixnode_delegations_paged(test.deps(), mix_id3, None, None).unwrap(); assert_eq!(res3.delegations.len(), 10); - assert!(res3.delegations.into_iter().all(|d| d.node_id == mix_id3)); + assert!(res3.delegations.into_iter().all(|d| d.mix_id == mix_id3)); let res4 = query_mixnode_delegations_paged(test.deps(), mix_id4, None, None).unwrap(); assert_eq!(res4.delegations.len(), 10); - assert!(res4.delegations.into_iter().all(|d| d.node_id == mix_id4)); + assert!(res4.delegations.into_iter().all(|d| d.mix_id == mix_id4)); } } @@ -453,6 +453,7 @@ mod tests { #[test] fn all_retrieved_delegations_are_from_the_specified_delegator() { let mut test = TestSetup::new(); + let env = test.env(); // it means we have, for example, delegation from "delegator1" towards mix1, mix2, ...., from "delegator2" towards mix1, mix2, ...., etc add_dummy_mixes_with_delegations(&mut test, 50, 100); @@ -462,6 +463,7 @@ mod tests { for mix_id in 1..=25 { try_delegate_to_mixnode_on_behalf( test.deps_mut(), + env.clone(), mock_info(vesting_contract.as_ref(), &[coin(100_000, TEST_COIN_DENOM)]), mix_id, with_proxy.into(), @@ -610,7 +612,7 @@ mod tests { assert_eq!(1, page1.delegations.len()); assert!( page1.delegations[0].owner.as_str() == delegator1 - && page1.delegations[0].node_id == mix_id1 + && page1.delegations[0].mix_id == mix_id1 ); test.add_immediate_delegation(delegator1, 1000u32, mix_id2); @@ -621,11 +623,11 @@ mod tests { assert_eq!(2, page1.delegations.len()); assert!( page1.delegations[0].owner.as_str() == delegator1 - && page1.delegations[0].node_id == mix_id1 + && page1.delegations[0].mix_id == mix_id1 ); assert!( page1.delegations[1].owner.as_str() == delegator1 - && page1.delegations[1].node_id == mix_id2 + && page1.delegations[1].mix_id == mix_id2 ); test.add_immediate_delegation(delegator2, 1000u32, mix_id1); @@ -636,11 +638,11 @@ mod tests { assert_eq!(2, another_page1.delegations.len()); assert!( another_page1.delegations[0].owner.as_str() == delegator1 - && another_page1.delegations[0].node_id == mix_id1 + && another_page1.delegations[0].mix_id == mix_id1 ); assert!( another_page1.delegations[1].owner.as_str() == delegator2 - && another_page1.delegations[1].node_id == mix_id1 + && another_page1.delegations[1].mix_id == mix_id1 ); // retrieving the next page should start after the last key on this page @@ -652,7 +654,7 @@ mod tests { assert_eq!(1, page2.delegations.len()); assert!( page2.delegations[0].owner.as_str() == delegator1 - && page2.delegations[0].node_id == mix_id2 + && page2.delegations[0].mix_id == mix_id2 ); // save another one @@ -665,11 +667,11 @@ mod tests { assert_eq!(2, page2.delegations.len()); assert!( page2.delegations[0].owner.as_str() == delegator1 - && page2.delegations[0].node_id == mix_id2 + && page2.delegations[0].mix_id == mix_id2 ); assert!( page2.delegations[1].owner.as_str() == delegator2 - && page2.delegations[1].node_id == mix_id2 + && page2.delegations[1].mix_id == mix_id2 ); } } diff --git a/contracts/mixnet/src/delegations/storage.rs b/contracts/mixnet/src/delegations/storage.rs index f33fa97d3d..04d07f57f8 100644 --- a/contracts/mixnet/src/delegations/storage.rs +++ b/contracts/mixnet/src/delegations/storage.rs @@ -6,15 +6,15 @@ use crate::constants::{ }; use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex}; use mixnet_contract_common::delegation::OwnerProxySubKey; -use mixnet_contract_common::{Addr, Delegation, NodeId}; +use mixnet_contract_common::{Addr, Delegation, MixId}; // It's a composite key on node's id and delegator address -type PrimaryKey = (NodeId, OwnerProxySubKey); +type PrimaryKey = (MixId, OwnerProxySubKey); pub(crate) struct DelegationIndex<'a> { pub(crate) owner: MultiIndex<'a, Addr, Delegation, PrimaryKey>, - pub(crate) mixnode: MultiIndex<'a, NodeId, Delegation, PrimaryKey>, + pub(crate) mixnode: MultiIndex<'a, MixId, Delegation, PrimaryKey>, } impl<'a> IndexList for DelegationIndex<'a> { @@ -32,7 +32,7 @@ pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, Delega DELEGATION_OWNER_IDX_NAMESPACE, ), mixnode: MultiIndex::new( - |d| d.node_id, + |d| d.mix_id, DELEGATION_PK_NAMESPACE, DELEGATION_MIXNODE_IDX_NAMESPACE, ), diff --git a/contracts/mixnet/src/delegations/transactions.rs b/contracts/mixnet/src/delegations/transactions.rs index 7018c65790..271a2c190d 100644 --- a/contracts/mixnet/src/delegations/transactions.rs +++ b/contracts/mixnet/src/delegations/transactions.rs @@ -6,35 +6,38 @@ use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::storage as mixnodes_storage; use crate::support::helpers::validate_delegation_stake; -use cosmwasm_std::{Addr, Coin, DepsMut, MessageInfo, Response}; +use cosmwasm_std::{Addr, Coin, DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_pending_delegation_event, new_pending_undelegation_event, }; -use mixnet_contract_common::pending_events::PendingEpochEventData; -use mixnet_contract_common::{Delegation, NodeId}; +use mixnet_contract_common::pending_events::PendingEpochEventKind; +use mixnet_contract_common::{Delegation, MixId}; pub(crate) fn try_delegate_to_mixnode( deps: DepsMut<'_>, + env: Env, info: MessageInfo, - mix_id: NodeId, + mix_id: MixId, ) -> Result { - _try_delegate_to_mixnode(deps, mix_id, info.sender, info.funds, None) + _try_delegate_to_mixnode(deps, env, mix_id, info.sender, info.funds, None) } pub(crate) fn try_delegate_to_mixnode_on_behalf( deps: DepsMut<'_>, + env: Env, info: MessageInfo, - mix_id: NodeId, + mix_id: MixId, delegate: String, ) -> Result { let delegate = deps.api.addr_validate(&delegate)?; - _try_delegate_to_mixnode(deps, mix_id, delegate, info.funds, Some(info.sender)) + _try_delegate_to_mixnode(deps, env, mix_id, delegate, info.funds, Some(info.sender)) } pub(crate) fn _try_delegate_to_mixnode( deps: DepsMut<'_>, - mix_id: NodeId, + env: Env, + mix_id: MixId, delegate: Addr, amount: Vec, proxy: Option, @@ -49,9 +52,9 @@ pub(crate) fn _try_delegate_to_mixnode( // check if the target node actually exists and is still bonded match mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)? { - None => return Err(MixnetContractError::MixNodeBondNotFound { id: mix_id }), + None => return Err(MixnetContractError::MixNodeBondNotFound { mix_id }), Some(bond) if bond.is_unbonding => { - return Err(MixnetContractError::MixnodeIsUnbonding { node_id: mix_id }) + return Err(MixnetContractError::MixnodeIsUnbonding { mix_id }) } _ => (), } @@ -59,38 +62,41 @@ pub(crate) fn _try_delegate_to_mixnode( // push the event onto the queue and wait for it to be picked up at the end of the epoch let cosmos_event = new_pending_delegation_event(&delegate, &proxy, &delegation, mix_id); - let epoch_event = PendingEpochEventData::Delegate { + let epoch_event = PendingEpochEventKind::Delegate { owner: delegate, mix_id, amount: delegation, proxy, }; - interval_storage::push_new_epoch_event(deps.storage, &epoch_event)?; + interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; Ok(Response::new().add_event(cosmos_event)) } pub(crate) fn try_remove_delegation_from_mixnode( deps: DepsMut<'_>, + env: Env, info: MessageInfo, - mix_id: NodeId, + mix_id: MixId, ) -> Result { - _try_remove_delegation_from_mixnode(deps, mix_id, info.sender, None) + _try_remove_delegation_from_mixnode(deps, env, mix_id, info.sender, None) } pub(crate) fn try_remove_delegation_from_mixnode_on_behalf( deps: DepsMut<'_>, + env: Env, info: MessageInfo, - mix_id: NodeId, + mix_id: MixId, delegate: String, ) -> Result { let delegate = deps.api.addr_validate(&delegate)?; - _try_remove_delegation_from_mixnode(deps, mix_id, delegate, Some(info.sender)) + _try_remove_delegation_from_mixnode(deps, env, mix_id, delegate, Some(info.sender)) } pub(crate) fn _try_remove_delegation_from_mixnode( deps: DepsMut<'_>, - mix_id: NodeId, + env: Env, + mix_id: MixId, delegate: Addr, proxy: Option, ) -> Result { @@ -111,12 +117,12 @@ pub(crate) fn _try_remove_delegation_from_mixnode( // push the event onto the queue and wait for it to be picked up at the end of the epoch let cosmos_event = new_pending_undelegation_event(&delegate, &proxy, mix_id); - let epoch_event = PendingEpochEventData::Undelegate { + let epoch_event = PendingEpochEventKind::Undelegate { owner: delegate, mix_id, proxy, }; - interval_storage::push_new_epoch_event(deps.storage, &epoch_event)?; + interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; Ok(Response::new().add_event(cosmos_event)) } @@ -138,30 +144,33 @@ mod tests { #[test] fn can_only_be_done_towards_an_existing_mixnode() { let mut test = TestSetup::new(); + let env = test.env(); let owner = "delegator"; let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); - let res = try_delegate_to_mixnode(test.deps_mut(), sender, 42); + let res = try_delegate_to_mixnode(test.deps_mut(), env, sender, 42); assert_eq!( res, - Err(MixnetContractError::MixNodeBondNotFound { id: 42 }) + Err(MixnetContractError::MixNodeBondNotFound { mix_id: 42 }) ) } #[test] fn must_contain_non_zero_amount_of_coins() { let mut test = TestSetup::new(); + let env = test.env(); + let owner = "delegator"; let mix_id = test.add_dummy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(0, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[]); let sender3 = mock_info(owner, &[coin(1000, "some-weird-coin")]); - let res = try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id); + let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id); assert_eq!(res, Err(MixnetContractError::EmptyDelegation)); - let res = try_delegate_to_mixnode(test.deps_mut(), sender2, mix_id); + let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender2, mix_id); assert_eq!(res, Err(MixnetContractError::EmptyDelegation)); - let res = try_delegate_to_mixnode(test.deps_mut(), sender3, mix_id); + let res = try_delegate_to_mixnode(test.deps_mut(), env, sender3, mix_id); assert_eq!( res, Err(MixnetContractError::WrongDenom { @@ -174,6 +183,8 @@ mod tests { #[test] fn if_applicable_must_contain_at_least_the_minimum_pledge() { let mut test = TestSetup::new(); + let env = test.env(); + let owner = "delegator"; let mix_id = test.add_dummy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); @@ -188,7 +199,7 @@ mod tests { .save(test.deps_mut().storage, &contract_state) .unwrap(); - let res = try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id); + let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id); assert_eq!( res, Err(MixnetContractError::InsufficientDelegation { @@ -197,13 +208,14 @@ mod tests { }) ); - let res = try_delegate_to_mixnode(test.deps_mut(), sender2, mix_id); + let res = try_delegate_to_mixnode(test.deps_mut(), env, sender2, mix_id); assert!(res.is_ok()) } #[test] fn can_only_be_done_towards_fully_bonded_mixnode() { let mut test = TestSetup::new(); + let env = test.env(); let owner = "delegator"; let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); @@ -226,37 +238,59 @@ mod tests { ) .unwrap(); - try_remove_mixnode(test.deps_mut(), mock_info("mix-owner-unbonded", &[])).unwrap(); try_remove_mixnode( test.deps_mut(), + env.clone(), + mock_info("mix-owner-unbonded", &[]), + ) + .unwrap(); + try_remove_mixnode( + test.deps_mut(), + env.clone(), mock_info("mix-owner-unbonded-leftover", &[]), ) .unwrap(); test.execute_all_pending_events(); - try_remove_mixnode(test.deps_mut(), mock_info("mix-owner-unbonding", &[])).unwrap(); + try_remove_mixnode( + test.deps_mut(), + env.clone(), + mock_info("mix-owner-unbonding", &[]), + ) + .unwrap(); - let res = try_delegate_to_mixnode(test.deps_mut(), sender.clone(), mix_id_unbonding); + let res = try_delegate_to_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id_unbonding, + ); assert_eq!( res, Err(MixnetContractError::MixnodeIsUnbonding { - node_id: mix_id_unbonding + mix_id: mix_id_unbonding }) ); - let res = try_delegate_to_mixnode(test.deps_mut(), sender.clone(), mix_id_unbonded); + let res = try_delegate_to_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id_unbonded, + ); assert_eq!( res, Err(MixnetContractError::MixNodeBondNotFound { - id: mix_id_unbonded + mix_id: mix_id_unbonded }) ); - let res = try_delegate_to_mixnode(test.deps_mut(), sender, mix_id_unbonded_leftover); + let res = + try_delegate_to_mixnode(test.deps_mut(), env, sender, mix_id_unbonded_leftover); assert_eq!( res, Err(MixnetContractError::MixNodeBondNotFound { - id: mix_id_unbonded_leftover + mix_id: mix_id_unbonded_leftover }) ); } @@ -264,22 +298,26 @@ mod tests { #[test] fn can_still_be_done_if_prior_delegation_exists() { let mut test = TestSetup::new(); + let env = test.env(); + let owner = "delegator"; let mix_id = test.add_dummy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[coin(50_000_000, TEST_COIN_DENOM)]); - let res = try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id); + let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id); assert!(res.is_ok()); // still OK - let res = try_delegate_to_mixnode(test.deps_mut(), sender2, mix_id); + let res = try_delegate_to_mixnode(test.deps_mut(), env, sender2, mix_id); assert!(res.is_ok()) } #[test] fn correctly_pushes_appropriate_epoch_event() { let mut test = TestSetup::new(); + let env = test.env(); + let owner = "delegator"; let mix_id = test.add_dummy_mixnode("mix-owner", None); @@ -289,15 +327,15 @@ mod tests { let sender1 = mock_info(owner, &[amount1.clone()]); let sender2 = mock_info(test.vesting_contract().as_str(), &[amount2.clone()]); - try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id).unwrap(); - try_delegate_to_mixnode_on_behalf(test.deps_mut(), sender2, mix_id, owner.into()) + try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); + try_delegate_to_mixnode_on_behalf(test.deps_mut(), env, sender2, mix_id, owner.into()) .unwrap(); let events = test.pending_epoch_events(); assert_eq!( - events[0], - PendingEpochEventData::Delegate { + events[0].kind, + PendingEpochEventKind::Delegate { owner: Addr::unchecked(owner), mix_id, amount: amount1, @@ -306,8 +344,8 @@ mod tests { ); assert_eq!( - events[1], - PendingEpochEventData::Delegate { + events[1].kind, + PendingEpochEventKind::Delegate { owner: Addr::unchecked(owner), mix_id, amount: amount2, @@ -329,11 +367,12 @@ mod tests { #[test] fn cannot_be_performed_if_delegation_never_existed() { let mut test = TestSetup::new(); + let env = test.env(); let owner = "delegator"; let sender = mock_info(owner, &[]); let mix_id = test.add_dummy_mixnode("mix-owner", None); - let res = try_remove_delegation_from_mixnode(test.deps_mut(), sender, mix_id); + let res = try_remove_delegation_from_mixnode(test.deps_mut(), env, sender, mix_id); assert_eq!( res, Err(MixnetContractError::NoMixnodeDelegationFound { @@ -347,14 +386,16 @@ mod tests { #[test] fn cannot_be_performed_if_the_delegation_is_still_pending() { let mut test = TestSetup::new(); + let env = test.env(); + let owner = "delegator"; let mix_id = test.add_dummy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[]); - try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id).unwrap(); + try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); - let res = try_remove_delegation_from_mixnode(test.deps_mut(), sender2, mix_id); + let res = try_remove_delegation_from_mixnode(test.deps_mut(), env, sender2, mix_id); assert_eq!( res, Err(MixnetContractError::NoMixnodeDelegationFound { @@ -368,6 +409,8 @@ mod tests { #[test] fn as_long_as_delegation_exists_can_always_be_performed() { let mut test = TestSetup::new(); + let env = test.env(); + let owner = "delegator"; let sender = mock_info(owner, &[]); @@ -382,19 +425,30 @@ mod tests { try_remove_mixnode( test.deps_mut(), + env.clone(), mock_info("mix-owner-unbonded-leftover", &[]), ) .unwrap(); test.execute_all_pending_events(); - try_remove_mixnode(test.deps_mut(), mock_info("mix-owner-unbonding", &[])).unwrap(); + try_remove_mixnode( + test.deps_mut(), + env.clone(), + mock_info("mix-owner-unbonding", &[]), + ) + .unwrap(); - let res = - try_remove_delegation_from_mixnode(test.deps_mut(), sender.clone(), normal_mix_id); + let res = try_remove_delegation_from_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + normal_mix_id, + ); assert!(res.is_ok()); let res = try_remove_delegation_from_mixnode( test.deps_mut(), + env.clone(), sender.clone(), mix_id_unbonding, ); @@ -402,6 +456,7 @@ mod tests { let res = try_remove_delegation_from_mixnode( test.deps_mut(), + env, sender, mix_id_unbonded_leftover, ); diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index 3b28394e46..55973f7589 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -274,7 +274,7 @@ pub mod tests { assert_eq!(Err(MixnetContractError::AlreadyOwnsMixnode), result); // but after he unbonds it, it's all fine again - pending_events::unbond_mixnode(deps.as_mut(), &env, mix_id).unwrap(); + pending_events::unbond_mixnode(deps.as_mut(), &env, 123, mix_id).unwrap(); let result = try_add_gateway(deps.as_mut(), env, info, gateway, sig); assert!(result.is_ok()); diff --git a/contracts/mixnet/src/interval/helpers.rs b/contracts/mixnet/src/interval/helpers.rs index 5187943154..dd45c7511a 100644 --- a/contracts/mixnet/src/interval/helpers.rs +++ b/contracts/mixnet/src/interval/helpers.rs @@ -6,11 +6,12 @@ use crate::rewards::storage as rewards_storage; use cosmwasm_std::{Response, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::new_interval_config_update_event; -use mixnet_contract_common::Interval; +use mixnet_contract_common::{BlockHeight, Interval}; use std::time::Duration; pub(crate) fn change_interval_config( store: &mut dyn Storage, + request_creation: BlockHeight, mut current_interval: Interval, epochs_in_interval: u32, epoch_duration_secs: u64, @@ -25,6 +26,7 @@ pub(crate) fn change_interval_config( storage::save_interval(store, ¤t_interval)?; Ok(Response::new().add_event(new_interval_config_update_event( + request_creation, epochs_in_interval, epoch_duration_secs, rewarding_params.interval, @@ -50,6 +52,7 @@ mod tests { // if we half the number of epochs, the reward budget should get doubled change_interval_config( &mut deps.storage, + 123, initial_interval, initial_interval.epochs_in_interval() / 2, initial_interval.epoch_length_secs(), @@ -72,6 +75,7 @@ mod tests { // and similarly when we double number of epochs, the reward budget should get halved change_interval_config( &mut deps.storage, + 123, initial_interval, initial_interval.epochs_in_interval() * 2, initial_interval.epoch_length_secs(), diff --git a/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs index 6fa599927c..84a5be1163 100644 --- a/contracts/mixnet/src/interval/pending_events.rs +++ b/contracts/mixnet/src/interval/pending_events.rs @@ -17,9 +17,12 @@ use mixnet_contract_common::events::{ new_rewarding_params_update_event, new_undelegation_event, }; use mixnet_contract_common::mixnode::MixNodeCostParams; -use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; +use mixnet_contract_common::pending_events::{ + PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData, + PendingIntervalEventKind, +}; use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; -use mixnet_contract_common::{Delegation, NodeId}; +use mixnet_contract_common::{BlockHeight, Delegation, MixId}; use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; pub(crate) trait ContractExecutableEvent { @@ -32,8 +35,9 @@ pub(crate) trait ContractExecutableEvent { pub(crate) fn delegate( deps: DepsMut<'_>, env: &Env, + created_at: BlockHeight, owner: Addr, - mix_id: NodeId, + mix_id: MixId, amount: Coin, proxy: Option, ) -> Result { @@ -105,7 +109,14 @@ pub(crate) fn delegate( // add the amount we're intending to delegate (whether it's fresh or we're adding to the existing one) mix_rewarding.add_base_delegation(stored_delegation_amount.amount); - let cosmos_event = new_delegation_event(&owner, &proxy, &new_delegation_amount, mix_id); + let cosmos_event = new_delegation_event( + created_at, + &owner, + &proxy, + &new_delegation_amount, + mix_id, + mix_rewarding.total_unit_reward, + ); let delegation = Delegation::new( owner, @@ -130,8 +141,9 @@ pub(crate) fn delegate( pub(crate) fn undelegate( deps: DepsMut<'_>, + created_at: BlockHeight, owner: Addr, - mix_id: NodeId, + mix_id: MixId, proxy: Option, ) -> Result { // see if the delegation still exists (in case of impatient user who decided to send multiple @@ -153,7 +165,7 @@ pub(crate) fn undelegate( let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![tokens_to_return.clone()]); let mut response = Response::new() .add_message(return_tokens) - .add_event(new_undelegation_event(&owner, &proxy, mix_id)); + .add_event(new_undelegation_event(created_at, &owner, &proxy, mix_id)); if let Some(proxy) = &proxy { // we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract @@ -177,7 +189,8 @@ pub(crate) fn undelegate( pub(crate) fn unbond_mixnode( deps: DepsMut<'_>, env: &Env, - mix_id: NodeId, + created_at: BlockHeight, + mix_id: MixId, ) -> Result { // if we're here it means user executed `_try_remove_mixnode` and as a result node was set to be // in unbonding state and thus nothing could have been done to it (such as attempting to double unbond it) @@ -207,7 +220,7 @@ pub(crate) fn unbond_mixnode( let mut response = Response::new() .add_message(return_tokens) - .add_event(new_mixnode_unbonding_event(mix_id)); + .add_event(new_mixnode_unbonding_event(created_at, mix_id)); if let Some(proxy) = &proxy { // we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract @@ -229,6 +242,7 @@ pub(crate) fn unbond_mixnode( pub(crate) fn update_active_set_size( deps: DepsMut<'_>, + created_at: BlockHeight, active_set_size: u32, ) -> Result { // We don't have to check for authorization as this event can only be pushed @@ -241,28 +255,30 @@ pub(crate) fn update_active_set_size( rewarding_params.try_change_active_set_size(active_set_size)?; rewards_storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; - Ok(Response::new().add_event(new_active_set_update_event(active_set_size))) + Ok(Response::new().add_event(new_active_set_update_event(created_at, active_set_size))) } impl ContractExecutableEvent for PendingEpochEventData { fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result { // note that the basic validation on all those events was already performed before // they were pushed onto the queue - match self { - PendingEpochEventData::Delegate { + match self.kind { + PendingEpochEventKind::Delegate { owner, mix_id, amount, proxy, - } => delegate(deps, env, owner, mix_id, amount, proxy), - PendingEpochEventData::Undelegate { + } => delegate(deps, env, self.created_at, owner, mix_id, amount, proxy), + PendingEpochEventKind::Undelegate { owner, mix_id, proxy, - } => undelegate(deps, owner, mix_id, proxy), - PendingEpochEventData::UnbondMixnode { mix_id } => unbond_mixnode(deps, env, mix_id), - PendingEpochEventData::UpdateActiveSetSize { new_size } => { - update_active_set_size(deps, new_size) + } => undelegate(deps, self.created_at, owner, mix_id, proxy), + PendingEpochEventKind::UnbondMixnode { mix_id } => { + unbond_mixnode(deps, env, self.created_at, mix_id) + } + PendingEpochEventKind::UpdateActiveSetSize { new_size } => { + update_active_set_size(deps, self.created_at, new_size) } } } @@ -270,7 +286,8 @@ impl ContractExecutableEvent for PendingEpochEventData { pub(crate) fn change_mix_cost_params( deps: DepsMut<'_>, - mix_id: NodeId, + created_at: BlockHeight, + mix_id: MixId, new_costs: MixNodeCostParams, ) -> Result { // almost an entire interval might have passed since the request was issued -> check if the @@ -285,7 +302,7 @@ pub(crate) fn change_mix_cost_params( _ => return Ok(Response::default()), }; - let cosmos_event = new_mixnode_cost_params_update_event(mix_id, &new_costs); + let cosmos_event = new_mixnode_cost_params_update_event(created_at, mix_id, &new_costs); // TODO: can we just change cost_params without breaking rewarding calculation? // (I'm almost certain we can, but well, it has to be tested) @@ -297,6 +314,7 @@ pub(crate) fn change_mix_cost_params( pub(crate) fn update_rewarding_params( deps: DepsMut<'_>, + created_at: BlockHeight, updated_params: IntervalRewardingParamsUpdate, ) -> Result { // We don't have to check for authorization as this event can only be pushed @@ -311,6 +329,7 @@ pub(crate) fn update_rewarding_params( rewards_storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; Ok(Response::new().add_event(new_rewarding_params_update_event( + created_at, updated_params, rewarding_params.interval, ))) @@ -318,6 +337,7 @@ pub(crate) fn update_rewarding_params( pub(crate) fn update_interval_config( deps: DepsMut, + created_at: BlockHeight, epochs_in_interval: u32, epoch_duration_secs: u64, ) -> Result { @@ -326,8 +346,10 @@ pub(crate) fn update_interval_config( // Furthermore, we don't need to check whether the interval is finished as the // queue is only emptied upon the interval finishing. let interval = storage::current_interval(deps.storage)?; + change_interval_config( deps.storage, + created_at, interval, epochs_in_interval, epoch_duration_secs, @@ -338,18 +360,23 @@ impl ContractExecutableEvent for PendingIntervalEventData { fn execute(self, deps: DepsMut<'_>, _env: &Env) -> Result { // note that the basic validation on all those events was already performed before // they were pushed onto the queue - match self { - PendingIntervalEventData::ChangeMixCostParams { + match self.kind { + PendingIntervalEventKind::ChangeMixCostParams { mix_id: mix, new_costs, - } => change_mix_cost_params(deps, mix, new_costs), - PendingIntervalEventData::UpdateRewardingParams { update } => { - update_rewarding_params(deps, update) + } => change_mix_cost_params(deps, self.created_at, mix, new_costs), + PendingIntervalEventKind::UpdateRewardingParams { update } => { + update_rewarding_params(deps, self.created_at, update) } - PendingIntervalEventData::UpdateIntervalConfig { + PendingIntervalEventKind::UpdateIntervalConfig { epochs_in_interval, epoch_duration_secs, - } => update_interval_config(deps, epochs_in_interval, epoch_duration_secs), + } => update_interval_config( + deps, + self.created_at, + epochs_in_interval, + epoch_duration_secs, + ), } } } @@ -390,11 +417,12 @@ mod tests { test.add_immediate_delegation(owner1, delegation, mix_id); let env = test.env(); - unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap(); + unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); let res_increase = delegate( test.deps_mut(), &env, + 123, Addr::unchecked(owner1), mix_id, delegation_coin.clone(), @@ -420,6 +448,7 @@ mod tests { let res_fresh = delegate( test.deps_mut(), &env, + 123, Addr::unchecked(owner2), mix_id, delegation_coin.clone(), @@ -453,11 +482,12 @@ mod tests { test.add_immediate_delegation(owner1, delegation, mix_id); let env = test.env(); - try_remove_mixnode(test.deps_mut(), mock_info("mix-owner", &[])).unwrap(); + try_remove_mixnode(test.deps_mut(), env.clone(), mock_info("mix-owner", &[])).unwrap(); let res_increase = delegate( test.deps_mut(), &env, + 123, Addr::unchecked(owner1), mix_id, delegation_coin.clone(), @@ -483,6 +513,7 @@ mod tests { let res_fresh = delegate( test.deps_mut(), &env, + 123, Addr::unchecked(owner2), mix_id, delegation_coin.clone(), @@ -518,6 +549,7 @@ mod tests { let res = delegate( test.deps_mut(), &env, + 123, Addr::unchecked(owner), mix_id, delegation_coin_new, @@ -580,6 +612,7 @@ mod tests { let res = delegate( test.deps_mut(), &env, + 123, Addr::unchecked(owner), mix_id, delegation_coin_new, @@ -645,6 +678,7 @@ mod tests { let res = delegate( test.deps_mut(), &env, + 123, Addr::unchecked(owner), mix_id, delegation_coin.clone(), @@ -679,7 +713,7 @@ mod tests { let owner2 = "delegator2"; let env = test.env(); - unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap(); + unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); let vesting_contract = test.vesting_contract(); let dummy_proxy = Addr::unchecked("not-vesting-contract"); @@ -688,6 +722,7 @@ mod tests { let res_vesting = delegate( test.deps_mut(), &env, + 123, Addr::unchecked(owner1), mix_id, delegation_coin.clone(), @@ -734,6 +769,7 @@ mod tests { let res_other_proxy = delegate( test.deps_mut(), &env, + 123, Addr::unchecked(owner1), mix_id, delegation_coin.clone(), @@ -778,7 +814,7 @@ mod tests { let owner = Addr::unchecked("delegator"); - let res = undelegate(test.deps_mut(), owner, mix_id, None).unwrap(); + let res = undelegate(test.deps_mut(), 123, owner, mix_id, None).unwrap(); assert!(get_bank_send_msg(&res).is_none()); } @@ -793,7 +829,7 @@ mod tests { // this should never happen in actual code, but if we manually messed something up, // lets make sure this throws an error rewards_storage::MIXNODE_REWARDING.remove(test.deps_mut().storage, mix_id); - let res = undelegate(test.deps_mut(), owner, mix_id, None); + let res = undelegate(test.deps_mut(), 123, owner, mix_id, None); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -827,7 +863,8 @@ mod tests { let expected_return = delegation + truncated_reward.u128(); - let res = undelegate(test.deps_mut(), Addr::unchecked(owner), mix_id, None).unwrap(); + let res = + undelegate(test.deps_mut(), 123, Addr::unchecked(owner), mix_id, None).unwrap(); let (receiver, sent_amount) = get_bank_send_msg(&res).unwrap(); assert_eq!(receiver, owner); assert_eq!(sent_amount[0].amount.u128(), expected_return); @@ -875,6 +912,7 @@ mod tests { // for a fresh delegation, nothing was added to the storage either let res_vesting = undelegate( test.deps_mut(), + 123, Addr::unchecked(owner1), mix_id, Some(vesting_contract.clone()), @@ -920,6 +958,7 @@ mod tests { let res_other_proxy = undelegate( test.deps_mut(), + 123, Addr::unchecked(owner2), mix_id, Some(dummy_proxy.clone()), @@ -964,7 +1003,7 @@ mod tests { let mut test = TestSetup::new(); let env = test.env(); - let res = unbond_mixnode(test.deps_mut(), &env, 1); + let res = unbond_mixnode(test.deps_mut(), &env, 123, 1); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -994,7 +1033,7 @@ mod tests { let expected_return = pledge + truncated_reward; let env = test.env(); - let res = unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap(); + let res = unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); let (receiver, sent_amount) = get_bank_send_msg(&res).unwrap(); assert_eq!(receiver, owner); assert_eq!(sent_amount[0].amount, expected_return); @@ -1042,7 +1081,7 @@ mod tests { test.add_dummy_mixnode_with_proxy(owner2, Some(pledge), dummy_proxy.clone()); let env = test.env(); - let res_vesting = unbond_mixnode(test.deps_mut(), &env, mix_id_vesting).unwrap(); + let res_vesting = unbond_mixnode(test.deps_mut(), &env, 123, mix_id_vesting).unwrap(); assert!(mixnodes_storage::mixnode_bonds() .may_load(test.deps().storage, mix_id_vesting) @@ -1077,7 +1116,7 @@ mod tests { assert!(found_track); let res_other_proxy = - unbond_mixnode(test.deps_mut(), &env, mix_id_other_proxy).unwrap(); + unbond_mixnode(test.deps_mut(), &env, 123, mix_id_other_proxy).unwrap(); assert!(mixnodes_storage::mixnode_bonds() .may_load(test.deps().storage, mix_id_other_proxy) .unwrap() @@ -1103,7 +1142,7 @@ mod tests { .load(test.deps().storage) .unwrap(); - update_active_set_size(test.deps_mut(), 50).unwrap(); + update_active_set_size(test.deps_mut(), 123, 50).unwrap(); let updated = rewards_storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap(); @@ -1124,14 +1163,14 @@ mod tests { let mix_id = test.add_dummy_mixnode("mix-owner", None); let env = test.env(); - unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap(); + unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); let new_params = MixNodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM), }; - let res = change_mix_cost_params(test.deps_mut(), mix_id, new_params); + let res = change_mix_cost_params(test.deps_mut(), 123, mix_id, new_params); assert_eq!(res, Ok(Response::default())); } @@ -1147,11 +1186,16 @@ mod tests { interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM), }; - let res = change_mix_cost_params(test.deps_mut(), mix_id, new_params.clone()); + let res = change_mix_cost_params(test.deps_mut(), 123, mix_id, new_params.clone()); assert_eq!( res, - Ok(Response::new() - .add_event(new_mixnode_cost_params_update_event(mix_id, &new_params))) + Ok( + Response::new().add_event(new_mixnode_cost_params_update_event( + 123, + mix_id, + &new_params + )) + ) ); let after = test.mix_rewarding(mix_id).cost_params; @@ -1182,7 +1226,7 @@ mod tests { rewarded_set_size: None, }; - let res = update_rewarding_params(test.deps_mut(), update); + let res = update_rewarding_params(test.deps_mut(), 123, update); assert!(res.is_ok()); let after = rewards_storage::REWARDING_PARAMS .load(test.deps().storage) @@ -1230,6 +1274,7 @@ mod tests { // and change epoch length update_interval_config( test.deps_mut(), + 123, interval_before.epochs_in_interval() / 2, 1234, ) diff --git a/contracts/mixnet/src/interval/queries.rs b/contracts/mixnet/src/interval/queries.rs index 14622ddd82..a0fe28ed28 100644 --- a/contracts/mixnet/src/interval/queries.rs +++ b/contracts/mixnet/src/interval/queries.rs @@ -11,7 +11,7 @@ use cosmwasm_std::{Deps, Env, Order, StdResult}; use cw_storage_plus::Bound; use mixnet_contract_common::pending_events::{PendingEpochEvent, PendingIntervalEvent}; use mixnet_contract_common::{ - CurrentIntervalResponse, EpochEventId, IntervalEventId, NodeId, PagedRewardedSetResponse, + CurrentIntervalResponse, EpochEventId, IntervalEventId, MixId, PagedRewardedSetResponse, PendingEpochEventsResponse, PendingIntervalEventsResponse, }; @@ -26,7 +26,7 @@ pub fn query_current_interval_details( pub fn query_rewarded_set_paged( deps: Deps<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -238,7 +238,7 @@ mod tests { mod pending_epoch_events { use super::*; use cosmwasm_std::Addr; - use mixnet_contract_common::pending_events::PendingEpochEventData; + use mixnet_contract_common::pending_events::PendingEpochEventKind; use rand_chacha::rand_core::RngCore; fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) { @@ -248,12 +248,13 @@ mod tests { } fn push_dummy_epoch_action(test: &mut TestSetup) { - let dummy_action = PendingEpochEventData::Undelegate { + let dummy_action = PendingEpochEventKind::Undelegate { owner: Addr::unchecked("foomp"), mix_id: test.rng.next_u32(), proxy: None, }; - storage::push_new_epoch_event(test.deps_mut().storage, &dummy_action).unwrap(); + let env = test.env(); + storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } #[test] @@ -379,7 +380,7 @@ mod tests { mod pending_interval_events { use super::*; use crate::support::tests::fixtures; - use mixnet_contract_common::pending_events::PendingIntervalEventData; + use mixnet_contract_common::pending_events::PendingIntervalEventKind; use rand_chacha::rand_core::RngCore; fn push_n_dummy_interval_actions(test: &mut TestSetup, n: usize) { @@ -389,11 +390,12 @@ mod tests { } fn push_dummy_interval_action(test: &mut TestSetup) { - let dummy_action = PendingIntervalEventData::ChangeMixCostParams { + let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { mix_id: test.rng.next_u32(), new_costs: fixtures::mix_node_cost_params_fixture(), }; - storage::push_new_interval_event(test.deps_mut().storage, &dummy_action).unwrap(); + let env = test.env(); + storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } #[test] diff --git a/contracts/mixnet/src/interval/storage.rs b/contracts/mixnet/src/interval/storage.rs index 733da8c3d0..282876f5f8 100644 --- a/contracts/mixnet/src/interval/storage.rs +++ b/contracts/mixnet/src/interval/storage.rs @@ -6,16 +6,18 @@ use crate::constants::{ LAST_EPOCH_EVENT_ID_KEY, LAST_INTERVAL_EVENT_ID_KEY, PENDING_EPOCH_EVENTS_NAMESPACE, PENDING_INTERVAL_EVENTS_NAMESPACE, REWARDED_SET_KEY, }; -use cosmwasm_std::{Order, StdResult, Storage}; +use cosmwasm_std::{Env, Order, StdResult, Storage}; use cw_storage_plus::{Item, Map}; -use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; +use mixnet_contract_common::pending_events::{ + PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData, +}; use mixnet_contract_common::{ - EpochEventId, Interval, IntervalEventId, NodeId, RewardedSetNodeStatus, + EpochEventId, Interval, IntervalEventId, MixId, PendingIntervalEventKind, RewardedSetNodeStatus, }; use std::collections::HashMap; pub(crate) const CURRENT_INTERVAL: Item<'_, Interval> = Item::new(CURRENT_INTERVAL_KEY); -pub(crate) const REWARDED_SET: Map = Map::new(REWARDED_SET_KEY); +pub(crate) const REWARDED_SET: Map = Map::new(REWARDED_SET_KEY); pub(crate) const EPOCH_EVENT_ID_COUNTER: Item = Item::new(EPOCH_EVENT_ID_COUNTER_KEY); pub(crate) const INTERVAL_EVENT_ID_COUNTER: Item = @@ -64,24 +66,28 @@ pub(crate) fn next_interval_event_id_counter( pub(crate) fn push_new_epoch_event( storage: &mut dyn Storage, - event: &PendingEpochEventData, + env: &Env, + event: PendingEpochEventKind, ) -> StdResult<()> { let event_id = next_epoch_event_id_counter(storage)?; - PENDING_EPOCH_EVENTS.save(storage, event_id, event) + let event_data = event.attach_source_height(env.block.height); + PENDING_EPOCH_EVENTS.save(storage, event_id, &event_data) } pub(crate) fn push_new_interval_event( storage: &mut dyn Storage, - event: &PendingIntervalEventData, + env: &Env, + event: PendingIntervalEventKind, ) -> StdResult<()> { let event_id = next_interval_event_id_counter(storage)?; - PENDING_INTERVAL_EVENTS.save(storage, event_id, event) + let event_data = event.attach_source_height(env.block.height); + PENDING_INTERVAL_EVENTS.save(storage, event_id, &event_data) } pub(crate) fn update_rewarded_set( storage: &mut dyn Storage, active_set_size: u32, - new_set: Vec, + new_set: Vec, ) -> StdResult<()> { // our goal is to reduce the number of reads and writes to the underlying storage, // whilst completely overwriting the current rewarded set. @@ -138,7 +144,7 @@ mod tests { use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::Order; - fn read_entire_set(storage: &mut dyn Storage) -> HashMap { + fn read_entire_set(storage: &mut dyn Storage) -> HashMap { REWARDED_SET .range(storage, None, None, Order::Ascending) .map(|r| r.unwrap()) diff --git a/contracts/mixnet/src/interval/transactions.rs b/contracts/mixnet/src/interval/transactions.rs index 3a6dd400df..705f36804a 100644 --- a/contracts/mixnet/src/interval/transactions.rs +++ b/contracts/mixnet/src/interval/transactions.rs @@ -15,8 +15,8 @@ use mixnet_contract_common::events::{ new_pending_interval_config_update_event, new_pending_interval_events_execution_event, new_reconcile_pending_events, }; -use mixnet_contract_common::pending_events::PendingIntervalEventData; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::pending_events::PendingIntervalEventKind; +use mixnet_contract_common::MixId; use std::collections::BTreeSet; // those two should be called in separate tx (from advancing epoch), @@ -111,12 +111,19 @@ pub(crate) fn perform_pending_interval_actions( pub fn try_reconcile_epoch_events( mut deps: DepsMut<'_>, env: Env, + info: MessageInfo, mut limit: Option, ) -> Result { + // we need to ensure the request actually comes from the rewarding validator. otherwise the following would be possible: + // - epoch has just finished (i.e. it's possible to call the reconcile function) + // - the validator API is ABOUT to start rewarding + // - somebody sneaks in some extra delegations + // - the same person decides to pay the transaction fees and reconcile epoch events themselves + // - the validator API distributes the rewards -> this new sneaky delegation is now included in reward calculation! + ensure_is_authorized(info.sender, deps.storage)?; + let mut response = Response::new().add_event(new_reconcile_pending_events()); - // there's no need for authorization, as anyone willing to pay the fees should be allowed to reconcile - // contract events ASSUMING the corresponding epoch/interval has finished let interval = storage::current_interval(deps.storage)?; if !interval.is_current_epoch_over(&env) { // if the current epoch is in progress, so must be the interval so there's no need to check that @@ -155,7 +162,7 @@ pub fn try_reconcile_epoch_events( fn update_rewarded_set( storage: &mut dyn Storage, - new_rewarded_set: Vec, + new_rewarded_set: Vec, expected_active_set_size: u32, ) -> Result<(), MixnetContractError> { let reward_params = rewards_storage::REWARDING_PARAMS.load(storage)?; @@ -180,7 +187,7 @@ fn update_rewarded_set( let mut tmp_set = BTreeSet::new(); for node_id in &new_rewarded_set { if !tmp_set.insert(node_id) { - return Err(MixnetContractError::DuplicateRewardedSetNode { node_id: *node_id }); + return Err(MixnetContractError::DuplicateRewardedSetNode { mix_id: *node_id }); } } @@ -195,7 +202,7 @@ pub fn try_advance_epoch( mut deps: DepsMut<'_>, env: Env, info: MessageInfo, - new_rewarded_set: Vec, + new_rewarded_set: Vec, expected_active_set_size: u32, ) -> Result { // Only rewarding validator can attempt to advance epoch @@ -263,17 +270,18 @@ pub(crate) fn try_update_interval_config( if force_immediately || interval.is_current_interval_over(&env) { change_interval_config( deps.storage, + env.block.height, interval, epochs_in_interval, epoch_duration_secs, ) } else { // push the interval event - let interval_event = PendingIntervalEventData::UpdateIntervalConfig { + let interval_event = PendingIntervalEventKind::UpdateIntervalConfig { epochs_in_interval, epoch_duration_secs, }; - push_new_interval_event(deps.storage, &interval_event)?; + push_new_interval_event(deps.storage, &env, interval_event)?; let time_left = interval.secs_until_current_interval_end(&env); Ok( Response::new().add_event(new_pending_interval_config_update_event( @@ -291,31 +299,33 @@ mod tests { use crate::support::tests::fixtures; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::Addr; - use mixnet_contract_common::pending_events::PendingEpochEventData; + use mixnet_contract_common::pending_events::PendingEpochEventKind; use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) { // if you attempt to undelegate non-existent delegation, // it will return an empty response, but will not fail + let env = test.env(); for i in 0..n { - let dummy_action = PendingEpochEventData::Undelegate { + let dummy_action = PendingEpochEventKind::Undelegate { owner: Addr::unchecked("foomp"), - mix_id: i as NodeId, + mix_id: i as MixId, proxy: None, }; - storage::push_new_epoch_event(test.deps_mut().storage, &dummy_action).unwrap(); + storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } } fn push_n_dummy_interval_actions(test: &mut TestSetup, n: usize) { // if you attempt to update cost parameters of an unbonded mixnode, // it will return an empty response, but will not fail + let env = test.env(); for i in 0..n { - let dummy_action = PendingIntervalEventData::ChangeMixCostParams { - mix_id: i as NodeId, + let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { + mix_id: i as MixId, new_costs: fixtures::mix_node_cost_params_fixture(), }; - storage::push_new_interval_event(test.deps_mut().storage, &dummy_action).unwrap(); + storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } } @@ -328,7 +338,7 @@ mod tests { new_active_set_update_event, new_delegation_on_unbonded_node_event, new_undelegation_event, }; - use mixnet_contract_common::pending_events::PendingEpochEventData; + use mixnet_contract_common::pending_events::PendingEpochEventKind; #[test] fn without_limit_executes_all_actions() { @@ -391,14 +401,15 @@ mod tests { ); push_n_dummy_epoch_actions(&mut test, 10); - let action_with_event = PendingEpochEventData::UpdateActiveSetSize { new_size: 50 }; - storage::push_new_epoch_event(test.deps_mut().storage, &action_with_event).unwrap(); + let action_with_event = PendingEpochEventKind::UpdateActiveSetSize { new_size: 50 }; + storage::push_new_epoch_event(test.deps_mut().storage, &env, action_with_event) + .unwrap(); push_n_dummy_epoch_actions(&mut test, 10); let (res, executed) = perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); assert_eq!( res, - Response::new().add_event(new_active_set_update_event(50)) + Response::new().add_event(new_active_set_update_event(env.block.height, 50)) ); assert_eq!(executed, 21); assert_eq!( @@ -426,13 +437,13 @@ mod tests { // delegate to node that doesn't exist, // we expect to receive BankMsg with tokens being returned, // and event regarding delegation - let non_existent_delegation = PendingEpochEventData::Delegate { + let non_existent_delegation = PendingEpochEventKind::Delegate { owner: Addr::unchecked("foomp"), mix_id: 123, amount: coin(123, TEST_COIN_DENOM), proxy: None, }; - storage::push_new_epoch_event(test.deps_mut().storage, &non_existent_delegation) + storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation) .unwrap(); expected_events.push(new_delegation_on_unbonded_node_event( &Addr::unchecked("foomp"), @@ -446,13 +457,13 @@ mod tests { // delegation to node that doesn't exist with vesting contract // we expect the same as above PLUS TrackUndelegation message - let non_existent_delegation = PendingEpochEventData::Delegate { + let non_existent_delegation = PendingEpochEventKind::Delegate { owner: Addr::unchecked("foomp2"), mix_id: 123, amount: coin(123, TEST_COIN_DENOM), proxy: Some(vesting_contract.clone()), }; - storage::push_new_epoch_event(test.deps_mut().storage, &non_existent_delegation) + storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation) .unwrap(); expected_events.push(new_delegation_on_unbonded_node_event( &Addr::unchecked("foomp2"), @@ -472,18 +483,24 @@ mod tests { expected_messages.push(SubMsg::new(track_undelegate_message)); // updating active set should only emit events and no cosmos messages - let action_with_event = PendingEpochEventData::UpdateActiveSetSize { new_size: 50 }; - storage::push_new_epoch_event(test.deps_mut().storage, &action_with_event).unwrap(); - expected_events.push(new_active_set_update_event(50)); + let action_with_event = PendingEpochEventKind::UpdateActiveSetSize { new_size: 50 }; + storage::push_new_epoch_event(test.deps_mut().storage, &env, action_with_event) + .unwrap(); + expected_events.push(new_active_set_update_event(env.block.height, 50)); // undelegation just returns tokens and emits event - let legit_undelegate = PendingEpochEventData::Undelegate { + let legit_undelegate = PendingEpochEventKind::Undelegate { owner: delegator.clone(), mix_id: legit_mix, proxy: None, }; - storage::push_new_epoch_event(test.deps_mut().storage, &legit_undelegate).unwrap(); - expected_events.push(new_undelegation_event(&delegator, &None, legit_mix)); + storage::push_new_epoch_event(test.deps_mut().storage, &env, legit_undelegate).unwrap(); + expected_events.push(new_undelegation_event( + env.block.height, + &delegator, + &None, + legit_mix, + )); expected_messages.push(SubMsg::new(BankMsg::Send { to_address: delegator.into_string(), amount: coins(amount, TEST_COIN_DENOM), @@ -633,16 +650,20 @@ mod tests { rewarded_set_size: Some(500), ..Default::default() }; - let action_with_event = PendingIntervalEventData::UpdateRewardingParams { update }; - storage::push_new_interval_event(test.deps_mut().storage, &action_with_event).unwrap(); + let action_with_event = PendingIntervalEventKind::UpdateRewardingParams { update }; + storage::push_new_interval_event(test.deps_mut().storage, &env, action_with_event) + .unwrap(); push_n_dummy_interval_actions(&mut test, 10); let (res, executed) = perform_pending_interval_actions(test.deps_mut(), &env, None).unwrap(); let updated_params = test.rewarding_params().interval; assert_eq!( res, - Response::new() - .add_event(new_rewarding_params_update_event(update, updated_params)) + Response::new().add_event(new_rewarding_params_update_event( + env.block.height, + update, + updated_params + )) ); assert_eq!(executed, 21); assert_eq!( @@ -666,38 +687,45 @@ mod tests { profit_margin_percent: Percent::from_percentage_value(12).unwrap(), interval_operating_cost: coin(123_000, TEST_COIN_DENOM), }; - let cost_change = PendingIntervalEventData::ChangeMixCostParams { + let cost_change = PendingIntervalEventKind::ChangeMixCostParams { mix_id: legit_mix, new_costs: new_costs.clone(), }; - storage::push_new_interval_event(test.deps_mut().storage, &cost_change).unwrap(); - expected_events.push(new_mixnode_cost_params_update_event(legit_mix, &new_costs)); + storage::push_new_interval_event(test.deps_mut().storage, &env, cost_change).unwrap(); + expected_events.push(new_mixnode_cost_params_update_event( + env.block.height, + legit_mix, + &new_costs, + )); let update = IntervalRewardingParamsUpdate { rewarded_set_size: Some(500), ..Default::default() }; - let change_params = PendingIntervalEventData::UpdateRewardingParams { update }; - storage::push_new_interval_event(test.deps_mut().storage, &change_params).unwrap(); + let change_params = PendingIntervalEventKind::UpdateRewardingParams { update }; + storage::push_new_interval_event(test.deps_mut().storage, &env, change_params).unwrap(); let interval = test.current_interval(); let mut expected_updated = test.rewarding_params(); expected_updated .try_apply_updates(update, interval.epochs_in_interval()) .unwrap(); expected_events.push(new_rewarding_params_update_event( + env.block.height, update, expected_updated.interval, )); - let change_interval = PendingIntervalEventData::UpdateIntervalConfig { + let change_interval = PendingIntervalEventKind::UpdateIntervalConfig { epochs_in_interval: 123, epoch_duration_secs: 1000, }; let mut expected_updated2 = expected_updated; expected_updated2.apply_epochs_in_interval_change(123); - storage::push_new_interval_event(test.deps_mut().storage, &change_interval).unwrap(); + storage::push_new_interval_event(test.deps_mut().storage, &env, change_interval) + .unwrap(); expected_events.push(new_interval_config_update_event( + env.block.height, 123, 1000, expected_updated2.interval, @@ -773,25 +801,44 @@ mod tests { mod reconciling_epoch_events { use super::*; use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::testing::mock_info; use cosmwasm_std::{coin, coins, BankMsg, Empty, SubMsg}; use mixnet_contract_common::events::{ new_delegation_on_unbonded_node_event, new_rewarding_params_update_event, }; - use mixnet_contract_common::pending_events::PendingEpochEventData; + use mixnet_contract_common::pending_events::PendingEpochEventKind; use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; #[test] fn returns_error_if_epoch_is_in_progress() { let mut test = TestSetup::new(); let env = test.env(); + let rewarding_validator = test.rewarding_validator(); - let res = try_reconcile_epoch_events(test.deps_mut(), env, None); + let res = try_reconcile_epoch_events(test.deps_mut(), env, rewarding_validator, None); assert!(matches!( res, Err(MixnetContractError::EpochInProgress { .. }) )) } + #[test] + fn returns_error_if_not_performed_by_the_rewarding_validator() { + let mut test = TestSetup::new(); + let env = test.env(); + + test.skip_to_current_epoch_end(); + + let random = mock_info("alice", &[]); + let owner = test.owner(); + + let res = try_reconcile_epoch_events(test.deps_mut(), env.clone(), random, None); + assert!(matches!(res, Err(MixnetContractError::Unauthorized))); + + let res = try_reconcile_epoch_events(test.deps_mut(), env, owner, None); + assert!(matches!(res, Err(MixnetContractError::Unauthorized))); + } + #[test] fn only_clears_epoch_events_if_interval_is_in_progress() { let mut test = TestSetup::new(); @@ -801,7 +848,9 @@ mod tests { test.skip_to_current_epoch_end(); let env = test.env(); - try_reconcile_epoch_events(test.deps_mut(), env, None).unwrap(); + let rewarding_validator = test.rewarding_validator(); + + try_reconcile_epoch_events(test.deps_mut(), env, rewarding_validator, None).unwrap(); let epoch_events = test.pending_epoch_events(); let interval_events = test.pending_interval_events(); @@ -816,9 +865,10 @@ mod tests { push_n_dummy_epoch_actions(&mut test, 10); push_n_dummy_interval_actions(&mut test, 10); test.skip_to_current_interval_end(); + let rewarding_validator = test.rewarding_validator(); let env = test.env(); - try_reconcile_epoch_events(test.deps_mut(), env, None).unwrap(); + try_reconcile_epoch_events(test.deps_mut(), env, rewarding_validator, None).unwrap(); let epoch_events = test.pending_epoch_events(); let interval_events = test.pending_interval_events(); @@ -840,50 +890,95 @@ mod tests { } let env = test1.env(); + // all test cases are using the same one + let rewarding_validator = test1.rewarding_validator(); - try_reconcile_epoch_events(test1.deps_mut(), env.clone(), Some(5)).unwrap(); + try_reconcile_epoch_events( + test1.deps_mut(), + env.clone(), + rewarding_validator.clone(), + Some(5), + ) + .unwrap(); let epoch_events = test1.pending_epoch_events(); let interval_events = test1.pending_interval_events(); assert_eq!(epoch_events.len(), 5); assert_eq!(interval_events.len(), 10); - try_reconcile_epoch_events(test1.deps_mut(), env.clone(), Some(7)).unwrap(); + try_reconcile_epoch_events( + test1.deps_mut(), + env.clone(), + rewarding_validator.clone(), + Some(7), + ) + .unwrap(); let epoch_events = test1.pending_epoch_events(); let interval_events = test1.pending_interval_events(); assert!(epoch_events.is_empty()); assert_eq!(interval_events.len(), 8); - try_reconcile_epoch_events(test1.deps_mut(), env.clone(), Some(7)).unwrap(); + try_reconcile_epoch_events( + test1.deps_mut(), + env.clone(), + rewarding_validator.clone(), + Some(7), + ) + .unwrap(); let epoch_events = test1.pending_epoch_events(); let interval_events = test1.pending_interval_events(); assert!(epoch_events.is_empty()); assert_eq!(interval_events.len(), 1); - try_reconcile_epoch_events(test1.deps_mut(), env.clone(), Some(7)).unwrap(); + try_reconcile_epoch_events( + test1.deps_mut(), + env.clone(), + rewarding_validator.clone(), + Some(7), + ) + .unwrap(); let epoch_events = test1.pending_epoch_events(); let interval_events = test1.pending_interval_events(); assert!(epoch_events.is_empty()); assert!(interval_events.is_empty()); - try_reconcile_epoch_events(test2.deps_mut(), env.clone(), Some(15)).unwrap(); + try_reconcile_epoch_events( + test2.deps_mut(), + env.clone(), + rewarding_validator.clone(), + Some(15), + ) + .unwrap(); let epoch_events = test2.pending_epoch_events(); let interval_events = test2.pending_interval_events(); assert!(epoch_events.is_empty()); assert_eq!(interval_events.len(), 5); - try_reconcile_epoch_events(test2.deps_mut(), env.clone(), Some(15)).unwrap(); + try_reconcile_epoch_events( + test2.deps_mut(), + env.clone(), + rewarding_validator.clone(), + Some(15), + ) + .unwrap(); let epoch_events = test2.pending_epoch_events(); let interval_events = test2.pending_interval_events(); assert!(epoch_events.is_empty()); assert!(interval_events.is_empty()); - try_reconcile_epoch_events(test3.deps_mut(), env.clone(), Some(20)).unwrap(); + try_reconcile_epoch_events( + test3.deps_mut(), + env.clone(), + rewarding_validator.clone(), + Some(20), + ) + .unwrap(); let epoch_events = test3.pending_epoch_events(); let interval_events = test3.pending_interval_events(); assert!(epoch_events.is_empty()); assert!(interval_events.is_empty()); - try_reconcile_epoch_events(test4.deps_mut(), env, Some(100)).unwrap(); + try_reconcile_epoch_events(test4.deps_mut(), env, rewarding_validator, Some(100)) + .unwrap(); let epoch_events = test4.pending_epoch_events(); let interval_events = test4.pending_interval_events(); assert!(epoch_events.is_empty()); @@ -893,18 +988,19 @@ mod tests { #[test] fn catches_all_emitted_cosmos_events_and_messages() { let mut test = TestSetup::new(); + let env = test.env(); let mut expected_events = vec![new_reconcile_pending_events()]; let mut expected_messages: Vec> = Vec::new(); // epoch event - let non_existent_delegation = PendingEpochEventData::Delegate { + let non_existent_delegation = PendingEpochEventKind::Delegate { owner: Addr::unchecked("foomp"), mix_id: 123, amount: coin(123, TEST_COIN_DENOM), proxy: None, }; - storage::push_new_epoch_event(test.deps_mut().storage, &non_existent_delegation) + storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation) .unwrap(); expected_events.push(new_delegation_on_unbonded_node_event( &Addr::unchecked("foomp"), @@ -922,14 +1018,15 @@ mod tests { rewarded_set_size: Some(500), ..Default::default() }; - let change_params = PendingIntervalEventData::UpdateRewardingParams { update }; - storage::push_new_interval_event(test.deps_mut().storage, &change_params).unwrap(); + let change_params = PendingIntervalEventKind::UpdateRewardingParams { update }; + storage::push_new_interval_event(test.deps_mut().storage, &env, change_params).unwrap(); let interval = test.current_interval(); let mut expected_updated = test.rewarding_params(); expected_updated .try_apply_updates(update, interval.epochs_in_interval()) .unwrap(); expected_events.push(new_rewarding_params_update_event( + env.block.height, update, expected_updated.interval, )); @@ -937,7 +1034,10 @@ mod tests { test.skip_to_current_interval_end(); let env = test.env(); - let res = try_reconcile_epoch_events(test.deps_mut(), env, None).unwrap(); + let rewarding_validator = test.rewarding_validator(); + + let res = try_reconcile_epoch_events(test.deps_mut(), env, rewarding_validator, None) + .unwrap(); let mut expected = Response::new().add_events(expected_events); expected.messages = expected_messages; assert_eq!(res, expected); @@ -1015,7 +1115,7 @@ mod tests { .unwrap_err(); assert_eq!( err, - MixnetContractError::DuplicateRewardedSetNode { node_id: 1 } + MixnetContractError::DuplicateRewardedSetNode { mix_id: 1 } ); let nodes_with_duplicate = vec![1, 2, 3, 5, 4, 5]; let err = update_rewarded_set( @@ -1026,7 +1126,7 @@ mod tests { .unwrap_err(); assert_eq!( err, - MixnetContractError::DuplicateRewardedSetNode { node_id: 5 } + MixnetContractError::DuplicateRewardedSetNode { mix_id: 5 } ); } @@ -1161,18 +1261,19 @@ mod tests { #[test] fn if_executes_any_events_it_propagates_responses() { let mut test = TestSetup::new(); + let env = test.env(); let current_active_set = test.rewarding_params().active_set_size; let mut expected_events = Vec::new(); let mut expected_messages: Vec> = Vec::new(); - let non_existent_delegation = PendingEpochEventData::Delegate { + let non_existent_delegation = PendingEpochEventKind::Delegate { owner: Addr::unchecked("foomp"), mix_id: 123, amount: coin(123, TEST_COIN_DENOM), proxy: None, }; - storage::push_new_epoch_event(test.deps_mut().storage, &non_existent_delegation) + storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation) .unwrap(); expected_events.push(new_delegation_on_unbonded_node_event( &Addr::unchecked("foomp"), @@ -1190,14 +1291,15 @@ mod tests { rewarded_set_size: Some(500), ..Default::default() }; - let change_params = PendingIntervalEventData::UpdateRewardingParams { update }; - storage::push_new_interval_event(test.deps_mut().storage, &change_params).unwrap(); + let change_params = PendingIntervalEventKind::UpdateRewardingParams { update }; + storage::push_new_interval_event(test.deps_mut().storage, &env, change_params).unwrap(); let interval = test.current_interval(); let mut expected_updated = test.rewarding_params(); expected_updated .try_apply_updates(update, interval.epochs_in_interval()) .unwrap(); expected_events.push(new_rewarding_params_update_event( + env.block.height, update, expected_updated.interval, )); @@ -1432,8 +1534,8 @@ mod tests { // make sure it's actually saved to pending events let events = test.pending_interval_events(); - assert!(matches!(events[0], - PendingIntervalEventData::UpdateIntervalConfig { epochs_in_interval, epoch_duration_secs } if epochs_in_interval == 100 && epoch_duration_secs == 1000 + assert!(matches!(events[0].kind, + PendingIntervalEventKind::UpdateIntervalConfig { epochs_in_interval, epoch_duration_secs } if epochs_in_interval == 100 && epoch_duration_secs == 1000 )); test.execute_all_pending_events(); diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 418b2c7ab5..3cff5ed93f 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -10,3 +10,6 @@ mod mixnet_contract_settings; mod mixnodes; mod rewards; mod support; + +#[cfg(feature = "contract-testing")] +mod testing; diff --git a/contracts/mixnet/src/mixnodes/helpers.rs b/contracts/mixnet/src/mixnodes/helpers.rs index 57f162c610..15e5e82f98 100644 --- a/contracts/mixnet/src/mixnodes/helpers.rs +++ b/contracts/mixnet/src/mixnodes/helpers.rs @@ -10,7 +10,7 @@ use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::{ MixNodeCostParams, MixNodeDetails, MixNodeRewarding, UnbondedMixnode, }; -use mixnet_contract_common::{Layer, MixNode, MixNodeBond, NodeId}; +use mixnet_contract_common::{Layer, MixId, MixNode, MixNodeBond}; pub(crate) fn must_get_mixnode_bond_by_owner( store: &dyn Storage, @@ -28,12 +28,12 @@ pub(crate) fn must_get_mixnode_bond_by_owner( pub(crate) fn get_mixnode_details_by_id( store: &dyn Storage, - node_id: NodeId, + mix_id: MixId, ) -> StdResult> { - if let Some(bond_information) = storage::mixnode_bonds().may_load(store, node_id)? { + if let Some(bond_information) = storage::mixnode_bonds().may_load(store, mix_id)? { // if bond exists, rewarding details MUST also exist let rewarding_details = - rewards_storage::MIXNODE_REWARDING.load(store, bond_information.id)?; + rewards_storage::MIXNODE_REWARDING.load(store, bond_information.mix_id)?; Ok(Some(MixNodeDetails::new( bond_information, rewarding_details, @@ -55,7 +55,7 @@ pub(crate) fn get_mixnode_details_by_owner( { // if bond exists, rewarding details MUST also exist let rewarding_details = - rewards_storage::MIXNODE_REWARDING.load(store, bond_information.id)?; + rewards_storage::MIXNODE_REWARDING.load(store, bond_information.mix_id)?; Ok(Some(MixNodeDetails::new( bond_information, rewarding_details, @@ -73,14 +73,14 @@ pub(crate) fn save_new_mixnode( owner: Addr, proxy: Option, pledge: Coin, -) -> Result<(NodeId, Layer), MixnetContractError> { +) -> Result<(MixId, Layer), MixnetContractError> { let layer = assign_layer(storage)?; - let node_id = next_mixnode_id_counter(storage)?; + let mix_id = next_mixnode_id_counter(storage)?; let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id(); let mixnode_rewarding = MixNodeRewarding::initialise_new(cost_params, &pledge, current_epoch); let mixnode_bond = MixNodeBond::new( - node_id, + mix_id, owner, pledge, layer, @@ -91,12 +91,12 @@ pub(crate) fn save_new_mixnode( // save mixnode bond data // note that this implicitly checks for uniqueness on identity key, sphinx key and owner - storage::mixnode_bonds().save(storage, node_id, &mixnode_bond)?; + storage::mixnode_bonds().save(storage, mix_id, &mixnode_bond)?; // save rewarding data - rewards_storage::MIXNODE_REWARDING.save(storage, node_id, &mixnode_rewarding)?; + rewards_storage::MIXNODE_REWARDING.save(storage, mix_id, &mixnode_rewarding)?; - Ok((node_id, layer)) + Ok((mix_id, layer)) } pub(crate) fn cleanup_post_unbond_mixnode_storage( @@ -104,13 +104,13 @@ pub(crate) fn cleanup_post_unbond_mixnode_storage( env: &Env, current_details: &MixNodeDetails, ) -> Result<(), MixnetContractError> { - let node_id = current_details.bond_information.id; + let mix_id = current_details.bond_information.mix_id; // remove all bond information (we don't need it anymore // note that "normal" remove is `may_load` followed by `replace` with a `None` // and we have already loaded the data from the storage storage::mixnode_bonds().replace( storage, - node_id, + mix_id, None, Some(¤t_details.bond_information), )?; @@ -118,14 +118,14 @@ pub(crate) fn cleanup_post_unbond_mixnode_storage( // if there are no pending delegations to return, we can also // purge all information regarding rewarding parameters if current_details.rewarding_details.unique_delegations == 0 { - rewards_storage::MIXNODE_REWARDING.remove(storage, node_id); + rewards_storage::MIXNODE_REWARDING.remove(storage, mix_id); } else { // otherwise just set operator's tokens to zero as to indicate they have unbonded // and already claimed those let mut zeroed = current_details.rewarding_details.clone(); zeroed.operator = Decimal::zero(); - rewards_storage::MIXNODE_REWARDING.save(storage, node_id, &zeroed)?; + rewards_storage::MIXNODE_REWARDING.save(storage, mix_id, &zeroed)?; } let identity = current_details.bond_information.identity().to_owned(); @@ -135,7 +135,7 @@ pub(crate) fn cleanup_post_unbond_mixnode_storage( // save minimal information about this mixnode storage::unbonded_mixnodes().save( storage, - node_id, + mix_id, &UnbondedMixnode { identity_key: identity, owner, @@ -161,7 +161,7 @@ mod tests { const OWNER_UNBONDED_LEFTOVER: &str = "mix-owner-unbonded-leftover"; // create a mixnode that is bonded, unbonded, in the process of unbonding and unbonded with leftover mix rewarding details - fn setup_mix_combinations(test: &mut TestSetup) -> Vec { + fn setup_mix_combinations(test: &mut TestSetup) -> Vec { let mix_id_exists = test.add_dummy_mixnode(OWNER_EXISTS, None); let mix_id_unbonding = test.add_dummy_mixnode(OWNER_UNBONDING, None); let mix_id_unbonded = test.add_dummy_mixnode(OWNER_UNBONDED, None); @@ -206,11 +206,11 @@ mod tests { // if this is a normally bonded mixnode, all should be fine let res = must_get_mixnode_bond_by_owner(test.deps().storage, &owner_exists).unwrap(); - assert_eq!(res.id, mix_id_exists); + assert_eq!(res.mix_id, mix_id_exists); // if node is in the process of unbonding, we still should be capable of retrieving its details let res = must_get_mixnode_bond_by_owner(test.deps().storage, &owner_unbonding).unwrap(); - assert_eq!(res.id, mix_id_unbonding); + assert_eq!(res.mix_id, mix_id_unbonding); // but if node has unbonded, the information is purged and query fails let res = must_get_mixnode_bond_by_owner(test.deps().storage, &owner_unbonded); @@ -243,12 +243,12 @@ mod tests { let res = get_mixnode_details_by_id(test.deps().storage, mix_id_exists) .unwrap() .unwrap(); - assert_eq!(res.bond_information.id, mix_id_exists); + assert_eq!(res.bond_information.mix_id, mix_id_exists); let res = get_mixnode_details_by_id(test.deps().storage, mix_id_unbonding) .unwrap() .unwrap(); - assert_eq!(res.bond_information.id, mix_id_unbonding); + assert_eq!(res.bond_information.mix_id, mix_id_unbonding); let res = get_mixnode_details_by_id(test.deps().storage, mix_id_unbonded).unwrap(); assert!(res.is_none()); @@ -274,13 +274,13 @@ mod tests { let res = get_mixnode_details_by_owner(test.deps().storage, owner_exists) .unwrap() .unwrap(); - assert_eq!(res.bond_information.id, mix_id_exists); + assert_eq!(res.bond_information.mix_id, mix_id_exists); // if node is in the process of unbonding, we still should be capable of retrieving its details let res = get_mixnode_details_by_owner(test.deps().storage, owner_unbonding) .unwrap() .unwrap(); - assert_eq!(res.bond_information.id, mix_id_unbonding); + assert_eq!(res.bond_information.mix_id, mix_id_unbonding); // but if node has unbonded, the information is purged and query fails let res = get_mixnode_details_by_owner(test.deps().storage, owner_unbonded).unwrap(); diff --git a/contracts/mixnet/src/mixnodes/queries.rs b/contracts/mixnet/src/mixnodes/queries.rs index 12ca4c3d36..5411b108ff 100644 --- a/contracts/mixnet/src/mixnodes/queries.rs +++ b/contracts/mixnet/src/mixnodes/queries.rs @@ -16,13 +16,13 @@ use mixnet_contract_common::mixnode::{ PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, }; use mixnet_contract_common::{ - IdentityKey, LayerDistribution, MixOwnershipResponse, MixnodeDetailsResponse, NodeId, + IdentityKey, LayerDistribution, MixId, MixOwnershipResponse, MixnodeDetailsResponse, PagedMixnodeBondsResponse, }; pub fn query_mixnode_bonds_paged( deps: Deps<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -37,7 +37,7 @@ pub fn query_mixnode_bonds_paged( .map(|res| res.map(|item| item.1)) .collect::>>()?; - let start_next_after = nodes.last().map(|node| node.id); + let start_next_after = nodes.last().map(|node| node.mix_id); Ok(PagedMixnodeBondsResponse::new( nodes, @@ -48,14 +48,14 @@ pub fn query_mixnode_bonds_paged( fn attach_rewarding_info( storage: &dyn Storage, - read_bond: StdResult<(NodeId, MixNodeBond)>, + read_bond: StdResult<(MixId, MixNodeBond)>, ) -> StdResult { match read_bond { Ok((_, bond)) => { // if we managed to read the bond we MUST be able to also read rewarding information. // if we fail, this is a hard error and the query should definitely fail and we should investigate // the reasons for that. - let mix_rewarding = rewards_storage::MIXNODE_REWARDING.load(storage, bond.id)?; + let mix_rewarding = rewards_storage::MIXNODE_REWARDING.load(storage, bond.mix_id)?; Ok(MixNodeDetails::new(bond, mix_rewarding)) } Err(err) => Err(err), @@ -64,7 +64,7 @@ fn attach_rewarding_info( pub fn query_mixnodes_details_paged( deps: Deps<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -90,7 +90,7 @@ pub fn query_mixnodes_details_paged( pub fn query_unbonded_mixnodes_paged( deps: Deps<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -116,7 +116,7 @@ pub fn query_unbonded_mixnodes_paged( pub fn query_unbonded_mixnodes_by_owner_paged( deps: Deps<'_>, owner: String, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let owner = deps.api.addr_validate(&owner)?; @@ -147,7 +147,7 @@ pub fn query_unbonded_mixnodes_by_owner_paged( pub fn query_unbonded_mixnodes_by_identity_paged( deps: Deps<'_>, identity_key: String, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -183,7 +183,7 @@ pub fn query_owned_mixnode(deps: Deps<'_>, address: String) -> StdResult, mix_id: NodeId) -> StdResult { +pub fn query_mixnode_details(deps: Deps<'_>, mix_id: MixId) -> StdResult { let mixnode_details = get_mixnode_details_by_id(deps.storage, mix_id)?; Ok(MixnodeDetailsResponse { @@ -204,7 +204,7 @@ pub fn query_mixnode_details_by_identity( { // if bond exists, rewarding details MUST also exist let rewarding_details = - rewards_storage::MIXNODE_REWARDING.load(deps.storage, bond_information.id)?; + rewards_storage::MIXNODE_REWARDING.load(deps.storage, bond_information.mix_id)?; Ok(Some(MixNodeDetails::new( bond_information, rewarding_details, @@ -216,7 +216,7 @@ pub fn query_mixnode_details_by_identity( pub fn query_mixnode_rewarding_details( deps: Deps<'_>, - mix_id: NodeId, + mix_id: MixId, ) -> StdResult { let rewarding_details = rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?; @@ -226,10 +226,7 @@ pub fn query_mixnode_rewarding_details( }) } -pub fn query_unbonded_mixnode( - deps: Deps<'_>, - mix_id: NodeId, -) -> StdResult { +pub fn query_unbonded_mixnode(deps: Deps<'_>, mix_id: MixId) -> StdResult { let unbonded_info = storage::unbonded_mixnodes().may_load(deps.storage, mix_id)?; Ok(UnbondedMixnodeResponse { @@ -238,10 +235,7 @@ pub fn query_unbonded_mixnode( }) } -pub fn query_stake_saturation( - deps: Deps<'_>, - mix_id: NodeId, -) -> StdResult { +pub fn query_stake_saturation(deps: Deps<'_>, mix_id: MixId) -> StdResult { let mix_rewarding = match rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { Some(mix_rewarding) => mix_rewarding, None => { @@ -566,7 +560,7 @@ pub(crate) mod tests { #[test] fn pagination_works() { - fn add_unbonded(storage: &mut dyn Storage, id: NodeId) { + fn add_unbonded(storage: &mut dyn Storage, id: MixId) { storage::unbonded_mixnodes() .save( storage, @@ -632,7 +626,7 @@ pub(crate) mod tests { use cosmwasm_std::Addr; use mixnet_contract_common::mixnode::UnbondedMixnode; - fn add_unbonded_with_owner(storage: &mut dyn Storage, id: NodeId, owner: &str) { + fn add_unbonded_with_owner(storage: &mut dyn Storage, id: MixId, owner: &str) { storage::unbonded_mixnodes() .save( storage, @@ -879,7 +873,7 @@ pub(crate) mod tests { use cosmwasm_std::Addr; use mixnet_contract_common::mixnode::UnbondedMixnode; - fn add_unbonded_with_identity(storage: &mut dyn Storage, id: NodeId, identity: &str) { + fn add_unbonded_with_identity(storage: &mut dyn Storage, id: MixId, identity: &str) { storage::unbonded_mixnodes() .save( storage, @@ -1180,7 +1174,7 @@ pub(crate) mod tests { .save(deps.as_mut().storage, id, &rewarding_details) .unwrap(); - pending_events::unbond_mixnode(deps.as_mut(), &mock_env(), id).unwrap(); + pending_events::unbond_mixnode(deps.as_mut(), &mock_env(), 123, id).unwrap(); let res = query_owned_mixnode(deps.as_ref(), address.clone()).unwrap(); assert!(res.mixnode_details.is_none()); assert_eq!(address, res.address); @@ -1202,7 +1196,7 @@ pub(crate) mod tests { test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "foomp", good_mixnode_pledge()); let res = query_mixnode_details(deps.as_ref(), mix_id).unwrap(); let details = res.mixnode_details.unwrap(); - assert_eq!(mix_id, details.bond_information.id); + assert_eq!(mix_id, details.bond_information.mix_id); assert_eq!( good_mixnode_pledge()[0], details.bond_information.original_pledge @@ -1269,7 +1263,7 @@ pub(crate) mod tests { // add and unbond the mixnode let mix_id = test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, sender, good_mixnode_pledge()); - pending_events::unbond_mixnode(deps.as_mut(), &mock_env(), mix_id).unwrap(); + pending_events::unbond_mixnode(deps.as_mut(), &mock_env(), 123, mix_id).unwrap(); let res = query_unbonded_mixnode(deps.as_ref(), mix_id).unwrap(); assert_eq!(res.unbonded_info.unwrap().owner, sender); diff --git a/contracts/mixnet/src/mixnodes/storage.rs b/contracts/mixnet/src/mixnodes/storage.rs index 45f9356d14..dd9ba56212 100644 --- a/contracts/mixnet/src/mixnodes/storage.rs +++ b/contracts/mixnet/src/mixnodes/storage.rs @@ -12,16 +12,16 @@ use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex, UniqueInde use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::UnbondedMixnode; use mixnet_contract_common::SphinxKey; -use mixnet_contract_common::{Addr, IdentityKey, Layer, LayerDistribution, MixNodeBond, NodeId}; +use mixnet_contract_common::{Addr, IdentityKey, Layer, LayerDistribution, MixId, MixNodeBond}; // keeps track of `node_id -> IdentityKey, Owner, unbonding_height` so we'd known a bit more about past mixnodes // if we ever decide it's too bloaty, we can deprecate it and start removing all data in // subsequent migrations pub(crate) struct UnbondedMixnodeIndex<'a> { - pub(crate) owner: MultiIndex<'a, Addr, UnbondedMixnode, NodeId>, + pub(crate) owner: MultiIndex<'a, Addr, UnbondedMixnode, MixId>, - pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedMixnode, NodeId>, + pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedMixnode, MixId>, } impl<'a> IndexList for UnbondedMixnodeIndex<'a> { @@ -32,7 +32,7 @@ impl<'a> IndexList for UnbondedMixnodeIndex<'a> { } pub(crate) fn unbonded_mixnodes<'a>( -) -> IndexedMap<'a, NodeId, UnbondedMixnode, UnbondedMixnodeIndex<'a>> { +) -> IndexedMap<'a, MixId, UnbondedMixnode, UnbondedMixnodeIndex<'a>> { let indexes = UnbondedMixnodeIndex { owner: MultiIndex::new( |d| d.owner.clone(), @@ -49,7 +49,7 @@ pub(crate) fn unbonded_mixnodes<'a>( } pub(crate) const LAYERS: Item<'_, LayerDistribution> = Item::new(LAYER_DISTRIBUTION_KEY); -pub const MIXNODE_ID_COUNTER: Item = Item::new(NODE_ID_COUNTER_KEY); +pub const MIXNODE_ID_COUNTER: Item = Item::new(NODE_ID_COUNTER_KEY); pub(crate) struct MixnodeBondIndex<'a> { pub(crate) owner: UniqueIndex<'a, Addr, MixNodeBond>, @@ -70,7 +70,7 @@ impl<'a> IndexList for MixnodeBondIndex<'a> { } // mixnode_bonds() is the storage access function. -pub(crate) fn mixnode_bonds<'a>() -> IndexedMap<'a, NodeId, MixNodeBond, MixnodeBondIndex<'a>> { +pub(crate) fn mixnode_bonds<'a>() -> IndexedMap<'a, MixId, MixNodeBond, MixnodeBondIndex<'a>> { let indexes = MixnodeBondIndex { owner: UniqueIndex::new(|d| d.owner.clone(), MIXNODES_OWNER_IDX_NAMESPACE), identity_key: UniqueIndex::new( @@ -109,8 +109,8 @@ pub(crate) fn assign_layer(store: &mut dyn Storage) -> StdResult { Ok(fewest) } -pub(crate) fn next_mixnode_id_counter(store: &mut dyn Storage) -> StdResult { - let id: NodeId = MIXNODE_ID_COUNTER.may_load(store)?.unwrap_or_default() + 1; +pub(crate) fn next_mixnode_id_counter(store: &mut dyn Storage) -> StdResult { + let id: MixId = MIXNODE_ID_COUNTER.may_load(store)?.unwrap_or_default() + 1; MIXNODE_ID_COUNTER.save(store, &id)?; Ok(id) } diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index c32f944d6c..0a1e816ffd 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -17,7 +17,7 @@ use mixnet_contract_common::events::{ new_mixnode_pending_cost_params_update_event, new_pending_mixnode_unbonding_event, }; use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; +use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; use mixnet_contract_common::MixNode; pub fn try_add_mixnode( @@ -119,23 +119,26 @@ fn _try_add_mixnode( pub fn try_remove_mixnode_on_behalf( deps: DepsMut<'_>, + env: Env, info: MessageInfo, owner: String, ) -> Result { let proxy = info.sender; let owner = deps.api.addr_validate(&owner)?; - _try_remove_mixnode(deps, owner, Some(proxy)) + _try_remove_mixnode(deps, env, owner, Some(proxy)) } pub fn try_remove_mixnode( deps: DepsMut<'_>, + env: Env, info: MessageInfo, ) -> Result { - _try_remove_mixnode(deps, info.sender, None) + _try_remove_mixnode(deps, env, info.sender, None) } pub(crate) fn _try_remove_mixnode( deps: DepsMut<'_>, + env: Env, owner: Addr, proxy: Option, ) -> Result { @@ -155,23 +158,23 @@ pub(crate) fn _try_remove_mixnode( updated_bond.is_unbonding = true; storage::mixnode_bonds().replace( deps.storage, - existing_bond.id, + existing_bond.mix_id, Some(&updated_bond), Some(&existing_bond), )?; // push the event to execute it at the end of the epoch - let epoch_event = PendingEpochEventData::UnbondMixnode { - mix_id: existing_bond.id, + let epoch_event = PendingEpochEventKind::UnbondMixnode { + mix_id: existing_bond.mix_id, }; - interval_storage::push_new_epoch_event(deps.storage, &epoch_event)?; + interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; Ok( Response::new().add_event(new_pending_mixnode_unbonding_event( &existing_bond.owner, &existing_bond.proxy, existing_bond.identity(), - existing_bond.id, + existing_bond.mix_id, )), ) } @@ -208,7 +211,7 @@ pub(crate) fn _try_update_mixnode_config( ensure_proxy_match(&proxy, &existing_bond.proxy)?; let cfg_update_event = - new_mixnode_config_update_event(existing_bond.id, &owner, &proxy, &new_config); + new_mixnode_config_update_event(existing_bond.mix_id, &owner, &proxy, &new_config); let mut updated_bond = existing_bond.clone(); updated_bond.mix_node.host = new_config.host; @@ -219,7 +222,7 @@ pub(crate) fn _try_update_mixnode_config( storage::mixnode_bonds().replace( deps.storage, - existing_bond.id, + existing_bond.mix_id, Some(&updated_bond), Some(&existing_bond), )?; @@ -229,26 +232,29 @@ pub(crate) fn _try_update_mixnode_config( pub(crate) fn try_update_mixnode_cost_params( deps: DepsMut<'_>, + env: Env, info: MessageInfo, new_costs: MixNodeCostParams, ) -> Result { let owner = info.sender; - _try_update_mixnode_cost_params(deps, new_costs, owner, None) + _try_update_mixnode_cost_params(deps, env, new_costs, owner, None) } pub(crate) fn try_update_mixnode_cost_params_on_behalf( deps: DepsMut, + env: Env, info: MessageInfo, new_costs: MixNodeCostParams, owner: String, ) -> Result { let owner = deps.api.addr_validate(&owner)?; let proxy = info.sender; - _try_update_mixnode_cost_params(deps, new_costs, owner, Some(proxy)) + _try_update_mixnode_cost_params(deps, env, new_costs, owner, Some(proxy)) } pub(crate) fn _try_update_mixnode_cost_params( deps: DepsMut, + env: Env, new_costs: MixNodeCostParams, owner: Addr, proxy: Option, @@ -259,15 +265,19 @@ pub(crate) fn _try_update_mixnode_cost_params( ensure_proxy_match(&proxy, &existing_bond.proxy)?; ensure_bonded(&existing_bond)?; - let cosmos_event = - new_mixnode_pending_cost_params_update_event(existing_bond.id, &owner, &proxy, &new_costs); + let cosmos_event = new_mixnode_pending_cost_params_update_event( + existing_bond.mix_id, + &owner, + &proxy, + &new_costs, + ); // push the interval event - let interval_event = PendingIntervalEventData::ChangeMixCostParams { - mix_id: existing_bond.id, + let interval_event = PendingIntervalEventKind::ChangeMixCostParams { + mix_id: existing_bond.mix_id, new_costs, }; - push_new_interval_event(deps.storage, &interval_event)?; + push_new_interval_event(deps.storage, &env, interval_event)?; Ok(Response::new().add_event(cosmos_event)) } @@ -387,7 +397,7 @@ pub mod tests { // make sure we got assigned the next id (note: we have already bonded a mixnode before in this test) let bond = must_get_mixnode_bond_by_owner(deps.as_ref().storage, &Addr::unchecked(sender2)) .unwrap(); - assert_eq!(2, bond.id); + assert_eq!(2, bond.mix_id); // and make sure we're on layer 2 (because it was the next empty one) assert_eq!(Layer::Two, bond.layer); @@ -414,7 +424,7 @@ pub mod tests { let info = mock_info(sender, &[]); // trying to remove your mixnode fails if you never had one in the first place - let res = try_remove_mixnode(deps.as_mut(), info.clone()); + let res = try_remove_mixnode(deps.as_mut(), env.clone(), info.clone()); assert_eq!( res, Err(MixnetContractError::NoAssociatedMixNodeBond { @@ -422,12 +432,18 @@ pub mod tests { }) ); - let mix_id = - test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, sender, good_mixnode_pledge()); + let mix_id = test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + sender, + good_mixnode_pledge(), + ); // attempted to remove on behalf with invalid proxy (current is `None`) let res = try_remove_mixnode_on_behalf( deps.as_mut(), + env.clone(), mock_info("proxy", &[]), sender.to_string(), ); @@ -440,7 +456,7 @@ pub mod tests { ); // "normal" unbonding succeeds and unbonding event is pushed to the pending epoch events - let res = try_remove_mixnode(deps.as_mut(), info.clone()); + let res = try_remove_mixnode(deps.as_mut(), env.clone(), info.clone()); assert!(res.is_ok()); let mut pending_events = interval_storage::PENDING_EPOCH_EVENTS .range(deps.as_ref().storage, None, None, Order::Ascending) @@ -449,14 +465,14 @@ pub mod tests { assert_eq!(pending_events.len(), 1); let event = pending_events.pop().unwrap(); assert_eq!(1, event.0); - assert_eq!(PendingEpochEventData::UnbondMixnode { mix_id }, event.1); + assert_eq!( + PendingEpochEventKind::UnbondMixnode { mix_id }, + event.1.kind + ); // but fails if repeated (since the node is already in the "unbonding" state)( - let res = try_remove_mixnode(deps.as_mut(), info); - assert_eq!( - res, - Err(MixnetContractError::MixnodeIsUnbonding { node_id: mix_id }) - ) + let res = try_remove_mixnode(deps.as_mut(), env, info); + assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id })) } #[test] @@ -487,7 +503,7 @@ pub mod tests { let mix_id = test_helpers::add_mixnode( &mut rng, deps.as_mut(), - env, + env.clone(), sender, tests::fixtures::good_mixnode_pledge(), ); @@ -520,12 +536,9 @@ pub mod tests { assert_eq!(mix.mix_node.version, update.version); // but we cannot perform any updates whilst the mixnode is already unbonding - try_remove_mixnode(deps.as_mut(), info.clone()).unwrap(); + try_remove_mixnode(deps.as_mut(), env, info.clone()).unwrap(); let res = try_update_mixnode_config(deps.as_mut(), info, update); - assert_eq!( - res, - Err(MixnetContractError::MixnodeIsUnbonding { node_id: mix_id }) - ) + assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id })) } #[test] @@ -542,7 +555,12 @@ pub mod tests { }; // try updating a non existing mixnode bond - let res = try_update_mixnode_cost_params(deps.as_mut(), info.clone(), update.clone()); + let res = try_update_mixnode_cost_params( + deps.as_mut(), + env.clone(), + info.clone(), + update.clone(), + ); assert_eq!( res, Err(MixnetContractError::NoAssociatedMixNodeBond { @@ -561,6 +579,7 @@ pub mod tests { // attempted to remove on behalf with invalid proxy (current is `None`) let res = try_update_mixnode_cost_params_on_behalf( deps.as_mut(), + env.clone(), mock_info("proxy", &[]), update.clone(), sender.to_string(), @@ -573,7 +592,12 @@ pub mod tests { }) ); // "normal" update succeeds - let res = try_update_mixnode_cost_params(deps.as_mut(), info.clone(), update.clone()); + let res = try_update_mixnode_cost_params( + deps.as_mut(), + env.clone(), + info.clone(), + update.clone(), + ); assert!(res.is_ok()); // see if the event has been pushed onto the queue @@ -585,15 +609,15 @@ pub mod tests { let event = pending_events.pop().unwrap(); assert_eq!(1, event.0); assert_eq!( - PendingIntervalEventData::ChangeMixCostParams { - mix_id: mix_id, + PendingIntervalEventKind::ChangeMixCostParams { + mix_id, new_costs: update.clone() }, - event.1 + event.1.kind ); // execute the event - test_helpers::execute_all_pending_events(deps.as_mut(), env); + test_helpers::execute_all_pending_events(deps.as_mut(), env.clone()); // and see if the config has actually been updated let mix = get_mixnode_details_by_id(deps.as_ref().storage, mix_id) @@ -602,12 +626,9 @@ pub mod tests { assert_eq!(mix.rewarding_details.cost_params, update); // but we cannot perform any updates whilst the mixnode is already unbonding - try_remove_mixnode(deps.as_mut(), info.clone()).unwrap(); - let res = try_update_mixnode_cost_params(deps.as_mut(), info, update); - assert_eq!( - res, - Err(MixnetContractError::MixnodeIsUnbonding { node_id: mix_id }) - ) + try_remove_mixnode(deps.as_mut(), env.clone(), info.clone()).unwrap(); + let res = try_update_mixnode_cost_params(deps.as_mut(), env, info, update); + assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id })) } #[test] diff --git a/contracts/mixnet/src/rewards/helpers.rs b/contracts/mixnet/src/rewards/helpers.rs index 43cbcfa1ae..02cdf83e68 100644 --- a/contracts/mixnet/src/rewards/helpers.rs +++ b/contracts/mixnet/src/rewards/helpers.rs @@ -57,7 +57,7 @@ pub(crate) fn withdraw_delegator_reward( delegation: Delegation, mut mix_rewarding: MixNodeRewarding, ) -> Result { - let mix_id = delegation.node_id; + let mix_id = delegation.mix_id; let mut updated_delegation = delegation.clone(); let reward = mix_rewarding.withdraw_delegator_reward(&mut updated_delegation)?; diff --git a/contracts/mixnet/src/rewards/queries.rs b/contracts/mixnet/src/rewards/queries.rs index af01a75bab..2b969c410b 100644 --- a/contracts/mixnet/src/rewards/queries.rs +++ b/contracts/mixnet/src/rewards/queries.rs @@ -13,7 +13,7 @@ use mixnet_contract_common::rewarding::helpers::truncate_reward; use mixnet_contract_common::rewarding::{ EstimatedCurrentEpochRewardResponse, PendingRewardResponse, }; -use mixnet_contract_common::{Delegation, NodeId}; +use mixnet_contract_common::{Delegation, MixId}; pub(crate) fn query_rewarding_params(deps: Deps<'_>) -> StdResult { storage::REWARDING_PARAMS.load(deps.storage) @@ -44,7 +44,7 @@ pub fn query_pending_operator_reward( pub fn query_pending_mixnode_operator_reward( deps: Deps, - mix_id: NodeId, + mix_id: MixId, ) -> StdResult { // in order to determine operator's reward we need to know its original pledge and thus // we have to load the entire thing @@ -55,7 +55,7 @@ pub fn query_pending_mixnode_operator_reward( pub fn query_pending_delegator_reward( deps: Deps, owner: String, - mix_id: NodeId, + mix_id: MixId, proxy: Option, ) -> StdResult { let owner_address = deps.api.addr_validate(&owner)?; @@ -103,7 +103,7 @@ fn zero_reward( pub(crate) fn query_estimated_current_epoch_operator_reward( deps: Deps<'_>, - mix_id: NodeId, + mix_id: MixId, estimated_performance: Performance, ) -> StdResult { let mix_details = match mixnodes::helpers::get_mixnode_details_by_id(deps.storage, mix_id)? { @@ -157,7 +157,7 @@ pub(crate) fn query_estimated_current_epoch_operator_reward( pub(crate) fn query_estimated_current_epoch_delegator_reward( deps: Deps<'_>, owner: String, - mix_id: NodeId, + mix_id: MixId, proxy: Option, estimated_performance: Performance, ) -> StdResult { @@ -345,7 +345,8 @@ mod tests { total_earned += dist.operator; let sender = mock_info(owner, &[]); - try_remove_mixnode(test.deps_mut(), sender).unwrap(); + let env = test.env(); + try_remove_mixnode(test.deps_mut(), env, sender).unwrap(); let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap(); @@ -371,7 +372,8 @@ mod tests { test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); let sender = mock_info(owner, &[]); - try_remove_mixnode(test.deps_mut(), sender).unwrap(); + let env = test.env(); + try_remove_mixnode(test.deps_mut(), env, sender).unwrap(); test.execute_all_pending_events(); let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); @@ -493,7 +495,8 @@ mod tests { total_earned += dist.delegates; let sender = mock_info("mix-owner", &[]); - try_remove_mixnode(test.deps_mut(), sender).unwrap(); + let env = test.env(); + try_remove_mixnode(test.deps_mut(), env, sender).unwrap(); let res = query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None).unwrap(); @@ -522,7 +525,8 @@ mod tests { total_earned += dist.delegates; let sender = mock_info("mix-owner", &[]); - try_remove_mixnode(test.deps_mut(), sender).unwrap(); + let env = test.env(); + try_remove_mixnode(test.deps_mut(), env, sender).unwrap(); test.execute_all_pending_events(); let res = @@ -620,7 +624,7 @@ mod tests { fn expected_current_operator( test: &TestSetup, - mix_id: NodeId, + mix_id: MixId, initial_stake: Uint128, ) -> EstimatedCurrentEpochRewardResponse { let mix_rewarding = test.mix_rewarding(mix_id); @@ -657,7 +661,8 @@ mod tests { test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); let sender = mock_info(owner, &[]); - try_remove_mixnode(test.deps_mut(), sender).unwrap(); + let env = test.env(); + try_remove_mixnode(test.deps_mut(), env, sender).unwrap(); let res = query_estimated_current_epoch_operator_reward( test.deps(), @@ -682,7 +687,8 @@ mod tests { test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); let sender = mock_info(owner, &[]); - try_remove_mixnode(test.deps_mut(), sender).unwrap(); + let env = test.env(); + try_remove_mixnode(test.deps_mut(), env, sender).unwrap(); test.execute_all_pending_events(); let res = query_estimated_current_epoch_operator_reward( @@ -779,7 +785,7 @@ mod tests { fn expected_current_delegator( test: &TestSetup, - mix_id: NodeId, + mix_id: MixId, owner: &str, ) -> EstimatedCurrentEpochRewardResponse { let mix_rewarding = test.mix_rewarding(mix_id); @@ -833,7 +839,8 @@ mod tests { test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); let sender = mock_info("mix-owner", &[]); - try_remove_mixnode(test.deps_mut(), sender).unwrap(); + let env = test.env(); + try_remove_mixnode(test.deps_mut(), env, sender).unwrap(); let res = query_estimated_current_epoch_delegator_reward( test.deps(), @@ -862,7 +869,8 @@ mod tests { test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); let sender = mock_info("mix-owner", &[]); - try_remove_mixnode(test.deps_mut(), sender).unwrap(); + let env = test.env(); + try_remove_mixnode(test.deps_mut(), env, sender).unwrap(); test.execute_all_pending_events(); let res = query_estimated_current_epoch_delegator_reward( diff --git a/contracts/mixnet/src/rewards/storage.rs b/contracts/mixnet/src/rewards/storage.rs index 09cbd57351..15ccd8e478 100644 --- a/contracts/mixnet/src/rewards/storage.rs +++ b/contracts/mixnet/src/rewards/storage.rs @@ -10,14 +10,14 @@ use cw_storage_plus::{Item, Map}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::MixNodeRewarding; use mixnet_contract_common::reward_params::RewardingParams; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; // current parameters used for rewarding purposes pub(crate) const REWARDING_PARAMS: Item<'_, RewardingParams> = Item::new(REWARDING_PARAMS_KEY); pub(crate) const PENDING_REWARD_POOL_CHANGE: Item<'_, RewardPoolChange> = Item::new(PENDING_REWARD_POOL_KEY); -pub const MIXNODE_REWARDING: Map = +pub const MIXNODE_REWARDING: Map = Map::new(MIXNODES_REWARDING_PK_NAMESPACE); pub fn reward_accounting( diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index 02bc1e1b82..b56686651f 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -22,18 +22,18 @@ use mixnet_contract_common::events::{ new_withdraw_delegator_reward_event, new_withdraw_operator_reward_event, new_zero_uptime_mix_operator_rewarding_event, }; -use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; +use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; use mixnet_contract_common::reward_params::{ IntervalRewardingParamsUpdate, NodeRewardParams, Performance, }; -use mixnet_contract_common::{Delegation, NodeId}; +use mixnet_contract_common::{Delegation, MixId}; use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; pub(crate) fn try_reward_mixnode( deps: DepsMut<'_>, env: Env, info: MessageInfo, - node_id: NodeId, + mix_id: MixId, node_performance: Performance, ) -> Result { ensure_is_authorized(info.sender, deps.storage)?; @@ -50,34 +50,34 @@ pub(crate) fn try_reward_mixnode( // there's a chance of this failing to load the details if the mixnode unbonded before rewards // were distributed and all of its delegators are also gone - let mut mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, node_id)? { + let mut mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { Some(mix_rewarding) if mix_rewarding.still_bonded() => mix_rewarding, // don't fail if the node has unbonded as we don't want to fail the underlying transaction _ => { - return Ok( - Response::new().add_event(new_not_found_mix_operator_rewarding_event( - interval, node_id, - )), - ); + return Ok(Response::new() + .add_event(new_not_found_mix_operator_rewarding_event(interval, mix_id))); } }; + let prior_delegates = mix_rewarding.delegates; + let prior_unit_reward = mix_rewarding.full_reward_ratio(); + // check if this node has already been rewarded for the current epoch. // unlike the previous check, this one should be a hard error since this cannot be // influenced by users actions let absolute_epoch_id = interval.current_epoch_absolute_id(); if absolute_epoch_id == mix_rewarding.last_rewarded_epoch { return Err(MixnetContractError::MixnodeAlreadyRewarded { - node_id, + mix_id, absolute_epoch_id, }); } // again a hard error since the rewarding validator should have known not to reward this node let node_status = interval_storage::REWARDED_SET - .load(deps.storage, node_id) + .load(deps.storage, mix_id) .map_err(|_| MixnetContractError::MixnodeNotInRewardedSet { - node_id, + mix_id, absolute_epoch_id, })?; @@ -85,10 +85,10 @@ pub(crate) fn try_reward_mixnode( // however, we still need to update last_rewarded_epoch field if node_performance.is_zero() { mix_rewarding.last_rewarded_epoch = absolute_epoch_id; - storage::MIXNODE_REWARDING.save(deps.storage, node_id, &mix_rewarding)?; + storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; return Ok( Response::new().add_event(new_zero_uptime_mix_operator_rewarding_event( - interval, node_id, + interval, mix_id, )), ); } @@ -106,13 +106,15 @@ pub(crate) fn try_reward_mixnode( mix_rewarding.distribute_rewards(reward_distribution, absolute_epoch_id); // persist changes happened to the storage - storage::MIXNODE_REWARDING.save(deps.storage, node_id, &mix_rewarding)?; + storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; storage::reward_accounting(deps.storage, node_reward)?; Ok(Response::new().add_event(new_mix_rewarding_event( interval, - node_id, + mix_id, reward_distribution, + prior_delegates, + prior_unit_reward, ))) } @@ -182,7 +184,7 @@ pub(crate) fn _try_withdraw_operator_reward( pub(crate) fn try_withdraw_delegator_reward( deps: DepsMut<'_>, info: MessageInfo, - mix_id: NodeId, + mix_id: MixId, ) -> Result { _try_withdraw_delegator_reward(deps, mix_id, info.sender, None) } @@ -190,7 +192,7 @@ pub(crate) fn try_withdraw_delegator_reward( pub(crate) fn try_withdraw_delegator_reward_on_behalf( deps: DepsMut<'_>, info: MessageInfo, - mix_id: NodeId, + mix_id: MixId, owner: String, ) -> Result { let proxy = info.sender; @@ -200,7 +202,7 @@ pub(crate) fn try_withdraw_delegator_reward_on_behalf( pub(crate) fn _try_withdraw_delegator_reward( deps: DepsMut<'_>, - mix_id: NodeId, + mix_id: MixId, owner: Addr, proxy: Option, ) -> Result { @@ -228,9 +230,9 @@ pub(crate) fn _try_withdraw_delegator_reward( // (in that case the expected path of getting your tokens back is via undelegation) match mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)? { Some(mix_bond) if mix_bond.is_unbonding => { - return Err(MixnetContractError::MixnodeIsUnbonding { node_id: mix_id }) + return Err(MixnetContractError::MixnodeIsUnbonding { mix_id }) } - None => return Err(MixnetContractError::MixnodeHasUnbonded { node_id: mix_id }), + None => return Err(MixnetContractError::MixnodeHasUnbonded { mix_id }), _ => (), }; @@ -286,13 +288,16 @@ pub(crate) fn try_update_active_set_size( if force_immediately || interval.is_current_epoch_over(&env) { rewarding_params.try_change_active_set_size(active_set_size)?; storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; - Ok(Response::new().add_event(new_active_set_update_event(active_set_size))) + Ok(Response::new().add_event(new_active_set_update_event( + env.block.height, + active_set_size, + ))) } else { // push the epoch event - let epoch_event = PendingEpochEventData::UpdateActiveSetSize { + let epoch_event = PendingEpochEventKind::UpdateActiveSetSize { new_size: active_set_size, }; - push_new_epoch_event(deps.storage, &epoch_event)?; + push_new_epoch_event(deps.storage, &env, epoch_event)?; let time_left = interval.secs_until_current_interval_end(&env); Ok( Response::new().add_event(new_pending_active_set_update_event( @@ -322,15 +327,16 @@ pub(crate) fn try_update_rewarding_params( rewarding_params.try_apply_updates(updated_params, interval.epochs_in_interval())?; storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; Ok(Response::new().add_event(new_rewarding_params_update_event( + env.block.height, updated_params, rewarding_params.interval, ))) } else { // push the interval event - let interval_event = PendingIntervalEventData::UpdateRewardingParams { + let interval_event = PendingIntervalEventKind::UpdateRewardingParams { update: updated_params, }; - push_new_interval_event(deps.storage, &interval_event)?; + push_new_interval_event(deps.storage, &env, interval_event)?; let time_left = interval.secs_until_current_interval_end(&env); Ok( Response::new().add_event(new_pending_rewarding_params_update_event( @@ -356,8 +362,10 @@ pub mod tests { use cosmwasm_std::{Decimal, Uint128}; use mixnet_contract_common::events::{ MixnetEventType, BOND_NOT_FOUND_VALUE, DELEGATES_REWARD_KEY, NO_REWARD_REASON_KEY, - OPERATOR_REWARD_KEY, ZERO_PERFORMANCE_VALUE, + OPERATOR_REWARD_KEY, PRIOR_DELEGATES_KEY, PRIOR_UNIT_REWARD_KEY, + ZERO_PERFORMANCE_VALUE, }; + use mixnet_contract_common::helpers::compare_decimals; use mixnet_contract_common::RewardedSetNodeStatus; #[test] @@ -415,9 +423,9 @@ pub mod tests { &rewarding_details, ) .unwrap(); - pending_events::unbond_mixnode(test.deps_mut(), &env, mix_id_unbonded).unwrap(); + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded).unwrap(); - pending_events::unbond_mixnode(test.deps_mut(), &env, mix_id_unbonded_leftover) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) .unwrap(); let env = test.env(); @@ -523,7 +531,7 @@ pub mod tests { assert!(res_standby.is_ok()); assert!(matches!( res_inactive, - Err(MixnetContractError::MixnodeNotInRewardedSet { node_id, .. }) if node_id == inactive_mix_id + Err(MixnetContractError::MixnodeNotInRewardedSet { mix_id, .. }) if mix_id == inactive_mix_id )); } @@ -552,7 +560,7 @@ pub mod tests { let res = try_reward_mixnode(test.deps_mut(), env, sender.clone(), mix_id, performance); assert!(matches!( res, - Err(MixnetContractError::MixnodeAlreadyRewarded { node_id, .. }) if node_id == mix_id + Err(MixnetContractError::MixnodeAlreadyRewarded { mix_id, .. }) if mix_id == mix_id )); // in the following epoch we're good again @@ -601,7 +609,7 @@ pub mod tests { ); assert!(matches!( res, - Err(MixnetContractError::MixnodeAlreadyRewarded { node_id, .. }) if node_id == mix_id + Err(MixnetContractError::MixnodeAlreadyRewarded { mix_id, .. }) if mix_id == mix_id )); // but in the next epoch, as always, we're good again @@ -639,11 +647,11 @@ pub mod tests { let env = test.env(); let sender = test.rewarding_validator(); - test.add_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); - test.add_delegation("delegator1", Uint128::new(100_000_000), mix_id3); - test.add_delegation("delegator2", Uint128::new(123_456_000), mix_id3); - test.add_delegation("delegator3", Uint128::new(9_100_000_000), mix_id3); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id3); + test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id3); + test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), mix_id3); let change = storage::PENDING_REWARD_POOL_CHANGE .load(test.deps().storage) @@ -721,11 +729,11 @@ pub mod tests { test.update_rewarded_set(vec![mix_id1, mix_id2, mix_id3]); let performance = test_helpers::performance(98.0); - test.add_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); - test.add_delegation("delegator1", Uint128::new(100_000_000), mix_id3); - test.add_delegation("delegator2", Uint128::new(123_456_000), mix_id3); - test.add_delegation("delegator3", Uint128::new(9_100_000_000), mix_id3); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id3); + test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id3); + test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), mix_id3); // repeat the rewarding the same set of delegates for few epochs for _ in 0..10 { @@ -736,7 +744,7 @@ pub mod tests { performance, in_active_set: true, }; - let sim_res = sim.simulate_epoch(node_params); + let sim_res = sim.simulate_epoch_single_node(node_params); assert_eq!(sim_res, dist); } test.skip_to_next_epoch_end(); @@ -745,11 +753,11 @@ pub mod tests { // add few more delegations and repeat it // (note: we're not concerned about whether particular delegation owner got the correct amount, // this is checked in other unit tests) - test.add_delegation("delegator1", Uint128::new(50_000_000), mix_id1); - test.add_delegation("delegator1", Uint128::new(200_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(50_000_000), mix_id1); + test.add_immediate_delegation("delegator1", Uint128::new(200_000_000), mix_id2); - test.add_delegation("delegator5", Uint128::new(123_000_000), mix_id3); - test.add_delegation("delegator6", Uint128::new(456_000_000), mix_id3); + test.add_immediate_delegation("delegator5", Uint128::new(123_000_000), mix_id3); + test.add_immediate_delegation("delegator6", Uint128::new(456_000_000), mix_id3); let performance = test_helpers::performance(12.3); for _ in 0..10 { @@ -760,16 +768,332 @@ pub mod tests { performance, in_active_set: true, }; - let sim_res = sim.simulate_epoch(node_params); + let sim_res = sim.simulate_epoch_single_node(node_params); assert_eq!(sim_res, dist); } test.skip_to_next_epoch_end(); } } + + #[test] + fn emitted_event_attributes_allow_for_delegator_reward_recomputation() { + let operator1 = Uint128::new(1_000_000_000); + let operator2 = Uint128::new(12_345_000_000); + + let mut test = TestSetup::new(); + let sender = test.rewarding_validator(); + + let mix_id1 = test.add_dummy_mixnode("mix-owner1", Some(operator1)); + let mix_id2 = test.add_dummy_mixnode("mix-owner2", Some(operator2)); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id1, mix_id2]); + let performance = test_helpers::performance(98.0); + + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id1); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + + test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id1); + + let del11 = test.delegation(mix_id1, "delegator1", &None); + let del12 = test.delegation(mix_id1, "delegator2", &None); + let del21 = test.delegation(mix_id2, "delegator1", &None); + + for _ in 0..10 { + // we know from the previous tests that actual rewarding distribution matches the simulator + let mut sim1 = test.instantiate_simulator(mix_id1); + let mut sim2 = test.instantiate_simulator(mix_id2); + + let node_params = NodeRewardParams { + performance, + in_active_set: true, + }; + + let dist1 = sim1.simulate_epoch_single_node(node_params); + let dist2 = sim2.simulate_epoch_single_node(node_params); + + let env = test.env(); + + let actual_prior1 = test.mix_rewarding(mix_id1); + let actual_prior2 = test.mix_rewarding(mix_id2); + + let res1 = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id1, + performance, + ) + .unwrap(); + + let prior_delegates1: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + PRIOR_DELEGATES_KEY, + &res1, + ) + .parse() + .unwrap(); + assert_eq!(prior_delegates1, actual_prior1.delegates); + + let delegates_reward1: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + DELEGATES_REWARD_KEY, + &res1, + ) + .parse() + .unwrap(); + assert_eq!(delegates_reward1, dist1.delegates); + + let prior_unit_reward: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + PRIOR_UNIT_REWARD_KEY, + &res1, + ) + .parse() + .unwrap(); + assert_eq!(actual_prior1.full_reward_ratio(), prior_unit_reward); + + // either use the constant for (which for now is the same for all nodes) + // or query the contract for per-node value + let unit_delegation_base = actual_prior1.unit_delegation; + + // recompute the state of fully compounded delegation from before this rewarding was distributed + let pre_rewarding_del11 = del11.dec_amount() + + (prior_unit_reward - del11.cumulative_reward_ratio) * del11.dec_amount() + / (del11.cumulative_reward_ratio + unit_delegation_base); + + let computed_del11_reward = + pre_rewarding_del11 / prior_delegates1 * delegates_reward1; + + let pre_rewarding_del12 = del12.dec_amount() + + (prior_unit_reward - del12.cumulative_reward_ratio) * del12.dec_amount() + / (del12.cumulative_reward_ratio + unit_delegation_base); + + let computed_del12_reward = + pre_rewarding_del12 / prior_delegates1 * delegates_reward1; + + // sanity check + compare_decimals( + computed_del11_reward + computed_del12_reward, + delegates_reward1, + None, + ); + + let res2 = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id2, + performance, + ) + .unwrap(); + + let prior_delegates2: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + PRIOR_DELEGATES_KEY, + &res2, + ) + .parse() + .unwrap(); + assert_eq!(prior_delegates2, actual_prior2.delegates); + + let delegates_reward2: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + DELEGATES_REWARD_KEY, + &res2, + ) + .parse() + .unwrap(); + assert_eq!(delegates_reward2, dist2.delegates); + + let prior_unit_reward: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + PRIOR_UNIT_REWARD_KEY, + &res2, + ) + .parse() + .unwrap(); + assert_eq!(actual_prior2.full_reward_ratio(), prior_unit_reward); + + // either use the constant for (which for now is the same for all nodes) + // or query the contract for per-node value + let unit_delegation_base = actual_prior2.unit_delegation; + + // recompute the state of fully compounded delegation from before this rewarding was distributed + let pre_rewarding_del21 = del21.dec_amount() + + (prior_unit_reward - del21.cumulative_reward_ratio) * del21.dec_amount() + / (del21.cumulative_reward_ratio + unit_delegation_base); + + let computed_del21_reward = + pre_rewarding_del21 / prior_delegates2 * delegates_reward2; + + assert_eq!(dist2.delegates, computed_del21_reward); + + test.skip_to_next_epoch_end(); + } + + // add more delegations and check few more epochs (so that the delegations would start from non-default unit delegation value) + test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), mix_id1); + test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), mix_id2); + + let del13 = test.delegation(mix_id1, "delegator3", &None); + let del23 = test.delegation(mix_id2, "delegator3", &None); + + for _ in 0..10 { + // we know from the previous tests that actual rewarding distribution matches the simulator + let mut sim1 = test.instantiate_simulator(mix_id1); + let mut sim2 = test.instantiate_simulator(mix_id2); + + let node_params = NodeRewardParams { + performance, + in_active_set: true, + }; + + let dist1 = sim1.simulate_epoch_single_node(node_params); + let dist2 = sim2.simulate_epoch_single_node(node_params); + + let env = test.env(); + + let actual_prior1 = test.mix_rewarding(mix_id1); + let actual_prior2 = test.mix_rewarding(mix_id2); + + let res1 = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id1, + performance, + ) + .unwrap(); + + let prior_delegates1: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + PRIOR_DELEGATES_KEY, + &res1, + ) + .parse() + .unwrap(); + assert_eq!(prior_delegates1, actual_prior1.delegates); + + let delegates_reward1: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + DELEGATES_REWARD_KEY, + &res1, + ) + .parse() + .unwrap(); + assert_eq!(delegates_reward1, dist1.delegates); + + let prior_unit_reward: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + PRIOR_UNIT_REWARD_KEY, + &res1, + ) + .parse() + .unwrap(); + assert_eq!(actual_prior1.full_reward_ratio(), prior_unit_reward); + + // either use the constant for (which for now is the same for all nodes) + // or query the contract for per-node value + let unit_delegation_base = actual_prior1.unit_delegation; + + // recompute the state of fully compounded delegation from before this rewarding was distributed + let pre_rewarding_del11 = del11.dec_amount() + + (prior_unit_reward - del11.cumulative_reward_ratio) * del11.dec_amount() + / (del11.cumulative_reward_ratio + unit_delegation_base); + + let computed_del11_reward = + pre_rewarding_del11 / prior_delegates1 * delegates_reward1; + + let pre_rewarding_del12 = del12.dec_amount() + + (prior_unit_reward - del12.cumulative_reward_ratio) * del12.dec_amount() + / (del12.cumulative_reward_ratio + unit_delegation_base); + + let computed_del12_reward = + pre_rewarding_del12 / prior_delegates1 * delegates_reward1; + + let pre_rewarding_del13 = del13.dec_amount() + + (prior_unit_reward - del13.cumulative_reward_ratio) * del13.dec_amount() + / (del13.cumulative_reward_ratio + unit_delegation_base); + + let computed_del13_reward = + pre_rewarding_del13 / prior_delegates1 * delegates_reward1; + + // sanity check + compare_decimals( + computed_del11_reward + computed_del12_reward + computed_del13_reward, + delegates_reward1, + None, + ); + + let res2 = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id2, + performance, + ) + .unwrap(); + + let prior_delegates2: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + PRIOR_DELEGATES_KEY, + &res2, + ) + .parse() + .unwrap(); + assert_eq!(prior_delegates2, actual_prior2.delegates); + + let delegates_reward2: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + DELEGATES_REWARD_KEY, + &res2, + ) + .parse() + .unwrap(); + assert_eq!(delegates_reward2, dist2.delegates); + + let prior_unit_reward: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + PRIOR_UNIT_REWARD_KEY, + &res2, + ) + .parse() + .unwrap(); + assert_eq!(actual_prior2.full_reward_ratio(), prior_unit_reward); + + // either use the constant for (which for now is the same for all nodes) + // or query the contract for per-node value + let unit_delegation_base = actual_prior2.unit_delegation; + + // recompute the state of fully compounded delegation from before this rewarding was distributed + let pre_rewarding_del21 = del21.dec_amount() + + (prior_unit_reward - del21.cumulative_reward_ratio) * del21.dec_amount() + / (del21.cumulative_reward_ratio + unit_delegation_base); + + let computed_del21_reward = + pre_rewarding_del21 / prior_delegates2 * delegates_reward2; + + let pre_rewarding_del23 = del23.dec_amount() + + (prior_unit_reward - del23.cumulative_reward_ratio) * del23.dec_amount() + / (del23.cumulative_reward_ratio + unit_delegation_base); + + let computed_del23_reward = + pre_rewarding_del23 / prior_delegates2 * delegates_reward2; + + compare_decimals( + computed_del21_reward + computed_del23_reward, + delegates_reward2, + None, + ); + + test.skip_to_next_epoch_end(); + } + } } #[cfg(test)] - mod withdrawing_operator_reward { + mod withdrawing_delegator_reward { use super::*; use crate::interval::pending_events; use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup}; @@ -904,7 +1228,7 @@ pub mod tests { .unwrap(); let env = test.env(); - pending_events::unbond_mixnode(test.deps_mut(), &env, mix_id_unbonded_leftover) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) .unwrap(); let res = @@ -912,7 +1236,7 @@ pub mod tests { assert_eq!( res, Err(MixnetContractError::MixnodeIsUnbonding { - node_id: mix_id_unbonding + mix_id: mix_id_unbonding }) ); @@ -921,7 +1245,7 @@ pub mod tests { assert_eq!( res, Err(MixnetContractError::MixnodeHasUnbonded { - node_id: mix_id_unbonded_leftover + mix_id: mix_id_unbonded_leftover }) ); } @@ -1093,7 +1417,7 @@ pub mod tests { } #[cfg(test)] - mod withdrawing_delegator_reward { + mod withdrawing_operator_reward { use super::*; use crate::interval::pending_events; use crate::support::tests::test_helpers::TestSetup; @@ -1186,14 +1510,14 @@ pub mod tests { .unwrap(); let env = test.env(); - pending_events::unbond_mixnode(test.deps_mut(), &env, mix_id_unbonded_leftover) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) .unwrap(); let res = try_withdraw_operator_reward(test.deps_mut(), sender1); assert_eq!( res, Err(MixnetContractError::MixnodeIsUnbonding { - node_id: mix_id_unbonding + mix_id: mix_id_unbonding }) ); @@ -1356,7 +1680,7 @@ pub mod tests { // make sure it's actually saved to pending events let events = test.pending_epoch_events(); assert!( - matches!(events[0], PendingEpochEventData::UpdateActiveSetSize { new_size } if new_size == 42) + matches!(events[0].kind, PendingEpochEventKind::UpdateActiveSetSize { new_size } if new_size == 42) ); test.execute_all_pending_events(); @@ -1511,7 +1835,7 @@ pub mod tests { // make sure it's actually saved to pending events let events = test.pending_interval_events(); assert!( - matches!(events[0],PendingIntervalEventData::UpdateRewardingParams { update } if update.rewarded_set_size == Some(123)) + matches!(events[0].kind,PendingIntervalEventKind::UpdateRewardingParams { update } if update.rewarded_set_size == Some(123)) ); test.execute_all_pending_events(); diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index 27d9202713..b4ed7f3fd2 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -152,7 +152,9 @@ pub(crate) fn ensure_proxy_match( pub(crate) fn ensure_bonded(bond: &MixNodeBond) -> Result<(), MixnetContractError> { if bond.is_unbonding { - return Err(MixnetContractError::MixnodeIsUnbonding { node_id: bond.id }); + return Err(MixnetContractError::MixnodeIsUnbonding { + mix_id: bond.mix_id, + }); } Ok(()) } diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 8e0fba5ed6..07b89671cd 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -49,10 +49,11 @@ pub mod test_helpers { use mixnet_contract_common::mixnode::{MixNodeRewarding, UnbondedMixnode}; use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; use mixnet_contract_common::reward_params::{Performance, RewardingParams}; + use mixnet_contract_common::rewarding::simulator::simulated_node::SimulatedNode; use mixnet_contract_common::rewarding::simulator::Simulator; use mixnet_contract_common::rewarding::RewardDistribution; use mixnet_contract_common::{ - Delegation, Gateway, InitialRewardingParams, InstantiateMsg, Interval, MixNode, NodeId, + Delegation, Gateway, InitialRewardingParams, InstantiateMsg, Interval, MixId, MixNode, Percent, RewardedSetNodeStatus, }; use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng}; @@ -141,14 +142,14 @@ pub mod test_helpers { interval_storage::current_interval(self.deps().storage).unwrap() } - pub fn rewarded_set(&self) -> Vec<(NodeId, RewardedSetNodeStatus)> { + pub fn rewarded_set(&self) -> Vec<(MixId, RewardedSetNodeStatus)> { interval_storage::REWARDED_SET .range(self.deps().storage, None, None, Order::Ascending) .map(|res| res.unwrap()) .collect::>() } - pub fn add_dummy_mixnode(&mut self, owner: &str, stake: Option) -> NodeId { + pub fn add_dummy_mixnode(&mut self, owner: &str, stake: Option) -> MixId { let stake = match stake { Some(amount) => { let denom = rewarding_denom(self.deps().storage).unwrap(); @@ -166,7 +167,7 @@ pub mod test_helpers { owner: &str, stake: Option, proxy: Addr, - ) -> NodeId { + ) -> MixId { let stake = match stake { Some(amount) => { let denom = rewarding_denom(self.deps().storage).unwrap(); @@ -210,25 +211,31 @@ pub mod test_helpers { current_id_counter + 1 } - pub fn start_unbonding_mixnode(&mut self, mix_id: NodeId) { + pub fn start_unbonding_mixnode(&mut self, mix_id: MixId) { let bond_details = mixnodes_storage::mixnode_bonds() .load(self.deps().storage, mix_id) .unwrap(); - try_remove_mixnode(self.deps_mut(), mock_info(bond_details.owner.as_str(), &[])) - .unwrap(); + let env = self.env(); + try_remove_mixnode( + self.deps_mut(), + env, + mock_info(bond_details.owner.as_str(), &[]), + ) + .unwrap(); } - pub fn immediately_unbond_mixnode(&mut self, mix_id: NodeId) { + pub fn immediately_unbond_mixnode(&mut self, mix_id: MixId) { let env = self.env(); - pending_events::unbond_mixnode(self.deps_mut(), &env, mix_id).unwrap(); + pending_events::unbond_mixnode(self.deps_mut(), &env, env.block.height, mix_id) + .unwrap(); } pub fn add_immediate_delegation( &mut self, delegator: &str, amount: impl Into, - target: NodeId, + target: MixId, ) { let denom = rewarding_denom(self.deps().storage).unwrap(); let amount = Coin { @@ -239,6 +246,7 @@ pub mod test_helpers { pending_events::delegate( self.deps_mut(), &env, + env.block.height, Addr::unchecked(delegator), target, amount, @@ -251,7 +259,7 @@ pub mod test_helpers { &mut self, delegator: &str, amount: impl Into, - target: NodeId, + target: MixId, proxy: Addr, ) { let denom = rewarding_denom(self.deps().storage).unwrap(); @@ -263,6 +271,7 @@ pub mod test_helpers { pending_events::delegate( self.deps_mut(), &env, + env.block.height, Addr::unchecked(delegator), target, amount, @@ -271,23 +280,32 @@ pub mod test_helpers { .unwrap(); } + #[allow(unused)] pub fn add_delegation( &mut self, delegator: &str, amount: impl Into, - target: NodeId, + target: MixId, ) { let denom = rewarding_denom(self.deps().storage).unwrap(); let amount = Coin { denom, amount: amount.into(), }; - delegate(self.deps_mut(), delegator, vec![amount], target) + let env = self.env(); + delegate(self.deps_mut(), env, delegator, vec![amount], target) } - pub fn remove_immediate_delegation(&mut self, delegator: &str, target: NodeId) { - pending_events::undelegate(self.deps_mut(), Addr::unchecked(delegator), target, None) - .unwrap(); + pub fn remove_immediate_delegation(&mut self, delegator: &str, target: MixId) { + let height = self.env.block.height; + pending_events::undelegate( + self.deps_mut(), + height, + Addr::unchecked(delegator), + target, + None, + ) + .unwrap(); } pub fn skip_to_next_epoch_end(&mut self) { @@ -326,7 +344,7 @@ pub mod test_helpers { interval_storage::save_interval(self.deps_mut().storage, &advanced).unwrap() } - pub fn update_rewarded_set(&mut self, nodes: Vec) { + pub fn update_rewarded_set(&mut self, nodes: Vec) { let active_set_size = rewards_storage::REWARDING_PARAMS .load(self.deps().storage) .unwrap() @@ -335,8 +353,8 @@ pub mod test_helpers { .unwrap(); } - pub fn instantiate_simulator(&self, node: NodeId) -> Simulator { - simulator_from_state(self.deps(), node) + pub fn instantiate_simulator(&self, node: MixId) -> Simulator { + simulator_from_single_node_state(self.deps(), node) } pub fn execute_all_pending_events(&mut self) { @@ -360,7 +378,7 @@ pub mod test_helpers { pub fn reward_with_distribution( &mut self, - mix_id: NodeId, + mix_id: MixId, performance: Performance, ) -> RewardDistribution { let env = self.env(); @@ -391,7 +409,7 @@ pub mod test_helpers { pub fn read_delegation( &mut self, - mix: NodeId, + mix: MixId, owner: &str, proxy: Option<&str>, ) -> Delegation { @@ -404,18 +422,18 @@ pub mod test_helpers { .unwrap() } - pub fn mix_rewarding(&self, node: NodeId) -> MixNodeRewarding { + pub fn mix_rewarding(&self, node: MixId) -> MixNodeRewarding { rewards_storage::MIXNODE_REWARDING .load(self.deps().storage, node) .unwrap() } - pub fn delegation(&self, mix: NodeId, owner: &str, proxy: &Option) -> Delegation { + pub fn delegation(&self, mix: MixId, owner: &str, proxy: &Option) -> Delegation { read_delegation(self.deps().storage, mix, &Addr::unchecked(owner), proxy).unwrap() } } - pub fn simulator_from_state(deps: Deps<'_>, node: NodeId) -> Simulator { + pub fn simulator_from_single_node_state(deps: Deps<'_>, node: MixId) -> Simulator { let mix_rewarding = rewards_storage::MIXNODE_REWARDING .load(deps.storage, node) .unwrap(); @@ -431,12 +449,21 @@ pub mod test_helpers { .load(deps.storage) .unwrap(); let interval = interval_storage::current_interval(deps.storage).unwrap(); - Simulator { - node_rewarding_details: mix_rewarding, - node_delegations: delegations.delegations, - system_rewarding_params: rewarding_params, - interval, - } + let mut simulator = Simulator::new(rewarding_params, interval); + simulator.nodes.insert( + 0, + SimulatedNode { + mix_id: 0, + rewarding_details: mix_rewarding, + delegations: delegations + .delegations + .into_iter() + .map(|d| (d.owner.to_string(), d)) + .collect(), + }, + ); + + simulator } pub fn get_bank_send_msg(response: &Response) -> Option<(String, Vec)> { @@ -448,11 +475,12 @@ pub mod test_helpers { None } - pub fn find_attribute( - event_type: Option, + pub fn find_attribute>( + event_type: Option, attribute: &str, response: &Response, ) -> String { + let event_type = event_type.map(Into::into); for event in &response.events { if let Some(typ) = &event_type { if &event.ty != typ { @@ -539,11 +567,12 @@ pub mod test_helpers { ) } - pub fn add_dummy_delegations(mut deps: DepsMut<'_>, env: Env, mix_id: NodeId, n: usize) { + pub fn add_dummy_delegations(mut deps: DepsMut<'_>, env: Env, mix_id: MixId, n: usize) { for i in 0..n { pending_events::delegate( deps.branch(), &env, + env.block.height, Addr::unchecked(&format!("owner{}", i)), mix_id, tests::fixtures::good_mixnode_pledge().pop().unwrap(), @@ -630,7 +659,7 @@ pub mod test_helpers { deps: DepsMut<'_>, identity_key: Option<&str>, owner: &str, - ) -> NodeId { + ) -> MixId { let id = loop { let candidate = rng.next_u32(); if !mixnodes_storage::unbonded_mixnodes().has(deps.storage, candidate) { @@ -668,7 +697,7 @@ pub mod test_helpers { env: Env, sender: &str, stake: Vec, - ) -> NodeId { + ) -> MixId { let keypair = crypto::asymmetric::identity::KeyPair::new(&mut rng); let owner_signature = keypair .private_key() @@ -763,14 +792,14 @@ pub mod test_helpers { deps } - pub fn delegate(deps: DepsMut<'_>, sender: &str, stake: Vec, mix_id: NodeId) { + pub fn delegate(deps: DepsMut<'_>, env: Env, sender: &str, stake: Vec, mix_id: MixId) { let info = mock_info(sender, &stake); - try_delegate_to_mixnode(deps, info, mix_id).unwrap(); + try_delegate_to_mixnode(deps, env, info, mix_id).unwrap(); } pub(crate) fn read_delegation( storage: &dyn Storage, - mix: NodeId, + mix: MixId, owner: &Addr, proxy: &Option, ) -> Option { diff --git a/contracts/mixnet/src/testing/mod.rs b/contracts/mixnet/src/testing/mod.rs new file mode 100644 index 0000000000..30de4b3136 --- /dev/null +++ b/contracts/mixnet/src/testing/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod transactions; diff --git a/contracts/mixnet/src/testing/transactions.rs b/contracts/mixnet/src/testing/transactions.rs new file mode 100644 index 0000000000..899481b3ca --- /dev/null +++ b/contracts/mixnet/src/testing/transactions.rs @@ -0,0 +1,43 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::interval::transactions::{ + perform_pending_epoch_actions, perform_pending_interval_actions, +}; +use cosmwasm_std::{DepsMut, Env, Response}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::events::{ + new_pending_epoch_events_execution_event, new_pending_interval_events_execution_event, + new_reconcile_pending_events, +}; + +pub fn try_resolve_all_pending_events( + mut deps: DepsMut<'_>, + env: Env, + mut limit: Option, +) -> Result { + let mut response = Response::new().add_event(new_reconcile_pending_events()); + + // epoch events + let (mut sub_response, executed) = perform_pending_epoch_actions(deps.branch(), &env, limit)?; + response.messages.append(&mut sub_response.messages); + response.attributes.append(&mut sub_response.attributes); + response.events.append(&mut sub_response.events); + response + .events + .push(new_pending_epoch_events_execution_event(executed)); + + limit = limit.map(|l| l - executed); + + // interval events + let (mut sub_response, executed) = + perform_pending_interval_actions(deps.branch(), &env, limit)?; + response.messages.append(&mut sub_response.messages); + response.attributes.append(&mut sub_response.attributes); + response.events.append(&mut sub_response.events); + response + .events + .push(new_pending_interval_events_execution_event(executed)); + + Ok(response) +} diff --git a/contracts/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index 3c03d92c91..f55bd649f1 100644 --- a/contracts/vesting/Cargo.toml +++ b/contracts/vesting/Cargo.toml @@ -15,6 +15,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } +contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" } cosmwasm-std = { version = "1.0.0 "} diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index 923c403733..9d1cee6711 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -1,8 +1,8 @@ use crate::errors::ContractError; use crate::queued_migrations::migrate_to_v2_mixnet_contract; use crate::storage::{ - account_from_address, locked_pledge_cap, update_locked_pledge_cap, BlockTimestampSecs, ADMIN, - DELEGATIONS, MIXNET_CONTRACT_ADDRESS, MIX_DENOM, + account_from_address, BlockTimestampSecs, ADMIN, DELEGATIONS, MIXNET_CONTRACT_ADDRESS, + MIX_DENOM, }; use crate::traits::{ DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, VestingAccount, @@ -14,7 +14,7 @@ use cosmwasm_std::{ }; use cw_storage_plus::Bound; use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use mixnet_contract_common::{Gateway, MixNode, NodeId}; +use mixnet_contract_common::{Gateway, MixId, MixNode}; use vesting_contract_common::events::{ new_ownership_transfer_event, new_periodic_vesting_account_event, new_staking_address_update_event, new_track_gateway_unbond_event, @@ -25,8 +25,8 @@ use vesting_contract_common::messages::{ ExecuteMsg, InitMsg, MigrateMsg, QueryMsg, VestingSpecification, }; use vesting_contract_common::{ - AllDelegationsResponse, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeData, - VestingDelegation, + AllDelegationsResponse, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeCap, + PledgeData, VestingDelegation, }; pub const INITIAL_LOCKED_PLEDGE_CAP: Uint128 = Uint128::new(100_000_000_000); @@ -59,8 +59,8 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::UpdateLockedPledgeCap { amount } => { - try_update_locked_pledge_cap(amount, info, deps) + ExecuteMsg::UpdateLockedPledgeCap { address, cap } => { + try_update_locked_pledge_cap(address, cap, info, deps) } ExecuteMsg::TrackReward { amount, address } => { try_track_reward(deps, info, amount, &address) @@ -88,10 +88,12 @@ pub fn execute( owner_address, staking_address, vesting_spec, + cap, } => try_create_periodic_vesting_account( &owner_address, staking_address, vesting_spec, + cap, info, env, deps, @@ -144,14 +146,18 @@ pub fn execute( /// /// Callable by ADMIN only, see [instantiate]. pub fn try_update_locked_pledge_cap( - amount: Uint128, + address: String, + cap: PledgeCap, info: MessageInfo, deps: DepsMut, ) -> Result { if info.sender != ADMIN.load(deps.storage)? { return Err(ContractError::NotAdmin(info.sender.as_str().to_string())); } - update_locked_pledge_cap(amount, deps.storage)?; + let mut account = account_from_address(&address, deps.storage, deps.api)?; + + account.pledge_cap = Some(cap); + // update_locked_pledge_cap(amount, deps.storage)?; Ok(Response::default()) } @@ -363,7 +369,7 @@ fn try_track_reward( /// Track undelegation, invoked by the mixnet contract after sucessful undelegation, message contains coins returned with any accrued rewards. fn try_track_undelegation( address: &str, - mix_id: NodeId, + mix_id: MixId, amount: Coin, info: MessageInfo, deps: DepsMut<'_>, @@ -379,7 +385,7 @@ fn try_track_undelegation( /// Delegate to mixnode, sends [mixnet_contract_common::ExecuteMsg::DelegateToMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].. fn try_delegate_to_mixnode( - mix_id: NodeId, + mix_id: MixId, amount: Coin, info: MessageInfo, env: Env, @@ -405,7 +411,7 @@ fn try_claim_operator_reward( fn try_claim_delegator_reward( deps: DepsMut<'_>, info: MessageInfo, - mix_id: NodeId, + mix_id: MixId, ) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; @@ -414,7 +420,7 @@ fn try_claim_delegator_reward( /// Undelegates from a mixnode, sends [mixnet_contract_common::ExecuteMsg::UndelegateFromMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. fn try_undelegate_from_mixnode( - mix_id: NodeId, + mix_id: MixId, info: MessageInfo, deps: DepsMut<'_>, ) -> Result { @@ -430,6 +436,7 @@ fn try_create_periodic_vesting_account( owner_address: &str, staking_address: Option, vesting_spec: Option, + cap: Option, info: MessageInfo, env: Env, deps: DepsMut<'_>, @@ -437,6 +444,7 @@ fn try_create_periodic_vesting_account( if info.sender != ADMIN.load(deps.storage)? { return Err(ContractError::NotAdmin(info.sender.as_str().to_string())); } + let mix_denom = MIX_DENOM.load(deps.storage)?; let account_exists = account_from_address(owner_address, deps.storage, deps.api).is_ok(); @@ -452,6 +460,11 @@ fn try_create_periodic_vesting_account( let owner_address = deps.api.addr_validate(owner_address)?; let staking_address = if let Some(staking_address) = staking_address { + let staking_account_exists = + account_from_address(&staking_address, deps.storage, deps.api).is_ok(); + if staking_account_exists { + return Err(ContractError::StakingAccountAlreadyExists(staking_address)); + } Some(deps.api.addr_validate(&staking_address)?) } else { None @@ -472,6 +485,7 @@ fn try_create_periodic_vesting_account( coin.clone(), start_time, periods, + cap, deps.storage, )?; @@ -486,7 +500,6 @@ fn try_create_periodic_vesting_account( #[entry_point] pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { let query_res = match msg { - QueryMsg::GetLockedPledgeCap {} => to_binary(&get_locked_pledge_cap(deps)), QueryMsg::LockedCoins { vesting_account_address, block_time, @@ -567,11 +580,6 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result) -> Uint128 { - locked_pledge_cap(deps.storage) -} - /// Get current vesting period for a given [crate::vesting::Account]. pub fn try_get_current_vesting_period( address: &str, @@ -695,7 +703,7 @@ pub fn try_get_delegated_vesting( pub fn try_get_delegation_times( deps: Deps<'_>, vesting_account_address: &str, - mix_id: NodeId, + mix_id: MixId, ) -> Result { let owner = deps.api.addr_validate(vesting_account_address)?; let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; @@ -715,7 +723,7 @@ pub fn try_get_delegation_times( pub fn try_get_all_delegations( deps: Deps<'_>, - start_after: Option<(u32, NodeId, BlockTimestampSecs)>, + start_after: Option<(u32, MixId, BlockTimestampSecs)>, limit: Option, ) -> Result { let limit = limit.unwrap_or(100).min(200) as usize; diff --git a/contracts/vesting/src/errors.rs b/contracts/vesting/src/errors.rs index 48dd152c24..bc992e7ca4 100644 --- a/contracts/vesting/src/errors.rs +++ b/contracts/vesting/src/errors.rs @@ -1,5 +1,5 @@ use cosmwasm_std::{Addr, StdError, Uint128}; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -31,7 +31,7 @@ pub enum ContractError { #[error("VESTING ({}): Received multiple denoms, expected 1", line!())] MultipleDenoms, #[error("VESTING ({}): No delegations found for account {0}, mix_identity {1}", line!())] - NoSuchDelegation(Addr, NodeId), + NoSuchDelegation(Addr, MixId), #[error("VESTING ({}): Only mixnet contract can perform this operation, got {0}", line!())] NotMixnetContract(Addr), #[error("VESTING ({}): Calculation underflowed", line!())] @@ -44,6 +44,8 @@ pub enum ContractError { InvalidAddress(String), #[error("VESTING ({}): Account already exists: {0}", line!())] AccountAlreadyExists(String), + #[error("VESTING ({}): Staking account already exists: {0}", line!())] + StakingAccountAlreadyExists(String), #[error("VESTING ({}): Too few coins sent for vesting account creation, sent {sent}, need at least {need}", line!())] MinVestingFunds { sent: u128, need: u128 }, #[error("VESTING ({}): Maximum amount of locked coins has already been pledged: {current}, cap is {cap}", line!())] diff --git a/contracts/vesting/src/storage.rs b/contracts/vesting/src/storage.rs index e42ee5621c..a1216fa87e 100644 --- a/contracts/vesting/src/storage.rs +++ b/contracts/vesting/src/storage.rs @@ -1,8 +1,8 @@ +use crate::errors::ContractError; use crate::vesting::Account; -use crate::{contract::INITIAL_LOCKED_PLEDGE_CAP, errors::ContractError}; use cosmwasm_std::{Addr, Api, Storage, Uint128}; use cw_storage_plus::{Item, Map}; -use mixnet_contract_common::{IdentityKey, NodeId}; +use mixnet_contract_common::{IdentityKey, MixId}; use vesting_contract_common::PledgeData; pub(crate) type BlockTimestampSecs = u64; @@ -16,28 +16,13 @@ const BOND_PLEDGES: Map<'_, u32, PledgeData> = Map::new("bnd"); const GATEWAY_PLEDGES: Map<'_, u32, PledgeData> = Map::new("gtw"); pub const _OLD_DELEGATIONS: Map<'_, (u32, IdentityKey, BlockTimestampSecs), Uint128> = Map::new("dlg"); -pub const DELEGATIONS: Map<'_, (u32, NodeId, BlockTimestampSecs), Uint128> = Map::new("dlg_v2"); +pub const DELEGATIONS: Map<'_, (u32, MixId, BlockTimestampSecs), Uint128> = Map::new("dlg_v2"); pub const ADMIN: Item<'_, String> = Item::new("adm"); pub const MIXNET_CONTRACT_ADDRESS: Item<'_, String> = Item::new("mix"); pub const MIX_DENOM: Item<'_, String> = Item::new("den"); -pub const LOCKED_PLEDGE_CAP: Item<'_, Uint128> = Item::new("lck"); - -pub fn locked_pledge_cap(storage: &dyn Storage) -> Uint128 { - LOCKED_PLEDGE_CAP - .load(storage) - .unwrap_or(INITIAL_LOCKED_PLEDGE_CAP) -} - -pub fn update_locked_pledge_cap( - amount: Uint128, - storage: &mut dyn Storage, -) -> Result<(), ContractError> { - LOCKED_PLEDGE_CAP.save(storage, &amount)?; - Ok(()) -} pub fn save_delegation( - key: (u32, NodeId, BlockTimestampSecs), + key: (u32, MixId, BlockTimestampSecs), amount: Uint128, storage: &mut dyn Storage, ) -> Result<(), ContractError> { @@ -46,7 +31,7 @@ pub fn save_delegation( } pub fn remove_delegation( - key: (u32, NodeId, BlockTimestampSecs), + key: (u32, MixId, BlockTimestampSecs), storage: &mut dyn Storage, ) -> Result<(), ContractError> { DELEGATIONS.remove(storage, key); diff --git a/contracts/vesting/src/support/tests.rs b/contracts/vesting/src/support/tests.rs index 8084e48739..8f24359f52 100644 --- a/contracts/vesting/src/support/tests.rs +++ b/contracts/vesting/src/support/tests.rs @@ -2,9 +2,12 @@ pub mod helpers { use crate::contract::instantiate; use crate::vesting::{populate_vesting_periods, Account}; + use contracts_common::Percent; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}; use cosmwasm_std::{Addr, Coin, Empty, Env, MemoryStorage, OwnedDeps, Storage, Uint128}; + use std::str::FromStr; use vesting_contract_common::messages::{InitMsg, VestingSpecification}; + use vesting_contract_common::PledgeCap; pub const TEST_COIN_DENOM: &str = "unym"; @@ -37,6 +40,7 @@ pub mod helpers { }, start_time_ts, periods, + None, storage, ) .unwrap() @@ -56,6 +60,29 @@ pub mod helpers { }, start_time, periods, + Some(PledgeCap::from_str("0.1").unwrap()), + storage, + ) + .unwrap() + } + + pub fn vesting_account_percent_fixture(storage: &mut dyn Storage, env: &Env) -> Account { + let start_time = env.block.time; + let periods = + populate_vesting_periods(start_time.seconds(), VestingSpecification::default()); + + Account::new( + Addr::unchecked("owner"), + Some(Addr::unchecked("staking")), + Coin { + amount: Uint128::new(1_000_000_000_000), + denom: TEST_COIN_DENOM.to_string(), + }, + start_time, + periods, + Some(PledgeCap::Percent( + Percent::from_percentage_value(10).unwrap(), + )), storage, ) .unwrap() diff --git a/contracts/vesting/src/traits/delegating_account.rs b/contracts/vesting/src/traits/delegating_account.rs index 230f8b3bd9..4beefabe28 100644 --- a/contracts/vesting/src/traits/delegating_account.rs +++ b/contracts/vesting/src/traits/delegating_account.rs @@ -1,17 +1,17 @@ use crate::errors::ContractError; use cosmwasm_std::{Coin, Env, Response, Storage, Uint128}; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; pub trait DelegatingAccount { fn try_claim_delegator_reward( &self, - mix_id: NodeId, + mix_id: MixId, storage: &dyn Storage, ) -> Result; fn try_delegate_to_mixnode( &self, - mix_id: NodeId, + mix_id: MixId, amount: Coin, env: &Env, storage: &mut dyn Storage, @@ -19,7 +19,7 @@ pub trait DelegatingAccount { fn try_undelegate_from_mixnode( &self, - mix_id: NodeId, + mix_id: MixId, storage: &dyn Storage, ) -> Result; @@ -30,7 +30,7 @@ pub trait DelegatingAccount { fn track_delegation( &self, block_height: u64, - mix_id: NodeId, + mix_id: MixId, // Save some gas by passing it in current_balance: Uint128, delegation: Coin, @@ -40,7 +40,7 @@ pub trait DelegatingAccount { // vesting account performs an undelegation. fn track_undelegation( &self, - mix_id: NodeId, + mix_id: MixId, amount: Coin, storage: &mut dyn Storage, ) -> Result<(), ContractError>; diff --git a/contracts/vesting/src/vesting/account/delegating_account.rs b/contracts/vesting/src/vesting/account/delegating_account.rs index 7699fb58c1..62fa42b21e 100644 --- a/contracts/vesting/src/vesting/account/delegating_account.rs +++ b/contracts/vesting/src/vesting/account/delegating_account.rs @@ -1,12 +1,11 @@ use crate::errors::ContractError; -use crate::storage::locked_pledge_cap; use crate::storage::save_delegation; use crate::storage::MIXNET_CONTRACT_ADDRESS; use crate::traits::DelegatingAccount; use crate::traits::VestingAccount; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; use mixnet_contract_common::ExecuteMsg as MixnetExecuteMsg; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use vesting_contract_common::events::{ new_vesting_delegation_event, new_vesting_undelegation_event, }; @@ -16,7 +15,7 @@ use super::Account; impl DelegatingAccount for Account { fn try_claim_delegator_reward( &self, - mix_id: NodeId, + mix_id: MixId, storage: &dyn Storage, ) -> Result { let msg = MixnetExecuteMsg::WithdrawDelegatorRewardOnBehalf { @@ -32,14 +31,15 @@ impl DelegatingAccount for Account { fn try_delegate_to_mixnode( &self, - mix_id: NodeId, + mix_id: MixId, coin: Coin, env: &Env, storage: &mut dyn Storage, ) -> Result { let current_balance = self.load_balance(storage)?; - let total_pledged_after = self.total_pledged_locked(storage, env)? + coin.amount; - let locked_pledge_cap = locked_pledge_cap(storage); + let total_pledged_locked = self.total_pledged_locked(storage, env)?; + let total_pledged_after = total_pledged_locked + coin.amount; + let locked_pledge_cap = self.absolute_pledge_cap()?; if locked_pledge_cap < total_pledged_after { return Err(ContractError::LockedPledgeCapReached { @@ -79,7 +79,7 @@ impl DelegatingAccount for Account { fn try_undelegate_from_mixnode( &self, - mix_id: NodeId, + mix_id: MixId, storage: &dyn Storage, ) -> Result { if !self.any_delegation_for_mix(mix_id, storage) { @@ -104,7 +104,7 @@ impl DelegatingAccount for Account { fn track_delegation( &self, block_timestamp_secs: u64, - mix_id: NodeId, + mix_id: MixId, current_balance: Uint128, delegation: Coin, storage: &mut dyn Storage, @@ -121,7 +121,7 @@ impl DelegatingAccount for Account { fn track_undelegation( &self, - mix_id: NodeId, + mix_id: MixId, amount: Coin, storage: &mut dyn Storage, ) -> Result<(), ContractError> { diff --git a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs index e9979b71a6..91cdd41c68 100644 --- a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs @@ -1,6 +1,5 @@ use super::PledgeData; use crate::errors::ContractError; -use crate::storage::locked_pledge_cap; use crate::storage::MIXNET_CONTRACT_ADDRESS; use crate::traits::GatewayBondingAccount; use crate::traits::VestingAccount; @@ -22,8 +21,9 @@ impl GatewayBondingAccount for Account { storage: &mut dyn Storage, ) -> Result { let current_balance = self.load_balance(storage)?; - let total_pledged_after = self.total_pledged_locked(storage, env)? + pledge.amount; - let locked_pledge_cap = locked_pledge_cap(storage); + let total_pledged_locked = self.total_pledged_locked(storage, env)?; + let total_pledged_after = total_pledged_locked + pledge.amount; + let locked_pledge_cap = self.absolute_pledge_cap()?; if locked_pledge_cap < total_pledged_after { return Err(ContractError::LockedPledgeCapReached { diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index 7fd182e8c9..e3aec82584 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -3,7 +3,6 @@ use super::Account; use crate::errors::ContractError; -use crate::storage::locked_pledge_cap; use crate::storage::MIXNET_CONTRACT_ADDRESS; use crate::traits::MixnodeBondingAccount; use crate::traits::VestingAccount; @@ -39,8 +38,9 @@ impl MixnodeBondingAccount for Account { storage: &mut dyn Storage, ) -> Result { let current_balance = self.load_balance(storage)?; - let total_pledged_after = self.total_pledged_locked(storage, env)? + pledge.amount; - let locked_pledge_cap = locked_pledge_cap(storage); + let total_pledged_locked = self.total_pledged_locked(storage, env)?; + let total_pledged_after = total_pledged_locked + pledge.amount; + let locked_pledge_cap = self.absolute_pledge_cap()?; if locked_pledge_cap < total_pledged_after { return Err(ContractError::LockedPledgeCapReached { diff --git a/contracts/vesting/src/vesting/account/mod.rs b/contracts/vesting/src/vesting/account/mod.rs index 171dfaf616..8520dad85e 100644 --- a/contracts/vesting/src/vesting/account/mod.rs +++ b/contracts/vesting/src/vesting/account/mod.rs @@ -5,12 +5,13 @@ use crate::storage::{ remove_delegation, remove_gateway_pledge, save_account, save_balance, save_bond_pledge, save_gateway_pledge, save_withdrawn, BlockTimestampSecs, DELEGATIONS, KEY, }; +use crate::traits::VestingAccount; use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128}; use cw_storage_plus::Bound; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use vesting_contract_common::{Period, PledgeData}; +use vesting_contract_common::{Period, PledgeCap, PledgeData}; mod delegating_account; mod gateway_bonding_account; @@ -23,7 +24,6 @@ fn generate_storage_key(storage: &mut dyn Storage) -> Result Ok(key) } -#[allow(clippy::derive_partial_eq_without_eq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct Account { pub owner_address: Addr, @@ -32,6 +32,8 @@ pub struct Account { pub periods: Vec, pub coin: Coin, storage_key: u32, + #[serde(default)] + pub pledge_cap: Option, } impl Account { @@ -41,6 +43,7 @@ impl Account { coin: Coin, start_time: Timestamp, periods: Vec, + pledge_cap: Option, storage: &mut dyn Storage, ) -> Result { let storage_key = generate_storage_key(storage)?; @@ -52,12 +55,24 @@ impl Account { periods, coin, storage_key, + pledge_cap, }; save_account(&account, storage)?; account.save_balance(amount, storage)?; Ok(account) } + pub fn pledge_cap(&self) -> PledgeCap { + self.pledge_cap.clone().unwrap_or_default() + } + + pub fn absolute_pledge_cap(&self) -> Result { + match self.pledge_cap() { + PledgeCap::Absolute(cap) => Ok(cap), + PledgeCap::Percent(p) => Ok(p * self.get_original_vesting().amount.amount), + } + } + pub fn coin(&self) -> Coin { self.coin.clone() } @@ -198,7 +213,7 @@ impl Account { remove_gateway_pledge(self.storage_key(), storage) } - pub fn any_delegation_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> bool { + pub fn any_delegation_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> bool { DELEGATIONS .prefix((self.storage_key(), mix_id)) .range(storage, None, None, Order::Ascending) @@ -208,7 +223,7 @@ impl Account { pub fn remove_delegations_for_mix( &self, - mix_id: NodeId, + mix_id: MixId, storage: &mut dyn Storage, ) -> Result<(), ContractError> { let limit = 50; @@ -245,7 +260,7 @@ impl Account { pub fn total_delegations_for_mix( &self, - mix_id: NodeId, + mix_id: MixId, storage: &dyn Storage, ) -> Result { Ok(DELEGATIONS diff --git a/contracts/vesting/src/vesting/account/vesting_account.rs b/contracts/vesting/src/vesting/account/vesting_account.rs index d7eeeb0946..53f39e2891 100644 --- a/contracts/vesting/src/vesting/account/vesting_account.rs +++ b/contracts/vesting/src/vesting/account/vesting_account.rs @@ -123,12 +123,13 @@ impl VestingAccount for Account { storage: &dyn Storage, ) -> Result { let block_time = block_time.unwrap_or(env.block.time); - let period = self.get_current_vesting_period(block_time); let withdrawn = self.load_withdrawn(storage)?; let max_available = self .get_vested_coins(Some(block_time), env, storage)? .amount .saturating_sub(withdrawn); + + let period = self.get_current_vesting_period(block_time); let start_time = match period { Period::Before => 0, Period::After => u64::MAX, @@ -155,15 +156,8 @@ impl VestingAccount for Account { let block_time = block_time.unwrap_or(env.block.time); let delegated_free = self.get_delegated_free(Some(block_time), env, storage)?; - let period = self.get_current_vesting_period(block_time); - let start_time = match period { - Period::Before => 0, - Period::After => u64::MAX, - Period::In(idx) => self.periods[idx as usize].start_time, - }; - let delegations_before_start_time = - self.total_delegations_at_timestamp(storage, start_time)?; + self.total_delegations_at_timestamp(storage, block_time.seconds())?; let amount = delegations_before_start_time - delegated_free.amount; diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index 3e46c46496..d678d2aab8 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -38,6 +38,7 @@ pub fn populate_vesting_periods( mod tests { use crate::contract::*; use crate::storage::*; + use crate::support::tests::helpers::vesting_account_percent_fixture; use crate::support::tests::helpers::{ init_contract, vesting_account_mid_fixture, vesting_account_new_fixture, TEST_COIN_DENOM, }; @@ -51,6 +52,7 @@ mod tests { use mixnet_contract_common::{Gateway, MixNode, Percent}; use vesting_contract_common::messages::{ExecuteMsg, VestingSpecification}; use vesting_contract_common::Period; + use vesting_contract_common::PledgeCap; #[test] fn test_account_creation() { @@ -61,6 +63,7 @@ mod tests { owner_address: "owner".to_string(), staking_address: Some("staking".to_string()), vesting_spec: None, + cap: Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))), }; // Try creating an account when not admin let response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); @@ -389,12 +392,38 @@ mod tests { assert_eq!(locked_coins.amount, Uint128::new(660_000_000_000)); } + #[test] + fn test_percent_cap() { + let mut deps = init_contract(); + let env = mock_env(); + + let account = vesting_account_percent_fixture(&mut deps.storage, &env); + + assert_eq!( + account.absolute_pledge_cap().unwrap(), + Uint128::new(100_000_000_000) + ) + } + #[test] fn test_delegations() { let mut deps = init_contract(); let env = mock_env(); - let account = vesting_account_new_fixture(&mut deps.storage, &env); + // let account = vesting_account_new_fixture(&mut deps.storage, &env); + + let msg = ExecuteMsg::CreateAccount { + owner_address: "owner".to_string(), + staking_address: Some("staking".to_string()), + vesting_spec: None, + cap: Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))), + }; + let info = mock_info("admin", &coins(1_000_000_000_000, TEST_COIN_DENOM)); + + let _response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); + let account = load_account(&Addr::unchecked("owner"), &deps.storage) + .unwrap() + .unwrap(); // Try delegating too much let err = account.try_delegate_to_mixnode( @@ -434,7 +463,7 @@ mod tests { let balance = account.load_balance(&deps.storage).unwrap(); assert_eq!(balance, Uint128::new(910000000000)); - // Try delegating too much again + // Try delegating too much againcalca let err = account.try_delegate_to_mixnode( 1, Coin { @@ -449,6 +478,10 @@ mod tests { let total_delegations = account.total_delegations_for_mix(1, &deps.storage).unwrap(); assert_eq!(Uint128::new(90_000_000_000), total_delegations); + let account = load_account(&Addr::unchecked("owner"), &deps.storage) + .unwrap() + .unwrap(); + // Current period -> block_time: None let delegated_free = account .get_delegated_free(None, &env, &deps.storage) @@ -813,6 +846,7 @@ mod tests { }, Timestamp::from_seconds(account_creation_timestamp), periods, + Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))), deps.as_mut().storage, ) .unwrap(); diff --git a/docker-compose.yml b/docker-compose.yml index b7ce864070..6c756e2ebd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,4 @@ -version: '3.7' +version: "3.7" x-network: &NETWORK BECH32_PREFIX: nymt @@ -23,7 +23,7 @@ services: networks: localnet: ipv4_address: 172.168.10.2 - command: [ "genesis" ] + command: ["genesis"] secondary_validator: build: context: docker/validator @@ -41,7 +41,7 @@ services: ipv4_address: 172.168.10.3 depends_on: - "genesis_validator" - command: [ "secondary" ] + command: ["secondary"] # mixnet_contract: # build: docker/mixnet_contract # image: contract:latest @@ -74,34 +74,49 @@ services: - "genesis_validator" - "secondary_validator" -# mongo: -# image: mongo:latest -# command: -# - --storageEngine=wiredTiger -# volumes: -# - mongo_data:/data/db -# block_explorer: -# build: -# context: https://github.com/forbole/big-dipper.git#v0.41.x-7 -# image: block_explorer:v0.41.x-7 -# ports: -# - "3080:3000" -# depends_on: -# - "mongo" -# environment: -# ROOT_URL: ${APP_ROOT_URL:-http://localhost} -# MONGO_URL: mongodb://mongo:27017/meteor -# PORT: 3000 -# METEOR_SETTINGS: ${METEOR_SETTINGS} -# explorer: -# build: -# context: docker/explorer -# image: explorer:latest -# ports: -# - "3040:3000" -# depends_on: -# - "genesis_validator" -# - "block_explorer" + # mongo: + # image: mongo:latest + # command: + # - --storageEngine=wiredTiger + # volumes: + # - mongo_data:/data/db + # block_explorer: + # build: + # context: https://github.com/forbole/big-dipper.git#v0.41.x-7 + # image: block_explorer:v0.41.x-7 + # ports: + # - "3080:3000" + # depends_on: + # - "mongo" + # environment: + # ROOT_URL: ${APP_ROOT_URL:-http://localhost} + # MONGO_URL: mongodb://mongo:27017/meteor + # PORT: 3000 + # METEOR_SETTINGS: ${METEOR_SETTINGS} + # explorer: + # build: + # context: docker/explorer + # image: explorer:latest + # ports: + # - "3040:3000" + # depends_on: + # - "genesis_validator" + # - "block_explorer" + + # service to update geoip binary database, for explorer-api + geoipupdate: + container_name: geoipupdate + image: maxmindinc/geoipupdate + restart: unless-stopped + environment: + GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID} + GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY} + GEOIPUPDATE_EDITION_IDS: ${GEOIPUPDATE_EDITION_IDS} + GEOIPUPDATE_FREQUENCY: ${GEOIPUPDATE_FREQUENCY} + networks: + - geoipupdate + volumes: + - ${GEOIP_DB_DIRECTORY}:/usr/share/GeoIP volumes: genesis_volume: @@ -111,11 +126,11 @@ volumes: # contract_volume: # mongo_data: - networks: + geoipupdate: localnet: driver: bridge ipam: driver: default config: - - subnet: 172.168.10.0/25 \ No newline at end of file + - subnet: 172.168.10.0/25 diff --git a/envs/qa.env b/envs/qa.env index 842fb54c4d..0e791db172 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -9,7 +9,7 @@ MIX_DENOM_DISPLAY=nym STAKE_DENOM=unyx STAKE_DENOM_DISPLAY=nyx DENOMS_EXPONENT=6 -MIXNET_CONTRACT_ADDRESS=n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep +MIXNET_CONTRACT_ADDRESS=n1rjzps6qrmdqmf0xz4cn4x4rcmqeqzq6hnzqg4wcvd0r2lyasdq5sepn5s8 VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw diff --git a/examples/cli/commands/verify-signature/README.md b/examples/cli/commands/verify-signature/README.md index e32c0b558c..b626b7910d 100644 --- a/examples/cli/commands/verify-signature/README.md +++ b/examples/cli/commands/verify-signature/README.md @@ -23,16 +23,21 @@ Nym signature verification example public key: {"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A4FdhUMasPmNhRZjtpKlmjNbq7EEUgPxfdI+E3vSajvc"} +account id: n1lntkptzz8grf2w4yht4szxktzwsucgv4s7vv9g signature: E3AA5AC0DA1B7DEBB7808000F719D8ACB9A0BE10AFA2756A788516268EB246A1257EC1097C5E364EF916145B01641DEDFE955994CB340BDAFA99A65BCA3F6F28 message: test 1234 Verify the correct message: - SUCCESS ✅ signature is valid -Verify another message: +Verify the correct message with the wrong address: +FAILURE ❌ signature is not valid: account id does not match -FAILURE ❌ signature is not valid: signature error +Verify the correct message with the wrong account and public key: +FAILURE ❌ signature is not valid: account id does not match + +Verify another message: +FAILURE ❌ signature is not valid: signature error - signature error ``` \ No newline at end of file diff --git a/examples/cli/commands/verify-signature/src/main.rs b/examples/cli/commands/verify-signature/src/main.rs index 28f47ca4a4..d4bc1b5506 100644 --- a/examples/cli/commands/verify-signature/src/main.rs +++ b/examples/cli/commands/verify-signature/src/main.rs @@ -6,6 +6,12 @@ fn main() { // the public key in JSON format (because Cosmos supports secp256k1 and ed25519 - NB: the helper only supports secp256k1) let public_key_as_json = r#"{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A4FdhUMasPmNhRZjtpKlmjNbq7EEUgPxfdI+E3vSajvc"}"#; + // the Cosmos address prefix for Nyx is just 'n' + let account_prefix = "n"; + + // the account id is a hash of the public key combined with the prefix + let account_id = "n1lntkptzz8grf2w4yht4szxktzwsucgv4s7vv9g".to_string(); + // the signature as a string of hex characters to represent the bytes in the signature let signature_as_hex = "E3AA5AC0DA1B7DEBB7808000F719D8ACB9A0BE10AFA2756A788516268EB246A1257EC1097C5E364EF916145B01641DEDFE955994CB340BDAFA99A65BCA3F6F28"; @@ -13,30 +19,69 @@ fn main() { let message = "test 1234".to_string(); println!("public key: {}", &public_key_as_json); + println!("account id: {}", &account_id); println!("signature: {}", &signature_as_hex); println!("message: {}", &message); println!(); // this will pass, because the signature was signed for this message - println!("\nVerify the correct message:\n"); + println!("\nVerify the correct message:"); do_verify( + account_id.clone(), + account_prefix, public_key_as_json.to_string(), signature_as_hex.to_string(), + message.clone(), + ); + + println!(); + + // this will fail, because the account id doesn't match the public key + println!("\nVerify the correct message with the wrong address:"); + do_verify( + "n19s8wj0lhkvhr73vy746q3c2hfdzew80rxs6qmy".to_string(), + account_prefix, + public_key_as_json.to_string(), + signature_as_hex.to_string(), + message.clone(), + ); + + // this will fail, because the message was signed with another account private key + println!("\nVerify the correct message with the wrong account and public key:"); + do_verify( + "n19s8wj0lhkvhr73vy746q3c2hfdzew80rxs6qmy".to_string(), + account_prefix, + r#"{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A8l8JuPJjXPUWlBN0XaqRClrq9NMf2qaFQ5CJzidvAvK"}"#.to_string(), + signature_as_hex.to_string(), message, ); // this will fail, because the signature is for another message - println!("\n\nVerify another message:\n"); + println!("\nVerify another message:"); do_verify( + account_id, + account_prefix, public_key_as_json.to_string(), signature_as_hex.to_string(), "another message that will fail".to_string(), ); } -fn do_verify(public_key_as_json: String, signature_as_hex: String, message: String) { - match secp256k1_verify_with_public_key_json(public_key_as_json, signature_as_hex, message) { +fn do_verify( + account_id: String, + prefix: &str, + public_key_as_json: String, + signature_as_hex: String, + message: String, +) { + match secp256k1_verify_with_public_key_json( + public_key_as_json, + signature_as_hex, + message, + account_id, + prefix, + ) { Ok(()) => println!("SUCCESS ✅ signature is valid"), Err(e) => println!("FAILURE ❌ signature is not valid: {}", e), } diff --git a/explorer-api/.env.sample b/explorer-api/.env.sample new file mode 100644 index 0000000000..1efd2de597 --- /dev/null +++ b/explorer-api/.env.sample @@ -0,0 +1,2 @@ +# The path to the geoip database file +GEOIP_DB_PATH=./geo_ip/GeoLite2-Country.mmdb diff --git a/explorer-api/.gitignore b/explorer-api/.gitignore index 8731db8ba6..66ab76436b 100644 --- a/explorer-api/.gitignore +++ b/explorer-api/.gitignore @@ -1,2 +1,3 @@ target explorer-api-state.json +/geo_ip diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index f65a46db03..8aae21b2ef 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] chrono = { version = "0.4.19", features = ["serde"] } -clap = { version = "3.2.8", features = ["cargo", "derive"] } +clap = { version = "3.2", features = ["cargo", "derive"] } humantime-serde = "1.0" isocountry = "0.3.2" itertools = "0.10.3" @@ -22,9 +22,12 @@ schemars = { version = "0.8", features = ["preserve_order"] } serde = "1.0.126" serde_json = "1.0.66" thiserror = "1.0.29" -tokio = {version = "1.19.1", features = ["full"] } +tokio = {version = "1.21.2", features = ["full"] } +maxminddb = "0.23.0" +dotenv = "0.15.0" mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } +contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } network-defaults = { path = "../common/network-defaults" } task = { path = "../common/task" } validator-client = { path = "../common/client-libs/validator-client", features=["nymd-client"] } diff --git a/explorer-api/README.md b/explorer-api/README.md index 0555275f14..d4e193dbb0 100644 --- a/explorer-api/README.md +++ b/explorer-api/README.md @@ -1,24 +1,58 @@ -Network Explorer API -==================== +# Network Explorer API An API that provides data for the [Network Explorer](../explorer). Features: - - geolocates mixnodes using https://app.ipbase.com/ - - calculates how many nodes are in each country - - proxies mixnode API requests to add HTTPS - +- geolocates mixnodes using https://dev.maxmind.com/geoip/geolite2-free-geolocation-data +- calculates how many nodes are in each country +- proxies mixnode API requests to add HTTPS + +## GeoIP db install/update + +First we need to install the geoip database. + +We use https://github.com/maxmind/geoipupdate to automatically +download and update GeoLite2 binary database. For convenience we +run it as a docker container. + +At the root of the repo, inside the `docker-compose.yml`, there +is a docker service `geoipupdate` for it. + +Supposed you provided an `.env` file with **all the environment +variables** listed in `.env.sample-dev` (found at the root), +simply run the service through docker: + +```shell +docker compose up -d geoipupdate +``` + +Running this command will automatically install (and update) the +db file inside the directory path provided by `GEOIP_DB_DIRECTORY` +env variable. + ## Running -Supply the environment variable `GEO_IP_SERVICE_API_KEY` with a key from https://app.ipbase.com/. +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-Country.mmdb cargo run +``` + +Note: explorer-api binary reads the provided `.env` file. Run as a service and reverse proxy with `nginx` to add `https` with Lets Encrypt. +Setup nginx to inject the request IP to the header `X-Real-IP`. + # TODO / Known Issues ## TODO -* record the number of mixnodes on a given date and write to a file for later retrieval -* dependency injection -* tests +- record the number of mixnodes on a given date and write to a file for later retrieval +- dependency injection +- tests diff --git a/explorer-api/src/buy_terms/http.rs b/explorer-api/src/buy_terms/http.rs new file mode 100644 index 0000000000..5594a1a3be --- /dev/null +++ b/explorer-api/src/buy_terms/http.rs @@ -0,0 +1,26 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::geo_ip::location::Location; +use crate::state::ExplorerApiStateContext; +use rocket::response::status; +use rocket::serde::json::Json; +use rocket::{Route, State}; +use rocket_okapi::okapi::openapi3::OpenApi; +use rocket_okapi::settings::OpenApiSettings; + +pub fn nym_terms_make_default_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { + openapi_get_routes_spec![settings: terms] +} + +#[openapi(tag = "terms")] +#[get("/")] +pub(crate) async fn terms( + _state: &State, + location: Location, +) -> Result, status::Forbidden> { + if location.iso_alpha2 == "US" { + return Err(status::Forbidden(Some("US government sucks".to_string()))); + } + Ok(Json("Nym Terms & Conditions: Welcome".to_string())) +} diff --git a/explorer-api/src/buy_terms/mod.rs b/explorer-api/src/buy_terms/mod.rs new file mode 100644 index 0000000000..d064c8bd6f --- /dev/null +++ b/explorer-api/src/buy_terms/mod.rs @@ -0,0 +1 @@ +pub(crate) mod http; diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index 10329c212a..60603a2aaf 100644 --- a/explorer-api/src/country_statistics/geolocate.rs +++ b/explorer-api/src/country_statistics/geolocate.rs @@ -1,12 +1,10 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::mix_nodes::location::{GeoLocation, Location}; +use crate::mix_nodes::location::Location; use crate::state::ExplorerApiStateContext; use log::{info, warn}; -use reqwest::Error as ReqwestError; use task::ShutdownListener; -use thiserror::Error; pub(crate) struct GeoLocateTask { state: ExplorerApiStateContext, @@ -19,13 +17,6 @@ impl GeoLocateTask { } pub(crate) fn start(mut self) { - if ::std::env::var("GEO_IP_SERVICE_API_KEY").is_err() { - error!( - "Env var GEO_IP_SERVICE_API_KEY is not set. Geolocation tasks will not be started." - ); - return; - } - info!("Spawning mix node locator task runner..."); tokio::spawn(async move { let mut interval_timer = tokio::time::interval(std::time::Duration::from_millis(50)); @@ -53,6 +44,8 @@ impl GeoLocateTask { .await .unwrap_or_default(); + let geo_ip = self.state.inner.geo_ip.0.clone(); + for (i, cache_item) in mixnode_bonds.values().enumerate() { if self .state @@ -65,99 +58,50 @@ impl GeoLocateTask { continue; } - // the mix node has not been located or is the cache time has expired - match locate(&cache_item.mix_node().host).await { - Ok(geo_location) => { - let location = Location::new(geo_location); + match geo_ip.query(&cache_item.mix_node().host) { + Ok(opt) => match opt { + Some(location) => { + let location = Location::new(location); - trace!( - "{} mix nodes already located. Ip {} is located in {:#?}", - i, - cache_item.mix_node().host, - location.three_letter_iso_country_code, - ); - - if i > 0 && (i % 100) == 0 { - info!( - "Located {} mixnodes...", - i + 1, + trace!( + "{} mix nodes already located. Ip {} is located in {:#?}", + i, + cache_item.mix_node().host, + location.three_letter_iso_country_code, ); + + if i > 0 && (i % 100) == 0 { + info!("Located {} mixnodes...", i + 1,); + } + + self.state + .inner + .mixnodes + .set_location(cache_item.mix_id(), Some(location)) + .await; + + // one node has been located, so return out of the loop + return; } - - self.state - .inner - .mixnodes - .set_location(cache_item.mix_id(), Some(location)) - .await; - - // one node has been located, so return out of the loop - return; - } - Err(e) => match e { - LocateError::ReqwestError(e) => warn!( - "❌ Oh no! Location for {} failed {}", - cache_item.mix_node().host, e - ), - LocateError::NotFound(e) => { - warn!( - "❌ Location for {} not found. Response body: {}", - cache_item.mix_node().host, e - ); + None => { + warn!("❌ Location for {} not found.", cache_item.mix_node().host); self.state .inner .mixnodes .set_location(cache_item.mix_id(), None) .await; - }, - LocateError::RateLimited(e) => warn!( - "❌ Oh no, we've been rate limited! Location for {} failed. Response body: {}", - cache_item.mix_node().host, e - ), + } }, - } + Err(e) => { + warn!( + "❌ Oh no! Location for {} failed. Error: {:#?}", + cache_item.mix_node().host, + e + ); + } + }; } trace!("All mix nodes located"); } } - -#[derive(Debug, Error)] -enum LocateError { - #[error("Oops, we have made too many requests and are being rate limited. Request body: {0}")] - RateLimited(String), - - #[error("Geolocation not found. Request body: {0}")] - NotFound(String), - - #[error(transparent)] - ReqwestError(#[from] ReqwestError), -} - -async fn locate(ip: &str) -> Result { - let api_key = ::std::env::var("GEO_IP_SERVICE_API_KEY") - .expect("Env var GEO_IP_SERVICE_API_KEY is not set"); - let uri = format!("{}/?apikey={}&ip={}", crate::GEO_IP_SERVICE, api_key, ip); - match reqwest::get(uri.clone()).await { - Ok(response) => { - if response.status() == 429 { - return Err(LocateError::RateLimited( - response - .text() - .await - .unwrap_or_else(|_| "(the response body is empty)".to_string()), - )); - } - if response.status() == 404 { - return Err(LocateError::NotFound( - response - .text() - .await - .unwrap_or_else(|_| "(the response body is empty)".to_string()), - )); - } - let location = response.json::().await?; - Ok(location) - } - Err(e) => Err(LocateError::ReqwestError(e)), - } -} diff --git a/explorer-api/src/geo_ip/location.rs b/explorer-api/src/geo_ip/location.rs new file mode 100644 index 0000000000..643de3e76d --- /dev/null +++ b/explorer-api/src/geo_ip/location.rs @@ -0,0 +1,112 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use isocountry::CountryCode; +use log::warn; +use maxminddb::{geoip2::Country, MaxMindDBError, Reader}; +use std::{net::IpAddr, str::FromStr, sync::Arc}; + +const DEFAULT_DATABASE_PATH: &str = "./geo_ip/GeoLite2-Country.mmdb"; + +#[derive(Debug)] +pub enum GeoIpError { + NoValidIP, + InternalError, +} + +// The current State implementation does not allow to fail on state +// creation, ie. returning Result<>. To avoid to use unwrap family, +// as a workaround, wrap the state inside an Option<> +// If Reader::open_readfile fails for some reason db will be set to None +// and an error will be logged. +pub(crate) struct GeoIp { + pub(crate) db: Option>>, +} + +#[derive(Clone)] +pub(crate) struct ThreadsafeGeoIp(pub Arc); + +pub(crate) struct Location { + /// two-letter country code (ISO 3166-1 alpha-2) + pub(crate) iso_alpha2: String, + /// three-letter country code (ISO 3166-1 alpha-3) + pub(crate) iso_alpha3: String, + /// English country short name (ISO 3166-1) + pub(crate) name: String, +} + +impl GeoIp { + pub fn new() -> Self { + let db_path = std::env::var("GEOIP_DB_PATH").unwrap_or_else(|e| { + warn!( + "Env variable GEOIP_DB_PATH is not set: {} - Fallback to {}", + e, DEFAULT_DATABASE_PATH + ); + DEFAULT_DATABASE_PATH.to_string() + }); + let reader = Reader::open_readfile(&db_path) + .map_err(|e| { + error!("Fail to open GeoLite2 database file {}: {}", db_path, e); + }) + .ok(); + GeoIp { db: reader } + } + + pub fn query(&self, address: &str) -> Result, GeoIpError> { + let ip: IpAddr = FromStr::from_str(address).map_err(|e| { + error!("Fail to create IpAddr from {}: {}", &address, e); + GeoIpError::NoValidIP + })?; + let result = self + .db + .as_ref() + .ok_or_else(|| { + error!("No registered GeoIP database"); + GeoIpError::InternalError + })? + .lookup::(ip); + match &result { + Ok(v) => Ok(Some( + Location::try_from(v).map_err(|_| GeoIpError::InternalError)?, + )), + Err(e) => match e { + MaxMindDBError::AddressNotFoundError(_) => Ok(None), + _ => Err(GeoIpError::InternalError), + }, + } + } +} + +impl<'a> TryFrom<&Country<'a>> for Location { + type Error = String; + + fn try_from(country: &Country) -> Result { + let data = country.country.as_ref().ok_or_else(|| { + warn!("No Country data found"); + "No Country data found" + })?; + let iso_alpha2 = String::from(data.iso_code.ok_or_else(|| { + warn!("No iso alpha-2 code found in Country data {:#?}", data); + "No iso alpha-2 code found in Country data" + })?); + let iso_codes = CountryCode::for_alpha2(&iso_alpha2).map_err(|e| { + let message = format!( + "Fail to get iso codes from iso alpha-2 country code {}: {}", + &iso_alpha2, e + ); + warn!("{}", &message); + message + })?; + Ok(Location { + iso_alpha2, + iso_alpha3: String::from(iso_codes.alpha3()), + name: String::from(iso_codes.name()), + }) + } +} + +impl ThreadsafeGeoIp { + pub fn new() -> Self { + ThreadsafeGeoIp(Arc::new(GeoIp::new())) + } +} diff --git a/explorer-api/src/geo_ip/mod.rs b/explorer-api/src/geo_ip/mod.rs new file mode 100644 index 0000000000..d137da2c97 --- /dev/null +++ b/explorer-api/src/geo_ip/mod.rs @@ -0,0 +1 @@ +pub(crate) mod location; diff --git a/explorer-api/src/guards/location.rs b/explorer-api/src/guards/location.rs new file mode 100644 index 0000000000..dd1bfe1ff8 --- /dev/null +++ b/explorer-api/src/guards/location.rs @@ -0,0 +1,76 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::geo_ip::location::{GeoIpError, Location}; +use crate::state::ExplorerApiStateContext; +use rocket::http::Status; +use rocket::request::FromRequest; +use rocket::request::Outcome; +use rocket::Request; +use rocket_okapi::gen::OpenApiGenerator; +use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput}; + +const IP_HEADER: &str = "X-Real-IP"; + +#[derive(Debug)] +pub enum LocationError { + NoIP, + LocationNotFound, + InternalError, +} + +fn find_location(request: &Request<'_>) -> Result { + let ip = request + .headers() + .get_one(IP_HEADER) + .map(|f| f.to_string()) + .ok_or_else(|| { + error!("Header not found, {}", IP_HEADER); + (Status::Forbidden, LocationError::NoIP) + })?; + + let geo_ip = &request + .rocket() + .state::() + .ok_or((Status::InternalServerError, LocationError::InternalError))? // should never fail + .inner + .geo_ip; + + let location = geo_ip + .0 + .clone() + .query(&ip) + .map_err(|e| match e { + GeoIpError::NoValidIP => (Status::Forbidden, LocationError::NoIP), + GeoIpError::InternalError => { + (Status::InternalServerError, LocationError::InternalError) + } + })? + .ok_or_else(|| { + warn!("Fail to find a matching location for {}", ip); + (Status::Forbidden, LocationError::LocationNotFound) + })?; + Ok(location) +} + +#[rocket::async_trait] +impl<'r> FromRequest<'r> for Location { + type Error = LocationError; + + async fn from_request(request: &'r Request<'_>) -> Outcome { + match find_location(request) { + Ok(loc) => Outcome::Success(loc), + Err(e) => Outcome::Failure(e), + } + } +} + +impl<'a> OpenApiFromRequest<'a> for Location { + fn from_request_input( + _gen: &mut OpenApiGenerator, + _name: String, + _required: bool, + ) -> rocket_okapi::Result { + Ok(RequestHeaderInput::None) + } +} diff --git a/explorer-api/src/guards/mod.rs b/explorer-api/src/guards/mod.rs new file mode 100644 index 0000000000..d137da2c97 --- /dev/null +++ b/explorer-api/src/guards/mod.rs @@ -0,0 +1 @@ +pub(crate) mod location; diff --git a/explorer-api/src/http/mod.rs b/explorer-api/src/http/mod.rs index 31f638ee5f..f5559124a5 100644 --- a/explorer-api/src/http/mod.rs +++ b/explorer-api/src/http/mod.rs @@ -5,6 +5,7 @@ use rocket::{Build, Request, Rocket}; use rocket_cors::{AllowedHeaders, AllowedOrigins}; use rocket_okapi::swagger_ui::make_swagger_ui; +use crate::buy_terms::http::nym_terms_make_default_routes; use crate::country_statistics::http::country_statistics_make_default_routes; use crate::gateways::http::gateways_make_default_routes; use crate::http::swagger::get_docs; @@ -56,6 +57,7 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket { "/overview" => overview_make_default_routes(&openapi_settings), "/ping" => ping_make_default_routes(&openapi_settings), "/validators" => validators_make_default_routes(&openapi_settings), + "/terms" => nym_terms_make_default_routes(&openapi_settings), }; building_rocket diff --git a/explorer-api/src/main.rs b/explorer-api/src/main.rs index 4e144b56a9..d08059442e 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -4,15 +4,19 @@ extern crate rocket; extern crate rocket_okapi; use clap::Parser; +use dotenv::dotenv; use log::info; use network_defaults::setup_env; use task::ShutdownNotifier; +mod buy_terms; pub(crate) mod cache; mod client; pub(crate) mod commands; mod country_statistics; mod gateways; +mod geo_ip; +mod guards; mod helpers; mod http; mod mix_node; @@ -23,11 +27,11 @@ mod state; mod tasks; mod validators; -const GEO_IP_SERVICE: &str = "https://api.ipbase.com/json"; const COUNTRY_DATA_REFRESH_INTERVAL: u64 = 60 * 15; // every 15 minutes #[tokio::main] async fn main() { + dotenv().ok(); setup_logging(); let args = commands::Cli::parse(); setup_env(args.config_env_file); diff --git a/explorer-api/src/mix_node/delegations.rs b/explorer-api/src/mix_node/delegations.rs index 57053fde3a..3d061c942a 100644 --- a/explorer-api/src/mix_node/delegations.rs +++ b/explorer-api/src/mix_node/delegations.rs @@ -4,11 +4,11 @@ use super::models::SummedDelegations; use crate::client::ThreadsafeValidatorClient; use itertools::Itertools; -use mixnet_contract_common::{Delegation, NodeId}; +use mixnet_contract_common::{Delegation, MixId}; pub(crate) async fn get_single_mixnode_delegations( client: &ThreadsafeValidatorClient, - mix_id: NodeId, + mix_id: MixId, ) -> Vec { match client .0 @@ -25,7 +25,7 @@ pub(crate) async fn get_single_mixnode_delegations( pub(crate) async fn get_single_mixnode_delegations_summed( client: &ThreadsafeValidatorClient, - mix_id: NodeId, + mix_id: MixId, ) -> Vec { let delegations_by_owner = get_single_mixnode_delegations(client, mix_id) .await diff --git a/explorer-api/src/mix_node/econ_stats.rs b/explorer-api/src/mix_node/econ_stats.rs index 377d542983..22f106b1aa 100644 --- a/explorer-api/src/mix_node/econ_stats.rs +++ b/explorer-api/src/mix_node/econ_stats.rs @@ -4,12 +4,12 @@ use crate::client::ThreadsafeValidatorClient; use crate::helpers::best_effort_small_dec_to_f64; use crate::mix_node::models::EconomicDynamicsStats; -use mixnet_contract_common::rewarding::helpers::truncate_decimal; -use mixnet_contract_common::NodeId; +use contracts_common::truncate_decimal; +use mixnet_contract_common::MixId; pub(crate) async fn retrieve_mixnode_econ_stats( client: &ThreadsafeValidatorClient, - mix_id: NodeId, + mix_id: MixId, ) -> Option { let stake_saturation = client .0 diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index 25b1cdf4bc..00d9b9fd70 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -8,9 +8,8 @@ use crate::mix_node::econ_stats::retrieve_mixnode_econ_stats; use crate::mix_node::models::{ EconomicDynamicsStats, NodeDescription, NodeStats, PrettyDetailedMixNodeBond, SummedDelegations, }; -use crate::mix_nodes; use crate::state::ExplorerApiStateContext; -use mixnet_contract_common::{Delegation, NodeId}; +use mixnet_contract_common::{Delegation, MixId}; use reqwest::Error as ReqwestError; use rocket::response::status::NotFound; use rocket::serde::json::Json; @@ -27,15 +26,6 @@ pub fn mix_node_make_default_routes(settings: &OpenApiSettings) -> (Vec, get_description, get_stats, get_economic_dynamics_stats, - // ================================================= - // TO REMOVE ONCE OTHER PARTS OF THE SYSTEM MIGRATED - // ================================================= - deprecated_get_delegations_by_identity, - deprecated_get_delegations_summed_by_identity, - deprecated_get_by_identity, - deprecated_get_description_by_identity, - deprecated_get_stats_by_identity, - deprecated_get_economic_dynamics_stats_by_identity, ] } @@ -56,7 +46,7 @@ async fn get_mix_node_stats(host: &str, port: u16) -> Result")] pub(crate) async fn get_by_id( - mix_id: NodeId, + mix_id: MixId, state: &State, ) -> Result, NotFound> { match state.inner.mixnodes.get_detailed_mixnode(mix_id).await { @@ -68,7 +58,7 @@ pub(crate) async fn get_by_id( #[openapi(tag = "mix_node")] #[get("//delegations")] pub(crate) async fn get_delegations( - mix_id: NodeId, + mix_id: MixId, state: &State, ) -> Json> { Json(get_single_mixnode_delegations(&state.inner.validator_client, mix_id).await) @@ -77,7 +67,7 @@ pub(crate) async fn get_delegations( #[openapi(tag = "mix_node")] #[get("//delegations/summed")] pub(crate) async fn get_delegations_summed( - mix_id: NodeId, + mix_id: MixId, state: &State, ) -> Json> { Json(get_single_mixnode_delegations_summed(&state.inner.validator_client, mix_id).await) @@ -86,7 +76,7 @@ pub(crate) async fn get_delegations_summed( #[openapi(tag = "mix_node")] #[get("//description")] pub(crate) async fn get_description( - mix_id: NodeId, + mix_id: MixId, state: &State, ) -> Option> { match state.inner.mixnode.clone().get_description(mix_id).await { @@ -134,7 +124,7 @@ pub(crate) async fn get_description( #[openapi(tag = "mix_node")] #[get("//stats")] pub(crate) async fn get_stats( - mix_id: NodeId, + mix_id: MixId, state: &State, ) -> Option> { match state.inner.mixnode.get_node_stats(mix_id).await { @@ -179,7 +169,7 @@ pub(crate) async fn get_stats( #[openapi(tag = "mix_node")] #[get("//economic-dynamics-stats")] pub(crate) async fn get_economic_dynamics_stats( - mix_id: NodeId, + mix_id: MixId, state: &State, ) -> Option> { match state.inner.mixnode.get_econ_stats(mix_id).await { @@ -204,126 +194,3 @@ pub(crate) async fn get_economic_dynamics_stats( } } } - -// ================================================= -// TO REMOVE ONCE OTHER PARTS OF THE SYSTEM MIGRATED -// ================================================= - -#[openapi(tag = "mix_nodes")] -#[get("/deprecated/")] -pub(crate) async fn deprecated_get_by_identity( - pubkey: &str, - state: &State, -) -> Result, NotFound> { - let mix_id = match mix_nodes::helpers::best_effort_pubkey_to_mix_id(state, pubkey).await { - Some(mix_id) => mix_id, - None => { - warn!( - "there doesn't seem to exist a mixnode with identity {}", - pubkey - ); - return Err(NotFound("Mixnode not found".into())); - } - }; - get_by_id(mix_id, state).await -} - -#[openapi(tag = "mix_node")] -#[get("/deprecated//delegations")] -pub(crate) async fn deprecated_get_delegations_by_identity( - pubkey: &str, - state: &State, -) -> Json> { - let mix_id = match mix_nodes::helpers::best_effort_pubkey_to_mix_id(state, pubkey).await { - Some(mix_id) => mix_id, - None => { - warn!( - "there doesn't seem to exist a mixnode with identity {}", - pubkey - ); - return Json(Vec::new()); - } - }; - - get_delegations(mix_id, state).await -} - -#[openapi(tag = "mix_node")] -#[get("/deprecated//delegations/summed")] -pub(crate) async fn deprecated_get_delegations_summed_by_identity( - pubkey: &str, - state: &State, -) -> Json> { - let mix_id = match mix_nodes::helpers::best_effort_pubkey_to_mix_id(state, pubkey).await { - Some(mix_id) => mix_id, - None => { - warn!( - "there doesn't seem to exist a mixnode with identity {}", - pubkey - ); - return Json(Vec::new()); - } - }; - - get_delegations_summed(mix_id, state).await -} - -#[openapi(tag = "mix_node")] -#[get("/deprecated//description")] -pub(crate) async fn deprecated_get_description_by_identity( - pubkey: &str, - state: &State, -) -> Option> { - let mix_id = match mix_nodes::helpers::best_effort_pubkey_to_mix_id(state, pubkey).await { - Some(mix_id) => mix_id, - None => { - warn!( - "there doesn't seem to exist a mixnode with identity {}", - pubkey - ); - return None; - } - }; - - get_description(mix_id, state).await -} - -#[openapi(tag = "mix_node")] -#[get("/deprecated//stats")] -pub(crate) async fn deprecated_get_stats_by_identity( - pubkey: &str, - state: &State, -) -> Option> { - let mix_id = match mix_nodes::helpers::best_effort_pubkey_to_mix_id(state, pubkey).await { - Some(mix_id) => mix_id, - None => { - warn!( - "there doesn't seem to exist a mixnode with identity {}", - pubkey - ); - return None; - } - }; - - get_stats(mix_id, state).await -} - -#[openapi(tag = "mix_node")] -#[get("/deprecated//economic-dynamics-stats")] -pub(crate) async fn deprecated_get_economic_dynamics_stats_by_identity( - pubkey: &str, - state: &State, -) -> Option> { - let mix_id = match mix_nodes::helpers::best_effort_pubkey_to_mix_id(state, pubkey).await { - Some(mix_id) => mix_id, - None => { - warn!( - "there doesn't seem to exist a mixnode with identity {}", - pubkey - ); - return None; - } - }; - - get_economic_dynamics_stats(mix_id, state).await -} diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index 9e24aa69b8..0332aeaa17 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -4,7 +4,7 @@ use crate::cache::Cache; use crate::mix_nodes::location::Location; use mixnet_contract_common::Delegation; -use mixnet_contract_common::{Addr, Coin, Layer, MixNode, NodeId}; +use mixnet_contract_common::{Addr, Coin, Layer, MixId, MixNode}; use serde::Deserialize; use serde::Serialize; use std::sync::Arc; @@ -40,7 +40,7 @@ pub(crate) struct PrettyDetailedMixNodeBond { #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct SummedDelegations { pub owner: Addr, - pub mix_id: NodeId, + pub mix_id: MixId, pub amount: Coin, } @@ -66,9 +66,9 @@ impl SummedDelegations { } pub(crate) struct MixNodeCache { - pub(crate) descriptions: Cache, - pub(crate) node_stats: Cache, - pub(crate) econ_stats: Cache, + pub(crate) descriptions: Cache, + pub(crate) node_stats: Cache, + pub(crate) econ_stats: Cache, } #[derive(Clone)] @@ -87,19 +87,19 @@ impl ThreadsafeMixNodeCache { } } - pub(crate) async fn get_description(&self, mix_id: NodeId) -> Option { + pub(crate) async fn get_description(&self, mix_id: MixId) -> Option { self.inner.read().await.descriptions.get(&mix_id) } - pub(crate) async fn get_node_stats(&self, mix_id: NodeId) -> Option { + pub(crate) async fn get_node_stats(&self, mix_id: MixId) -> Option { self.inner.read().await.node_stats.get(&mix_id) } - pub(crate) async fn get_econ_stats(&self, mix_id: NodeId) -> Option { + pub(crate) async fn get_econ_stats(&self, mix_id: MixId) -> Option { self.inner.read().await.econ_stats.get(&mix_id) } - pub(crate) async fn set_description(&self, mix_id: NodeId, description: NodeDescription) { + pub(crate) async fn set_description(&self, mix_id: MixId, description: NodeDescription) { self.inner .write() .await @@ -107,11 +107,11 @@ impl ThreadsafeMixNodeCache { .set(mix_id, description); } - pub(crate) async fn set_node_stats(&self, mix_id: NodeId, node_stats: NodeStats) { + pub(crate) async fn set_node_stats(&self, mix_id: MixId, node_stats: NodeStats) { self.inner.write().await.node_stats.set(mix_id, node_stats); } - pub(crate) async fn set_econ_stats(&self, mix_id: NodeId, econ_stats: EconomicDynamicsStats) { + pub(crate) async fn set_econ_stats(&self, mix_id: MixId, econ_stats: EconomicDynamicsStats) { self.inner.write().await.econ_stats.set(mix_id, econ_stats); } } @@ -172,11 +172,11 @@ fn get_common_owner(delegations: &[Delegation]) -> Option { Some(owner) } -fn get_common_mix_id(delegations: &[Delegation]) -> Option { - let mix_id = delegations.iter().next()?.node_id; +fn get_common_mix_id(delegations: &[Delegation]) -> Option { + let mix_id = delegations.iter().next()?.mix_id; if delegations .iter() - .any(|delegation| delegation.node_id != mix_id) + .any(|delegation| delegation.mix_id != mix_id) { log::warn!("Unexpected different node identities when summing delegations"); return None; diff --git a/explorer-api/src/mix_nodes/helpers.rs b/explorer-api/src/mix_nodes/helpers.rs deleted file mode 100644 index 8dec26ebf4..0000000000 --- a/explorer-api/src/mix_nodes/helpers.rs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2022 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::state::ExplorerApiStateContext; -use mixnet_contract_common::NodeId; - -pub(crate) async fn best_effort_pubkey_to_mix_id( - state: &ExplorerApiStateContext, - pub_key: &str, -) -> Option { - state - .inner - .get_mix_node_by_pubkey(pub_key) - .await - .map(|node| node.mix_id()) -} diff --git a/explorer-api/src/mix_nodes/location.rs b/explorer-api/src/mix_nodes/location.rs index 5591a2b6a7..d14eaf771e 100644 --- a/explorer-api/src/mix_nodes/location.rs +++ b/explorer-api/src/mix_nodes/location.rs @@ -1,13 +1,13 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::mix_nodes::utils::map_2_letter_to_3_letter_country_code; -use mixnet_contract_common::NodeId; +use crate::geo_ip::location; +use mixnet_contract_common::MixId; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::time::{Duration, SystemTime}; -pub(crate) type LocationCache = HashMap; +pub(crate) type LocationCache = HashMap; #[allow(dead_code)] #[derive(Debug, Deserialize)] @@ -36,19 +36,14 @@ pub(crate) struct Location { pub(crate) two_letter_iso_country_code: String, pub(crate) three_letter_iso_country_code: String, pub(crate) country_name: String, - pub(crate) lat: f32, - pub(crate) lng: f32, } impl Location { - pub(crate) fn new(geo_location: GeoLocation) -> Self { - let three_letter_iso_country_code = map_2_letter_to_3_letter_country_code(&geo_location); + pub(crate) fn new(location: location::Location) -> Self { Location { - country_name: geo_location.country_name, - two_letter_iso_country_code: geo_location.country_code, - three_letter_iso_country_code, - lat: geo_location.latitude, - lng: geo_location.longitude, + country_name: location.name, + two_letter_iso_country_code: location.iso_alpha2, + three_letter_iso_country_code: location.iso_alpha3, } } } diff --git a/explorer-api/src/mix_nodes/mod.rs b/explorer-api/src/mix_nodes/mod.rs index a02e2bc043..82c2aedd1f 100644 --- a/explorer-api/src/mix_nodes/mod.rs +++ b/explorer-api/src/mix_nodes/mod.rs @@ -3,11 +3,9 @@ use std::time::Duration; -pub(crate) mod helpers; pub(crate) mod http; pub(crate) mod location; pub(crate) mod models; -pub(crate) mod utils; pub(crate) const CACHE_REFRESH_RATE: Duration = Duration::from_secs(30); pub(crate) const CACHE_ENTRY_TTL: Duration = Duration::from_secs(60); diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index e0b40a21b5..11d91a6eb1 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use std::time::{Duration, SystemTime}; use mixnet_contract_common::rewarding::helpers::truncate_reward; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use serde::Serialize; use tokio::sync::{RwLock, RwLockReadGuard}; @@ -33,9 +33,9 @@ pub(crate) struct MixNodeSummary { #[derive(Clone, Debug)] pub(crate) struct MixNodesResult { pub(crate) valid_until: SystemTime, - pub(crate) all_mixnodes: HashMap, - active_mixnodes: HashSet, - rewarded_mixnodes: HashSet, + pub(crate) all_mixnodes: HashMap, + active_mixnodes: HashSet, + rewarded_mixnodes: HashSet, } impl MixNodesResult { @@ -48,7 +48,7 @@ impl MixNodesResult { } } - fn determine_node_status(&self, mix_id: NodeId) -> MixnodeStatus { + fn determine_node_status(&self, mix_id: MixId) -> MixnodeStatus { if self.active_mixnodes.contains(&mix_id) { MixnodeStatus::Active } else if self.rewarded_mixnodes.contains(&mix_id) { @@ -62,7 +62,7 @@ impl MixNodesResult { self.valid_until >= SystemTime::now() } - fn get_mixnode(&self, mix_id: NodeId) -> Option { + fn get_mixnode(&self, mix_id: MixId) -> Option { if self.is_valid() { self.all_mixnodes.get(&mix_id).cloned() } else { @@ -70,7 +70,7 @@ impl MixNodesResult { } } - fn get_mixnodes(&self) -> Option> { + fn get_mixnodes(&self) -> Option> { if self.is_valid() { Some(self.all_mixnodes.clone()) } else { @@ -100,7 +100,7 @@ impl ThreadsafeMixNodesCache { } } - pub(crate) async fn is_location_valid(&self, mix_id: NodeId) -> bool { + pub(crate) async fn is_location_valid(&self, mix_id: MixId) -> bool { self.locations .read() .await @@ -114,7 +114,7 @@ impl ThreadsafeMixNodesCache { self.locations.read().await.clone() } - pub(crate) async fn set_location(&self, mix_id: NodeId, location: Option) { + pub(crate) async fn set_location(&self, mix_id: MixId, 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() @@ -122,7 +122,7 @@ impl ThreadsafeMixNodesCache { .insert(mix_id, LocationCacheItem::new_from_location(location)); } - pub(crate) async fn get_mixnode(&self, mix_id: NodeId) -> Option { + pub(crate) async fn get_mixnode(&self, mix_id: MixId) -> Option { self.mixnodes.read().await.get_mixnode(mix_id) } @@ -139,13 +139,13 @@ impl ThreadsafeMixNodesCache { None } - pub(crate) async fn get_mixnodes(&self) -> Option> { + pub(crate) async fn get_mixnodes(&self) -> Option> { self.mixnodes.read().await.get_mixnodes() } fn create_detailed_mixnode( &self, - mix_id: NodeId, + mix_id: MixId, mixnodes_guard: &RwLockReadGuard<'_, MixNodesResult>, location: Option<&LocationCacheItem>, node: &MixNodeBondAnnotated, @@ -170,7 +170,7 @@ impl ThreadsafeMixNodesCache { pub(crate) async fn get_detailed_mixnode( &self, - mix_id: NodeId, + mix_id: MixId, ) -> Option { let mixnodes_guard = self.mixnodes.read().await; let location_guard = self.locations.read().await; @@ -198,8 +198,8 @@ impl ThreadsafeMixNodesCache { pub(crate) async fn update_cache( &self, all_bonds: Vec, - rewarded_nodes: HashSet, - active_nodes: HashSet, + rewarded_nodes: HashSet, + active_nodes: HashSet, ) { let mut guard = self.mixnodes.write().await; guard.all_mixnodes = all_bonds diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index e8f89dbf3f..7ca0ae3105 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -3,10 +3,11 @@ use std::path::Path; use chrono::{DateTime, Utc}; use log::info; -use mixnet_contract_common::{IdentityKeyRef, NodeId}; +use mixnet_contract_common::{IdentityKeyRef, MixId}; use serde::{Deserialize, Serialize}; use crate::client::ThreadsafeValidatorClient; +use crate::geo_ip::location::ThreadsafeGeoIp; use validator_client::models::MixNodeBondAnnotated; use crate::country_statistics::country_nodes_distribution::{ @@ -30,13 +31,14 @@ pub struct ExplorerApiState { pub(crate) mixnodes: ThreadsafeMixNodesCache, pub(crate) ping: ThreadsafePingCache, pub(crate) validators: ThreadsafeValidatorCache, + pub(crate) geo_ip: ThreadsafeGeoIp, // TODO: discuss with @MS whether this is an appropriate spot for it pub(crate) validator_client: ThreadsafeValidatorClient, } impl ExplorerApiState { - pub(crate) async fn get_mix_node(&self, mix_id: NodeId) -> Option { + pub(crate) async fn get_mix_node(&self, mix_id: MixId) -> Option { self.mixnodes.get_mixnode(mix_id).await } @@ -87,6 +89,7 @@ impl ExplorerApiStateContext { ping: ThreadsafePingCache::new(), validators: ThreadsafeValidatorCache::new(), validator_client: ThreadsafeValidatorClient::new(), + geo_ip: ThreadsafeGeoIp::new(), } } else { warn!( @@ -102,6 +105,7 @@ impl ExplorerApiStateContext { ping: ThreadsafePingCache::new(), validators: ThreadsafeValidatorCache::new(), validator_client: ThreadsafeValidatorClient::new(), + geo_ip: ThreadsafeGeoIp::new(), } } } diff --git a/explorer/.env.qa b/explorer/.env.qa index 60b30e1bd1..ed0868e732 100644 --- a/explorer/.env.qa +++ b/explorer/.env.qa @@ -1,6 +1,6 @@ -EXPLORER_API_URL=https://qa-explorer.nymtech.net/api/v1 -VALIDATOR_API_URL=https://qa-validator-api.nymtech.net -VALIDATOR_URL=https://qa-validator.nymtech.net -BIG_DIPPER_URL=https://qa-blocks.nymtech.net +EXPLORER_API_URL=https://qwerty-network-explorer.qa.nymte.ch/api/v1 +VALIDATOR_API_URL=https://qwerty-validator-api.qa.nymte.ch +VALIDATOR_URL=https://qwerty-validator.qa.nymte.ch +BIG_DIPPER_URL=https://qwerty-blocks.nymtech.net CURRENCY_DENOM=unym -CURRENCY_STAKING_DENOM=unyx +CURRENCY_STAKING_DENOM=unyx \ No newline at end of file diff --git a/explorer/src/api/constants.ts b/explorer/src/api/constants.ts index 442fd68ad1..5c2b044261 100644 --- a/explorer/src/api/constants.ts +++ b/explorer/src/api/constants.ts @@ -1,7 +1,7 @@ // master APIs export const API_BASE_URL = process.env.EXPLORER_API_URL; export const VALIDATOR_API_BASE_URL = process.env.VALIDATOR_API_URL; -export const { VALIDATOR_URL } = process.env; +export const VALIDATOR_BASE_URL = process.env.VALIDATOR_URL; export const BIG_DIPPER = process.env.BIG_DIPPER_URL; // specific API routes @@ -10,7 +10,7 @@ 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_API = `${VALIDATOR_API_BASE_URL}/api/v1/gateways`; -export const VALIDATORS_API = `${VALIDATOR_URL}/validators`; +export const VALIDATORS_API = `${VALIDATOR_BASE_URL}/validators`; export const BLOCK_API = `${VALIDATOR_API_BASE_URL}/block`; export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`; export const UPTIME_STORY_API = `${VALIDATOR_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this. diff --git a/explorer/src/components/Filters/Filters.tsx b/explorer/src/components/Filters/Filters.tsx index b4e7752bcd..a01d64361e 100644 --- a/explorer/src/components/Filters/Filters.tsx +++ b/explorer/src/components/Filters/Filters.tsx @@ -60,6 +60,7 @@ export const Filters = () => { const [showFilters, setShowFilters] = useState(false); const [isFiltered, setIsFiltered] = useState(false); const [filters, setFilters] = React.useState(); + const [upperSaturationValue, setUpperSaturationValue] = React.useState(100); const baseFilters = useRef(); const prevFilters = useRef(); @@ -67,11 +68,10 @@ export const Filters = () => { const handleToggleShowFilters = () => setShowFilters(!showFilters); const initialiseFilters = useCallback(async () => { - let upperSaturationValue; const allMixnodes = await Api.fetchMixnodes(); if (allMixnodes) { - upperSaturationValue = Math.round(Math.max(...allMixnodes.map((m) => m.stake_saturation)) * 100 + 1); - const initFilters = generateFilterSchema(upperSaturationValue); + setUpperSaturationValue(Math.round(Math.max(...allMixnodes.map((m) => m.stake_saturation)) * 100 + 1)); + const initFilters = generateFilterSchema(); baseFilters.current = initFilters; prevFilters.current = initFilters; setFilters(initFilters); @@ -79,8 +79,18 @@ export const Filters = () => { }, []); const handleOnChange = (id: EnumFilterKey, newValue: number[]) => { + if (id === 'stakeSaturation' && newValue[1] === 100) { + newValue.splice(1, 1, upperSaturationValue); + } setFilters((ftrs) => { - if (ftrs) return { ...ftrs, [id]: { ...ftrs[id], value: newValue } }; + if (ftrs) + return { + ...ftrs, + [id]: { + ...ftrs[id], + value: newValue, + }, + }; return undefined; }); }; @@ -142,11 +152,10 @@ export const Filters = () => { diff --git a/explorer/src/components/Filters/filterSchema.ts b/explorer/src/components/Filters/filterSchema.ts index 7c097c9918..c5011114b1 100644 --- a/explorer/src/components/Filters/filterSchema.ts +++ b/explorer/src/components/Filters/filterSchema.ts @@ -1,6 +1,6 @@ import { EnumFilterKey, TFilters } from '../../typeDefs/filters'; -export const generateFilterSchema = (upperSaturationValue?: number) => ({ +export const generateFilterSchema = () => ({ profitMargin: { label: 'Profit margin (%)', id: EnumFilterKey.profitMargin, @@ -25,13 +25,12 @@ export const generateFilterSchema = (upperSaturationValue?: number) => ({ stakeSaturation: { label: 'Stake saturation (%)', id: EnumFilterKey.stakeSaturation, - value: [0, upperSaturationValue || 100], + value: [0, 100], isSmooth: true, - marks: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, upperSaturationValue || 100].map((value) => ({ + marks: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) => ({ value: value < 100 ? value : 100, label: value < 100 ? value : '>100', })), - max: 100, tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards", }, routingScore: { diff --git a/explorer/src/components/MixNodes/Economics/Rows.ts b/explorer/src/components/MixNodes/Economics/Rows.ts index e4c674f551..ba2ad60b6c 100644 --- a/explorer/src/components/MixNodes/Economics/Rows.ts +++ b/explorer/src/components/MixNodes/Economics/Rows.ts @@ -5,11 +5,11 @@ import { EconomicsInfoRowWithIndex } from './types'; const selectionChance = (economicDynamicsStats: ApiState | undefined) => { const inclusionProbability = economicDynamicsStats?.data?.active_set_inclusion_probability; + // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow switch (inclusionProbability) { case 'High': case 'Good': case 'Low': - return inclusionProbability; default: return '-'; } diff --git a/explorer/src/components/MixNodes/StatusDropdown.tsx b/explorer/src/components/MixNodes/StatusDropdown.tsx index 1dba2c2f95..18263edf6e 100644 --- a/explorer/src/components/MixNodes/StatusDropdown.tsx +++ b/explorer/src/components/MixNodes/StatusDropdown.tsx @@ -46,7 +46,7 @@ export const MixNodeStatusDropdown: React.FC = ({ st } }} sx={{ - width: isMobile ? 'auto' : 200, + width: isMobile ? '50%' : 200, ...sx, }} > diff --git a/explorer/src/components/Nav.tsx b/explorer/src/components/Nav.tsx index ab6c2d36c8..96a0e43fbc 100644 --- a/explorer/src/components/Nav.tsx +++ b/explorer/src/components/Nav.tsx @@ -23,7 +23,7 @@ import { Footer } from './Footer'; import { DarkLightSwitchDesktop } from './Switch'; import { NavOptionType } from '../context/nav'; -const drawerWidth = 300; +const drawerWidth = 255; const openedMixin = (theme: Theme): CSSObject => ({ width: drawerWidth, @@ -182,7 +182,7 @@ export const ExpandableButton: React.FC = ({ background: isChild ? palette.nym.networkExplorer.nav.selected.nested : 'none', }} > - {Icon} + {Icon} = ({ width: '100%', marginBottom: 2, display: 'flex', - flexDirection: isMobile ? 'column-reverse' : 'row', + flexDirection: isMobile ? 'column' : 'row', justifyContent: 'space-between', }} > - - {childrenBefore} - - - + + + {childrenBefore} + + = ({ placeholder="search" onChange={(event) => onChangeSearch(event.target.value)} /> + + {withFilters && } {childrenAfter} diff --git a/explorer/src/components/Universal-DataGrid.tsx b/explorer/src/components/Universal-DataGrid.tsx index 7564ebd436..d800f23675 100644 --- a/explorer/src/components/Universal-DataGrid.tsx +++ b/explorer/src/components/Universal-DataGrid.tsx @@ -70,6 +70,24 @@ export const UniversalDataGrid: React.FC = ({ rows, columns, load width: '100%', border: 'none', }} + sx={{ + '*::-webkit-scrollbar': { + width: '1em', + }, + '*::-webkit-scrollbar-track': { + background: (t) => t.palette.nym.networkExplorer.scroll.backgroud, + outline: (t) => `1px solid ${t.palette.nym.networkExplorer.scroll.border}`, + boxShadow: 'auto', + borderRadius: 'auto', + }, + '*::-webkit-scrollbar-thumb': { + backgroundColor: (t) => t.palette.nym.networkExplorer.scroll.color, + borderRadius: '20px', + width: '.4em', + border: (t) => `3px solid ${t.palette.nym.networkExplorer.scroll.backgroud}`, + shadow: 'auto', + }, + }} /> ); return null; diff --git a/explorer/src/components/UptimeChart.tsx b/explorer/src/components/UptimeChart.tsx index 8d6c934775..51f2c9b62b 100644 --- a/explorer/src/components/UptimeChart.tsx +++ b/explorer/src/components/UptimeChart.tsx @@ -8,7 +8,7 @@ import { ApiState, UptimeStoryResponse } from '../typeDefs/explorer-api'; interface ChartProps { title?: string; xLabel: string; - yLabel: string; + yLabel?: string; uptimeStory: ApiState; loading: boolean; } @@ -23,7 +23,7 @@ export const UptimeChart: React.FC = ({ title, xLabel, yLabel, uptim const color = theme.palette.text.primary; React.useEffect(() => { if (uptimeStory.data?.history) { - const allFormattedChartData: FormattedChartData = [['Date', 'Uptime']]; + const allFormattedChartData: FormattedChartData = [['Date', 'Score']]; uptimeStory.data.history.forEach((eachDate) => { const formattedDateUptimeRecord: FormattedDateRecord = [ format(new Date(eachDate.date), 'MMM dd'), @@ -34,7 +34,7 @@ export const UptimeChart: React.FC = ({ title, xLabel, yLabel, uptim setFormattedChartData(allFormattedChartData); } else { const emptyData: any = [ - ['Date', 'Uptime'], + ['Date', 'Score'], ['Jul 27', 10], ]; setFormattedChartData(emptyData); @@ -55,7 +55,7 @@ export const UptimeChart: React.FC = ({ title, xLabel, yLabel, uptim uptimeStory.data ? formattedChartData : [ - ['Date', 'Uptime'], + ['Date', 'Routing Score'], [format(new Date(Date.now()), 'MMM dd'), 0], ] } @@ -87,7 +87,7 @@ export const UptimeChart: React.FC = ({ title, xLabel, yLabel, uptim }, }, vAxis: { - // % uptime vertical + // vertical / % Routing Score viewWindow: { min: 0, max: 100, diff --git a/explorer/src/pages/MixnodeDetail/index.tsx b/explorer/src/pages/MixnodeDetail/index.tsx index 121f451a52..ac53ebebf0 100644 --- a/explorer/src/pages/MixnodeDetail/index.tsx +++ b/explorer/src/pages/MixnodeDetail/index.tsx @@ -137,9 +137,9 @@ const PageMixnodeDetailWithState: React.FC = () => { {uptimeStory && ( - - {uptimeStory.error && } - + + {uptimeStory.error && } + )} diff --git a/explorer/src/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx index 3cf1d05e7b..8e4774e721 100644 --- a/explorer/src/pages/Mixnodes/index.tsx +++ b/explorer/src/pages/Mixnodes/index.tsx @@ -81,30 +81,12 @@ export const PageMixnodes: React.FC = () => { }; const columns: GridColDef[] = [ - { - field: 'owner', - headerName: 'Owner', - renderHeader: () => , - width: 200, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - - {splice(7, 29, params.value)} - - ), - }, { field: 'identity_key', headerName: 'Identity Key', renderHeader: () => , headerClassName: 'MuiDataGrid-header-override', - width: 180, + width: 170, headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( <> @@ -124,49 +106,13 @@ export const PageMixnodes: React.FC = () => { ), }, - { - field: 'location', - headerName: 'Location', - renderHeader: () => , - width: 150, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - - ), - }, - { - field: 'host', - headerName: 'Host', - renderHeader: () => , - headerClassName: 'MuiDataGrid-header-override', - width: 130, - headerAlign: 'left', - renderCell: (params: GridRenderCellParams) => ( - - {params.value} - - ), - }, { field: 'bond', headerName: 'Stake', renderHeader: () => , type: 'number', headerClassName: 'MuiDataGrid-header-override', - width: 200, + width: 170, headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( { { field: 'pledge_amount', headerName: 'Bond', - width: 200, + width: 175, headerClassName: 'MuiDataGrid-header-override', renderHeader: () => , type: 'number', @@ -265,6 +211,60 @@ export const PageMixnodes: React.FC = () => { ), }, + { + field: 'owner', + headerName: 'Owner', + renderHeader: () => , + width: 120, + headerAlign: 'left', + headerClassName: 'MuiDataGrid-header-override', + renderCell: (params: GridRenderCellParams) => ( + + {splice(7, 29, params.value)} + + ), + }, + { + field: 'location', + headerName: 'Location', + renderHeader: () => , + width: 120, + headerAlign: 'left', + headerClassName: 'MuiDataGrid-header-override', + renderCell: (params: GridRenderCellParams) => ( + + ), + }, + { + field: 'host', + headerName: 'Host', + renderHeader: () => , + headerClassName: 'MuiDataGrid-header-override', + width: 130, + headerAlign: 'left', + renderCell: (params: GridRenderCellParams) => ( + + {params.value} + + ), + }, ]; const handlePageSize = (event: SelectChangeEvent) => { diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index adf98f871d..178ab70c59 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -19,7 +19,7 @@ anyhow = "1.0.53" async-trait = { version = "0.1.51" } bip39 = "1.0.1" bs58 = "0.4.0" -clap = { version = "3.0.10", features = ["cargo", "derive"] } +clap = { version = "3.2", features = ["cargo", "derive"] } colored = "2.0" dashmap = "4.0" dirs = "4.0" @@ -39,7 +39,7 @@ sqlx = { version = "0.5", features = [ ] } subtle-encoding = { version = "0.5", features = ["bech32-preview"] } thiserror = "1" -tokio = { version = "1.19.1", features = [ +tokio = { version = "1.21.2", features = [ "rt-multi-thread", "net", "signal", @@ -55,6 +55,7 @@ coconut-interface = { path = "../common/coconut-interface", optional = true } credentials = { path = "../common/credentials" } config = { path = "../common/config" } crypto = { path = "../common/crypto" } +completions = { path = "../common/completions" } gateway-requests = { path = "gateway-requests" } mixnet-client = { path = "../common/client-libs/mixnet-client" } mixnode-common = { path = "../common/mixnode-common" } @@ -77,7 +78,7 @@ coconut = [ ] [build-dependencies] -tokio = { version = "1.19.1", features = ["rt-multi-thread", "macros"] } +tokio = { version = "1.21.2", features = ["rt-multi-thread", "macros"] } sqlx = { version = "0.5", features = [ "runtime-tokio-rustls", "sqlite", diff --git a/gateway/src/commands/mod.rs b/gateway/src/commands/mod.rs index 14c90031fd..223a77db8f 100644 --- a/gateway/src/commands/mod.rs +++ b/gateway/src/commands/mod.rs @@ -4,10 +4,13 @@ use std::{process, str::FromStr}; use crate::{config::Config, Cli}; +use clap::CommandFactory; use clap::Subcommand; use colored::Colorize; +use completions::{fig_generate, ArgShell}; use config::parse_validators; use crypto::bech32_address_validation; +use network_defaults::mainnet::read_var_if_not_default; use network_defaults::var_names::{ API_VALIDATOR, BECH32_PREFIX, CONFIGURED, NYMD_VALIDATOR, STATISTICS_SERVICE_DOMAIN_ADDRESS, }; @@ -34,6 +37,12 @@ pub(crate) enum Commands { /// Try to upgrade the gateway Upgrade(upgrade::Upgrade), + + /// Generate shell completions + Completions(ArgShell), + + /// Generate Fig specification + GenerateFigSpec, } // Configuration that can be overridden. @@ -55,12 +64,16 @@ pub(crate) struct OverrideConfig { } pub(crate) async fn execute(args: Cli) { + let bin_name = "nym-gateway"; + match &args.command { Commands::Init(m) => init::execute(m).await, Commands::NodeDetails(m) => node_details::execute(m).await, Commands::Run(m) => run::execute(m).await, Commands::Sign(m) => sign::execute(m), Commands::Upgrade(m) => upgrade::execute(m).await, + Commands::Completions(s) => s.generate(&mut crate::Cli::into_app(), bin_name), + Commands::GenerateFigSpec => fig_generate(&mut crate::Cli::into_app(), bin_name), } } @@ -97,27 +110,29 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi .expect("the provided statistics service url is invalid!"), ); } else if std::env::var(CONFIGURED).is_ok() { - let raw_url = std::env::var(STATISTICS_SERVICE_DOMAIN_ADDRESS) - .expect("statistics service url not set"); - config = config.with_custom_statistics_service_url( - raw_url - .parse() - .expect("the provided statistics service url is invalid"), - ) + if let Some(raw_url) = read_var_if_not_default(STATISTICS_SERVICE_DOMAIN_ADDRESS) { + config = config.with_custom_statistics_service_url( + raw_url + .parse() + .expect("the provided statistics service url is invalid"), + ); + } } if let Some(raw_validators) = args.validator_apis { config = config.with_custom_validator_apis(parse_validators(&raw_validators)); } else if std::env::var(CONFIGURED).is_ok() { - let raw_validators = std::env::var(API_VALIDATOR).expect("api validator not set"); - config = config.with_custom_validator_apis(parse_validators(&raw_validators)) + if let Some(raw_validators) = read_var_if_not_default(API_VALIDATOR) { + config = config.with_custom_validator_apis(::config::parse_validators(&raw_validators)) + } } if let Some(ref raw_validators) = args.validators { config = config.with_custom_validator_nymd(parse_validators(raw_validators)); } else if std::env::var(CONFIGURED).is_ok() { - let raw_validators = std::env::var(NYMD_VALIDATOR).expect("nymd validator not set"); - config = config.with_custom_validator_nymd(parse_validators(&raw_validators)) + if let Some(raw_validators) = read_var_if_not_default(NYMD_VALIDATOR) { + config = config.with_custom_validator_nymd(::config::parse_validators(&raw_validators)) + } } if let Some(wallet_address) = args.wallet_address { diff --git a/gateway/src/commands/upgrade.rs b/gateway/src/commands/upgrade.rs index cc62c5219e..7d8e5481a6 100644 --- a/gateway/src/commands/upgrade.rs +++ b/gateway/src/commands/upgrade.rs @@ -101,13 +101,13 @@ fn minor_0_12_upgrade( Version::new(0, 12, 0) }; - print_start_upgrade(&config_version, &to_version); + print_start_upgrade(config_version, &to_version); let upgraded_config = config.with_custom_version(to_version.to_string().as_ref()); upgraded_config.save_to_file(None).unwrap_or_else(|err| { eprintln!("failed to overwrite config file! - {:?}", err); - print_failed_upgrade(&config_version, &to_version); + print_failed_upgrade(config_version, &to_version); process::exit(1); }); diff --git a/integrations/bity/Cargo.toml b/integrations/bity/Cargo.toml new file mode 100644 index 0000000000..feae848a70 --- /dev/null +++ b/integrations/bity/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "nym-bity-integration" +version = "0.1.0" +edition = "2021" +rust-version = "1.56" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "1.0" +k256 = { version = "0.10", features = ["ecdsa", "sha256"] } +eyre = "0.6.5" + +cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" } + +nym-cli-commands = { path = "../../common/commands" } +validator-client = { path = "../../common/client-libs/validator-client", features = [ + "nymd-client", +] } + +[dev-dependencies] +anyhow = "1" diff --git a/integrations/bity/README.md b/integrations/bity/README.md new file mode 100644 index 0000000000..c06f59148e --- /dev/null +++ b/integrations/bity/README.md @@ -0,0 +1,53 @@ +# Buy NYM with Bity + +This crate allows Bity to verify orders for purchasing NYM tokens. The same crate is used by the wallet to sign orders for purchases. + +## Signing + +The Nym Wallet user will sign an order message provided by Bity to create a signed order with the following fields: + +``` +account_id: n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf +message: "This is the order message from Bity" +order signature: +{ + "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom" + }, + "signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A" +} +``` + +The `signature` field of the order contains a JSON representation of: + +- the Cosmos address of the signer (`account_id`) +- the Cosmos public key +- a hex string digest of the Bity order message signed by the user + +Note: the `signature_as_hex` is not in recoverable form (e.g. allows recovering the public key from the signature in `secp256k1`). This is why the public key is supplied along with the account id, as the prefix cannot be recovered. + +## Verification + +Verification has been wrapped up into taking a single struct that can be parsed from JSON: + +``` +{ + "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", + "message": "This is the order message from Bity", + "signature": << ORDER SIGNATURE JSON GOES HERE >> +} +``` + +The following will be checked: + +- the `account_id` supplied matches: + - the account id derived from the public key + - the account id field in the order signature JSON +- the account id is for Nym mainnet +- the signature is for the message +- all data structures parse correctly + - nested structs + - account ids + - Cosmos public keys \ No newline at end of file diff --git a/integrations/bity/src/lib.rs b/integrations/bity/src/lib.rs new file mode 100644 index 0000000000..9af11eecfb --- /dev/null +++ b/integrations/bity/src/lib.rs @@ -0,0 +1,220 @@ +pub mod order; +pub mod sign; +pub mod verify; + +#[cfg(test)] +mod tests { + use crate::order::{Order, OrderSignature}; + use crate::{sign, verify}; + use cosmrs::AccountId; + use std::str::FromStr; + use validator_client::nymd::wallet::DirectSecp256k1HdWallet; + + fn get_order(prefix: &str) -> anyhow::Result<(OrderSignature, String)> { + let mnemonic = "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 wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.parse().unwrap())?; + + let accounts = wallet.try_derive_accounts().unwrap(); + + let message = "This is the order message from Bity"; + let signature = sign::sign_order(&wallet, &accounts[0], message.to_string())?; + + Ok((signature, message.to_string())) + } + + #[test] + fn integration_happy_path() -> anyhow::Result<()> { + let (signature, message) = get_order("n")?; + + let account_id = AccountId::from_str("n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf").unwrap(); + assert_eq!(account_id.to_string(), signature.account_id.to_string()); + + println!("Order signature:"); + println!("{}", ::serde_json::to_string_pretty(&signature)?); + + Ok(verify::verify_order(Order { + account_id, + signature, + message, + })?) + } + + #[test] + fn integration_fails_on_non_mainnet_address() -> anyhow::Result<()> { + let (signature, message) = get_order("nymt")?; + + println!("Order signature:"); + println!("{}", ::serde_json::to_string_pretty(&signature)?); + + let res = verify::verify_order(Order { + account_id: signature.clone().account_id, + signature, + message, + }); + + println!("Expecting error, got: {:?}", res); + + assert!(res.is_err()); + + Ok(()) + } + + #[test] + fn integration_fails_on_non_mainnet_address_variant2() -> anyhow::Result<()> { + let (signature, message) = get_order("atom")?; + + println!("Order signature:"); + println!("{}", ::serde_json::to_string_pretty(&signature)?); + + let res = verify::verify_order(Order { + account_id: signature.clone().account_id, + signature, + message, + }); + + println!("Expecting error, got: {:?}", res); + + assert!(res.is_err()); + + Ok(()) + } + + #[test] + fn integration_change_account_id() -> anyhow::Result<()> { + let (signature, message) = get_order("n")?; + + let OrderSignature { + signature_as_hex, + public_key, + account_id: _, + } = signature; + + // use a different account id to the one that signed the order + let account_id = AccountId::from_str("n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es").unwrap(); + + let res = verify::verify_order(Order { + account_id: account_id.clone(), + signature: OrderSignature { + account_id, + signature_as_hex, + public_key, + }, + message, + }); + + println!("Expecting error, got: {:?}", res); + + assert!(res.is_err()); + + Ok(()) + } + + #[test] + fn integration_change_signature() -> anyhow::Result<()> { + let (signature, message) = get_order("n")?; + + let OrderSignature { + signature_as_hex: _, + public_key, + account_id, + } = signature; + + let res = verify::verify_order(Order { + account_id: account_id.clone(), + signature: OrderSignature { + account_id, + signature_as_hex: "this is not the signature you were looking for".to_string(), + public_key, + }, + message, + }); + + println!("Expecting error, got: {:?}", res); + + assert!(res.is_err()); + + Ok(()) + } + + #[test] + fn integration_with_json_happy_path() -> anyhow::Result<()> { + let json_order = r#"{ + "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", + "message": "This is the order message from Bity", + "signature": { + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom" + }, + "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", + "signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A" + } +}"#; + let order: Order = serde_json::from_str(json_order)?; + + Ok(verify::verify_order(order)?) + } + + #[test] + fn integration_with_json_bad_signature() -> anyhow::Result<()> { + let json_order = r#"{ + "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", + "message": "A different message to the one signed", + "signature": { + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom" + }, + "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", + "signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A" + } +}"#; + let order: Order = serde_json::from_str(json_order)?; + let res = verify::verify_order(order); + println!("Expecting error, got: {:?}", res); + assert!(res.is_err()); + Ok(()) + } + + #[test] + fn integration_with_json_bad_account_id() -> anyhow::Result<()> { + let json_order = r#"{ + "account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es", + "message": "This is the order message from Bity", + "signature": { + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom" + }, + "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", + "signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A" + } +}"#; + let order: Order = serde_json::from_str(json_order)?; + let res = verify::verify_order(order); + println!("Expecting error, got: {:?}", res); + assert!(res.is_err()); + Ok(()) + } + + #[test] + fn integration_with_json_bad_account_id_variation_2() -> anyhow::Result<()> { + let json_order = r#"{ + "account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", + "message": "This is the order message from Bity", + "signature": { + "public_key": { + "@type": "/cosmos.crypto.secp256k1.PubKey", + "key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom" + }, + "account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es", + "signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A" + } +}"#; + let order: Order = serde_json::from_str(json_order)?; + let res = verify::verify_order(order); + println!("Expecting error, got: {:?}", res); + assert!(res.is_err()); + Ok(()) + } +} diff --git a/integrations/bity/src/order.rs b/integrations/bity/src/order.rs new file mode 100644 index 0000000000..d5a4a24009 --- /dev/null +++ b/integrations/bity/src/order.rs @@ -0,0 +1,17 @@ +use cosmrs::crypto::PublicKey; +use cosmrs::AccountId; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct OrderSignature { + pub public_key: PublicKey, + pub account_id: AccountId, + pub signature_as_hex: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Order { + pub account_id: AccountId, + pub message: String, + pub signature: OrderSignature, +} diff --git a/integrations/bity/src/sign/mod.rs b/integrations/bity/src/sign/mod.rs new file mode 100644 index 0000000000..feed824868 --- /dev/null +++ b/integrations/bity/src/sign/mod.rs @@ -0,0 +1,18 @@ +use crate::order::OrderSignature; +use validator_client::nymd::error::NymdError; +use validator_client::nymd::wallet::{AccountData, DirectSecp256k1HdWallet}; + +/// Signs an order message to purchase Nym with Bity +pub fn sign_order( + wallet: &DirectSecp256k1HdWallet, + signer: &AccountData, + message: String, +) -> Result { + Ok(OrderSignature { + account_id: signer.address().clone(), + public_key: signer.public_key(), + signature_as_hex: wallet + .sign_raw_with_account(signer, &message.into_bytes())? + .to_string(), + }) +} diff --git a/integrations/bity/src/verify/mod.rs b/integrations/bity/src/verify/mod.rs new file mode 100644 index 0000000000..4fb4d77147 --- /dev/null +++ b/integrations/bity/src/verify/mod.rs @@ -0,0 +1,59 @@ +use thiserror::Error; + +use nym_cli_commands::validator::signature::helpers::secp256k1_verify_with_public_key; + +use crate::order::Order; + +#[derive(Error, Debug)] +pub enum VerifyOrderError { + #[error("{source}")] + K256Error { + #[from] + source: k256::ecdsa::Error, + }, + #[error("{source}")] + ErrorReport { + #[from] + source: eyre::Report, + }, + #[error("Account id does not match public key")] + AccountIdDoesNotMatchPubKey, + #[error("Unsupported key type. Only secp256k1 is currently supported.")] + UnsupportedKeyType, + #[error("Signature error - {0}")] + SignatureError(k256::ecdsa::signature::Error), + #[error("Account id is not a Nyx mainnet account")] + AccountIdPrefixIncorrect, +} + +/// Verifies an order +pub fn verify_order(order: Order) -> Result<(), VerifyOrderError> { + let account_id = order.signature.public_key.account_id("n")?; + + if order.signature.account_id.prefix() != "n" || order.account_id.prefix() != "n" { + return Err(VerifyOrderError::AccountIdPrefixIncorrect); + } + + // the account id in the order must match the account id derived from the public key + if account_id != order.signature.account_id { + return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey); + } + + // the user provided account id in the order must match the derived account id + if account_id != order.account_id || account_id != order.signature.account_id { + return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey); + } + + if order.signature.public_key.type_url() != cosmrs::crypto::PublicKey::SECP256K1_TYPE_URL { + return Err(VerifyOrderError::UnsupportedKeyType); + } + + match secp256k1_verify_with_public_key( + &order.signature.public_key.to_bytes(), + order.signature.signature_as_hex, + order.message, + ) { + Ok(()) => Ok(()), + Err(e) => Err(VerifyOrderError::SignatureError(e)), + } +} diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index 259649ce0b..a3cfe6c513 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -18,7 +18,7 @@ rust-version = "1.58.1" [dependencies] anyhow = "1.0.40" bs58 = "0.4.0" -clap = { version = "3.0.10", features = ["cargo", "derive"] } +clap = { version = "3.2", features = ["cargo", "derive"] } colored = "2.0" cupid = "0.6.1" dirs = "4.0" @@ -32,7 +32,7 @@ rand = "0.7.3" rocket = { version = "0.5.0-rc.2", features = ["json"] } serde = { version="1.0", features = ["derive"] } sysinfo = "0.24.1" -tokio = { version="1.19.1", features = ["rt-multi-thread", "net", "signal"] } +tokio = { version="1.21.2", features = ["rt-multi-thread", "net", "signal"] } tokio-util = { version="0.7.3", features = ["codec"] } toml = "0.5.8" url = { version = "2.2", features = ["serde"] } @@ -40,6 +40,7 @@ url = { version = "2.2", features = ["serde"] } ## internal config = { path="../common/config" } crypto = { path="../common/crypto" } +completions = { path="../common/completions" } mixnet-client = { path="../common/client-libs/mixnet-client" } mixnode-common = { path="../common/mixnode-common" } nonexhaustive-delayqueue = { path="../common/nonexhaustive-delayqueue" } @@ -51,7 +52,7 @@ validator-client = { path="../common/client-libs/validator-client" } version-checker = { path="../common/version-checker" } [dev-dependencies] -tokio = { version="1.19.1", features = ["rt-multi-thread", "net", "signal", "test-util"] } +tokio = { version="1.21.2", features = ["rt-multi-thread", "net", "signal", "test-util"] } nymsphinx-types = { path = "../common/nymsphinx/types" } nymsphinx-params = { path = "../common/nymsphinx/params" } diff --git a/mixnode/src/commands/mod.rs b/mixnode/src/commands/mod.rs index 62468ca809..f716953c11 100644 --- a/mixnode/src/commands/mod.rs +++ b/mixnode/src/commands/mod.rs @@ -4,8 +4,11 @@ use std::process; use crate::{config::Config, Cli}; +use clap::CommandFactory; use clap::Subcommand; use colored::Colorize; +use completions::{fig_generate, ArgShell}; +use config::defaults::mainnet::read_var_if_not_default; use config::{ defaults::var_names::{API_VALIDATOR, BECH32_PREFIX, CONFIGURED}, parse_validators, @@ -38,6 +41,12 @@ pub(crate) enum Commands { /// Show details of this mixnode NodeDetails(node_details::NodeDetails), + + /// Generate shell completions + Completions(ArgShell), + + /// Generate Fig specification + GenerateFigSpec, } // Configuration that can be overridden. @@ -53,6 +62,8 @@ struct OverrideConfig { } pub(crate) async fn execute(args: Cli) { + let bin_name = "nym-mixnode"; + match &args.command { Commands::Describe(m) => describe::execute(m), Commands::Init(m) => init::execute(m), @@ -60,6 +71,8 @@ pub(crate) async fn execute(args: Cli) { Commands::Sign(m) => sign::execute(m), Commands::Upgrade(m) => upgrade::execute(m), Commands::NodeDetails(m) => node_details::execute(m), + Commands::Completions(s) => s.generate(&mut crate::Cli::into_app(), bin_name), + Commands::GenerateFigSpec => fig_generate(&mut crate::Cli::into_app(), bin_name), } } @@ -85,8 +98,9 @@ fn override_config(mut config: Config, args: OverrideConfig) -> Config { if let Some(ref raw_validators) = args.validators { config = config.with_custom_validator_apis(parse_validators(raw_validators)); } else if std::env::var(CONFIGURED).is_ok() { - let raw_validators = std::env::var(API_VALIDATOR).expect("api validator not set"); - config = config.with_custom_validator_apis(parse_validators(&raw_validators)) + if let Some(raw_validators) = read_var_if_not_default(API_VALIDATOR) { + config = config.with_custom_validator_apis(::config::parse_validators(&raw_validators)) + } } if let Some(ref announce_host) = args.announce_host { diff --git a/mixnode/src/commands/upgrade.rs b/mixnode/src/commands/upgrade.rs index 85352e77c3..d104d47988 100644 --- a/mixnode/src/commands/upgrade.rs +++ b/mixnode/src/commands/upgrade.rs @@ -101,13 +101,13 @@ fn minor_0_12_upgrade( Version::new(0, 12, 0) }; - print_start_upgrade(&config_version, &to_version); + print_start_upgrade(config_version, &to_version); let upgraded_config = config.with_custom_version(to_version.to_string().as_ref()); upgraded_config.save_to_file(None).unwrap_or_else(|err| { eprintln!("failed to overwrite config file! - {:?}", err); - print_failed_upgrade(&config_version, &to_version); + print_failed_upgrade(config_version, &to_version); process::exit(1); }); diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index 7b443309c5..9d95d2eef8 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -268,18 +268,6 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "bitvec" -version = "0.20.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "blake2" version = "0.8.1" @@ -396,12 +384,6 @@ version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" -[[package]] -name = "byte-slice-cast" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c5fdd0166095e1d463fc6cc01aa8ce547ad77a4e84d42eb6762b084e28067e" - [[package]] name = "byte-tools" version = "0.3.1" @@ -571,6 +553,25 @@ dependencies = [ "textwrap", ] +[[package]] +name = "clap_complete" +version = "3.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f7a2e0a962c45ce25afce14220bc24f9dade0a1787f185cecf96bfba7847cd8" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_complete_fig" +version = "3.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed37b4c0c1214673eba6ad8ea31666626bf72be98ffb323067d973c48b4964b9" +dependencies = [ + "clap", + "clap_complete", +] + [[package]] name = "clap_derive" version = "3.2.18" @@ -603,6 +604,7 @@ dependencies = [ "futures", "gateway-client", "gateway-requests", + "gloo-timers", "humantime-serde", "log", "nonexhaustive-delayqueue", @@ -618,6 +620,9 @@ dependencies = [ "topology", "url", "validator-client", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-timer", ] [[package]] @@ -708,6 +713,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "completions" +version = "0.1.0" +dependencies = [ + "clap", + "clap_complete", + "clap_complete_fig", +] + [[package]] name = "config" version = "0.1.0" @@ -1457,49 +1471,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "ethabi" -version = "14.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01317735d563b3bad2d5f90d2e1799f414165408251abb762510f40e790e69a" -dependencies = [ - "anyhow", - "ethereum-types", - "hex", - "serde", - "serde_json", - "sha3", - "thiserror", - "uint", -] - -[[package]] -name = "ethbloom" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb684ac8fa8f6c5759f788862bb22ec6fe3cb392f6bfd08e3c64b603661e3f8" -dependencies = [ - "crunchy", - "fixed-hash", - "impl-rlp", - "impl-serde", - "tiny-keccak", -] - -[[package]] -name = "ethereum-types" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64b5df66a228d85e4b17e5d6c6aa43b0310898ffe8a85988c4c032357aaabfd" -dependencies = [ - "ethbloom", - "fixed-hash", - "impl-rlp", - "impl-serde", - "primitive-types", - "uint", -] - [[package]] name = "event-listener" version = "2.5.3" @@ -1584,18 +1555,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "fixed-hash" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" -dependencies = [ - "byteorder", - "rand 0.8.5", - "rustc-hex", - "static_assertions", -] - [[package]] name = "flate2" version = "1.0.24" @@ -1628,21 +1587,6 @@ dependencies = [ "spin 0.9.4", ] -[[package]] -name = "fluvio-wasm-timer" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b768c170dc045fa587a8f948c91f9bcfb87f774930477c6215addf54317f137f" -dependencies = [ - "futures", - "js-sys", - "parking_lot 0.11.2", - "pin-utils", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1695,12 +1639,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" -[[package]] -name = "funty" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" - [[package]] name = "futf" version = "0.1.5" @@ -1793,12 +1731,6 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" -[[package]] -name = "futures-timer" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" - [[package]] name = "futures-util" version = "0.3.24" @@ -1834,7 +1766,6 @@ dependencies = [ "credential-storage", "credentials", "crypto", - "fluvio-wasm-timer", "futures", "gateway-requests", "getrandom 0.2.7", @@ -1843,7 +1774,6 @@ dependencies = [ "nymsphinx", "pemstore", "rand 0.7.3", - "secp256k1", "task", "thiserror", "tokio", @@ -1853,8 +1783,8 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-timer", "wasm-utils", - "web3", ] [[package]] @@ -2124,6 +2054,18 @@ dependencies = [ "regex", ] +[[package]] +name = "gloo-timers" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gobject-sys" version = "0.15.10" @@ -2228,7 +2170,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.4", + "tokio-util", "tracing", ] @@ -2597,44 +2539,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "impl-codec" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" -dependencies = [ - "parity-scale-codec", -] - -[[package]] -name = "impl-rlp" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" -dependencies = [ - "rlp", -] - -[[package]] -name = "impl-serde" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4551f042f3438e64dbd6226b20527fc84a6e1fe65688b58746a2f53623f25f5c" -dependencies = [ - "serde", -] - -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "indenter" version = "0.3.3" @@ -2799,21 +2703,6 @@ dependencies = [ "treediff", ] -[[package]] -name = "jsonrpc-core" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" -dependencies = [ - "futures", - "futures-executor", - "futures-util", - "log", - "serde", - "serde_derive", - "serde_json", -] - [[package]] name = "k256" version = "0.10.4" @@ -3105,6 +2994,7 @@ dependencies = [ "bs58", "contracts-common", "cosmwasm-std", + "humantime-serde", "log", "schemars", "serde", @@ -3211,9 +3101,12 @@ dependencies = [ name = "nonexhaustive-delayqueue" version = "0.1.0" dependencies = [ + "futures-core", + "slab", "tokio", "tokio-stream", - "tokio-util 0.7.4", + "tokio-util", + "wasm-timer", ] [[package]] @@ -3347,6 +3240,7 @@ version = "1.0.2" dependencies = [ "clap", "client-core", + "completions", "config", "credential-storage", "crypto", @@ -3491,7 +3385,7 @@ dependencies = [ "bytes", "nymsphinx-params", "nymsphinx-types", - "tokio-util 0.7.4", + "tokio-util", ] [[package]] @@ -3668,32 +3562,6 @@ dependencies = [ "system-deps 6.0.2", ] -[[package]] -name = "parity-scale-codec" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" -dependencies = [ - "arrayvec 0.7.2", - "bitvec", - "byte-slice-cast", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "serde", -] - -[[package]] -name = "parity-scale-codec-derive" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "parking_lot" version = "0.11.2" @@ -4065,19 +3933,6 @@ dependencies = [ "log", ] -[[package]] -name = "primitive-types" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06345ee39fbccfb06ab45f3a1a5798d9dafa04cb8921a76d227040003a234b0e" -dependencies = [ - "fixed-hash", - "impl-codec", - "impl-rlp", - "impl-serde", - "uint", -] - [[package]] name = "proc-macro-crate" version = "1.2.1" @@ -4172,7 +4027,7 @@ dependencies = [ "socks5-requests", "task", "tokio", - "tokio-util 0.7.4", + "tokio-util", ] [[package]] @@ -4196,12 +4051,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" - [[package]] name = "rand" version = "0.6.5" @@ -4570,22 +4419,6 @@ dependencies = [ "opaque-debug 0.3.0", ] -[[package]] -name = "rlp" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999508abb0ae792aabed2460c45b89106d97fe4adac593bdaef433c2605847b5" -dependencies = [ - "bytes", - "rustc-hex", -] - -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - [[package]] name = "rustc_version" version = "0.3.3" @@ -4726,24 +4559,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secp256k1" -version = "0.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97d03ceae636d0fed5bae6a7f4f664354c5f4fcedf6eef053fef17e49f837d0a" -dependencies = [ - "secp256k1-sys", -] - -[[package]] -name = "secp256k1-sys" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957da2573cde917463ece3570eab4a0b3f19de6f1646cde62e6fd3868f566036" -dependencies = [ - "cc", -] - [[package]] name = "security-framework" version = "2.7.0" @@ -5124,21 +4939,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "soketto" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4919971d141dbadaa0e82b5d369e2d7666c98e4625046140615ca363e50d4daa" -dependencies = [ - "base64", - "bytes", - "futures", - "httparse", - "log", - "rand 0.8.5", - "sha-1", -] - [[package]] name = "soup2" version = "0.2.1" @@ -5903,20 +5703,11 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - [[package]] name = "tokio" -version = "1.21.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg 1.1.0", "bytes", @@ -5924,8 +5715,6 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", - "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2", @@ -5974,7 +5763,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", - "tokio-util 0.7.4", + "tokio-util", ] [[package]] @@ -5990,21 +5779,6 @@ dependencies = [ "tungstenite", ] -[[package]] -name = "tokio-util" -version = "0.6.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "log", - "pin-project-lite", - "tokio", -] - [[package]] name = "tokio-util" version = "0.7.4" @@ -6465,9 +6239,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -6475,13 +6249,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -6502,9 +6276,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6512,9 +6286,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -6525,9 +6299,23 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "git+https://github.com/mmsinclair/wasm-timer?rev=b9d1a54ad514c2f230a026afe0dde341e98cd7b6#b9d1a54ad514c2f230a026afe0dde341e98cd7b6" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] [[package]] name = "wasm-utils" @@ -6551,52 +6339,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web3" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd24abe6f2b68e0677f843059faea87bcbd4892e39f02886f366d8222c3c540d" -dependencies = [ - "arrayvec 0.5.2", - "base64", - "bytes", - "derive_more", - "ethabi", - "ethereum-types", - "futures", - "futures-timer", - "headers", - "hex", - "jsonrpc-core", - "log", - "parking_lot 0.11.2", - "pin-project", - "reqwest", - "rlp", - "secp256k1", - "serde", - "serde_json", - "soketto", - "tiny-keccak", - "tokio", - "tokio-stream", - "tokio-util 0.6.10", - "url", - "web3-async-native-tls", -] - -[[package]] -name = "web3-async-native-tls" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f6d8d1636b2627fe63518d5a9b38a569405d9c9bc665c43c9c341de57227ebb" -dependencies = [ - "native-tls", - "thiserror", - "tokio", - "url", -] - [[package]] name = "webkit2gtk" version = "0.18.0" @@ -6991,12 +6733,6 @@ dependencies = [ "windows-implement", ] -[[package]] -name = "wyz" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" - [[package]] name = "x11" version = "2.20.0" @@ -7074,4 +6810,4 @@ dependencies = [ "byteorder", "crc32fast", "crossbeam-utils", -] +] \ No newline at end of file diff --git a/nym-connect/src-tauri/Cargo.toml b/nym-connect/src-tauri/Cargo.toml index 00d79ec9eb..fd6ef638cf 100644 --- a/nym-connect/src-tauri/Cargo.toml +++ b/nym-connect/src-tauri/Cargo.toml @@ -35,7 +35,7 @@ tap = "1.0.1" tauri = { version = "^1.1.1", features = ["clipboard-write-text", "shell-open", "system-tray", "updater"] } tendermint-rpc = "0.23.0" thiserror = "1.0" -tokio = { version = "1.19.1", features = ["sync", "time"] } +tokio = { version = "1.21.2", features = ["sync", "time"] } url = "2.2" client-core = { path = "../../clients/client-core" } diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 629811ab68..1c44d275a5 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -665,7 +665,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", "serde", + "thiserror", ] [[package]] @@ -1260,7 +1262,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ "atty", - "humantime", + "humantime 1.3.0", "log", "regex", "termcolor", @@ -2045,6 +2047,22 @@ dependencies = [ "quick-error 1.2.3", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime 2.1.0", + "serde", +] + [[package]] name = "hyper" version = "0.14.17" @@ -2365,9 +2383,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.119" +version = "0.2.134" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" +checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb" [[package]] name = "line-wrap" @@ -2516,25 +2534,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", - "miow", - "ntapi", "wasi 0.11.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", + "windows-sys", ] [[package]] @@ -2544,6 +2551,7 @@ dependencies = [ "bs58", "contracts-common", "cosmwasm-std", + "humantime-serde", "log", "schemars", "serde", @@ -2637,15 +2645,6 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" -[[package]] -name = "ntapi" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" -dependencies = [ - "winapi", -] - [[package]] name = "num-derive" version = "0.3.3" @@ -2785,7 +2784,9 @@ dependencies = [ "config", "cosmrs", "cosmwasm-std", + "hex-literal", "mixnet-contract-common", + "network-defaults", "nym-types", "serde", "serde_json", @@ -4946,16 +4947,16 @@ checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" [[package]] name = "tokio" -version = "1.19.2" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ + "autocfg 1.1.0", "bytes", "libc", "memchr", "mio", "num_cpus", - "once_cell", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", @@ -5309,6 +5310,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", @@ -5322,7 +5324,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", + "log", "mixnet-contract-common", "schemars", "serde", diff --git a/nym-wallet/nym-wallet-recovery-cli/Cargo.toml b/nym-wallet/nym-wallet-recovery-cli/Cargo.toml index ff74de6709..e4ac1eb284 100644 --- a/nym-wallet/nym-wallet-recovery-cli/Cargo.toml +++ b/nym-wallet/nym-wallet-recovery-cli/Cargo.toml @@ -11,7 +11,7 @@ anyhow = "1.0" argon2 = "0.4" base64 = "0.13" bip39 = "1.0" -clap = { version = "3.2.20", features = ["derive"] } +clap = { version = "3.2", features = ["derive"] } log = "0.4" pretty_env_logger = "0.4" serde_json = "1.0.0" diff --git a/nym-wallet/nym-wallet-recovery-cli/src/main.rs b/nym-wallet/nym-wallet-recovery-cli/src/main.rs index 068745fcdd..e507581def 100644 --- a/nym-wallet/nym-wallet-recovery-cli/src/main.rs +++ b/nym-wallet/nym-wallet-recovery-cli/src/main.rs @@ -170,7 +170,7 @@ fn get_login_entry(login: &Value) -> Result<(&str, &str, &str)> { } fn base64_decode(ciphertext: &str, iv: &str, salt: &str) -> Result<(Vec, Vec, Vec)> { - let ciphertext = base64::decode(&ciphertext) + let ciphertext = base64::decode(ciphertext) .map_err(|err| anyhow!("Unable to base64 decode ciphertext: {}", err))?; let iv = base64::decode(iv).map_err(|err| anyhow!("Unable to base64 decode iv: {}", err))?; let salt = diff --git a/nym-wallet/nym-wallet-types/Cargo.toml b/nym-wallet/nym-wallet-types/Cargo.toml index 56d7f8ae72..322aca5c06 100644 --- a/nym-wallet/nym-wallet-types/Cargo.toml +++ b/nym-wallet/nym-wallet-types/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" rust-version = "1.58" [dependencies] +hex-literal = "0.3.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" strum = { version = "0.23", features = ["derive"] } @@ -14,6 +15,7 @@ cosmwasm-std = "1.0.0-beta8" cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" } config = { path = "../../common/config" } +network-defaults = { path = "../../common/network-defaults" } mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } validator-client = { path = "../../common/client-libs/validator-client", features = [ "nymd-client", diff --git a/nym-wallet/nym-wallet-types/src/network.rs b/nym-wallet/nym-wallet-types/src/network.rs index 8848b41da8..1be3c80732 100644 --- a/nym-wallet/nym-wallet-types/src/network.rs +++ b/nym-wallet/nym-wallet-types/src/network.rs @@ -1,11 +1,10 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use config::defaults::all::Network as ConfigNetwork; -use config::defaults::{mainnet, qa, sandbox, DenomDetails}; -use nym_types::currency::DecCoin; +use config::defaults::{mainnet, DenomDetails, NymNetworkDetails}; +use nym_types::{currency::DecCoin, error::TypesError}; use serde::{Deserialize, Serialize}; -use std::fmt; +use std::{fmt, ops::Not, str::FromStr}; use strum::EnumIter; #[allow(clippy::upper_case_acronyms)] @@ -67,23 +66,141 @@ impl fmt::Display for Network { } } -impl From for Network { - fn from(network: ConfigNetwork) -> Self { +impl From for NymNetworkDetails { + fn from(network: Network) -> Self { match network { - ConfigNetwork::QA => Network::QA, - ConfigNetwork::SANDBOX => Network::SANDBOX, - ConfigNetwork::MAINNET => Network::MAINNET, - ConfigNetwork::CUSTOM { .. } => panic!("custom network is not supported"), + Network::QA => qa::network_details(), + Network::SANDBOX => sandbox::network_details(), + Network::MAINNET => NymNetworkDetails::new_mainnet(), } } } -impl From for ConfigNetwork { - fn from(network: Network) -> Self { - match network { - Network::QA => ConfigNetwork::QA, - Network::SANDBOX => ConfigNetwork::SANDBOX, - Network::MAINNET => ConfigNetwork::MAINNET, +impl FromStr for Network { + type Err = TypesError; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "qa" => Ok(Network::QA), + "sandbox" => Ok(Network::SANDBOX), + "mainnet" => Ok(Network::MAINNET), + _ => Err(TypesError::UnknownNetwork(s.to_string())), + } + } +} + +fn parse_optional_str(raw: &str) -> Option { + raw.is_empty().not().then(|| raw.into()) +} + +mod sandbox { + use network_defaults::{ChainDetails, DenomDetails, NymContracts, ValidatorDetails}; + + use super::parse_optional_str; + + pub(crate) const BECH32_PREFIX: &str = "nymt"; + + pub(crate) const MIX_DENOM: DenomDetails = DenomDetails::new("unymt", "nymt", 6); + pub(crate) const STAKE_DENOM: DenomDetails = DenomDetails::new("unyxt", "nyxt", 6); + + pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt1ghd753shjuwexxywmgs4xz7x2q732vcnstz02j"; + pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt14ejqjyq8um4p3xfqj74yld5waqljf88fn549lh"; + pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = + "nymt17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9f8xzkv"; + pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = + "nymt1nz0r0au8aj6dc00wmm3ufy4g4k86rjzlgq608r"; + pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = + "nymt1k8re7jwz6rnnwrktnejdwkwnncte7ek7kk6fvg"; + pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] = + hex_literal::hex!("8e0DcFF7F3085235C32E845f3667aEB3f1e83133"); + pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = + hex_literal::hex!("E8883BAeF3869e14E4823F46662e81D4F7d2A81F"); + //pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = + //"nymt1jh0s6qu6tuw9ut438836mmn7f3f2wencrnmdj4"; + + //pub(crate) const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "http://0.0.0.0"; + pub(crate) fn validators() -> Vec { + vec![ValidatorDetails::new( + "https://sandbox-validator.nymtech.net", + Some("https://sandbox-validator.nymtech.net/api"), + )] + } + + pub(crate) fn network_details() -> network_defaults::NymNetworkDetails { + network_defaults::NymNetworkDetails { + chain_details: ChainDetails { + bech32_account_prefix: BECH32_PREFIX.to_string(), + mix_denom: MIX_DENOM.into(), + stake_denom: STAKE_DENOM.into(), + }, + endpoints: validators(), + contracts: NymContracts { + mixnet_contract_address: parse_optional_str(MIXNET_CONTRACT_ADDRESS), + vesting_contract_address: parse_optional_str(VESTING_CONTRACT_ADDRESS), + bandwidth_claim_contract_address: parse_optional_str( + BANDWIDTH_CLAIM_CONTRACT_ADDRESS, + ), + coconut_bandwidth_contract_address: parse_optional_str( + COCONUT_BANDWIDTH_CONTRACT_ADDRESS, + ), + multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), + }, + } + } +} + +mod qa { + use network_defaults::{ChainDetails, DenomDetails, NymContracts, ValidatorDetails}; + + use super::parse_optional_str; + + pub(crate) const BECH32_PREFIX: &str = "n"; + + pub(crate) const MIX_DENOM: DenomDetails = DenomDetails::new("unym", "nym", 6); + pub(crate) const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6); + + pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = + "n1frq2hzkjtatsupc6jtyaz67ytydk9nya437q92qg76ny3y8fcnjsw806vg"; + pub(crate) const VESTING_CONTRACT_ADDRESS: &str = + "n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g"; + pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = + "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; + pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = + "n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw"; + pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs"; + pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] = + hex_literal::hex!("0000000000000000000000000000000000000000"); + pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = + hex_literal::hex!("0000000000000000000000000000000000000000"); + //pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n17dcjwykjj9ydd8p9hk65uf2flrhhlfklju8flp"; + + //pub(crate) const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "http://0.0.0.0"; + pub(crate) fn validators() -> Vec { + vec![ValidatorDetails::new( + "https://adv-epoch-qa-validator.qa.nymte.ch/", + Some("https://adv-epoch-qa-val-api.qa.nymte.ch/api"), + )] + } + + pub(crate) fn network_details() -> network_defaults::NymNetworkDetails { + network_defaults::NymNetworkDetails { + chain_details: ChainDetails { + bech32_account_prefix: BECH32_PREFIX.to_string(), + mix_denom: MIX_DENOM.into(), + stake_denom: STAKE_DENOM.into(), + }, + endpoints: validators(), + contracts: NymContracts { + mixnet_contract_address: parse_optional_str(MIXNET_CONTRACT_ADDRESS), + vesting_contract_address: parse_optional_str(VESTING_CONTRACT_ADDRESS), + bandwidth_claim_contract_address: parse_optional_str( + BANDWIDTH_CLAIM_CONTRACT_ADDRESS, + ), + coconut_bandwidth_contract_address: parse_optional_str( + COCONUT_BANDWIDTH_CONTRACT_ADDRESS, + ), + multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), + }, } } } diff --git a/nym-wallet/package.json b/nym-wallet/package.json index d1493c43b0..2f8830b620 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -34,6 +34,7 @@ "@storybook/react": "^6.5.8", "@tauri-apps/api": "^1.0.2", "@tauri-apps/tauri-forage": "^1.0.0-beta.2", + "big.js": "^6.2.1", "bs58": "^4.0.1", "clsx": "^1.1.1", "date-fns": "^2.28.0", @@ -46,6 +47,7 @@ "react-error-boundary": "^3.1.3", "react-hook-form": "^7.14.2", "react-router-dom": "6", + "recharts": "^2.1.13", "semver": "^6.3.0", "string-to-color": "^2.2.2", "use-clipboard-copy": "^0.2.0", @@ -65,6 +67,7 @@ "@tauri-apps/cli": "^1.0.5", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", + "@types/big.js": "^6.1.6", "@types/bs58": "^4.0.1", "@types/jest": "^27.0.1", "@types/node": "^16.7.13", diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index 8fcfc95a6c..9da3a1b73e 100644 --- a/nym-wallet/src-tauri/src/config/mod.rs +++ b/nym-wallet/src-tauri/src/config/mod.rs @@ -13,8 +13,7 @@ use strum::IntoEnumIterator; use url::Url; use validator_client::nymd::AccountId as CosmosAccountId; -use config::defaults::all::Network; -use config::defaults::{all::SupportedNetworks, ValidatorDetails}; +use config::defaults::{DenomDetailsOwned, NymNetworkDetails, ValidatorDetails}; use nym_wallet_types::network::Network as WalletNetwork; use nym_wallet_types::network_config; @@ -138,7 +137,7 @@ impl Config { // One file per network for (network, config) in &self.networks { - let network = match Network::from_str(network).map(Into::into) { + let network = match WalletNetwork::from_str(network) { Ok(network) => network, Err(err) => { log::warn!("Unexpected name for network configuration, not saving: {err}"); @@ -198,7 +197,7 @@ impl Config { &self, network: WalletNetwork, ) -> impl Iterator + '_ { - self.base.networks.validators(network.into()).map(|v| { + self.base.networks.validators(&network).map(|v| { v.clone() .try_into() .expect("The hardcoded validators are assumed to be valid urls") @@ -218,7 +217,7 @@ impl Config { pub fn get_mixnet_contract_address(&self, network: WalletNetwork) -> CosmosAccountId { self.base .networks - .mixnet_contract_address(network.into()) + .mixnet_contract_address(&network) .expect("No mixnet contract address found in config") .parse() .expect("Wrong format for mixnet contract address") @@ -227,7 +226,7 @@ impl Config { pub fn get_vesting_contract_address(&self, network: WalletNetwork) -> CosmosAccountId { self.base .networks - .vesting_contract_address(network.into()) + .vesting_contract_address(&network) .expect("No vesting contract address found in config") .parse() .expect("Wrong format for vesting contract address") @@ -236,7 +235,7 @@ impl Config { pub fn get_bandwidth_claim_contract_address(&self, network: WalletNetwork) -> CosmosAccountId { self.base .networks - .bandwidth_claim_contract_address(network.into()) + .bandwidth_claim_contract_address(&network) .expect("No bandwidth claim contract address found in config") .parse() .expect("Wrong format for bandwidth claim contract address") @@ -421,6 +420,90 @@ impl fmt::Display for OptionalValidators { } } +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +struct SupportedNetworks { + networks: HashMap, +} + +impl SupportedNetworks { + fn new(support: Vec) -> Self { + SupportedNetworks { + networks: support + .into_iter() + .map(|n| { + let details = NetworkDetails::from(NymNetworkDetails::from(n)); + (n, details) + }) + .collect(), + } + } + + fn mixnet_contract_address(&self, network: &WalletNetwork) -> Option<&str> { + self.networks + .get(network) + .map(|network_details| network_details.mixnet_contract_address.as_str()) + } + + fn vesting_contract_address(&self, network: &WalletNetwork) -> Option<&str> { + self.networks + .get(network) + .map(|network_details| network_details.vesting_contract_address.as_str()) + } + + fn bandwidth_claim_contract_address(&self, network: &WalletNetwork) -> Option<&str> { + self.networks + .get(network) + .map(|network_details| network_details.bandwidth_claim_contract_address.as_str()) + } + + fn validators(&self, network: &WalletNetwork) -> impl Iterator { + self.networks + .get(network) + .map(|network_details| &network_details.validators) + .into_iter() + .flatten() + } +} + +// Simplified variant of NymNetworkDetails for serialization to config file +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +struct NetworkDetails { + bech32_prefix: String, + mix_denom: DenomDetailsOwned, + stake_denom: DenomDetailsOwned, + mixnet_contract_address: String, + vesting_contract_address: String, + bandwidth_claim_contract_address: String, + statistics_service_url: String, + validators: Vec, +} + +// Possibly a bit naff, but WalletNetwork is converted into the more general NymNetworkDetails, which here +// is converted to the format specific for serialization to config +impl From for NetworkDetails { + fn from(details: NymNetworkDetails) -> Self { + NetworkDetails { + bech32_prefix: details.chain_details.bech32_account_prefix, + mix_denom: details.chain_details.mix_denom, + stake_denom: details.chain_details.stake_denom, + mixnet_contract_address: details + .contracts + .mixnet_contract_address + .unwrap_or_default(), + vesting_contract_address: details + .contracts + .vesting_contract_address + .unwrap_or_default(), + bandwidth_claim_contract_address: details + .contracts + .bandwidth_claim_contract_address + .unwrap_or_default(), + statistics_service_url: "".to_string(), + validators: details.endpoints, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index d40bedc777..db6795ff49 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -64,6 +64,7 @@ fn main() { mixnet::bond::update_mixnode_config, mixnet::bond::get_number_of_mixnode_delegators, mixnet::bond::get_mix_node_description, + mixnet::bond::get_mixnode_avg_uptime, mixnet::delegate::delegate_to_mixnode, mixnet::delegate::get_pending_delegator_rewards, mixnet::delegate::get_pending_delegation_events, diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index 5d092e3f44..5811c007d1 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -4,8 +4,7 @@ use crate::network_config; use crate::state::{WalletAccountIds, WalletState}; use crate::wallet_storage::{self, DEFAULT_LOGIN_ID}; use bip39::{Language, Mnemonic}; -use config::defaults::all::Network; -use config::defaults::COSMOS_DERIVATION_PATH; +use config::defaults::{NymNetworkDetails, COSMOS_DERIVATION_PATH}; use cosmrs::bip32::DerivationPath; use itertools::Itertools; use nym_types::account::{Account, AccountEntry, Balance}; @@ -175,8 +174,8 @@ async fn run_connection_test( untested_api_urls: HashMap>, config: &Config, ) -> ( - HashMap>, - HashMap>, + HashMap>, + HashMap>, ) { let mixnet_contract_address = WalletNetwork::iter() .map(|network| (network.into(), config.get_mixnet_contract_address(network))) @@ -199,8 +198,8 @@ async fn run_connection_test( } fn create_clients( - nymd_urls: &HashMap>, - api_urls: &HashMap>, + nymd_urls: &HashMap>, + api_urls: &HashMap>, default_nymd_urls: &HashMap, default_api_urls: &HashMap, config: &Config, @@ -239,8 +238,8 @@ fn create_clients( log::info!("Connecting to: nymd_url: {nymd_url} for {network}"); log::info!("Connecting to: api_url: {api_url} for {network}"); - let network_details = Network::from(network) - .details() + let network_details = NymNetworkDetails::from(network) + .clone() .with_mixnet_contract(Some(config.get_mixnet_contract_address(network).as_ref())) .with_vesting_contract(Some(config.get_vesting_contract_address(network).as_ref())) .with_bandwidth_claim_contract(Some( @@ -260,7 +259,7 @@ fn create_clients( } fn select_random_responding_url( - urls: &HashMap>, + urls: &HashMap>, network: WalletNetwork, ) -> Option { urls.get(&network.into()).and_then(|urls| { @@ -273,7 +272,7 @@ fn select_random_responding_url( } fn select_first_responding_url( - urls: &HashMap>, + urls: &HashMap>, network: WalletNetwork, //config: &Config, ) -> Option { @@ -426,8 +425,8 @@ pub async fn add_account_for_password( let address = { let state = state.read().await; - let network: Network = state.current_network().into(); - derive_address(mnemonic, &network.bech32_prefix())?.to_string() + let network: NymNetworkDetails = state.current_network().into(); + derive_address(mnemonic, &network.chain_details.bech32_account_prefix)?.to_string() }; // Re-read all the acccounts from the wallet to reset the state, rather than updating it @@ -466,10 +465,14 @@ async fn set_state_with_all_accounts( let mnemonic = account.mnemonic(); let addresses: HashMap = WalletNetwork::iter() .map(|network| { - let config_network: Network = network.into(); + let config_network: NymNetworkDetails = network.into(); ( network, - derive_address(mnemonic.clone(), &config_network.bech32_prefix()).unwrap(), + derive_address( + mnemonic.clone(), + &config_network.chain_details.bech32_account_prefix, + ) + .unwrap(), ) }) .collect(); @@ -562,16 +565,11 @@ fn _show_mnemonic_for_account_in_password( let stored_account = wallet_storage::load_existing_login(login_id, password)?; let mnemonic = match stored_account { wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(), - wallet_storage::StoredLogin::Multiple(ref accounts) => { - for account in accounts.get_accounts() { - log::debug!("{:?}", account); - } - accounts - .get_account(account_id) - .ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)? - .mnemonic() - .clone() - } + wallet_storage::StoredLogin::Multiple(ref accounts) => accounts + .get_account(account_id) + .ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)? + .mnemonic() + .clone(), }; Ok(mnemonic) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 08f9dc1daa..7098577545 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -4,7 +4,7 @@ use crate::error::BackendError; use crate::state::WalletState; use crate::{nymd_client, Gateway, MixNode}; -use mixnet_contract_common::{MixNodeConfigUpdate, NodeId}; +use mixnet_contract_common::{MixId, MixNodeConfigUpdate}; use nym_types::currency::DecCoin; use nym_types::gateway::GatewayBond; use nym_types::mixnode::{MixNodeCostParams, MixNodeDetails}; @@ -170,6 +170,32 @@ pub async fn update_mixnode_config( )?) } +#[tauri::command] +pub async fn get_mixnode_avg_uptime( + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::info!(">>> Get mixnode bond details"); + let guard = state.read().await; + let client = guard.current_client()?; + let res = client.nymd.get_owned_mixnode(client.nymd.address()).await?; + + match res.mixnode_details { + Some(details) => { + let id = details.mix_id(); + log::trace!(" >>> Get average uptime percentage: mix_id = {}", id); + let avg_uptime_percent = client + .validator_api + .get_mixnode_avg_uptime(id) + .await + .ok() + .map(|r| r.avg_uptime); + log::trace!(" <<< {:?}", avg_uptime_percent); + Ok(avg_uptime_percent) + } + None => Ok(None), + } +} + #[tauri::command] pub async fn mixnode_bond_details( state: tauri::State<'_, WalletState>, @@ -190,7 +216,7 @@ pub async fn mixnode_bond_details( log::info!( "<<< mix_id/identity_key = {:?}", details.as_ref().map(|r| ( - r.bond_information.id, + r.bond_information.mix_id, &r.bond_information.mix_node.identity_key )) ); @@ -263,7 +289,7 @@ pub async fn get_pending_operator_rewards( #[tauri::command] pub async fn get_number_of_mixnode_delegators( - mix_id: NodeId, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(nymd_client!(state) diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 7b94ede3ab..7fa05ef0ea 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -4,7 +4,7 @@ use crate::error::BackendError; use crate::state::WalletState; use crate::vesting::delegate::vesting_undelegate_from_mixnode; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use nym_types::currency::DecCoin; use nym_types::delegation::{Delegation, DelegationWithEverything, DelegationsSummaryResponse}; use nym_types::deprecated::{ @@ -64,7 +64,7 @@ pub async fn get_pending_delegation_events( #[tauri::command] pub async fn delegate_to_mixnode( - mix_id: NodeId, + mix_id: MixId, amount: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, @@ -94,7 +94,7 @@ pub async fn delegate_to_mixnode( #[tauri::command] pub async fn undelegate_from_mixnode( - mix_id: NodeId, + mix_id: MixId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -120,7 +120,7 @@ pub async fn undelegate_from_mixnode( #[tauri::command] pub async fn undelegate_all_from_mixnode( - mix_id: NodeId, + mix_id: MixId, uses_vesting_contract_tokens: bool, fee_liquid: Option, fee_vesting: Option, @@ -291,7 +291,7 @@ pub async fn get_all_mix_delegations( } fn filter_pending_events( - mix_id: NodeId, + mix_id: MixId, pending_events: &[WrappedDelegationEvent], ) -> Vec { pending_events @@ -305,7 +305,7 @@ fn filter_pending_events( #[tauri::command] pub async fn get_pending_delegator_rewards( address: String, - mix_id: NodeId, + mix_id: MixId, proxy: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs index 6305ef981d..6fb8893ee9 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs @@ -1,7 +1,7 @@ use crate::error::BackendError; use crate::state::WalletState; use crate::vesting::rewards::vesting_claim_delegator_reward; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use nym_types::transaction::TransactionExecuteResult; use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; use validator_client::nymd::Fee; @@ -29,7 +29,7 @@ pub async fn claim_operator_reward( #[tauri::command] pub async fn claim_delegator_reward( - mix_id: NodeId, + mix_id: MixId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -50,7 +50,7 @@ pub async fn claim_delegator_reward( #[tauri::command] pub async fn claim_locked_and_unlocked_delegator_reward( - mix_id: NodeId, + mix_id: MixId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index c41e692356..51912e1332 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -5,7 +5,7 @@ use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; use mixnet_contract_common::MixNodeConfigUpdate; -use mixnet_contract_common::{ExecuteMsg, Gateway, MixNode, NodeId}; +use mixnet_contract_common::{ExecuteMsg, Gateway, MixId, MixNode}; use nym_types::currency::DecCoin; use nym_types::mixnode::MixNodeCostParams; @@ -123,7 +123,7 @@ pub async fn simulate_update_mixnode_config( #[tauri::command] pub async fn simulate_delegate_to_mixnode( - mix_id: NodeId, + mix_id: MixId, amount: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { @@ -137,7 +137,7 @@ pub async fn simulate_delegate_to_mixnode( #[tauri::command] pub async fn simulate_undelegate_from_mixnode( - mix_id: NodeId, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { simulate_mixnet_operation(ExecuteMsg::UndelegateFromMixnode { mix_id }, None, &state).await @@ -152,7 +152,7 @@ pub async fn simulate_claim_operator_reward( #[tauri::command] pub async fn simulate_claim_delegator_reward( - mix_id: NodeId, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { simulate_mixnet_operation(ExecuteMsg::WithdrawDelegatorReward { mix_id }, None, &state).await diff --git a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs index bd9a5b341a..c7cebf2ce5 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -5,7 +5,7 @@ use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; use mixnet_contract_common::MixNodeConfigUpdate; -use mixnet_contract_common::{Gateway, MixNode, NodeId}; +use mixnet_contract_common::{Gateway, MixId, MixNode}; use nym_types::currency::DecCoin; use nym_types::mixnode::MixNodeCostParams; use vesting_contract_common::ExecuteMsg; @@ -130,7 +130,7 @@ pub async fn simulate_vesting_update_mixnode_config( #[tauri::command] pub async fn simulate_vesting_delegate_to_mixnode( - mix_id: NodeId, + mix_id: MixId, amount: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { @@ -147,7 +147,7 @@ pub async fn simulate_vesting_delegate_to_mixnode( #[tauri::command] pub async fn simulate_vesting_undelegate_from_mixnode( - mix_id: NodeId, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { simulate_vesting_operation(ExecuteMsg::UndelegateFromMixnode { mix_id }, None, &state).await @@ -172,7 +172,7 @@ pub async fn simulate_vesting_claim_operator_reward( #[tauri::command] pub async fn simulate_vesting_claim_delegator_reward( - mix_id: NodeId, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { simulate_vesting_operation(ExecuteMsg::ClaimDelegatorReward { mix_id }, None, &state).await diff --git a/nym-wallet/src-tauri/src/operations/validator_api/status.rs b/nym-wallet/src-tauri/src/operations/validator_api/status.rs index 56fdc23e9c..aefca433ce 100644 --- a/nym-wallet/src-tauri/src/operations/validator_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/validator_api/status.rs @@ -4,7 +4,7 @@ use crate::api_client; use crate::error::BackendError; use crate::state::WalletState; -use mixnet_contract_common::{IdentityKeyRef, NodeId}; +use mixnet_contract_common::{IdentityKeyRef, MixId}; use validator_client::models::{ GatewayCoreStatusResponse, InclusionProbabilityResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, @@ -12,7 +12,7 @@ use validator_client::models::{ #[tauri::command] pub async fn mixnode_core_node_status( - mix_id: NodeId, + mix_id: MixId, since: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -34,7 +34,7 @@ pub async fn gateway_core_node_status( #[tauri::command] pub async fn mixnode_status( - mix_id: NodeId, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state).get_mixnode_status(mix_id).await?) @@ -42,7 +42,7 @@ pub async fn mixnode_status( #[tauri::command] pub async fn mixnode_reward_estimation( - mix_id: NodeId, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) @@ -52,7 +52,7 @@ pub async fn mixnode_reward_estimation( #[tauri::command] pub async fn mixnode_stake_saturation( - mix_id: NodeId, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) @@ -62,7 +62,7 @@ pub async fn mixnode_stake_saturation( #[tauri::command] pub async fn mixnode_inclusion_probability( - mix_id: NodeId, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) diff --git a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs index 28bb8eaa6d..59183878e5 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs @@ -3,14 +3,14 @@ use crate::error::BackendError; use crate::state::WalletState; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use nym_types::currency::DecCoin; use nym_types::transaction::TransactionExecuteResult; use validator_client::nymd::{Fee, VestingSigningClient}; #[tauri::command] pub async fn vesting_delegate_to_mixnode( - mix_id: NodeId, + mix_id: MixId, amount: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, @@ -40,7 +40,7 @@ pub async fn vesting_delegate_to_mixnode( #[tauri::command] pub async fn vesting_undelegate_from_mixnode( - mix_id: NodeId, + mix_id: MixId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs index 347ae3576f..ad3760a364 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs @@ -3,7 +3,7 @@ use crate::error::BackendError; use crate::state::WalletState; -use mixnet_contract_common::NodeId; +use mixnet_contract_common::MixId; use nym_types::transaction::TransactionExecuteResult; use validator_client::nymd::Fee; @@ -29,7 +29,7 @@ pub async fn vesting_claim_operator_reward( #[tauri::command] pub async fn vesting_claim_delegator_reward( - mix_id: NodeId, + mix_id: MixId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/state.rs b/nym-wallet/src-tauri/src/state.rs index 572c0bddf4..e36dfe1b5c 100644 --- a/nym-wallet/src-tauri/src/state.rs +++ b/nym-wallet/src-tauri/src/state.rs @@ -1,6 +1,7 @@ use crate::config; use crate::error::BackendError; use crate::simulate::SimulateResult; +use ::config::defaults::NymNetworkDetails; use cosmwasm_std::Decimal; use itertools::Itertools; use log::warn; @@ -215,8 +216,9 @@ impl WalletStateInner { } pub fn register_default_denoms(&mut self, network: Network) { + let details = NymNetworkDetails::from(network); self.registered_coins - .insert(network, RegisteredCoins::default_denoms(network.into())); + .insert(network, RegisteredCoins::default_denoms(&details)); } pub fn set_network(&mut self, network: Network) { diff --git a/nym-wallet/src-tauri/src/utils.rs b/nym-wallet/src-tauri/src/utils.rs index 1efcd43ea1..f04d47722f 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -5,7 +5,7 @@ use crate::error::BackendError; use crate::nymd_client; use crate::state::WalletState; use cosmwasm_std::Decimal; -use mixnet_contract_common::{IdentityKey, NodeId, Percent}; +use mixnet_contract_common::{IdentityKey, MixId, Percent}; use nym_types::currency::DecCoin; use nym_types::mixnode::MixNodeCostParams; use nym_wallet_types::app::AppEnv; @@ -51,7 +51,7 @@ pub async fn owns_gateway(state: tauri::State<'_, WalletState>) -> Result, mix_identity: IdentityKey, -) -> Result, BackendError> { +) -> Result, BackendError> { let res = nymd_client!(state) .get_mixnode_details_by_identity(mix_identity) .await?; diff --git a/nym-wallet/src/components/Accounts/AccountAvatar.tsx b/nym-wallet/src/components/Accounts/AccountAvatar.tsx index 9335c1a743..c5ed939f2d 100644 --- a/nym-wallet/src/components/Accounts/AccountAvatar.tsx +++ b/nym-wallet/src/components/Accounts/AccountAvatar.tsx @@ -4,7 +4,7 @@ import stc from 'string-to-color'; import { TAccount } from 'src/types'; export const AccountAvatar = ({ name, small }: { name: TAccount['name']; small?: boolean }) => ( - + {name?.split('')[0]} diff --git a/nym-wallet/src/components/Accounts/AccountOverview.tsx b/nym-wallet/src/components/Accounts/AccountOverview.tsx index 5df9cdf2ed..95c3245ae3 100644 --- a/nym-wallet/src/components/Accounts/AccountOverview.tsx +++ b/nym-wallet/src/components/Accounts/AccountOverview.tsx @@ -6,7 +6,7 @@ import { AccountAvatar } from './AccountAvatar'; export const AccountOverview = ({ account, onClick }: { account: AccountEntry; onClick: () => void }) => ( - - + + + + Accounts + + + + + + Switch between accounts + + + + {accounts?.map(({ id, address }) => ( + { + if (selectedAccount?.id !== id) { + setAccountToSwitchTo(id); + } + }} + /> + ))} + + + + + + + ); }; diff --git a/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx b/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx index 9c03cb164a..8a76dba709 100644 --- a/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx +++ b/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx @@ -115,7 +115,7 @@ const NameAccount = ({ onNext, onBack }: { onNext: (value: string) => void; onBa {error} { setValue(e.target.value); diff --git a/nym-wallet/src/components/Accounts/modals/ConfirmPasswordModal.tsx b/nym-wallet/src/components/Accounts/modals/ConfirmPasswordModal.tsx index 29694fcda8..07baa9b8d8 100644 --- a/nym-wallet/src/components/Accounts/modals/ConfirmPasswordModal.tsx +++ b/nym-wallet/src/components/Accounts/modals/ConfirmPasswordModal.tsx @@ -1,6 +1,6 @@ import React, { useContext } from 'react'; -import { Box, Paper, Dialog, DialogTitle, IconButton, Typography } from '@mui/material'; -import { ArrowBack } from '@mui/icons-material'; +import { Paper, Dialog, DialogTitle, Typography } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; import { ConfirmPassword } from 'src/components/ConfirmPassword'; import { AccountsContext } from 'src/context'; @@ -14,28 +14,32 @@ export const ConfirmPasswordModal = ({ onConfirm: (password: string) => Promise; }) => { const { isLoading, error } = useContext(AccountsContext); + const theme = useTheme(); return ( - - Switch account - - Confirm password - - - + + + Switch account + + Confirm password + + + + ); }; diff --git a/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx b/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx index 91a0a65e9d..8ddbb207b3 100644 --- a/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx +++ b/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx @@ -12,6 +12,7 @@ import { Typography, } from '@mui/material'; import { Close } from '@mui/icons-material'; +import { useTheme } from '@mui/material/styles'; import { AccountsContext } from 'src/context'; export const EditAccountModal = () => { @@ -19,6 +20,8 @@ export const EditAccountModal = () => { const { accountToEdit, dialogToDisplay, setDialogToDisplay, handleEditAccount } = useContext(AccountsContext); + const theme = useTheme(); + useEffect(() => { setAccountName(accountToEdit ? accountToEdit?.id : ''); }, [accountToEdit]); @@ -28,48 +31,51 @@ export const EditAccountModal = () => { open={dialogToDisplay === 'Edit'} onClose={() => setDialogToDisplay('Accounts')} fullWidth - PaperComponent={Paper} - PaperProps={{ elevation: 0 }} + PaperProps={{ + style: { border: `1px solid ${theme.palette.nym.nymWallet.modal.border}` }, + }} > - - - Edit account name - setDialogToDisplay('Accounts')}> - - - - - New wallet address - - - - - + + + Edit account name + setDialogToDisplay('Accounts')}> + + + + + New wallet address + + + + + setAccountName(e.target.value)} + autoFocus + /> + + + + - + disableElevation + variant="contained" + size="large" + onClick={() => { + if (accountToEdit) { + handleEditAccount({ ...accountToEdit, id: accountName }); + setDialogToDisplay('Accounts'); + } + }} + disabled={!accountName?.length} + > + Edit + + + ); }; diff --git a/nym-wallet/src/components/Accounts/modals/MnemonicModal.tsx b/nym-wallet/src/components/Accounts/modals/MnemonicModal.tsx index cffdeea213..bede36211b 100644 --- a/nym-wallet/src/components/Accounts/modals/MnemonicModal.tsx +++ b/nym-wallet/src/components/Accounts/modals/MnemonicModal.tsx @@ -15,6 +15,7 @@ import { ArrowBackSharp } from '@mui/icons-material'; import { AccountsContext } from 'src/context'; import { useClipboard } from 'use-clipboard-copy'; import { PasswordInput, Mnemonic } from 'src/components'; +import { StyledBackButton } from 'src/components/StyledBackButton'; export const MnemonicModal = () => { const [password, setPassword] = useState(''); @@ -50,9 +51,6 @@ export const MnemonicModal = () => { Display mnemonic - - - theme.palette.text.disabled }}> {`Display mnemonic for: ${accountMnemonic?.accountName}`} @@ -80,7 +78,8 @@ export const MnemonicModal = () => { )} - + + {!accountMnemonic.value && ( - } - > - - {network && ( - - Check more stats of your node on the{' '} - - explorer - - - )} - + + + + + Mix node + + + + {name && ( + + + {name} + + + )} + + + } + Action={ + isMixnode(mixnode) && ( + + ) + } + > + + {network && ( + + Check more stats of your node on the{' '} + + explorer + + + )} + + + ); }; diff --git a/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx b/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx index 83cc2a0198..37b52452c7 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx @@ -1,16 +1,18 @@ import React, { useState } from 'react'; import { Typography } from '@mui/material'; import { ActionsMenu, ActionsMenuItem } from 'src/components/ActionsMenu'; -import { Unbond as UnbondIcon } from '../../svg-icons'; +import { Unbond as UnbondIcon, Bond as BondIcon } from '../../svg-icons'; -export type TBondedMixnodeActions = 'nodeSettings' | 'bondMore' | 'unbond' | 'redeem' | 'compound'; +export type TBondedMixnodeActions = 'nodeSettings' | 'bondMore' | 'unbond' | 'redeem'; export const BondedMixnodeActions = ({ onActionSelect, disabledRedeemAndCompound, + disabledBondMore, }: { onActionSelect: (action: TBondedMixnodeActions) => void; disabledRedeemAndCompound: boolean; + disabledBondMore?: boolean; }) => { const [isOpen, setIsOpen] = useState(false); @@ -24,25 +26,24 @@ export const BondedMixnodeActions = ({ return ( + {!disabledBondMore && ( + } + onClick={() => handleActionClick('bondMore')} + /> + )} + R} + onClick={() => handleActionClick('redeem')} + disabled={disabledRedeemAndCompound} + /> } onClick={() => handleActionClick('unbond')} /> - C} - description={disabledRedeemAndCompound ? 'No rewards to compound' : 'Add your rewards to you balance'} - onClick={() => handleActionClick('compound')} - disabled={disabledRedeemAndCompound} - /> - R} - description={disabledRedeemAndCompound ? 'No rewards to redeem' : 'Add your rewards to you balance'} - onClick={() => handleActionClick('redeem')} - disabled={disabledRedeemAndCompound} - /> ); }; diff --git a/nym-wallet/src/components/Bonding/NodeStats.tsx b/nym-wallet/src/components/Bonding/NodeStats.tsx new file mode 100644 index 0000000000..65e6f7436e --- /dev/null +++ b/nym-wallet/src/components/Bonding/NodeStats.tsx @@ -0,0 +1,142 @@ +import React from 'react'; +import { Stack, Typography, Box, useTheme, Grid, LinearProgress, LinearProgressProps } from '@mui/material'; +import { TBondedMixnode } from 'src/context'; +import { Cell, Pie, PieChart, Legend, ResponsiveContainer } from 'recharts'; +import { SelectionChance } from '@nymproject/types'; +import { NymCard } from '../NymCard'; +import { InfoTooltip } from '../InfoToolTip'; + +const LinearProgressWithLabel = (props: LinearProgressProps & { value: number }) => { + const { value } = props; + return ( + + + {`${Math.round(value)}%`} + + + + + + ); +}; + +const StatRow = ({ + label, + tooltipText, + value, + textColor, + progressValue, +}: { + label: string; + tooltipText?: string; + value: string | number; + textColor?: string; + progressValue?: number; +}) => ( + + t.palette.nym.text.muted }}> + {tooltipText && } + {label} + + {typeof progressValue === 'number' ? ( + + ) : ( + {value} + )} + +); + +export const NodeStats = ({ mixnode }: { mixnode: TBondedMixnode }) => { + const { activeSetProbability, routingScore } = mixnode; + const theme = useTheme(); + + // clamp routing score to [0-100] + const score = Math.min(Math.max(routingScore, 0), 100); + + const data = [ + { key: 'routingScore', value: score }, + { key: 'rest', value: 100 - score }, + ]; + const colors = [theme.palette.success.main, theme.palette.nym.nymWallet.chart.grey]; + + const getSetProbabilityLabel = (chance?: SelectionChance): { value: string; color?: string } => { + switch (chance) { + case 'High': + return { value: 'High', color: theme.palette.nym.success }; + case 'Good': + return { value: 'Good' }; + case 'Low': + return { value: 'Low', color: theme.palette.nym.red }; + default: + return { value: 'Unknown' }; + } + }; + + const renderLegend = () => ( + + + {routingScore}% + + + ); + + const activeSetProb = getSetProbabilityLabel(activeSetProbability); + + return ( + + + + Node stats + + } + > + + + + + {data.map(({ key }, index) => ( + + ))} + + + + + Routing score + + + + + + + ); +}; diff --git a/nym-wallet/src/components/Bonding/NodeTable.tsx b/nym-wallet/src/components/Bonding/NodeTable.tsx index d1e620a1ce..9fd1692eba 100644 --- a/nym-wallet/src/components/Bonding/NodeTable.tsx +++ b/nym-wallet/src/components/Bonding/NodeTable.tsx @@ -28,7 +28,7 @@ export const NodeTable = ({ headers, cells }: TableProps) => ( {headers.map(({ header, id, tooltipText }) => ( - + {tooltipText && } {header} diff --git a/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx b/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx index c4f827dcd5..12403f5e98 100644 --- a/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx @@ -32,7 +32,7 @@ const NodeFormData = ({ gatewayData, onNext }: { gatewayData: GatewayData; onNex }, []); return ( - + - + {showAdvancedOptions && ( - + - + + {hasVestingTokens && setValue('tokenPool', pool)} />} + setValue('operatorCost', newValue, { shouldValidate: true })} + validationError={errors.operatorCost?.amount?.message} + denom={denom} + initialValue={amountData.operatorCost.amount} + /> ); diff --git a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx index 99be47d96b..79cd8617e0 100644 --- a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; -import { Box, Checkbox, FormControlLabel, Stack, TextField } from '@mui/material'; +import { Box, Checkbox, FormControlLabel, FormHelperText, Stack, TextField } from '@mui/material'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import { CurrencyDenom, TNodeType } from '@nymproject/types'; @@ -32,7 +32,7 @@ const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNex }, []); return ( - + - + {showAdvancedOptions && ( - + - + + {hasVestingTokens && setValue('tokenPool', pool)} />} - + + { + setValue('operatorCost', newValue, { shouldValidate: true }); + }} + validationError={errors.operatorCost?.amount?.message} + denom={denom} + initialValue={amountData.operatorCost.amount} + /> + + Monthly operational costs of running your node. If your node is in the active set the amount will be paid back + to you from the rewards. + + + + + + The percentage of node rewards that you as the node operator take before rewards are distributed to operator + and delegators. + + ); }; @@ -205,7 +229,7 @@ export const BondMixnodeForm = ({ <> {step === 1 && ( <> - + diff --git a/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts index fb1625adc1..c126d6c309 100644 --- a/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts +++ b/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts @@ -1,5 +1,6 @@ import * as Yup from 'yup'; import { + isLessThan, isValidHostname, validateAmount, validateKey, @@ -56,4 +57,15 @@ export const amountSchema = Yup.object().shape({ return true; }), }), + operatorCost: Yup.object().shape({ + amount: Yup.string() + .required('An operating cost is required') + .test('valid-operating-cost', 'A valid amount is required (min 40)', async function isValidAmount(this, value) { + if (value && (!Number(value) || isLessThan(+value, 40))) { + return false; + } + + return true; + }), + }), }); diff --git a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts index aa9e66db1c..aa5752f4aa 100644 --- a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts +++ b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts @@ -1,5 +1,5 @@ import * as Yup from 'yup'; -import { isValidHostname, validateAmount, validateKey, validateRawPort, validateVersion } from 'src/utils'; +import { isLessThan, isValidHostname, validateAmount, validateKey, validateRawPort, validateVersion } from 'src/utils'; export const mixnodeValidationSchema = Yup.object().shape({ identityKey: Yup.string() @@ -35,6 +35,21 @@ export const mixnodeValidationSchema = Yup.object().shape({ .test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)), }); +const operatingCostAndPmValidation = { + profitMargin: Yup.number().required('Profit Percentage is required').min(0).max(100), + operatorCost: Yup.object().shape({ + amount: Yup.string() + .required('An operating cost is required') + .test('valid-operating-cost', 'A valid amount is required (min 40)', async function isValidAmount(this, value) { + if (value && (!Number(value) || isLessThan(+value, 40))) { + return false; + } + + return true; + }), + }), +}; + export const amountSchema = Yup.object().shape({ amount: Yup.object().shape({ amount: Yup.string() @@ -47,5 +62,31 @@ export const amountSchema = Yup.object().shape({ return true; }), }), - profitMargin: Yup.number().required('Profit Percentage is required').min(0).max(100), + ...operatingCostAndPmValidation, +}); + +export const bondedInfoParametersValidationSchema = Yup.object().shape({ + host: Yup.string() + .required('A host is required') + .test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)), + + version: Yup.string() + .required('A version is required') + .test('valid-version', 'A valid version is required', (value) => (value ? validateVersion(value) : false)), + + mixPort: Yup.number() + .required('A mixport is required') + .test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)), + + verlocPort: Yup.number() + .required('A verloc port is required') + .test('valid-verloc', 'A valid verloc port is required', (value) => (value ? validateRawPort(value) : false)), + + httpApiPort: Yup.number() + .required('A http-api port is required') + .test('valid-http', 'A valid http-api port is required', (value) => (value ? validateRawPort(value) : false)), +}); + +export const bondedNodeParametersValidationSchema = Yup.object().shape({ + ...operatingCostAndPmValidation, }); diff --git a/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx b/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx index 779eb25449..d983da0f51 100644 --- a/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx @@ -24,6 +24,7 @@ const defaultGatewayValues: GatewayData = { const defaultAmountValues = (denom: CurrencyDenom) => ({ amount: { amount: '100', denom }, + operatorCost: { amount: '40', denom }, tokenPool: 'balance', }); diff --git a/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx b/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx index c2bf747d83..fcc7685f87 100644 --- a/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from 'react'; -import { Box } from '@mui/material'; import { CurrencyDenom, TNodeType } from '@nymproject/types'; import { ConfirmTx } from 'src/components/ConfirmTX'; import { ModalListItem } from 'src/components/Modals/ModalListItem'; @@ -10,7 +9,7 @@ import { MixnodeAmount, MixnodeData } from 'src/pages/bonding/types'; import { simulateBondMixnode, simulateVestingBondMixnode } from 'src/requests'; import { TBondMixNodeArgs } from 'src/types'; import { BondMixnodeForm } from '../forms/BondMixnodeForm'; -import { attachDefaultOperatingCost, toPercentFloatString } from '../../../utils'; +import { toPercentFloatString } from '../../../utils'; const defaultMixnodeValues: MixnodeData = { identityKey: '', @@ -25,6 +24,7 @@ const defaultMixnodeValues: MixnodeData = { const defaultAmountValues = (denom: CurrencyDenom) => ({ amount: { amount: '100', denom }, + operatorCost: { amount: '40', denom }, profitMargin: '10', tokenPool: 'balance', }); @@ -71,11 +71,7 @@ export const BondMixnodeModal = ({ }; const handleUpdateAmountData = async (data: MixnodeAmount) => { - const pm = toPercentFloatString(data.profitMargin); - setAmountData({ ...data, profitMargin: pm }); - - // TODO: this will have to be updated with allowing users to provide their operating cost in the form - const defaultCostParams = await attachDefaultOperatingCost(pm); + setAmountData({ ...data }); const payload = { pledge: data.amount, @@ -88,7 +84,13 @@ export const BondMixnodeModal = ({ sphinx_key: mixnodeData.sphinxKey, identity_key: mixnodeData.identityKey, }, - costParams: defaultCostParams, + costParams: { + profit_margin_percent: toPercentFloatString(data.profitMargin), + interval_operating_cost: { + amount: data.operatorCost.amount.toString(), + denom: data.operatorCost.denom, + }, + }, }; if (data.tokenPool === 'balance') { @@ -99,12 +101,8 @@ export const BondMixnodeModal = ({ }; const handleConfirm = async () => { - // TODO: this will have to be updated with allowing users to provide their operating cost in the form - const defaultCostParams = await attachDefaultOperatingCost(amountData.profitMargin); - await onBondMixnode( { - costParams: defaultCostParams, pledge: amountData.amount, ownerSignature: mixnodeData.ownerSignature, mixnode: { @@ -115,6 +113,13 @@ export const BondMixnodeModal = ({ sphinx_key: mixnodeData.sphinxKey, identity_key: mixnodeData.identityKey, }, + costParams: { + profit_margin_percent: toPercentFloatString(amountData.profitMargin), + interval_operating_cost: { + amount: amountData.operatorCost.amount, + denom: amountData.operatorCost.denom, + }, + }, }, amountData.tokenPool as TPoolOption, ); @@ -152,18 +157,16 @@ export const BondMixnodeModal = ({ subHeader={`Step ${step}/2`} okLabel="Next" > - - - + ); }; diff --git a/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx b/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx index 235795e77a..39aeb6443c 100644 --- a/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx @@ -83,7 +83,7 @@ export const BondMoreModal = ({ okDisabled={errorAmount || errorSignature} onClose={onClose} > - + {hasVestingTokens && setTokenPool(pool)} />} Promise; onClose: () => void; onError: (err: string) => void; }) => { - const [pm, setPm] = useState(currentPm.toString()); + const [pm, setPm] = useState(currentPm?.toString()); const [error, setError] = useState(false); const { fee, getFee, resetFeeState, isFeeLoading, feeError } = useGetFee(); @@ -52,13 +53,15 @@ export const NodeSettings = ({ return; } - // TODO: this will have to be updated with allowing users to provide their operating cost in the form - const defaultCostParams = await attachDefaultOperatingCost(toPercentFloatString(pm)); + if (pm) { + // TODO: this will have to be updated with allowing users to provide their operating cost in the form + const defaultCostParams = await attachDefaultOperatingCost(toPercentFloatString(pm)); - if (isVesting) { - await getFee(simulateVestingUpdateMixnodeCostParams, defaultCostParams); - } else { - await getFee(simulateUpdateMixnodeCostParams, defaultCostParams); + if (isVesting) { + await getFee(simulateVestingUpdateMixnodeCostParams, defaultCostParams); + } else { + await getFee(simulateUpdateMixnodeCostParams, defaultCostParams); + } } }; @@ -74,7 +77,7 @@ export const NodeSettings = ({ if (isFeeLoading) return ; - if (fee) + if (fee && pm) return ( - + Set profit margin - setPm(e.target.value)} fullWidth /> + setPm(e.target.value)} fullWidth /> {error && ( Profit margin should be a number between 0 and 100 diff --git a/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx index 07b1e90b0e..15b46b517b 100644 --- a/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Box, TextField, Typography } from '@mui/material'; +import { Typography } from '@mui/material'; import { useEffect, useState } from 'react'; import { TBondedGateway, TBondedMixnode } from 'src/context'; import { useGetFee } from 'src/hooks/useGetFee'; @@ -13,8 +13,6 @@ import { simulateVestingUnbondGateway, simulateVestingUnbondMixnode, } from '../../../requests'; -import { ConfirmationModal } from '../../Modals/ConfirmationModal'; -import { Error } from '../../Error'; interface Props { node: TBondedMixnode | TBondedGateway; @@ -25,9 +23,6 @@ interface Props { export const UnbondModal = ({ node, onConfirm, onClose, onError }: Props) => { const { fee, isFeeLoading, getFee, feeError } = useGetFee(); - const [isConfirmed, setIsConfirmed] = useState(false); - const [showConfirmModal, setShowConfirmModal] = useState(true); - const [confirmField, setConfirmField] = useState(''); useEffect(() => { if (feeError) { @@ -53,63 +48,18 @@ export const UnbondModal = ({ node, onConfirm, onClose, onError }: Props) => { } }, [node]); - if (showConfirmModal) { - return ( - { - setIsConfirmed(true); - setShowConfirmModal(false); - }} - onClose={onClose} - disabled={confirmField !== 'UNBOND'} - > - - If you unbond your node you will loose all your delegators! - - - - To unbond, type{' '} - t.palette.nym.highlight }}> - UNBOND - {' '} - in the field below and click UNBOND button - - setConfirmField(e.target.value)} /> - - ); - } - - if (isConfirmed) { - return ( - - - {isMixnode(node) && ( - - )} - - Tokens will be transferred to the account you are logged in with now - - ); - } - return ; + return ( + + + + Tokens will be transferred to the account you are logged in with now + + ); }; diff --git a/nym-wallet/src/components/ConfirmTX.stories.tsx b/nym-wallet/src/components/ConfirmTX.stories.tsx index 77bbf4fdcf..2eec07b7e3 100644 --- a/nym-wallet/src/components/ConfirmTX.stories.tsx +++ b/nym-wallet/src/components/ConfirmTX.stories.tsx @@ -10,9 +10,9 @@ export default { const Template: ComponentStory = (args) => ( - - - + + + ); diff --git a/nym-wallet/src/components/Delegation/DelegateModal.tsx b/nym-wallet/src/components/Delegation/DelegateModal.tsx index 0c47b4a94c..a6c1bb0dbf 100644 --- a/nym-wallet/src/components/Delegation/DelegateModal.tsx +++ b/nym-wallet/src/components/Delegation/DelegateModal.tsx @@ -213,8 +213,8 @@ export const DelegateModal: React.FC<{ onPrev={resetFeeState} onConfirm={handleOk} > - - + + ); } @@ -246,7 +246,7 @@ export const DelegateModal: React.FC<{ sx={sx} backdropProps={backdropProps} > - + {errorIdentityKey} - + {hasVestingContract && setTokenPool(pool)} />} {errorAmount} diff --git a/nym-wallet/src/components/Delegation/DelegationList.tsx b/nym-wallet/src/components/Delegation/DelegationList.tsx index 29349c2612..436b5e027d 100644 --- a/nym-wallet/src/components/Delegation/DelegationList.tsx +++ b/nym-wallet/src/components/Delegation/DelegationList.tsx @@ -40,7 +40,7 @@ interface HeadCell { const headCells: HeadCell[] = [ { id: 'node_identity', label: 'Node ID', sortable: true, align: 'left' }, - { id: 'avg_uptime_percent', label: 'Uptime', sortable: true, align: 'left' }, + { id: 'avg_uptime_percent', label: 'Routing score', sortable: true, align: 'left' }, { id: 'profit_margin_percent', label: 'Profit margin', sortable: true, align: 'left' }, { id: 'stake_saturation', label: 'Stake saturation', sortable: true, align: 'left' }, { id: 'delegated_on_iso_datetime', label: 'Delegated on', sortable: true, align: 'left' }, @@ -57,6 +57,7 @@ function descendingComparator(a: any, b: any, orderBy: string) { } return 0; } +// Sorting function needs fixing function sortPendingDelegation(a: DelegationWithEvent, b: DelegationWithEvent) { if (isPendingDelegation(a) && isPendingDelegation(b)) return 0; @@ -154,7 +155,7 @@ export const DelegationList: React.FC<{ {items?.length ? ( - items.sort(getComparator(order, orderBy)).map((item) => ( + items.map((item) => ( ( diff --git a/nym-wallet/src/components/Mnemonic.tsx b/nym-wallet/src/components/Mnemonic.tsx index 7b65e7aabf..c41fde3d86 100644 --- a/nym-wallet/src/components/Mnemonic.tsx +++ b/nym-wallet/src/components/Mnemonic.tsx @@ -18,7 +18,24 @@ export const Mnemonic = ({ Below is your 24 word mnemonic, make sure to store it in a safe place for accessing your wallet in the future - +