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/build.yml b/.github/workflows/build.yml index d4a639253b..0777368574 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,7 @@ jobs: RUSTC_WRAPPER: /home/ubuntu/.cargo/bin/sccache steps: - name: Install Dependencies (Linux) - run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools + run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools - name: Check out repository code uses: actions/checkout@v2 diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index e03a57bbb5..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 @@ -44,6 +44,12 @@ jobs: command: build args: --workspace + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + - name: Run all tests uses: actions-rs/cargo@v1 with: @@ -101,6 +107,12 @@ jobs: command: build args: --workspace --features=coconut + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + - name: Run all tests with coconut enabled uses: actions-rs/cargo@v1 with: diff --git a/.github/workflows/nightly_build_on_dispatch.yml b/.github/workflows/nightly_build_on_dispatch.yml index 76e141efed..a8e18bcbf7 100644 --- a/.github/workflows/nightly_build_on_dispatch.yml +++ b/.github/workflows/nightly_build_on_dispatch.yml @@ -167,7 +167,7 @@ jobs: KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMTECH_TEAM }}" - KEYBASE_NYM_CHANNEL: "test" + KEYBASE_NYM_CHANNEL: "${{ secrets.KEYBASE_CHANNEL_DEV_CORE_ID }}" IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}" uses: docker://keybaseio/client:stable-node with: diff --git a/.github/workflows/nym-cli-publish.yml b/.github/workflows/nym-cli-publish.yml new file mode 100644 index 0000000000..065e9f1658 --- /dev/null +++ b/.github/workflows/nym-cli-publish.yml @@ -0,0 +1,50 @@ +name: Publish Nym CLI binaries + +on: + workflow_dispatch: + release: + types: [created] + +env: + NETWORK: mainnet + +jobs: + publish-nym-cli: + strategy: + fail-fast: false + matrix: + platform: [ubuntu-latest, windows-latest, macos-latest] + + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v3 + + - name: Check the release tag starts with `nym-cli-` + if: startsWith(github.ref, 'refs/tags/nym-cli-') == false && github.event_name != 'workflow_dispatch' + uses: actions/github-script@v3 + with: + script: | + core.setFailed('Release tag did not start with nym-cli-...') + + - name: Install Rust stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Build binary + run: make build-nym-cli + + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: nym-cli-${{ matrix.platform }} + path: | + target/release/nym-cli* + retention-days: 30 + + - name: Upload to release based on tag name + uses: softprops/action-gh-release@v1 + if: github.event_name == 'release' + with: + files: | + target/release/nym-cli diff --git a/.github/workflows/nym-release-publish.yml b/.github/workflows/nym-release-publish.yml index ed966ecffc..7fd455dd46 100644 --- a/.github/workflows/nym-release-publish.yml +++ b/.github/workflows/nym-release-publish.yml @@ -1,5 +1,13 @@ 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] @@ -18,11 +26,16 @@ jobs: - uses: actions/checkout@v3 - name: Check the release tag starts with `nym-binaries-` - if: startsWith(github.ref, 'refs/tags/nym-binaries-') == false + if: startsWith(github.ref, 'refs/tags/nym-binaries-') == false && github.event_name != 'workflow_dispatch' uses: actions/github-script@v3 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 @@ -35,8 +48,24 @@ jobs: command: build args: --workspace --release + - name: Upload Artifact + uses: actions/upload-artifact@v3 + with: + name: my-artifact + path: | + target/release/nym-client + target/release/nym-gateway + target/release/nym-mixnode + target/release/nym-socks5-client + target/release/nym-validator-api + target/release/nym-network-requester + target/release/nym-network-statistics + target/release/nym-cli + retention-days: 30 + - name: Upload to release based on tag name uses: softprops/action-gh-release@v1 + if: github.event_name == 'release' with: files: | target/release/nym-client @@ -45,4 +74,5 @@ jobs: target/release/nym-socks5-client target/release/nym-validator-api target/release/nym-network-requester - target/release/nym-network-statistics \ No newline at end of file + target/release/nym-network-statistics + target/release/nym-cli diff --git a/.github/workflows/support-files/notifications/send_message.js b/.github/workflows/support-files/notifications/send_message.js index 1563f53e6d..fbc256d011 100644 --- a/.github/workflows/support-files/notifications/send_message.js +++ b/.github/workflows/support-files/notifications/send_message.js @@ -89,7 +89,7 @@ async function sendKeybaseMessage(messageBody) { }); const channel = { - name: 'nymtech_bot', + name: context.env.KEYBASE_NYMBOT_TEAM || 'nymtech_bot', membersType: 'team', topicName: context.keybase.channel, topic_type: 'CHAT', diff --git a/CHANGELOG.md b/CHANGELOG.md index bbb1fff18b..8ed0ebe379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,58 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- nym-cli: added CLI tool for interacting with the Nyx blockchain and Nym mixnet smart contracts ([#1577]) +- 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 +- clients: add testing-only support for two more extended packet sizes (8kb and 16kb). + +### Fixed + +- validator-api, mixnode, gateway should now prefer values in config.toml over mainnet defaults ([#1645]) +- validator-api should now correctly update historical uptimes for all mixnodes and gateways every 24h ([#1721]) +- socks5-client: fix bug where in some cases packet reordering could trigger a connection being closed too early ([#1702],[#1724]) + +### 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]) +- validator-api: changed error serialization on `inclusion_probability`, `stake-saturation` and `reward-estimation` endpoints to provide more accurate information ([#1681]) +- moved `Percent` struct to to `contracts-common`, change affects explorer-api +- clients: bound the sphinx packet channel and reduce sending rate if gateway can't keep up ([#1703],[#1725]) + +[#1541]: https://github.com/nymtech/nym/pull/1541 +[#1558]: https://github.com/nymtech/nym/pull/1558 +[#1577]: https://github.com/nymtech/nym/pull/1577 +[#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 +[#1681]: https://github.com/nymtech/nym/pull/1681 +[#1687]: https://github.com/nymtech/nym/pull/1687 +[#1702]: https://github.com/nymtech/nym/pull/1702 +[#1703]: https://github.com/nymtech/nym/pull/1703 +[#1721]: https://github.com/nymtech/nym/pull/1721 +[#1724]: https://github.com/nymtech/nym/pull/1724 +[#1725]: https://github.com/nymtech/nym/pull/1725 + ## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2) @@ -19,12 +71,14 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - validator-api: add Swagger to document the REST API ([#1249]). - validator-api: Added new endpoints for coconut spending flow and communications with coconut & multisig contracts ([#1261]) - validator-api: add `uptime`, `estimated_operator_apy`, `estimated_delegators_apy` to `/mixnodes/detailed` endpoint ([#1393]) +- validator-api: add node info cache storing simulated active set inclusion probabilities - network-statistics: a new mixnet service that aggregates and exposes anonymized data about mixnet services ([#1328]) - mixnode: Added basic mixnode hardware reporting to the HTTP API ([#1308]). - validator-api: endpoint, in coconut mode, for returning the validator-api cosmos address ([#1404]). - validator-client: add `denom` argument and add simple test for querying an account balance - gateway, validator-api: Checks for coconut credential double spending attempts, taking the coconut bandwidth contract as source of truth ([#1457]) - coconut-bandwidth-contract: Record the state of a coconut credential; create specific proposal for releasing funds ([#1457]) +- inclusion-probability: add simulator for active set inclusion probability ### Fixed @@ -37,7 +91,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - validator: fixed local docker-compose setup to work on Apple M1 ([#1329]) - explorer-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1482]). - network-requester: fix filter for suffix-only domains ([#1487]) -- validator-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1496]). +- validator-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service; cleaner shutdown, without panics ([#1496], [#1573]). ### Changed @@ -54,6 +108,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - gateway, network-statistics: include gateway id in the sent statistical data ([#1478]) - network explorer: tweak how active set probability is shown ([#1503]) - validator-api: rewarder set update fails without panicking on possible nymd queries ([#1520]) +- network-requester, socks5 client (nym-connect): send and receive respectively a message error to be displayed about filter check failure ([#1576]) [#1249]: https://github.com/nymtech/nym/pull/1249 @@ -83,90 +138,8 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// [#1496]: https://github.com/nymtech/nym/pull/1496 [#1503]: https://github.com/nymtech/nym/pull/1503 [#1520]: https://github.com/nymtech/nym/pull/1520 - -## [nym-connect-v1.0.1](https://github.com/nymtech/nym/tree/nym-connect-v1.0.1) (2022-07-22) - -### Added - -- nym-connect: initial proof-of-concept of a UI around the socks5 client was added -- nym-connect: add ability to select network requester and gateway ([#1427]) -- nym-connect: add ability to export gateway keys as JSON -- nym-connect: add auto updater - -### Changed - -- nym-connect: reuse config id instead of creating a new id on each connection - -[#1427]: https://github.com/nymtech/nym/pull/1427 - -## [nym-wallet-v1.0.7](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.7) (2022-07-11) - -- wallet: dark mode -- wallet: when simulating gas costs, an automatic adjustment is being used ([#1388]). - -[#1388]: https://github.com/nymtech/nym/pull/1388 - -## [nym-contracts-v1.0.1](https://github.com/nymtech/nym/tree/nym-contracts-v1.0.1) (2022-06-22) - -### Added - -- mixnet-contract: Added ClaimOperatorReward and ClaimDelegatorReward messages ([#1292]) -- mixnet-contract: Replace all naked `-` with `saturating_sub`. -- mixnet-contract: Added staking_supply field to ContractStateParams. -- mixnet-contract: Added a query to get MixnodeBond by identity key ([#1369]). -- mixnet-contract: Added a query to get GatewayBond by identity key ([#1369]). -- vesting-contract: Added ClaimOperatorReward and ClaimDelegatorReward messages ([#1292]) -- vesting-contract: Added limit to the amount of tokens one can pledge ([#1331]) - -### Fixed - -- mixnet-contract: `estimated_delegator_reward` calculation ([#1284]) -- mixnet-contract: delegator and operator rewards use lambda and sigma instead of lambda_ticked and sigma_ticked ([#1284]) -- mixnet-contract: removed `expect` in `query_delegator_reward` and queries containing invalid proxy address should now return a more human-readable error ([#1257]) -- mixnet-contract: replaced integer division with fixed for performance calculations ([#1284]) -- mixnet-contract: Under certain circumstances nodes could not be unbonded ([#1255](https://github.com/nymtech/nym/issues/1255)) ([#1258]) -- mixnet-contract: Using correct staking supply when distributing rewards. ([#1373]) -- vesting-contract: replaced `checked_sub` with `saturating_sub` to fix the underflow in `get_vesting_tokens` ([#1275]) - - -[#1255]: https://github.com/nymtech/nym/pull/1255 -[#1257]: https://github.com/nymtech/nym/pull/1257 -[#1258]: https://github.com/nymtech/nym/pull/1258 -[#1275]: https://github.com/nymtech/nym/pull/1275 -[#1284]: https://github.com/nymtech/nym/pull/1284 -[#1292]: https://github.com/nymtech/nym/pull/1292 -[#1331]: https://github.com/nymtech/nym/pull/1331 -[#1369]: https://github.com/nymtech/nym/pull/1369 -[#1373]: https://github.com/nymtech/nym/pull/1373 - -## [nym-wallet-v1.0.6](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.6) (2022-06-21) - -- wallet: undelegating now uses either the mixnet or vesting contract, or both, depending on how delegations were made -- wallet: redeeming and compounding now uses both the mixnet and vesting contract -- wallet: the wallet backend learned how to archive wallet files -- wallet: add ENABLE_QA_MODE environment variable to enable QA mode on built wallet - -## [nym-wallet-v1.0.5](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.5) (2022-06-14) - -- wallet: add simple CLI tool for decrypting and recovering the wallet file. -- wallet: added support for multiple accounts ([#1265]) -- wallet: compound and claim reward endpoints for operators and delegators ([#1302]) -- wallet: require password to switch accounts -- wallet: the wallet backend learned how to keep track of validator name, either hardcoded or by querying the status endpoint. -- wallet: new delegation and rewards UI -- wallet: show version in nav bar -- wallet: contract admin route put back -- wallet: staking_supply field to StateParams -- wallet: show transaction hash for redeeming or compounding rewards - -[#1265]: https://github.com/nymtech/nym/pull/1265 -[#1302]: https://github.com/nymtech/nym/pull/1302 - -## [nym-wallet-v1.0.4](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.4) (2022-05-04) - -### Changed - -- all: the default behaviour of validator client is changed to use `broadcast_sync` and poll for transaction inclusion instead of using `broadcast_commit` to deal with timeouts ([#1246]) +[#1573]: https://github.com/nymtech/nym/pull/1573 +[#1576]: https://github.com/nymtech/nym/pull/1576 ## [v1.0.1](https://github.com/nymtech/nym/tree/v1.0.1) (2022-05-04) @@ -199,77 +172,10 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// [Full Changelog](https://github.com/nymtech/nym/compare/nym-wallet-v1.0.3...nym-binaries-1.0.0) -## [nym-wallet-v1.0.3](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.3) (2022-04-25) - -[Full Changelog](https://github.com/nymtech/nym/compare/nym-binaries-1.0.0-rc.2...nym-wallet-v1.0.3) - -**Fixed bugs:** - -- \[Issue\] Wallet 1.0.2 cannot send NYM tokens from a DelayedVestingAccount [\#1215](https://github.com/nymtech/nym/issues/1215) -- Main README not showing properly with GitHub dark mode [\#1211](https://github.com/nymtech/nym/issues/1211) - -**Merged pull requests:** - -- Bugfix - wallet undelegation for vesting accounts [\#1220](https://github.com/nymtech/nym/pull/1220) ([mmsinclair](https://github.com/mmsinclair)) -- Bugfix/delegation reconcile [\#1219](https://github.com/nymtech/nym/pull/1219) ([jstuczyn](https://github.com/jstuczyn)) -- Bugfix/query proxied pending delegations [\#1218](https://github.com/nymtech/nym/pull/1218) ([jstuczyn](https://github.com/jstuczyn)) -- Using custom gas multiplier in the wallet [\#1217](https://github.com/nymtech/nym/pull/1217) ([jstuczyn](https://github.com/jstuczyn)) -- Feature/vesting accounts support [\#1216](https://github.com/nymtech/nym/pull/1216) ([jstuczyn](https://github.com/jstuczyn)) -- Release/1.0.0 rc.2 [\#1214](https://github.com/nymtech/nym/pull/1214) ([jstuczyn](https://github.com/jstuczyn)) -- chore: fix dark mode rendering [\#1212](https://github.com/nymtech/nym/pull/1212) ([pwnfoo](https://github.com/pwnfoo)) -- Feature/spend coconut [\#1210](https://github.com/nymtech/nym/pull/1210) ([neacsu](https://github.com/neacsu)) -- Bugfix/unique sphinx key [\#1207](https://github.com/nymtech/nym/pull/1207) ([jstuczyn](https://github.com/jstuczyn)) -- Add cache read and write timeouts [\#1206](https://github.com/nymtech/nym/pull/1206) ([durch](https://github.com/durch)) -- Additional, more informative routes [\#1204](https://github.com/nymtech/nym/pull/1204) ([durch](https://github.com/durch)) -- Feature/aggregated econ dynamics explorer endpoint [\#1203](https://github.com/nymtech/nym/pull/1203) ([jstuczyn](https://github.com/jstuczyn)) -- Debugging validator [\#1198](https://github.com/nymtech/nym/pull/1198) ([durch](https://github.com/durch)) -- wallet: expose additional validator configuration functionality to the frontend [\#1195](https://github.com/nymtech/nym/pull/1195) ([octol](https://github.com/octol)) -- Update rewarding validator address [\#1193](https://github.com/nymtech/nym/pull/1193) ([durch](https://github.com/durch)) -- Crypto part of the Groth's NIDKG [\#1182](https://github.com/nymtech/nym/pull/1182) ([jstuczyn](https://github.com/jstuczyn)) -- fix unbond page [\#1180](https://github.com/nymtech/nym/pull/1180) ([tommyv1987](https://github.com/tommyv1987)) -- Type safe bounds [\#1179](https://github.com/nymtech/nym/pull/1179) ([durch](https://github.com/durch)) -- Fix delegation paging [\#1174](https://github.com/nymtech/nym/pull/1174) ([durch](https://github.com/durch)) -- Update binaries to rc version [\#1172](https://github.com/nymtech/nym/pull/1172) ([tommyv1987](https://github.com/tommyv1987)) -- Bump ansi-regex from 4.1.0 to 4.1.1 in /docker/typescript\_client/upload\_contract [\#1171](https://github.com/nymtech/nym/pull/1171) ([dependabot[bot]](https://github.com/apps/dependabot)) - ## [nym-binaries-1.0.0-rc.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.0-rc.2) (2022-04-15) [Full Changelog](https://github.com/nymtech/nym/compare/nym-wallet-v1.0.2...nym-binaries-1.0.0-rc.2) -## [nym-wallet-v1.0.2](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.2) (2022-04-05) - -[Full Changelog](https://github.com/nymtech/nym/compare/nym-wallet-v1.0.1...nym-wallet-v1.0.2) - -**Merged pull requests:** - -- Wallet 1.0.2 visual tweaks [\#1197](https://github.com/nymtech/nym/pull/1197) ([mmsinclair](https://github.com/mmsinclair)) -- Password for wallet with routes [\#1196](https://github.com/nymtech/nym/pull/1196) ([fmtabbara](https://github.com/fmtabbara)) -- Add auto-updater to Nym Wallet [\#1194](https://github.com/nymtech/nym/pull/1194) ([mmsinclair](https://github.com/mmsinclair)) -- Fix clippy warnings for beta toolchain [\#1191](https://github.com/nymtech/nym/pull/1191) ([octol](https://github.com/octol)) -- wallet: expose validator urls to the frontend [\#1190](https://github.com/nymtech/nym/pull/1190) ([octol](https://github.com/octol)) -- wallet: add test for decrypting stored wallet file [\#1189](https://github.com/nymtech/nym/pull/1189) ([octol](https://github.com/octol)) -- Fix clippy warnings [\#1188](https://github.com/nymtech/nym/pull/1188) ([octol](https://github.com/octol)) -- Password for wallet with routes [\#1187](https://github.com/nymtech/nym/pull/1187) ([mmsinclair](https://github.com/mmsinclair)) -- wallet: add validate\_mnemonic [\#1186](https://github.com/nymtech/nym/pull/1186) ([octol](https://github.com/octol)) -- wallet: support removing accounts from the wallet file [\#1185](https://github.com/nymtech/nym/pull/1185) ([octol](https://github.com/octol)) -- Feature/adding discord [\#1184](https://github.com/nymtech/nym/pull/1184) ([gala1234](https://github.com/gala1234)) -- wallet: config backend for validator selection [\#1183](https://github.com/nymtech/nym/pull/1183) ([octol](https://github.com/octol)) -- Add storybook to wallet [\#1178](https://github.com/nymtech/nym/pull/1178) ([mmsinclair](https://github.com/mmsinclair)) -- wallet: connection test nymd and api urls independently [\#1170](https://github.com/nymtech/nym/pull/1170) ([octol](https://github.com/octol)) -- wallet: wire up account storage [\#1153](https://github.com/nymtech/nym/pull/1153) ([octol](https://github.com/octol)) -- Feature/signature on deposit [\#1151](https://github.com/nymtech/nym/pull/1151) ([neacsu](https://github.com/neacsu)) - -## [nym-wallet-v1.0.1](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.1) (2022-04-05) - -[Full Changelog](https://github.com/nymtech/nym/compare/nym-binaries-1.0.0-rc.1...nym-wallet-v1.0.1) - -**Closed issues:** - -- Check enabling bbbc simultaneously with open access. Estimate what it would take to make this the default compilation target. [\#1175](https://github.com/nymtech/nym/issues/1175) -- Get coconut credential for deposited tokens [\#1138](https://github.com/nymtech/nym/issues/1138) -- Make payments lazy [\#1135](https://github.com/nymtech/nym/issues/1135) -- Uptime on node selection for sets [\#1049](https://github.com/nymtech/nym/issues/1049) - ## [nym-binaries-1.0.0-rc.1](https://github.com/nymtech/nym/tree/nym-binaries-1.0.0-rc.1) (2022-03-28) [Full Changelog](https://github.com/nymtech/nym/compare/nym-wallet-v1.0.0...nym-binaries-1.0.0-rc.1) @@ -348,108 +254,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - feature/pedersen-commitments [\#1048](https://github.com/nymtech/nym/pull/1048) ([danielementary](https://github.com/danielementary)) - Feature/reuse init owner [\#970](https://github.com/nymtech/nym/pull/970) ([neacsu](https://github.com/neacsu)) -## [nym-wallet-v1.0.0](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.0) (2022-02-03) - -[Full Changelog](https://github.com/nymtech/nym/compare/v0.12.1...nym-wallet-v1.0.0) - -**Implemented enhancements:** - -- \[Feature Request\] Please enable registration without need for Telegram account [\#1016](https://github.com/nymtech/nym/issues/1016) -- Fast mixnode launch with a pre-built ISO + VM software [\#1001](https://github.com/nymtech/nym/issues/1001) - -**Fixed bugs:** - -- \[Issue\] [\#1000](https://github.com/nymtech/nym/issues/1000) -- \[Issue\] `nym-client` requires multiple attempts to run a server [\#869](https://github.com/nymtech/nym/issues/869) -- De-'float'-ing `Interval` \(`Display` impl + `serde`\) [\#1065](https://github.com/nymtech/nym/pull/1065) ([jstuczyn](https://github.com/jstuczyn)) -- display client address on wallet creation [\#1058](https://github.com/nymtech/nym/pull/1058) ([fmtabbara](https://github.com/fmtabbara)) - -**Closed issues:** - -- Rewarded set inclusion probability API endpoint [\#1037](https://github.com/nymtech/nym/issues/1037) -- Update cw-storage-plus to 0.11 [\#1032](https://github.com/nymtech/nym/issues/1032) -- Change `u128` fields in `RewardEstimationResponse` to `u64` [\#1029](https://github.com/nymtech/nym/issues/1029) -- Test out the mainnet Gravity Bridge [\#1006](https://github.com/nymtech/nym/issues/1006) -- Add vesting contract interface to nym-wallet [\#959](https://github.com/nymtech/nym/issues/959) -- Mixnode crash [\#486](https://github.com/nymtech/nym/issues/486) - -**Merged pull requests:** - -- create custom urls for mainnet [\#1095](https://github.com/nymtech/nym/pull/1095) ([fmtabbara](https://github.com/fmtabbara)) -- Wallet signing on MacOS [\#1093](https://github.com/nymtech/nym/pull/1093) ([mmsinclair](https://github.com/mmsinclair)) -- Fix rust 2018 idioms warnings [\#1092](https://github.com/nymtech/nym/pull/1092) ([octol](https://github.com/octol)) -- Prevent contract overwriting [\#1090](https://github.com/nymtech/nym/pull/1090) ([durch](https://github.com/durch)) -- Logout operation [\#1087](https://github.com/nymtech/nym/pull/1087) ([jstuczyn](https://github.com/jstuczyn)) -- Update to rust edition 2021 everywhere [\#1086](https://github.com/nymtech/nym/pull/1086) ([octol](https://github.com/octol)) -- Tag contract errors, and print out lines for easier QA [\#1084](https://github.com/nymtech/nym/pull/1084) ([durch](https://github.com/durch)) -- Feature/flexible vesting + utility queries [\#1083](https://github.com/nymtech/nym/pull/1083) ([durch](https://github.com/durch)) -- Bump @openzeppelin/contracts from 4.3.1 to 4.4.2 in /contracts/basic-bandwidth-generation [\#1082](https://github.com/nymtech/nym/pull/1082) ([dependabot[bot]](https://github.com/apps/dependabot)) -- Bump nth-check from 2.0.0 to 2.0.1 in /clients/native/examples/js-examples/websocket [\#1081](https://github.com/nymtech/nym/pull/1081) ([dependabot[bot]](https://github.com/apps/dependabot)) -- Bump url-parse from 1.5.1 to 1.5.4 in /clients/native/examples/js-examples/websocket [\#1080](https://github.com/nymtech/nym/pull/1080) ([dependabot[bot]](https://github.com/apps/dependabot)) -- Bump follow-redirects from 1.14.1 to 1.14.7 in /clients/native/examples/js-examples/websocket [\#1079](https://github.com/nymtech/nym/pull/1079) ([dependabot[bot]](https://github.com/apps/dependabot)) -- Bump nanoid from 3.1.23 to 3.2.0 in /clients/native/examples/js-examples/websocket [\#1078](https://github.com/nymtech/nym/pull/1078) ([dependabot[bot]](https://github.com/apps/dependabot)) -- Setup basic test for mixnode stats reporting [\#1077](https://github.com/nymtech/nym/pull/1077) ([octol](https://github.com/octol)) -- Make wallet\_address mandatory for mixnode init [\#1076](https://github.com/nymtech/nym/pull/1076) ([octol](https://github.com/octol)) -- Tidy nym-mixnode module visibility [\#1075](https://github.com/nymtech/nym/pull/1075) ([octol](https://github.com/octol)) -- Feature/wallet login with password [\#1074](https://github.com/nymtech/nym/pull/1074) ([fmtabbara](https://github.com/fmtabbara)) -- Add trait to mock client dependency in DelayForwarder [\#1073](https://github.com/nymtech/nym/pull/1073) ([octol](https://github.com/octol)) -- Bump rust-version to latest stable for nym-mixnode [\#1072](https://github.com/nymtech/nym/pull/1072) ([octol](https://github.com/octol)) -- Fixes CI for our wasm build [\#1069](https://github.com/nymtech/nym/pull/1069) ([jstuczyn](https://github.com/jstuczyn)) -- Add @octol as codeowner [\#1068](https://github.com/nymtech/nym/pull/1068) ([octol](https://github.com/octol)) -- set-up inclusion probability [\#1067](https://github.com/nymtech/nym/pull/1067) ([fmtabbara](https://github.com/fmtabbara)) -- Feature/wasm client [\#1066](https://github.com/nymtech/nym/pull/1066) ([neacsu](https://github.com/neacsu)) -- Changed bech32\_prefix from punk to nymt [\#1064](https://github.com/nymtech/nym/pull/1064) ([jstuczyn](https://github.com/jstuczyn)) -- Bump nanoid from 3.1.30 to 3.2.0 in /testnet-faucet [\#1063](https://github.com/nymtech/nym/pull/1063) ([dependabot[bot]](https://github.com/apps/dependabot)) -- Bump nanoid from 3.1.30 to 3.2.0 in /nym-wallet [\#1062](https://github.com/nymtech/nym/pull/1062) ([dependabot[bot]](https://github.com/apps/dependabot)) -- Rework vesting contract storage [\#1061](https://github.com/nymtech/nym/pull/1061) ([durch](https://github.com/durch)) -- Mixnet Contract constants extraction [\#1060](https://github.com/nymtech/nym/pull/1060) ([jstuczyn](https://github.com/jstuczyn)) -- fix: make explorer footer year dynamic [\#1059](https://github.com/nymtech/nym/pull/1059) ([martinyung](https://github.com/martinyung)) -- Add mnemonic just on creation, to display it [\#1057](https://github.com/nymtech/nym/pull/1057) ([neacsu](https://github.com/neacsu)) -- Network Explorer: updates to API and UI to show the active set [\#1056](https://github.com/nymtech/nym/pull/1056) ([mmsinclair](https://github.com/mmsinclair)) -- Made contract addresses for query NymdClient construction optional [\#1055](https://github.com/nymtech/nym/pull/1055) ([jstuczyn](https://github.com/jstuczyn)) -- Introduced RPC query for total token supply [\#1053](https://github.com/nymtech/nym/pull/1053) ([jstuczyn](https://github.com/jstuczyn)) -- Feature/tokio console [\#1052](https://github.com/nymtech/nym/pull/1052) ([durch](https://github.com/durch)) -- Implemented beta clippy lint recommendations [\#1051](https://github.com/nymtech/nym/pull/1051) ([jstuczyn](https://github.com/jstuczyn)) -- add new function to update profit percentage [\#1050](https://github.com/nymtech/nym/pull/1050) ([fmtabbara](https://github.com/fmtabbara)) -- Upgrade Clap and use declarative argument parsing for nym-mixnode [\#1047](https://github.com/nymtech/nym/pull/1047) ([octol](https://github.com/octol)) -- Feature/additional bond validation [\#1046](https://github.com/nymtech/nym/pull/1046) ([fmtabbara](https://github.com/fmtabbara)) -- Fix clippy on relevant lints [\#1044](https://github.com/nymtech/nym/pull/1044) ([neacsu](https://github.com/neacsu)) -- Bump shelljs from 0.8.4 to 0.8.5 in /contracts/basic-bandwidth-generation [\#1043](https://github.com/nymtech/nym/pull/1043) ([dependabot[bot]](https://github.com/apps/dependabot)) -- Endpoint for rewarded set inclusion probabilities [\#1042](https://github.com/nymtech/nym/pull/1042) ([durch](https://github.com/durch)) -- Bump follow-redirects from 1.14.4 to 1.14.7 in /contracts/basic-bandwidth-generation [\#1041](https://github.com/nymtech/nym/pull/1041) ([dependabot[bot]](https://github.com/apps/dependabot)) -- Bump follow-redirects from 1.14.5 to 1.14.7 in /testnet-faucet [\#1040](https://github.com/nymtech/nym/pull/1040) ([dependabot[bot]](https://github.com/apps/dependabot)) -- Feature/node settings update [\#1036](https://github.com/nymtech/nym/pull/1036) ([fmtabbara](https://github.com/fmtabbara)) -- Migrate to cw-storage-plus 0.11.1 [\#1035](https://github.com/nymtech/nym/pull/1035) ([durch](https://github.com/durch)) -- Bump @openzeppelin/contracts from 4.4.1 to 4.4.2 in /contracts/basic-bandwidth-generation [\#1034](https://github.com/nymtech/nym/pull/1034) ([dependabot[bot]](https://github.com/apps/dependabot)) -- Feature/configurable wallet [\#1033](https://github.com/nymtech/nym/pull/1033) ([neacsu](https://github.com/neacsu)) -- Feature/downcast reward estimation [\#1031](https://github.com/nymtech/nym/pull/1031) ([durch](https://github.com/durch)) -- Wallet UI updates [\#1028](https://github.com/nymtech/nym/pull/1028) ([fmtabbara](https://github.com/fmtabbara)) -- Remove migration code [\#1027](https://github.com/nymtech/nym/pull/1027) ([neacsu](https://github.com/neacsu)) -- Chore/stricter dependency requirements [\#1025](https://github.com/nymtech/nym/pull/1025) ([jstuczyn](https://github.com/jstuczyn)) -- Feature/validator api client endpoints [\#1024](https://github.com/nymtech/nym/pull/1024) ([jstuczyn](https://github.com/jstuczyn)) -- Updated cosmrs to 0.4.1 [\#1023](https://github.com/nymtech/nym/pull/1023) ([jstuczyn](https://github.com/jstuczyn)) -- Feature/testnet deploy scripts [\#1022](https://github.com/nymtech/nym/pull/1022) ([mfahampshire](https://github.com/mfahampshire)) -- Changed wallet's client to a full validator client [\#1021](https://github.com/nymtech/nym/pull/1021) ([jstuczyn](https://github.com/jstuczyn)) -- Fix 404 link [\#1020](https://github.com/nymtech/nym/pull/1020) ([RiccardoMasutti](https://github.com/RiccardoMasutti)) -- Feature/additional mixnode endpoints [\#1019](https://github.com/nymtech/nym/pull/1019) ([jstuczyn](https://github.com/jstuczyn)) -- Introduced denom check when trying to withdraw vested coins [\#1018](https://github.com/nymtech/nym/pull/1018) ([jstuczyn](https://github.com/jstuczyn)) -- Add network defaults for qa [\#1017](https://github.com/nymtech/nym/pull/1017) ([neacsu](https://github.com/neacsu)) -- Feature/expanded events [\#1015](https://github.com/nymtech/nym/pull/1015) ([jstuczyn](https://github.com/jstuczyn)) -- update frontend to use new profit update api [\#1014](https://github.com/nymtech/nym/pull/1014) ([fmtabbara](https://github.com/fmtabbara)) -- Feature/node state endpoint [\#1013](https://github.com/nymtech/nym/pull/1013) ([jstuczyn](https://github.com/jstuczyn)) -- Feature/hourly set updates [\#1012](https://github.com/nymtech/nym/pull/1012) ([durch](https://github.com/durch)) -- Feature/remove unused profit margin [\#1011](https://github.com/nymtech/nym/pull/1011) ([neacsu](https://github.com/neacsu)) -- Feature/explorer node status [\#1010](https://github.com/nymtech/nym/pull/1010) ([jstuczyn](https://github.com/jstuczyn)) -- Use serial integer instead of random [\#1009](https://github.com/nymtech/nym/pull/1009) ([durch](https://github.com/durch)) -- Feature/configure profit [\#1008](https://github.com/nymtech/nym/pull/1008) ([neacsu](https://github.com/neacsu)) -- Feature/fix gateway sign [\#1004](https://github.com/nymtech/nym/pull/1004) ([neacsu](https://github.com/neacsu)) -- Fix clippy [\#1003](https://github.com/nymtech/nym/pull/1003) ([neacsu](https://github.com/neacsu)) -- Update wallet version [\#998](https://github.com/nymtech/nym/pull/998) ([tommyv1987](https://github.com/tommyv1987)) -- Fix wallet build instructions [\#997](https://github.com/nymtech/nym/pull/997) ([tommyv1987](https://github.com/tommyv1987)) -- Make the separation between testnet-mode and erc20 bandwidth mode clearer [\#994](https://github.com/nymtech/nym/pull/994) ([neacsu](https://github.com/neacsu)) -- Bump @openzeppelin/contracts from 3.4.0 to 4.4.1 in /contracts/basic-bandwidth-generation [\#983](https://github.com/nymtech/nym/pull/983) ([dependabot[bot]](https://github.com/apps/dependabot)) -- Feature/implicit runtime [\#973](https://github.com/nymtech/nym/pull/973) ([jstuczyn](https://github.com/jstuczyn)) -- Differentiate staking and ownership [\#961](https://github.com/nymtech/nym/pull/961) ([durch](https://github.com/durch)) ## [v0.12.1](https://github.com/nymtech/nym/tree/v0.12.1) (2021-12-23) diff --git a/Cargo.lock b/Cargo.lock index a075fc0a4f..207872c863 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" @@ -155,6 +149,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atoi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic" version = "0.5.1" @@ -164,23 +167,6 @@ dependencies = [ "autocfg 1.1.0", ] -[[package]] -name = "attohttpc" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e69e13a99a7e6e070bb114f7ff381e58c7ccc188630121fc4c2fe4bcf24cd072" -dependencies = [ - "flate2", - "http", - "log", - "native-tls", - "openssl", - "serde", - "serde_json", - "url", - "wildmatch", -] - [[package]] name = "atty" version = "0.2.14" @@ -207,12 +193,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "az" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f771a5d1f5503f7f4279a30f3643d3421ba149848b89ecaaec0ea2acf04a5ac4" - [[package]] name = "bandwidth-claim-contract" version = "0.1.0" @@ -297,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]] @@ -340,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", @@ -448,24 +416,12 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" -[[package]] -name = "bytemuck" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" - [[package]] name = "byteorder" version = "1.4.3" @@ -557,13 +513,9 @@ version = "2.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" dependencies = [ - "ansi_term", - "atty", "bitflags", - "strsim 0.8.0", "textwrap 0.11.0", "unicode-width", - "vec_map", ] [[package]] @@ -578,11 +530,30 @@ dependencies = [ "clap_lex", "indexmap", "once_cell", - "strsim 0.10.0", + "strsim", "termcolor", "textwrap 0.15.0", ] +[[package]] +name = "clap_complete" +version = "3.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4179da71abd56c26b54dd0c248cc081c1f43b0a1a7e8448e28e57a29baa993d" +dependencies = [ + "clap 3.2.8", +] + +[[package]] +name = "clap_complete_fig" +version = "3.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed37b4c0c1214673eba6ad8ea31666626bf72be98ffb323067d973c48b4964b9" +dependencies = [ + "clap 3.2.8", + "clap_complete", +] + [[package]] name = "clap_derive" version = "3.2.7" @@ -615,6 +586,7 @@ dependencies = [ "futures", "gateway-client", "gateway-requests", + "gloo-timers", "humantime-serde", "log", "nonexhaustive-delayqueue", @@ -623,11 +595,17 @@ dependencies = [ "rand 0.7.3", "serde", "sled", + "tap", + "task", "tempfile", + "thiserror", "tokio", "topology", "url", "validator-client", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-timer", ] [[package]] @@ -671,13 +649,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "comfy-table" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121d8a5b0346092c18a4b2fd6f620d7a06f0eb7ac0a45860939a0884bc579c56" +dependencies = [ + "crossterm", + "strum 0.24.1", + "strum_macros 0.24.3", + "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" dependencies = [ "cfg-if 1.0.0", "handlebars", - "humantime-serde", "log", "network-defaults", "serde", @@ -739,14 +737,12 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", + "serde", + "serde_json", + "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" @@ -761,7 +757,7 @@ dependencies = [ "rand 0.8.5", "sha2 0.10.2", "subtle 2.4.1", - "time 0.3.9", + "time 0.3.14", "version_check", ] @@ -867,7 +863,16 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23" dependencies = [ - "crc-catalog", + "crc-catalog 1.1.1", +] + +[[package]] +name = "crc" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53757d12b596c16c78b83458d732a5d1a17ab3f53f2f7412f6fb57cc8a140ab3" +dependencies = [ + "crc-catalog 2.1.0", ] [[package]] @@ -876,6 +881,12 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" +[[package]] +name = "crc-catalog" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d0165d2900ae6778e36e80bbc4da3b5eefccee9ba939761f9c2882a5d9af3ff" + [[package]] name = "crc32fast" version = "1.3.2" @@ -894,6 +905,8 @@ dependencies = [ "cfg-if 0.1.10", "clap 3.2.8", "coconut-interface", + "completions", + "config", "credential-storage", "credentials", "crypto", @@ -914,8 +927,7 @@ version = "0.1.0" dependencies = [ "async-trait", "log", - "nymcoconut", - "sqlx", + "sqlx 0.5.11", "thiserror", "tokio", ] @@ -1026,6 +1038,31 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crossterm" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot 0.12.0", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -1040,7 +1077,6 @@ dependencies = [ "blake3", "bs58", "cipher 0.4.3", - "config", "ctr 0.9.1", "digest 0.10.3", "ed25519-dalek", @@ -1127,7 +1163,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8" dependencies = [ - "sct", + "sct 0.6.1", ] [[package]] @@ -1239,7 +1275,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", + "strsim", "syn", ] @@ -1273,19 +1309,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" @@ -1372,7 +1395,7 @@ dependencies = [ name = "dkg" version = "0.1.0" dependencies = [ - "bitvec 1.0.0", + "bitvec", "bls12_381 0.6.0", "bs58", "criterion", @@ -1401,6 +1424,15 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "dotenvy" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9155c8f4dc55c7470ae9da3f63c6785245093b3f6aeb0f5bf2e968efbba314" +dependencies = [ + "dirs", +] + [[package]] name = "dyn-clone" version = "1.0.5" @@ -1535,47 +1567,10 @@ dependencies = [ ] [[package]] -name = "ethabi" -version = "14.1.0" +name = "event-listener" +version = "2.5.3" 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", -] +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "execute" @@ -1591,10 +1586,14 @@ version = "1.0.1" dependencies = [ "chrono", "clap 3.2.8", + "contracts-common", + "dotenv", "humantime-serde", "isocountry", "itertools", "log", + "logging", + "maxminddb", "mixnet-contract-common", "network-defaults", "okapi", @@ -1671,31 +1670,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "fixed" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a65312835c1097a0c926ff3702df965285fadc33d948b87397ff8961bad881" -dependencies = [ - "az", - "bytemuck", - "half", - "serde", - "typenum", -] - -[[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" @@ -1736,21 +1710,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" @@ -1804,12 +1763,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" @@ -1818,9 +1771,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" dependencies = [ "futures-channel", "futures-core", @@ -1833,9 +1786,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" dependencies = [ "futures-core", "futures-sink", @@ -1843,15 +1796,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" dependencies = [ "futures-core", "futures-task", @@ -1871,15 +1824,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" dependencies = [ "proc-macro2", "quote", @@ -1888,31 +1841,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" - -[[package]] -name = "futures-timer" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" -dependencies = [ - "gloo-timers", - "send_wrapper", -] +checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" dependencies = [ "futures-channel", "futures-core", @@ -1944,27 +1887,26 @@ dependencies = [ "credential-storage", "credentials", "crypto", - "fluvio-wasm-timer", "futures", "gateway-requests", "getrandom 0.2.6", - "json", "log", "network-defaults", "nymsphinx", "pemstore", "rand 0.7.3", - "secp256k1", + "task", "thiserror", "tokio", - "tokio-tungstenite", - "tungstenite", + "tokio-stream", + "tokio-tungstenite 0.14.0", + "tungstenite 0.13.0", "url", "validator-client", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-timer", "wasm-utils", - "web3", ] [[package]] @@ -1983,7 +1925,7 @@ dependencies = [ "serde", "serde_json", "thiserror", - "tungstenite", + "tungstenite 0.13.0", ] [[package]] @@ -2093,9 +2035,9 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "gloo-timers" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d12a7f4e95cfe710f1d624fb1210b7d961a5fb05c4fd942f4feab06e61f590e" +checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" dependencies = [ "futures-channel", "futures-core", @@ -2175,13 +2117,31 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + [[package]] name = "hashlink" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" dependencies = [ - "hashbrown", + "hashbrown 0.11.2", +] + +[[package]] +name = "hashlink" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa" +dependencies = [ + "hashbrown 0.12.3", ] [[package]] @@ -2236,6 +2196,9 @@ name = "heck" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" @@ -2258,6 +2221,17 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" +[[package]] +name = "hidapi" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d26e1151deaab68f34fbfd16d491a2a0170cf98d69d3efa23873b567a4199e1" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "hkdf" version = "0.11.0" @@ -2393,9 +2367,9 @@ dependencies = [ "hyper-rustls", "rustls-native-certs", "tokio", - "tokio-rustls", + "tokio-rustls 0.22.0", "tower-service", - "webpki", + "webpki 0.21.4", ] [[package]] @@ -2408,12 +2382,12 @@ dependencies = [ "futures-util", "hyper", "log", - "rustls", + "rustls 0.19.1", "rustls-native-certs", "tokio", - "tokio-rustls", - "webpki", - "webpki-roots", + "tokio-rustls 0.22.0", + "webpki 0.21.4", + "webpki-roots 0.21.1", ] [[package]] @@ -2459,41 +2433,12 @@ dependencies = [ ] [[package]] -name = "impl-codec" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "161ebdfec3c8e3b52bf61c4f3550a1eea4f9579d10dc1b936f3171ebdcd6c443" +name = "inclusion-probability" +version = "0.1.0" 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", + "log", + "rand 0.8.5", + "thiserror", ] [[package]] @@ -2509,7 +2454,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" dependencies = [ "autocfg 1.1.0", - "hashbrown", + "hashbrown 0.11.2", "serde", ] @@ -2557,9 +2502,18 @@ checksum = "35e70ee094dc02fd9c13fdad4940090f22dbd6ac7c9e7094a46cf0232a50bc7c" [[package]] name = "ipnetwork" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c3eaab3ac0ede60ffa41add21970a7df7d91772c03383aac6c2c3d53cc716b" +checksum = "4088d739b183546b239688ddbc79891831df421773df95e236daf7867866d355" +dependencies = [ + "serde", +] + +[[package]] +name = "ipnetwork" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf466541e9d546596ee94f9f69590f89473455f88372423e0008fc1a7daf100e" dependencies = [ "serde", ] @@ -2613,27 +2567,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" - -[[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" @@ -2666,6 +2599,54 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "ledger" +version = "0.1.0" +dependencies = [ + "bip32", + "k256", + "ledger-transport", + "ledger-transport-hid", + "thiserror", +] + +[[package]] +name = "ledger-apdu" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe435806c197dfeaa5efcded5e623c4b8230fd28fdf1e91e7a86e40ef2acbf90" +dependencies = [ + "arrayref", + "no-std-compat", + "snafu 0.7.1", +] + +[[package]] +name = "ledger-transport" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1117f2143d92c157197785bf57711d7b02f2cfa101e162f8ca7900fb7f976321" +dependencies = [ + "async-trait", + "ledger-apdu", +] + +[[package]] +name = "ledger-transport-hid" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ba81a1f5f24396b37211478aff7fbcd605dd4544df8dbed07b9da3c2057aee" +dependencies = [ + "byteorder", + "cfg-if 1.0.0", + "hex", + "hidapi", + "ledger-transport", + "libc", + "log", + "thiserror", +] + [[package]] name = "libc" version = "0.2.122" @@ -2692,9 +2673,9 @@ checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" [[package]] name = "libsqlite3-sys" -version = "0.23.2" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" dependencies = [ "cc", "pkg-config", @@ -2743,13 +2724,21 @@ dependencies = [ [[package]] name = "log" -version = "0.4.16" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6389c490849ff5bc16be905ae24bc913a9c8892e19b2341dbc175e14c341c2b8" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "logging" +version = "0.1.0" +dependencies = [ + "log", + "pretty_env_logger", +] + [[package]] name = "loom" version = "0.5.4" @@ -2786,6 +2775,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" @@ -2831,25 +2832,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]] @@ -2867,16 +2857,18 @@ dependencies = [ name = "mixnet-contract-common" version = "0.1.0" dependencies = [ - "az", + "bs58", "contracts-common", "cosmwasm-std", - "fixed", + "humantime-serde", "log", + "rand_chacha 0.3.1", "schemars", "serde", + "serde_json", "serde_repr", "thiserror", - "time 0.3.9", + "time 0.3.14", "ts-rs", ] @@ -2886,11 +2878,9 @@ version = "0.1.0" dependencies = [ "bytes", "crypto", - "dashmap", "futures", "humantime-serde", "log", - "nonexhaustive-delayqueue", "nymsphinx-acknowledgements", "nymsphinx-addressing", "nymsphinx-forwarding", @@ -2976,6 +2966,12 @@ dependencies = [ "url", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.1" @@ -2990,9 +2986,12 @@ dependencies = [ name = "nonexhaustive-delayqueue" version = "0.1.0" dependencies = [ + "futures-core", + "slab", "tokio", "tokio-stream", "tokio-util 0.7.3", + "wasm-timer", ] [[package]] @@ -3054,6 +3053,73 @@ 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" +dependencies = [ + "anyhow", + "base64", + "bip39", + "bs58", + "clap 3.2.8", + "clap_complete", + "clap_complete_fig", + "dotenv", + "log", + "logging", + "network-defaults", + "nym-cli-commands", + "pretty_env_logger", + "serde", + "serde_json", + "tokio", + "validator-client", +] + +[[package]] +name = "nym-cli-commands" +version = "1.0.0" +dependencies = [ + "base64", + "bip39", + "bs58", + "cfg-if 1.0.0", + "clap 3.2.8", + "comfy-table", + "cosmrs", + "cosmwasm-std", + "handlebars", + "humantime-serde", + "k256", + "log", + "mixnet-contract-common", + "network-defaults", + "rand 0.6.5", + "serde", + "serde_json", + "thiserror", + "time 0.3.14", + "toml", + "url", + "validator-client", + "vesting-contract-common", +] + [[package]] name = "nym-client" version = "1.0.2" @@ -3061,6 +3127,7 @@ dependencies = [ "clap 3.2.8", "client-core", "coconut-interface", + "completions", "config", "credential-storage", "credentials", @@ -3070,6 +3137,7 @@ dependencies = [ "gateway-client", "gateway-requests", "log", + "logging", "network-defaults", "nymsphinx", "pemstore", @@ -3078,12 +3146,13 @@ dependencies = [ "serde", "serde_json", "sled", + "task", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.14.0", "topology", "url", "validator-client", - "vergen", + "vergen 5.1.17", "version-checker", "websocket-requests", ] @@ -3094,12 +3163,12 @@ version = "1.0.2" dependencies = [ "anyhow", "async-trait", - "bandwidth-claim-contract", "bip39", "bs58", "clap 3.2.8", "coconut-interface", "colored", + "completions", "config", "credentials", "crypto", @@ -3107,10 +3176,10 @@ dependencies = [ "dirs", "dotenv", "futures", - "gateway-client", "gateway-requests", "humantime-serde", "log", + "logging", "mixnet-client", "mixnode-common", "network-defaults", @@ -3120,20 +3189,19 @@ dependencies = [ "pretty_env_logger", "rand 0.7.3", "serde", - "sqlx", + "sqlx 0.5.11", "statistics-common", "subtle-encoding", "thiserror", "tokio", "tokio-stream", - "tokio-tungstenite", + "tokio-tungstenite 0.14.0", "tokio-util 0.7.3", "url", "validator-api-requests", "validator-client", - "vergen", + "vergen 5.1.17", "version-checker", - "web3", ] [[package]] @@ -3144,6 +3212,7 @@ dependencies = [ "bs58", "clap 3.2.8", "colored", + "completions", "config", "crypto", "cupid", @@ -3153,6 +3222,7 @@ dependencies = [ "humantime-serde", "lazy_static", "log", + "logging", "mixnet-client", "mixnode-common", "nonexhaustive-delayqueue", @@ -3172,7 +3242,7 @@ dependencies = [ "topology", "url", "validator-client", - "vergen", + "vergen 5.1.17", "version-checker", ] @@ -3181,11 +3251,13 @@ name = "nym-network-requester" version = "1.0.2" dependencies = [ "async-trait", - "clap 2.34.0", + "clap 3.2.8", + "completions", "dirs", "futures", - "ipnetwork", + "ipnetwork 0.20.0", "log", + "logging", "network-defaults", "nymsphinx", "ordered-buffer", @@ -3196,11 +3268,12 @@ dependencies = [ "reqwest", "serde", "socks5-requests", - "sqlx", + "sqlx 0.6.2", "statistics-common", + "task", "thiserror", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.17.2", "websocket-requests", ] @@ -3210,14 +3283,14 @@ version = "1.0.2" dependencies = [ "dirs", "log", + "logging", "pretty_env_logger", "rocket", "serde", - "sqlx", + "sqlx 0.5.11", "statistics-common", "thiserror", "tokio", - "tokio-tungstenite", ] [[package]] @@ -3227,6 +3300,7 @@ dependencies = [ "clap 3.2.8", "client-core", "coconut-interface", + "completions", "config", "credential-storage", "credentials", @@ -3236,6 +3310,7 @@ dependencies = [ "gateway-client", "gateway-requests", "log", + "logging", "network-defaults", "nymsphinx", "ordered-buffer", @@ -3245,13 +3320,14 @@ dependencies = [ "proxy-helpers", "rand 0.7.3", "serde", - "snafu", + "snafu 0.6.10", "socks5-requests", + "task", "tokio", "topology", "url", "validator-client", - "vergen", + "vergen 5.1.17", "version-checker", ] @@ -3271,7 +3347,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "strum", + "strum 0.23.0", "tempfile", "thiserror", "ts-rs", @@ -3287,13 +3363,13 @@ version = "1.0.2" dependencies = [ "anyhow", "async-trait", - "attohttpc", "cfg-if 1.0.0", - "clap 2.34.0", + "clap 3.2.8", "coconut-bandwidth-contract-common", "coconut-interface", "config", "console-subscriber", + "contracts-common", "cosmwasm-std", "credential-storage", "credentials", @@ -3306,7 +3382,9 @@ dependencies = [ "gateway-client", "getset", "humantime-serde", + "inclusion-probability", "log", + "logging", "mixnet-contract-common", "multisig-contract-common", "nymcoconut", @@ -3320,14 +3398,14 @@ dependencies = [ "rocket", "rocket_cors", "rocket_okapi", - "rocket_sync_db_pools", "schemars", "serde", "serde_json", - "sqlx", + "sqlx 0.6.2", + "tap", "task", "thiserror", - "time 0.3.9", + "time 0.3.14", "tokio", "tokio-stream", "topology", @@ -3335,7 +3413,7 @@ dependencies = [ "url", "validator-api-requests", "validator-client", - "vergen", + "vergen 7.2.1", "version-checker", ] @@ -3346,11 +3424,13 @@ dependencies = [ "config", "cosmrs", "cosmwasm-std", + "hex-literal", "mixnet-contract-common", + "network-defaults", "nym-types", "serde", "serde_json", - "strum", + "strum 0.23.0", "ts-rs", "validator-client", "vesting-contract", @@ -3596,32 +3676,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" @@ -3667,7 +3721,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec 1.8.0", - "windows-sys", + "windows-sys 0.34.0", ] [[package]] @@ -3930,29 +3984,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" @@ -4094,6 +4125,7 @@ dependencies = [ "log", "ordered-buffer", "socks5-requests", + "task", "tokio", "tokio-test", "tokio-util 0.7.3", @@ -4134,23 +4166,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "r2d2" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "545c5bc2b880973c9c10e4067418407a0ccaa3091781d1671d46eb35107cb26f" -dependencies = [ - "log", - "parking_lot 0.11.2", - "scheduled-thread-pool", -] - -[[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" @@ -4455,9 +4470,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" dependencies = [ "base64", "bytes", @@ -4482,6 +4497,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-native-tls", + "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -4526,16 +4542,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" @@ -4566,7 +4572,7 @@ dependencies = [ "serde_json", "state", "tempfile", - "time 0.3.9", + "time 0.3.14", "tokio", "tokio-stream", "tokio-util 0.7.3", @@ -4628,7 +4634,7 @@ dependencies = [ "smallvec 1.8.0", "stable-pattern", "state", - "time 0.3.9", + "time 0.3.14", "tokio", "uncased", ] @@ -4662,35 +4668,6 @@ dependencies = [ "syn", ] -[[package]] -name = "rocket_sync_db_pools" -version = "0.1.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fa48b6ab25013e9812f1b0c592741900b3a2a83c0936292e0565c0ac842f558" -dependencies = [ - "r2d2", - "rocket", - "rocket_sync_db_pools_codegen", - "serde", - "tokio", -] - -[[package]] -name = "rocket_sync_db_pools_codegen" -version = "0.1.0-rc.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280ef2d232923e69cb93da156972eb5476a7cce5ba44843f6608f46a4abf7aab" -dependencies = [ - "devise", - "quote", -] - -[[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" @@ -4718,8 +4695,20 @@ dependencies = [ "base64", "log", "ring", - "sct", - "webpki", + "sct 0.6.1", + "webpki 0.21.4", +] + +[[package]] +name = "rustls" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +dependencies = [ + "log", + "ring", + "sct 0.7.0", + "webpki 0.22.0", ] [[package]] @@ -4729,11 +4718,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" dependencies = [ "openssl-probe", - "rustls", + "rustls 0.19.1", "schannel", "security-framework", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +dependencies = [ + "base64", +] + [[package]] name = "rustversion" version = "1.0.6" @@ -4765,15 +4763,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "scheduled-thread-pool" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6f74fd1204073fa02d5d5d68bec8021be4c38690b61264b2fdb48083d0e7d7" -dependencies = [ - "parking_lot 0.11.2", -] - [[package]] name = "schemars" version = "0.8.10" @@ -4821,6 +4810,16 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sec1" version = "0.2.1" @@ -4834,24 +4833,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" @@ -4914,12 +4895,6 @@ dependencies = [ "pest", ] -[[package]] -name = "send_wrapper" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" - [[package]] name = "serde" version = "1.0.136" @@ -4981,9 +4956,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" dependencies = [ "itoa 1.0.1", "ryu", @@ -5106,6 +5081,27 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -5169,7 +5165,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eab12d3c261b2308b0d80c26fffb58d17eba81a4be97890101f416b478c79ca7" dependencies = [ "doc-comment", - "snafu-derive", + "snafu-derive 0.6.10", +] + +[[package]] +name = "snafu" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5177903bf45656592d9eb5c0e22f408fc023aae51dbe2088889b71633ba451f2" +dependencies = [ + "doc-comment", + "snafu-derive 0.7.1", ] [[package]] @@ -5183,6 +5189,18 @@ dependencies = [ "syn", ] +[[package]] +name = "snafu-derive" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410b26ed97440d90ced3e2488c868d56a86e2064f5d7d6f417909b286afe25e5" +dependencies = [ + "heck 0.4.0", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "socket2" version = "0.4.4" @@ -5198,22 +5216,7 @@ name = "socks5-requests" version = "0.1.0" dependencies = [ "nymsphinx-addressing", - "ordered-buffer", -] - -[[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", + "thiserror", ] [[package]] @@ -5275,38 +5278,60 @@ dependencies = [ "unicode_categories", ] +[[package]] +name = "sqlformat" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87e292b4291f154971a43c3774364e2cbcaec599d3f5bf6fa9d122885dbc38a" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + [[package]] name = "sqlx" version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc15591eb44ffb5816a4a70a7efd5dd87bfd3aa84c4c200401c4396140525826" dependencies = [ - "sqlx-core", - "sqlx-macros", + "sqlx-core 0.5.13", + "sqlx-macros 0.5.11", +] + +[[package]] +name = "sqlx" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9249290c05928352f71c077cc44a464d880c63f26f7534728cca008e135c0428" +dependencies = [ + "sqlx-core 0.6.2", + "sqlx-macros 0.6.2", ] [[package]] name = "sqlx-core" -version = "0.5.11" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "195183bf6ff8328bb82c0511a83faf60aacf75840103388851db61d7a9854ae3" +checksum = "e48c61941ccf5ddcada342cd59e3e5173b007c509e1e8e990dafc830294d9dc5" dependencies = [ "ahash", - "atoi", + "atoi 0.4.0", "bitflags", "byteorder", "bytes", "chrono", - "crc", + "crc 2.1.0", "crossbeam-queue", "either", + "event-listener", "flume", "futures-channel", "futures-core", "futures-executor", "futures-intrusive", "futures-util", - "hashlink", + "hashlink 0.7.0", "hex", "indexmap", "itoa 1.0.1", @@ -5317,17 +5342,64 @@ dependencies = [ "once_cell", "paste", "percent-encoding", - "rustls", - "sha2 0.9.9", + "rustls 0.19.1", + "sha2 0.10.2", "smallvec 1.8.0", - "sqlformat", - "sqlx-rt", + "sqlformat 0.1.8", + "sqlx-rt 0.5.13", "stringprep", "thiserror", "tokio-stream", "url", - "webpki", - "webpki-roots", + "webpki 0.21.4", + "webpki-roots 0.21.1", +] + +[[package]] +name = "sqlx-core" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbc16ddba161afc99e14d1713a453747a2b07fc097d2009f4c300ec99286105" +dependencies = [ + "ahash", + "atoi 1.0.0", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc 3.0.0", + "crossbeam-queue", + "dotenvy", + "either", + "event-listener", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "hashlink 0.8.1", + "hex", + "indexmap", + "itoa 1.0.1", + "libc", + "libsqlite3-sys", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls 0.20.6", + "rustls-pemfile", + "sha2 0.10.2", + "smallvec 1.8.0", + "sqlformat 0.2.0", + "sqlx-rt 0.6.2", + "stringprep", + "thiserror", + "tokio-stream", + "url", + "webpki-roots 0.22.4", ] [[package]] @@ -5343,21 +5415,51 @@ dependencies = [ "proc-macro2", "quote", "sha2 0.9.9", - "sqlx-core", - "sqlx-rt", + "sqlx-core 0.5.13", + "sqlx-rt 0.5.13", + "syn", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b850fa514dc11f2ee85be9d055c512aa866746adfacd1cb42d867d68e6a5b0d9" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.0", + "once_cell", + "proc-macro2", + "quote", + "sha2 0.10.2", + "sqlx-core 0.6.2", + "sqlx-rt 0.6.2", "syn", "url", ] [[package]] name = "sqlx-rt" -version = "0.5.11" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b555e70fbbf84e269ec3858b7a6515bcfe7a166a7cc9c636dd6efd20431678b6" +checksum = "4db708cd3e459078f85f39f96a00960bd841f66ee2a669e90bf36907f5a79aae" dependencies = [ "once_cell", "tokio", - "tokio-rustls", + "tokio-rustls 0.22.0", +] + +[[package]] +name = "sqlx-rt" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24c5b2d25fa654cc5f841750b8e1cdedbe21189bf9a9382ee90bfa9dd3562396" +dependencies = [ + "once_cell", + "tokio", + "tokio-rustls 0.23.4", ] [[package]] @@ -5393,7 +5495,7 @@ dependencies = [ "reqwest", "serde", "serde_json", - "sqlx", + "sqlx 0.5.11", "thiserror", "tokio", ] @@ -5408,12 +5510,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.10.0" @@ -5426,9 +5522,15 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" dependencies = [ - "strum_macros", + "strum_macros 0.23.1", ] +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + [[package]] name = "strum_macros" version = "0.23.1" @@ -5442,6 +5544,19 @@ dependencies = [ "syn", ] +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck 0.4.0", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "1.0.0" @@ -5513,7 +5628,6 @@ version = "0.1.0" dependencies = [ "log", "tokio", - "tokio-util 0.7.3", ] [[package]] @@ -5557,7 +5671,7 @@ dependencies = [ "subtle 2.4.1", "subtle-encoding", "tendermint-proto", - "time 0.3.9", + "time 0.3.14", "zeroize", ] @@ -5590,7 +5704,7 @@ dependencies = [ "serde", "serde_bytes", "subtle-encoding", - "time 0.3.9", + "time 0.3.14", ] [[package]] @@ -5618,7 +5732,7 @@ dependencies = [ "tendermint-config", "tendermint-proto", "thiserror", - "time 0.3.9", + "time 0.3.14", "tokio", "tracing", "url", @@ -5652,18 +5766,18 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" dependencies = [ "proc-macro2", "quote", @@ -5692,9 +5806,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.9" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" dependencies = [ "itoa 1.0.1", "libc", @@ -5709,15 +5823,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" @@ -5730,16 +5835,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", @@ -5786,9 +5891,20 @@ version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ - "rustls", + "rustls 0.19.1", "tokio", - "webpki", + "webpki 0.21.4", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +dependencies = [ + "rustls 0.20.6", + "tokio", + "webpki 0.22.0", ] [[package]] @@ -5800,6 +5916,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util 0.7.3", ] [[package]] @@ -5825,7 +5942,19 @@ dependencies = [ "log", "pin-project", "tokio", - "tungstenite", + "tungstenite 0.13.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.17.3", ] [[package]] @@ -5836,7 +5965,6 @@ checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" dependencies = [ "bytes", "futures-core", - "futures-io", "futures-sink", "log", "pin-project-lite", @@ -6093,6 +6221,25 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" +dependencies = [ + "base64", + "byteorder", + "bytes", + "http", + "httparse", + "log", + "rand 0.8.5", + "sha-1 0.10.0", + "thiserror", + "url", + "utf-8", +] + [[package]] name = "typenum" version = "1.15.0" @@ -6242,6 +6389,7 @@ dependencies = [ "bs58", "coconut-interface", "cosmrs", + "cosmwasm-std", "getset", "mixnet-contract-common", "schemars", @@ -6260,6 +6408,7 @@ dependencies = [ "coconut-interface", "colored", "config", + "contracts-common", "cosmrs", "cosmwasm-std", "cw3", @@ -6297,12 +6446,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "vergen" version = "5.1.17" @@ -6320,6 +6463,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "vergen" +version = "7.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f44ef1afcf5979e34748c12595f9589f3dc4e34abf156fb6d95f9b835568dc" +dependencies = [ + "anyhow", + "cfg-if 1.0.0", + "enum-iterator", + "getset", + "git2", + "rustc_version 0.4.0", + "rustversion", + "thiserror", + "time 0.3.14", +] + [[package]] name = "version-checker" version = "0.1.0" @@ -6335,14 +6495,16 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" -version = "1.0.1" +version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", "schemars", "serde", "thiserror", + "vergen 5.1.17", "vesting-contract-common", ] @@ -6350,9 +6512,9 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ - "config", + "contracts-common", "cosmwasm-std", - "cw-storage-plus", + "log", "mixnet-contract-common", "schemars", "serde", @@ -6400,25 +6562,23 @@ 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", - "serde", - "serde_json", "wasm-bindgen-macro", ] [[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", @@ -6439,9 +6599,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", @@ -6449,9 +6609,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", @@ -6462,9 +6622,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" @@ -6472,7 +6646,7 @@ version = "0.1.0" dependencies = [ "futures", "js-sys", - "tungstenite", + "tungstenite 0.13.0", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -6488,57 +6662,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", - "getrandom 0.2.6", - "headers", - "hex", - "js-sys", - "jsonrpc-core", - "log", - "parking_lot 0.11.2", - "pin-project", - "rand 0.8.5", - "reqwest", - "rlp", - "secp256k1", - "serde", - "serde_json", - "soketto", - "tiny-keccak", - "tokio", - "tokio-stream", - "tokio-util 0.6.9", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "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" @@ -6549,13 +6672,32 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "webpki-roots" version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" dependencies = [ - "webpki", + "webpki 0.21.4", +] + +[[package]] +name = "webpki-roots" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +dependencies = [ + "webpki 0.22.0", ] [[package]] @@ -6578,12 +6720,6 @@ dependencies = [ "libc", ] -[[package]] -name = "wildmatch" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c48bd20df7e4ced539c12f570f937c6b4884928a87fee70a479d72f031d4e0" - [[package]] name = "winapi" version = "0.3.9" @@ -6621,11 +6757,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]] @@ -6634,30 +6783,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" @@ -6667,12 +6846,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 8b8d9d560b..4866366280 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,22 +22,26 @@ members = [ "clients/native", "clients/native/websocket-requests", "clients/socks5", + "common/bandwidth-claim-contract", "common/client-libs/gateway-client", "common/client-libs/mixnet-client", "common/client-libs/validator-client", - "common/credential-storage", "common/coconut-interface", + "common/commands", "common/config", - "common/credentials", - "common/crypto", - "common/crypto/dkg", - "common/execute", - "common/bandwidth-claim-contract", "common/cosmwasm-smart-contracts/coconut-bandwidth-contract", "common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/mixnet-contract", "common/cosmwasm-smart-contracts/multisig-contract", "common/cosmwasm-smart-contracts/vesting-contract", + "common/credential-storage", + "common/credentials", + "common/crypto", + "common/crypto/dkg", + "common/execute", + "common/inclusion-probability", + "common/ledger", + "common/logging", "common/mixnode-common", "common/network-defaults", "common/nonexhaustive-delayqueue", @@ -53,21 +57,24 @@ members = [ "common/nymsphinx/params", "common/nymsphinx/types", "common/pemstore", - "common/statistics", "common/socks5/proxy-helpers", "common/socks5/requests", + "common/statistics", "common/task", "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", "validator-api", "validator-api/validator-api-requests", + "tools/nym-cli", "tools/ts-rs-cli" ] @@ -82,4 +89,4 @@ default-members = [ "explorer-api", ] -exclude = ["explorer", "contracts", "tokenomics-py", "clients/webassembly", "nym-wallet", "nym-connect"] +exclude = ["explorer", "contracts", "clients/webassembly", "nym-wallet", "nym-connect"] diff --git a/Makefile b/Makefile index fb1393d11a..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-all-main 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 -cargo-test-expensive: test-main-expensive test-contracts-expensive test-wallet-expensive test-connect-expensive -build: build-contracts build-wallet build-main build-connect -fmt: fmt-main fmt-contracts fmt-wallet fmt-connect +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 build-wasm-client +fmt: fmt-main fmt-contracts fmt-wallet fmt-connect fmt-wasm-client clippy-happy-main: cargo clippy @@ -21,8 +21,15 @@ clippy-happy-wallet: clippy-happy-connect: cargo clippy --manifest-path nym-connect/Cargo.toml -clippy-all-main: - cargo clippy --workspace --all-features -- -D warnings +clippy-main: + cargo clippy --workspace -- -D warnings + +clippy-coconut: + cargo clippy --workspace --features coconut -- -D warnings + +clippy-wasm: + cargo clippy --workspace --features wasm -- -D warnings + clippy-all-contracts: cargo clippy --workspace --manifest-path contracts/Cargo.toml --all-features --target wasm32-unknown-unknown -- -D warnings @@ -33,11 +40,24 @@ 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 --all-features --workspace + cargo test --workspace + +test-coconut: + cargo test --workspace --features coconut + +test-wasm: + cargo test --workspace --features wasm + test-main-expensive: - cargo test --all-features --workspace -- --ignored + cargo test --workspace -- --ignored + +test-coconut-expensive: + cargo test --workspace --features coconut -- --ignored test-contracts: cargo test --manifest-path contracts/Cargo.toml --all-features @@ -51,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 @@ -69,6 +92,12 @@ 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 + fmt-main: cargo fmt --all @@ -81,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 c628d8bd44..0f279f4425 100644 --- a/clients/client-core/Cargo.toml +++ b/clients/client-core/Cargo.toml @@ -13,23 +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" -tokio = { version = "1.19.1", features = ["macros"] } +sled = { version = "0.34", optional = true } +thiserror = "1.0.34" 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" } 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 8c47b197c8..84fd277634 100644 --- a/clients/client-core/src/client/cover_traffic_stream.rs +++ b/clients/client-core/src/client/cover_traffic_stream.rs @@ -3,19 +3,27 @@ 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 tokio::task::JoinHandle; +use std::time::Duration; +use tokio::sync::mpsc::error::TrySendError; + +#[cfg(not(target_arch = "wasm32"))] use tokio::time; +#[cfg(target_arch = "wasm32")] +use wasm_timer; + pub struct LoopCoverTrafficStream where R: CryptoRng + Rng, @@ -24,18 +32,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, @@ -48,6 +60,9 @@ where /// Accessor to the common instance of network topology. topology_access: TopologyAccessor, + + /// Predefined packet size used for the loop cover messages. + packet_size: PacketSize, } impl Stream for LoopCoverTrafficStream @@ -69,13 +84,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(())) } @@ -84,30 +107,42 @@ where // obviously when we finally make shared rng that is on 'higher' level, this should become // generic `R` 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, ) -> 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, + 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!"); @@ -129,18 +164,26 @@ 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"); - // if this one fails, there's no retrying because it means that either: - // - we run out of memory - // - the receiver channel is closed - // in either case there's no recovery and we can only panic - self.mix_tx.unbounded_send(vec![cover_message]).unwrap(); + if let Err(err) = self.mix_tx.try_send(vec![cover_message]) { + match err { + TrySendError::Full(_) => { + // This isn't a problem, if the channel is full means we're already sending the + // max amount of messages downstream can handle. + log::debug!("Failed to send cover message - channel full"); + } + TrySendError::Closed(_) => { + log::warn!("Failed to send cover message - channel closed"); + } + } + } // TODO: I'm not entirely sure whether this is really required, because I'm not 100% // sure how `yield_now()` works - whether it just notifies the scheduler or whether it @@ -149,24 +192,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)); - while self.next().await.is_some() { - self.on_new_message().await; - } + 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!(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 86312f1aa8..d5702cc0a8 100644 --- a/clients/client-core/src/client/mix_traffic.rs +++ b/clients/client-core/src/client/mix_traffic.rs @@ -1,16 +1,16 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use futures::channel::mpsc; -use futures::StreamExt; +use crate::spawn_future; use gateway_client::GatewayClient; use log::*; use nymsphinx::forwarding::packet::MixPacket; -use tokio::task::JoinHandle; -pub type BatchMixMessageSender = mpsc::UnboundedSender>; -pub type BatchMixMessageReceiver = mpsc::UnboundedReceiver>; +pub type BatchMixMessageSender = tokio::sync::mpsc::Sender>; +pub type BatchMixMessageReceiver = tokio::sync::mpsc::Receiver>; +// We remind ourselves that 32 x 32kb = 1024kb, a reasonable size for a network buffer. +pub const MIX_MESSAGE_RECEIVER_BUFFER_SIZE: usize = 32; const MAX_FAILURE_COUNT: usize = 100; pub struct MixTrafficController { @@ -25,15 +25,17 @@ pub struct MixTrafficController { } impl MixTrafficController { - pub fn new( - mix_rx: BatchMixMessageReceiver, - gateway_client: GatewayClient, - ) -> MixTrafficController { - MixTrafficController { - gateway_client, - mix_rx, - consecutive_gateway_failure_count: 0, - } + pub fn new(gateway_client: GatewayClient) -> (MixTrafficController, BatchMixMessageSender) { + let (sphinx_message_sender, sphinx_message_receiver) = + tokio::sync::mpsc::channel(MIX_MESSAGE_RECEIVER_BUFFER_SIZE); + ( + MixTrafficController { + gateway_client, + mix_rx: sphinx_message_receiver, + consecutive_gateway_failure_count: 0, + }, + sphinx_message_sender, + ) } async fn on_messages(&mut self, mut mix_packets: Vec) { @@ -65,15 +67,40 @@ impl MixTrafficController { } } - pub async fn run(&mut self) { - while let Some(mix_packets) = self.mix_rx.next().await { - 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.recv() => match mix_packets { + Some(mix_packets) => { + self.on_messages(mix_packets).await; + }, + None => { + log::trace!("MixTrafficController: Stopping since channel closed"); + break; + } + }, + _ = shutdown.recv() => { + log::trace!("MixTrafficController: Received shutdown"); + } + } + } + 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.recv().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 510c072073..bce1a089dc 100644 --- a/clients/client-core/src/client/mod.rs +++ b/clients/client-core/src/client/mod.rs @@ -1,8 +1,18 @@ +use std::sync::atomic::AtomicBool; + pub mod cover_traffic_stream; pub mod inbound_messages; 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; + +// This is *NOT* used to signal shutdown. +// It's critical that we don't have any tasks finishing early, this is an additional safety check +// that tasks exiting are doing so because shutdown has been signalled, and no other reason. +// In particular for tasks that rely on their associated channel being closed to signal shutdown, +// and don't have access to a shutdown listener channel. +pub static SHUTDOWN_HAS_BEEN_SIGNALLED: AtomicBool = AtomicBool::new(false); 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 77294b8cd6..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 @@ -63,14 +63,41 @@ impl AcknowledgementListener { .unwrap(); } - pub(super) async fn run(&mut self) { - debug!("Started AcknowledgementListener"); - while let Some(acks) = self.ack_receiver.next().await { - // realistically we would only be getting one ack at the time - for ack in acks { - self.on_ack(ack).await; + 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) => self.handle_ack_receiver_item(acks).await, + None => { + log::trace!("AcknowledgementListener: Stopping since channel closed"); + break; + } + }, + _ = shutdown.recv() => { + log::trace!("AcknowledgementListener: Received shutdown"); + } } } - error!("TODO: error msg. Or maybe panic?") + 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 f8417b0285..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 @@ -245,13 +245,44 @@ impl ActionController { } } - pub(super) async fn run(&mut self) { - loop { - // at some point there will be a global shutdown signal here as the third option + #[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), + None => { + log::trace!( + "ActionController: Stopping since incoming actions channel closed" + ); + break; + } + }, + expired_ack = self.pending_acks_timers.next() => match expired_ack { + Some(expired_ack) => self.handle_expired_ack_timer(expired_ack), + None => { + log::trace!("ActionController: Stopping since ack channel closed"); + break; + } + }, + _ = shutdown.recv() => { + log::trace!("ActionController: Received shutdown"); + } + } + } + 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! { - // we NEVER expect for ANY sender to get dropped so unwrap here is fine action = self.incoming_actions.next() => self.process_action(action.unwrap()), - // pending ack queue Stream CANNOT return a `None` so unwrap here is fine 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 2e34451de3..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}, @@ -17,6 +16,9 @@ use nymsphinx::{acknowledgements::AckKey, addressing::clients::Recipient}; use rand::{CryptoRng, Rng}; use std::sync::Arc; +#[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. /// It also makes an initial sending attempt for said messages. @@ -31,6 +33,7 @@ where action_sender: ActionSender, real_message_sender: BatchRealMessageSender, topology_access: TopologyAccessor, + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, } @@ -49,7 +52,7 @@ where action_sender: ActionSender, real_message_sender: BatchRealMessageSender, topology_access: TopologyAccessor, - reply_key_storage: ReplyKeyStorage, + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, ) -> Self { InputMessageListener { ack_key, @@ -59,6 +62,7 @@ where action_sender, real_message_sender, topology_access, + #[cfg(feature = "reply-surb")] reply_key_storage, } } @@ -117,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()); @@ -133,7 +141,6 @@ where let prepared_fragment = self .message_preparer .prepare_chunk_for_sending(chunk_clone, topology, &self.ack_key, &recipient) - .await .unwrap(); real_messages.push(RealMessage::new( @@ -181,11 +188,35 @@ where } } + #[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) => { + self.on_input_message(input_msg).await; + }, + None => { + log::trace!("InputMessageListener: Stopping since channel closed"); + break; + } + }, + _ = shutdown.recv() => { + log::trace!("InputMessageListener: Received shutdown"); + } + } + } + assert!(shutdown.is_shutdown_poll()); + log::debug!("InputMessageListener: Exiting"); + } + + #[cfg(target_arch = "wasm32")] pub(super) async fn run(&mut self) { - debug!("Started InputMessageListener"); + debug!("Started InputMessageListener without graceful shutdown support"); while let Some(input_msg) = self.input_receiver.next().await { self.on_input_message(input_msg).await; } - error!("TODO: error msg. Or maybe panic?") } } 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 a987497af1..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,7 +26,9 @@ use std::{ sync::{Arc, Weak}, time::Duration, }; -use tokio::task::JoinHandle; + +#[cfg(feature = "reply-surb")] +use crate::client::reply_key_storage::ReplyKeyStorage; mod acknowledgement_listener; mod action_controller; @@ -119,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 { @@ -133,33 +139,40 @@ 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 where R: 'static + CryptoRng + Rng + Clone + Send, { + #[allow(clippy::too_many_arguments)] pub(super) fn new( config: Config, rng: R, topology_access: TopologyAccessor, ack_key: Arc, ack_recipient: Recipient, - reply_key_storage: ReplyKeyStorage, connectors: AcknowledgementControllerConnectors, + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, ) -> Self { let (retransmission_tx, retransmission_rx) = mpsc::unbounded(); @@ -173,7 +186,8 @@ where 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( @@ -191,6 +205,7 @@ where action_sender.clone(), connectors.real_message_sender.clone(), topology_access.clone(), + #[cfg(feature = "reply-surb")] reply_key_storage, ); @@ -211,66 +226,87 @@ where 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 { + let shutdown_handle = shutdown.clone(); + spawn_future(async move { + acknowledgement_listener + .run_with_shutdown(shutdown_handle) + .await; + debug!("The acknowledgement listener has finished execution!"); + }); + + 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!"); + }); + } + + #[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!"); - acknowledgement_listener }); - let input_listener_fut = tokio::spawn(async move { + spawn_future(async move { input_message_listener.run().await; error!("The input listener has finished execution!"); - input_message_listener }); - let retransmission_req_fut = tokio::spawn(async move { + spawn_future(async move { retransmission_request_listener.run().await; error!("The retransmission request listener has finished execution!"); - retransmission_request_listener }); - let sent_notification_fut = tokio::spawn(async move { + spawn_future(async move { sent_notification_listener.run().await; error!("The sent notification listener has finished execution!"); - sent_notification_listener }); - let action_controller_fut = tokio::spawn(async move { + spawn_future(async move { action_controller.run().await; error!("The controller has finished execution!"); - action_controller }); - - // 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()); - } - - #[allow(dead_code)] - pub(super) fn start(mut self) -> JoinHandle { - tokio::spawn(async move { - self.run().await; - self - }) } } 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 d0081f4d04..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 @@ -33,6 +33,7 @@ impl RetransmissionRequestListener where R: CryptoRng + Rng, { + #[allow(clippy::too_many_arguments)] pub(super) fn new( ack_key: Arc, ack_recipient: Recipient, @@ -83,7 +84,6 @@ where let prepared_fragment = self .message_preparer .prepare_chunk_for_sending(chunk_clone, topology_ref, &self.ack_key, packet_recipient) - .await .unwrap(); // if we have the ONLY strong reference to the ack data, it means it was removed from the @@ -120,11 +120,34 @@ where .unwrap(); } + #[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 !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, + None => { + log::trace!("RetransmissionRequestListener: Stopping since channel closed"); + break; + } + }, + _ = shutdown.recv() => { + log::trace!("RetransmissionRequestListener: Received shutdown"); + } + } + } + assert!(shutdown.is_shutdown_poll()); + log::debug!("RetransmissionRequestListener: Exiting"); + } + + #[cfg(target_arch = "wasm32")] pub(super) async fn run(&mut self) { - debug!("Started RetransmissionRequestListener"); + 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; } - error!("TODO: error msg. Or maybe panic?") } } 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 2bd2eac933..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 @@ -42,11 +42,36 @@ impl SentNotificationListener { .unwrap(); } + #[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) => { + self.on_sent_message(frag_id).await; + } + None => { + log::trace!("SentNotificationListener: Stopping since channel closed"); + break; + } + }, + _ = shutdown.recv() => { + log::trace!("SentNotificationListener: Received shutdown"); + } + } + } + assert!(shutdown.is_shutdown_poll()); + log::debug!("SentNotificationListener: Exiting"); + } + + #[cfg(target_arch = "wasm32")] pub(super) async fn run(&mut self) { - debug!("Started SentNotificationListener"); + debug!("Started SentNotificationListener without graceful shutdown support"); + while let Some(frag_id) = self.sent_notifier.next().await { self.on_sent_message(frag_id).await; } - error!("TODO: error msg. Or maybe panic?") } } 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 610f1b586e..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,20 +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 tokio::task::JoinHandle; + +#[cfg(feature = "reply-surb")] +use crate::client::reply_key_storage::ReplyKeyStorage; mod acknowledgement_control; mod real_traffic_stream; @@ -49,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, @@ -59,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 { @@ -69,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 @@ -90,7 +109,7 @@ impl RealMessagesController { input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, topology_access: TopologyAccessor, - reply_key_storage: ReplyKeyStorage, + #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, ) -> Self { let rng = OsRng; @@ -109,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, @@ -117,15 +137,18 @@ impl RealMessagesController { topology_access.clone(), Arc::clone(&config.ack_key), config.self_recipient, - reply_key_storage, ack_controller_connectors, + #[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,40 +162,33 @@ impl RealMessagesController { ); 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; - error!("The out queue controller has finished execution!"); - out_queue_control + 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!"); }); - let ack_control_fut = tokio::spawn(async move { - ack_control.run().await; - error!("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 47d95d0d26..284211824b 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,14 +13,37 @@ 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; + +#[cfg(not(target_arch = "wasm32"))] use tokio::time; +#[cfg(target_arch = "wasm32")] +use wasm_timer; + +// The minimum time between increasing the average delay between packets. If we hit the ceiling in +// the available buffer space we want to take somewhat swift action, but we still need to give a +// short time to give the channel a chance reduce pressure. +const INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS: u64 = 1; +// The minimum time between decreasing the average delay between packets. We don't want to change +// to quickly to keep things somewhat stable. Also there are buffers downstreams meaning we need to +// wait a little to see the effect before we decrease further. +const DECREASE_DELAY_MIN_CHANGE_INTERVAL_SECS: u64 = 30; +// If we enough time passes without any sign of backpressure in the channel, we can consider +// lowering the average delay. The goal is to keep somewhat stable, rather than maxing out +// bandwidth at all times. +const ACCEPTABLE_TIME_WITHOUT_BACKPRESSURE_SECS: u64 = 30; +// The maximum multiplier we apply to the base average Poisson delay. +const MAX_DELAY_MULTIPLIER: u32 = 6; +// The minium multiplier we apply to the base average Poisson delay. +const MIN_DELAY_MULTIPLIER: u32 = 1; + /// Configurable parameters of the `OutQueueControl` pub(crate) struct Config { /// Average delay an acknowledgement packet is going to get delay at a single mixnode. @@ -31,6 +54,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 { @@ -38,13 +68,116 @@ 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 + } +} + +struct SendingDelayController { + /// Multiply the average sending delay. + /// This is normally set to unity, but if we detect backpressure we increase this + /// multiplier. We use discrete steps. + current_multiplier: u32, + + /// Maximum delay multiplier + upper_bound: u32, + + /// Minimum delay multiplier + lower_bound: u32, + + /// To make sure we don't change the multiplier to fast, we limit a change to some duration + #[cfg(not(target_arch = "wasm32"))] + time_when_changed: time::Instant, + + #[cfg(target_arch = "wasm32")] + time_when_changed: wasm_timer::Instant, + + /// If we have a long enough time without any backpressure detected we try reducing the sending + /// delay multiplier + #[cfg(not(target_arch = "wasm32"))] + time_when_backpressure_detected: time::Instant, + + #[cfg(target_arch = "wasm32")] + time_when_backpressure_detected: wasm_timer::Instant, +} + +#[cfg(not(target_arch = "wasm32"))] +fn get_time_now() -> time::Instant { + time::Instant::now() +} + +#[cfg(target_arch = "wasm32")] +fn get_time_now() -> wasm_timer::Instant { + wasm_timer::Instant::now() +} + +impl SendingDelayController { + fn new(lower_bound: u32, upper_bound: u32) -> Self { + assert!(lower_bound <= upper_bound); + let now = get_time_now(); + SendingDelayController { + current_multiplier: MIN_DELAY_MULTIPLIER, + upper_bound, + lower_bound, + time_when_changed: now, + time_when_backpressure_detected: now, + } + } + + fn current_multiplier(&self) -> u32 { + self.current_multiplier + } + + fn increase_delay_multiplier(&mut self) { + self.current_multiplier = + (self.current_multiplier + 1).clamp(self.lower_bound, self.upper_bound); + self.time_when_changed = get_time_now(); + log::debug!( + "Increasing sending delay multiplier to: {}", + self.current_multiplier + ); + } + + fn decrease_delay_multiplier(&mut self) { + self.current_multiplier = + (self.current_multiplier - 1).clamp(self.lower_bound, self.upper_bound); + self.time_when_changed = get_time_now(); + log::debug!( + "Decreasing sending delay multiplier to: {}", + self.current_multiplier + ); + } + + fn record_backpressure_detected(&mut self) { + self.time_when_backpressure_detected = get_time_now(); + } + + fn not_increased_delay_recently(&self) -> bool { + get_time_now() + > self.time_when_changed + Duration::from_secs(INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS) + } + + fn is_sending_reliable(&self) -> bool { + let now = get_time_now(); + let delay_change_interval = Duration::from_secs(DECREASE_DELAY_MIN_CHANGE_INTERVAL_SECS); + let acceptable_time_without_backpressure = + Duration::from_secs(ACCEPTABLE_TIME_WITHOUT_BACKPRESSURE_SECS); + + now > self.time_when_backpressure_detected + acceptable_time_without_backpressure + && now > self.time_when_changed + delay_change_interval + } } pub(crate) struct OutQueueControl @@ -62,7 +195,15 @@ 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>>, + + // To make sure we don't overload the mix_tx channel, we limit the rate we are pushing + // messages. + sending_rate_controller: SendingDelayController, /// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them /// out to the network without any further delays. @@ -109,55 +250,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, @@ -179,7 +271,11 @@ where config, ack_key, sent_notifier, - next_delay: Box::pin(time::sleep(Default::default())), + next_delay: None, + sending_rate_controller: SendingDelayController::new( + MIN_DELAY_MULTIPLIER, + MAX_DELAY_MULTIPLIER, + ), mix_tx, real_receiver, our_full_destination, @@ -222,10 +318,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") } @@ -235,35 +332,208 @@ where } }; - // if this one fails, there's no retrying because it means that either: - // - we run out of memory - // - the receiver channel is closed - // in either case there's no recovery and we can only panic - self.mix_tx.unbounded_send(vec![next_message]).unwrap(); + if let Err(err) = self.mix_tx.send(vec![next_message]).await { + log::error!("Failed to send - channel closed: {}", err); + } // JS: Not entirely sure why or how it fixes stuff, but without the yield call, // the UnboundedReceiver [of mix_rx] will not get a chance to read anything // 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 current_average_message_sending_delay(&self) -> Duration { + self.config.average_message_sending_delay + * self.sending_rate_controller.current_multiplier() + } + + fn adjust_current_average_message_sending_delay(&mut self) { + let used_slots = self.mix_tx.max_capacity() - self.mix_tx.capacity(); + log::trace!( + "used_slots: {used_slots}, current_multiplier: {}", + self.sending_rate_controller.current_multiplier() + ); + + // Even just a single used slot is enough to signal backpressure + if used_slots > 0 { + log::trace!("Backpressure detected"); + self.sending_rate_controller.record_backpressure_detected(); + } + + // If the buffer is running out, slow down the sending rate + if self.mix_tx.capacity() == 0 + && self.sending_rate_controller.not_increased_delay_recently() + { + self.sending_rate_controller.increase_delay_multiplier(); + } + + // Very carefully step up the sending rate in case it seems like we can solidly handle the + // current rate. + if self.sending_rate_controller.is_sending_reliable() { + self.sending_rate_controller.decrease_delay_multiplier(); + } + } + + fn poll_poisson(&mut self, cx: &mut Context<'_>) -> Poll> { + // The average delay could change depending on if backpressure in the downstream channel + // (mix_tx) was detected. + self.adjust_current_average_message_sending_delay(); + let avg_delay = self.current_average_message_sending_delay(); + + 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 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"); + + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + log::trace!("OutQueueControl: Received shutdown"); + } + next_message = self.next() => match next_message { + Some(next_message) => { + self.on_message(next_message).await; + }, + None => { + log::trace!("OutQueueControl: Stopping since channel closed"); + break; + } + } + } + } + assert!(shutdown.is_shutdown_poll()); + log::debug!("OutQueueControl: Exiting"); + } + + #[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; } } +} - pub(crate) async fn run_out_queue_control(&mut self) { - debug!("Starting out queue controller..."); - self.run_normal_out_queue().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 ad77b82e1a..9830f0a869 100644 --- a/clients/client-core/src/client/received_buffer.rs +++ b/clients/client-core/src/client/received_buffer.rs @@ -1,21 +1,25 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::client::reply_key_storage::ReplyKeyStorage; +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::Arc; -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" @@ -113,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 { @@ -129,6 +134,7 @@ impl ReceivedMessagesBuffer { message_sender: None, recently_reconstructed: HashSet::new(), })), + #[cfg(feature = "reply-surb")] reply_key_storage, } } @@ -177,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, @@ -209,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() { @@ -290,19 +309,48 @@ impl RequestReceiver { } } - fn start(mut self) -> JoinHandle<()> { - tokio::spawn(async move { - while let Some(request) = self.query_receiver.next().await { - match request { - ReceivedBufferMessage::ReceiverAnnounce(sender) => { - self.received_buffer.connect_sender(sender).await; - } - ReceivedBufferMessage::ReceiverDisconnect => { - self.received_buffer.disconnect_sender().await - } - } + 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 + } + } + } + + #[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 + } } } @@ -321,12 +369,37 @@ impl FragmentedMessageReceiver { mixnet_packet_receiver, } } - fn start(mut self) -> JoinHandle<()> { - tokio::spawn(async move { - while let Some(new_messages) = self.mixnet_packet_receiver.next().await { - self.received_buffer.handle_new_received(new_messages).await; + + #[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!(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; + } } } @@ -340,10 +413,13 @@ impl ReceivedMessagesBufferController { local_encryption_keypair: Arc, query_receiver: ReceivedBufferRequestReceiver, mixnet_packet_receiver: MixnetMessageReceiver, - reply_key_storage: ReplyKeyStorage, + #[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( @@ -354,9 +430,31 @@ impl ReceivedMessagesBufferController { } } + #[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 7f38bdc680..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; @@ -11,8 +12,7 @@ use std::sync::Arc; use std::time; use std::time::Duration; use tokio::sync::{RwLock, RwLockReadGuard}; -use tokio::task::JoinHandle; -use topology::{nym_topology_from_bonds, NymTopology}; +use topology::{nym_topology_from_detailed, NymTopology}; use url::Url; // I'm extremely curious why compiler NEVER complained about lack of Debug here before @@ -265,8 +265,8 @@ impl TopologyRefresher { }; let mixnodes_count = mixnodes.len(); - let topology = - nym_topology_from_bonds(mixnodes, gateways).filter_system_version(&self.client_version); + let topology = nym_topology_from_detailed(mixnodes, gateways) + .filter_system_version(&self.client_version); if !self.check_layer_distribution(&topology, mixnodes_count) { warn!("The current filtered active topology has extremely skewed layer distribution. It cannot be used."); @@ -303,10 +303,34 @@ impl TopologyRefresher { self.topology_accessor.is_routable().await } - pub fn start(mut self) -> JoinHandle<()> { - tokio::spawn(async move { - loop { - tokio::time::sleep(self.refresh_rate).await; + #[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) => { + self.refresh().await; + }, + _ = shutdown.recv() => { + log::trace!("TopologyRefresher: Received shutdown"); + }, + } + } + assert!(shutdown.is_shutdown_poll()); + 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 1c81d5e079..678b70ef54 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -2,12 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 use config::NymConfig; +use nymsphinx::params::PacketSize; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; 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"; @@ -121,16 +125,6 @@ impl Config { self.client.gateway_endpoint.gateway_id = id.into(); } - #[cfg(not(feature = "coconut"))] - pub fn with_eth_private_key>(&mut self, eth_private_key: S) { - self.client.eth_private_key = eth_private_key.into(); - } - - #[cfg(not(feature = "coconut"))] - pub fn with_eth_endpoint>(&mut self, eth_endpoint: S) { - self.client.eth_endpoint = eth_endpoint.into(); - } - pub fn set_custom_validator_apis(&mut self, validator_api_urls: Vec) { self.client.validator_api_urls = validator_api_urls; } @@ -209,16 +203,6 @@ impl Config { self.client.database_path.clone() } - #[cfg(not(feature = "coconut"))] - pub fn get_eth_endpoint(&self) -> String { - self.client.eth_endpoint.clone() - } - - #[cfg(not(feature = "coconut"))] - pub fn get_eth_private_key(&self) -> String { - self.client.eth_private_key.clone() - } - // Debug getters pub fn get_average_packet_delay(&self) -> Duration { self.debug.average_packet_delay @@ -256,6 +240,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) -> Option { + self.debug.use_extended_packet_size.clone() + } + pub fn get_version(&self) -> &str { &self.client.version } @@ -272,6 +268,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. @@ -342,14 +339,6 @@ pub struct Client { /// Path to the database containing bandwidth credentials of this client. database_path: PathBuf, - /// Ethereum private key. - #[cfg(not(feature = "coconut"))] - eth_private_key: String, - - /// Address to an Ethereum full node. - #[cfg(not(feature = "coconut"))] - eth_endpoint: String, - /// nym_home_directory specifies absolute path to the home nym Clients directory. /// It is expected to use default value and hence .toml file should not redefine this field. nym_root_directory: PathBuf, @@ -375,10 +364,6 @@ impl Default for Client { reply_encryption_key_store_path: Default::default(), gateway_endpoint: Default::default(), database_path: Default::default(), - #[cfg(not(feature = "coconut"))] - eth_private_key: "".to_string(), - #[cfg(not(feature = "coconut"))] - eth_endpoint: "".to_string(), nym_root_directory: T::default_root_directory(), super_struct: Default::default(), } @@ -430,53 +415,72 @@ 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 a NON-DEFAULT bigger size. + pub use_extended_packet_size: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum ExtendedPacketSize { + Extended8, + Extended16, + Extended32, } impl Default for Debug { @@ -491,6 +495,19 @@ 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: None, + } + } +} + +impl From for PacketSize { + fn from(size: ExtendedPacketSize) -> PacketSize { + match size { + ExtendedPacketSize::Extended8 => PacketSize::ExtendedPacket8, + ExtendedPacketSize::Extended16 => PacketSize::ExtendedPacket16, + ExtendedPacketSize::Extended32 => PacketSize::ExtendedPacket32, } } } diff --git a/clients/client-core/src/error.rs b/clients/client-core/src/error.rs new file mode 100644 index 0000000000..12e27fdd58 --- /dev/null +++ b/clients/client-core/src/error.rs @@ -0,0 +1,27 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crypto::asymmetric::identity::Ed25519RecoveryError; +use gateway_client::error::GatewayClientError; +use validator_client::ValidatorClientError; + +#[derive(thiserror::Error, Debug)] +pub enum ClientCoreError { + #[error("I/O error: {0}")] + IoError(#[from] std::io::Error), + #[error("Gateway client error: {0}")] + GatewayClientError(#[from] GatewayClientError), + #[error("Ed25519 error: {0}")] + Ed25519RecoveryError(#[from] Ed25519RecoveryError), + #[error("Validator client error: {0}")] + ValidatorClientError(#[from] ValidatorClientError), + + #[error("No gateway with id: {0}")] + NoGatewayWithId(String), + #[error("No gateways on network")] + NoGatewaysOnNetwork, + #[error("List of validator apis is empty")] + ListOfValidatorApisIsEmpty, + #[error("Could not load existing gateway configuration: {0}")] + CouldNotLoadExistingGatewayConfiguration(std::io::Error), +} diff --git a/clients/client-core/src/init.rs b/clients/client-core/src/init.rs index 16921cd3f5..98cc42df64 100644 --- a/clients/client-core/src/init.rs +++ b/clients/client-core/src/init.rs @@ -14,25 +14,27 @@ use nymsphinx::addressing::nodes::NodeIdentity; use rand::rngs::OsRng; use rand::seq::SliceRandom; use rand::thread_rng; +use tap::TapFallible; use topology::{filter::VersionFilterable, gateway}; use url::Url; use crate::{ client::key_manager::KeyManager, config::{persistence::key_pathfinder::ClientKeyPathfinder, Config}, + error::ClientCoreError, }; pub async fn query_gateway_details( validator_servers: Vec, chosen_gateway_id: Option<&str>, -) -> gateway::Node { +) -> Result { let validator_api = validator_servers .choose(&mut thread_rng()) - .expect("The list of validator apis is empty"); + .ok_or(ClientCoreError::ListOfValidatorApisIsEmpty)?; let validator_client = validator_client::ApiClient::new(validator_api.clone()); log::trace!("Fetching list of gateways from: {}", validator_api); - let gateways = validator_client.get_cached_gateways().await.unwrap(); + let gateways = validator_client.get_cached_gateways().await?; let valid_gateways = gateways .into_iter() .filter_map(|gateway| gateway.try_into().ok()) @@ -47,38 +49,40 @@ pub async fn query_gateway_details( filtered_gateways .iter() .find(|gateway| gateway.identity_key.to_base58_string() == gateway_id) - .expect(&*format!("no gateway with id {} exists!", gateway_id)) - .clone() + .ok_or_else(|| ClientCoreError::NoGatewayWithId(gateway_id.to_string())) + .cloned() } else { filtered_gateways .choose(&mut rand::thread_rng()) - .expect("there are no gateways on the network!") - .clone() + .ok_or(ClientCoreError::NoGatewaysOnNetwork) + .cloned() } } pub async fn register_with_gateway_and_store_keys( gateway_details: gateway::Node, config: &Config, -) where +) -> Result<(), ClientCoreError> +where T: NymConfig, { let mut rng = OsRng; let mut key_manager = KeyManager::new(&mut rng); - let shared_keys = register_with_gateway(&gateway_details, key_manager.identity_keypair()).await; + let shared_keys = + register_with_gateway(&gateway_details, key_manager.identity_keypair()).await?; key_manager.insert_gateway_shared_key(shared_keys); let pathfinder = ClientKeyPathfinder::new_from_config(config); - key_manager + Ok(key_manager .store_keys(&pathfinder) - .expect("Failed to generated keys"); + .tap_err(|err| log::error!("Failed to generate keys: {err}"))?) } async fn register_with_gateway( gateway: &gateway::Node, our_identity: Arc, -) -> Arc { +) -> Result, ClientCoreError> { let timeout = Duration::from_millis(1500); let mut gateway_client = GatewayClient::new_init( gateway.clients_address(), @@ -86,52 +90,60 @@ async fn register_with_gateway( gateway.owner.clone(), our_identity.clone(), timeout, + #[cfg(not(target_arch = "wasm32"))] + None, ); gateway_client .establish_connection() .await - .expect("failed to establish connection with the gateway!"); - gateway_client + .tap_err(|_| log::warn!("Failed to establish connection with gateway!"))?; + let shared_keys = gateway_client .perform_initial_authentication() .await - .expect("failed to register with the gateway!") + .tap_err(|_| log::warn!("Failed to register with the gateway!"))?; + Ok(shared_keys) } -pub fn show_address(config: &Config) +pub fn show_address(config: &Config) -> Result<(), ClientCoreError> where T: config::NymConfig, { - fn load_identity_keys(pathfinder: &ClientKeyPathfinder) -> identity::KeyPair { + fn load_identity_keys( + pathfinder: &ClientKeyPathfinder, + ) -> Result { let identity_keypair: identity::KeyPair = pemstore::load_keypair(&pemstore::KeyPairPath::new( pathfinder.private_identity_key().to_owned(), pathfinder.public_identity_key().to_owned(), )) - .expect("Failed to read stored identity key files"); - identity_keypair + .tap_err(|_| log::error!("Failed to read stored identity key files"))?; + Ok(identity_keypair) } - fn load_sphinx_keys(pathfinder: &ClientKeyPathfinder) -> encryption::KeyPair { + fn load_sphinx_keys( + pathfinder: &ClientKeyPathfinder, + ) -> Result { let sphinx_keypair: encryption::KeyPair = pemstore::load_keypair(&pemstore::KeyPairPath::new( pathfinder.private_encryption_key().to_owned(), pathfinder.public_encryption_key().to_owned(), )) - .expect("Failed to read stored sphinx key files"); - sphinx_keypair + .tap_err(|_| log::error!("Failed to read stored sphinx key files"))?; + Ok(sphinx_keypair) } let pathfinder = ClientKeyPathfinder::new_from_config(config); - let identity_keypair = load_identity_keys(&pathfinder); - let sphinx_keypair = load_sphinx_keys(&pathfinder); + let identity_keypair = load_identity_keys(&pathfinder)?; + let sphinx_keypair = load_sphinx_keys(&pathfinder)?; let client_recipient = Recipient::new( *identity_keypair.public_key(), *sphinx_keypair.public_key(), // TODO: below only works under assumption that gateway address == gateway id // (which currently is true) - NodeIdentity::from_base58_string(config.get_gateway_id()).unwrap(), + NodeIdentity::from_base58_string(config.get_gateway_id())?, ); println!("\nThe address of this client is: {}", client_recipient); + Ok(()) } diff --git a/clients/client-core/src/lib.rs b/clients/client-core/src/lib.rs index 6bbed6b087..04e215daec 100644 --- a/clients/client-core/src/lib.rs +++ b/clients/client-core/src/lib.rs @@ -1,3 +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 4b975aacf5..81fe361ad0 100644 --- a/clients/credential/Cargo.toml +++ b/clients/credential/Cargo.toml @@ -9,15 +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/client.rs b/clients/credential/src/client.rs index 005aab356e..0eb63bf340 100644 --- a/clients/credential/src/client.rs +++ b/clients/credential/src/client.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::Result; -use crate::{MNEMONIC, NYMD_URL}; use bip39::Mnemonic; use network_defaults::{NymNetworkDetails, VOUCHER_INFO}; use std::str::FromStr; @@ -17,9 +16,9 @@ pub(crate) struct Client { } impl Client { - pub fn new() -> Self { - let nymd_url = Url::from_str(NYMD_URL).unwrap(); - let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap(); + pub fn new(nymd_url: &str, mnemonic: &str) -> Self { + let nymd_url = Url::from_str(nymd_url).unwrap(); + let mnemonic = Mnemonic::from_str(mnemonic).unwrap(); let network_details = NymNetworkDetails::new_from_env(); let config = nymd::Config::try_from_nym_network_details(&network_details) .expect("failed to construct valid validator client config with the provided network"); diff --git a/clients/credential/src/commands.rs b/clients/credential/src/commands.rs index f95c476120..c281332810 100644 --- a/clients/credential/src/commands.rs +++ b/clients/credential/src/commands.rs @@ -3,10 +3,10 @@ use async_trait::async_trait; use clap::{Args, Subcommand}; +use completions::ArgShell; use pickledb::PickleDb; use rand::rngs::OsRng; use std::str::FromStr; -use url::Url; use coconut_interface::{Attribute, Base58, BlindSignRequest, Bytable, Parameters}; use credential_storage::storage::Storage; @@ -20,7 +20,6 @@ use validator_client::nymd::tx::Hash; use crate::client::Client; use crate::error::{CredentialClientError, Result}; use crate::state::{KeyPair, RequestData, State}; -use crate::SIGNER_AUTHORITIES; #[derive(Subcommand)] pub(crate) enum Commands { @@ -30,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] @@ -39,6 +44,12 @@ pub(crate) trait Execute { #[derive(Args, Clone)] pub(crate) struct Deposit { + /// The nymd URL that should be used + #[clap(long)] + nymd_url: String, + /// A mnemonic for the account that does the deposit + #[clap(long)] + mnemonic: String, /// The amount that needs to be deposited #[clap(long)] amount: u64, @@ -51,7 +62,7 @@ impl Execute for Deposit { let signing_keypair = KeyPair::from(identity::KeyPair::new(&mut rng)); let encryption_keypair = KeyPair::from(encryption::KeyPair::new(&mut rng)); - let client = Client::new(); + let client = Client::new(&self.nymd_url, &self.mnemonic); let tx_hash = client .deposit( self.amount, @@ -96,6 +107,10 @@ pub(crate) struct GetCredential { /// The hash of a successful deposit transaction #[clap(long)] tx_hash: String, + /// The URLs to the validator-api endpoints the are run as coconut signer authorities, separated + /// by comma (,) + #[clap(long)] + signer_authorities: String, /// If we want to get the signature without attaching a blind sign request; it is expected that /// there is already a signature stored on the signer #[clap(long, parse(from_flag))] @@ -108,7 +123,8 @@ impl Execute for GetCredential { let mut state = db .get::(&self.tx_hash) .ok_or(CredentialClientError::NoDeposit)?; - let urls = SIGNER_AUTHORITIES.map(|addr| Url::from_str(addr).unwrap()); + + let urls = config::parse_validators(&self.signer_authorities); let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap(); let bandwidth_credential_attributes = if self.__no_request { diff --git a/clients/credential/src/main.rs b/clients/credential/src/main.rs index 6a34a71d8b..a3d286f09f 100644 --- a/clients/credential/src/main.rs +++ b/clients/credential/src/main.rs @@ -11,20 +11,26 @@ 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}; - pub const MNEMONIC: &str = "jazz fatigue diagram account outer wrist slide cherry mother grid network pause wolf pig round answer mail junior better hair dismiss toward access end"; - pub const NYMD_URL: &str = "http://127.0.0.1:26657"; - pub const CONTRACT_ADDRESS: &str = "nymt1nc5tatafv6eyq7llkr2gv50ff9e22mnfp9pc5s"; - pub const SIGNER_AUTHORITIES: [&str; 1] = [ - "http://127.0.0.1:8080", - ]; - #[derive(Parser)] #[clap(author = "Nymtech", version, about)] struct Cli { + /// Path pointing to an env file that configures the client. + #[clap(long)] + pub(crate) config_env_file: Option, + + /// Path where the sqlite credental database will be located. + /// It should point to a $HOME/$CLIENT_ID/data/db.sqlite file of + /// the client that is supposed to use the credential. + #[clap(long)] + pub(crate) credential_db_path: std::path::PathBuf, + #[clap(subcommand)] command: Commands, } @@ -32,8 +38,9 @@ cfg_if::cfg_if! { #[tokio::main] async fn main() -> Result<()> { let args = Cli::parse(); + setup_env(args.config_env_file.clone()); - let shared_storage = credential_storage::initialise_storage(std::path::PathBuf::from("/tmp/credential.db")).await; + let shared_storage = credential_storage::initialise_storage(args.credential_db_path.clone()).await; let mut db = match PickleDb::load( "credential.db", PickleDbDumpPolicy::AutoDump, @@ -47,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 4d712233b2..476ffec29a 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -20,36 +20,38 @@ 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 } -credentials = { path = "../../common/credentials", optional = true } -credential-storage = { path = "../../common/credential-storage" } config = { path = "../../common/config" } +completions = { path = "../../common/completions" } +credential-storage = { path = "../../common/credential-storage" } +credentials = { path = "../../common/credentials", optional = true } crypto = { path = "../../common/crypto" } +logging = { path = "../../common/logging"} gateway-client = { path = "../../common/client-libs/gateway-client" } gateway-requests = { path = "../../gateway/gateway-requests" } +network-defaults = { path = "../../common/network-defaults" } nymsphinx = { path = "../../common/nymsphinx" } pemstore = { path = "../../common/pemstore" } +task = { path = "../../common/task" } topology = { path = "../../common/topology" } -websocket-requests = { path = "websocket-requests" } validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] } version-checker = { path = "../../common/version-checker" } -network-defaults = { path = "../../common/network-defaults" } +websocket-requests = { path = "websocket-requests" } [features] coconut = ["coconut-interface", "credentials", "credentials/coconut", "gateway-requests/coconut", "gateway-client/coconut", "client-core/coconut"] -eth = [] [dev-dependencies] serde_json = "1.0" # for the "textsend" example diff --git a/clients/native/examples/js-examples/websocket/package-lock.json b/clients/native/examples/js-examples/websocket/package-lock.json index 36c731306a..4fc3fbbd83 100644 --- a/clients/native/examples/js-examples/websocket/package-lock.json +++ b/clients/native/examples/js-examples/websocket/package-lock.json @@ -27,6 +27,58 @@ "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==", + "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==", + "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==", + "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==", + "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==" + }, + "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==", + "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", @@ -3529,13 +3581,13 @@ } }, "node_modules/terser": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz", - "integrity": "sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", + "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", "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": { @@ -3583,14 +3635,6 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, - "node_modules/terser/node_modules/source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "engines": { - "node": ">= 8" - } - }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", @@ -4330,6 +4374,49 @@ "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==", + "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==" + }, + "@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==" + }, + "@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==", + "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==" + }, + "@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==", + "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", @@ -7058,13 +7145,13 @@ "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" }, "terser": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz", - "integrity": "sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.0.tgz", + "integrity": "sha512-L1BJiXVmheAQQy+as0oF3Pwtlo4s3Wi1X2zNZ2NxOB4wx9bdS9Vk67XQENLFdLYGCK/Z2di53mTj/hBafR+dTA==", "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": { @@ -7072,11 +7159,6 @@ "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==" - }, - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==" } } }, diff --git a/clients/native/src/client/config/mod.rs b/clients/native/src/client/config/mod.rs index 3d23e02e90..f9c04ca1d7 100644 --- a/clients/native/src/client/config/mod.rs +++ b/clients/native/src/client/config/mod.rs @@ -50,6 +50,10 @@ impl NymConfig for Config { .join("clients") } + fn try_default_root_directory() -> Option { + dirs::home_dir().map(|path| path.join(".nym").join("clients")) + } + fn root_directory(&self) -> PathBuf { self.base.get_nym_root_directory() } diff --git a/clients/native/src/client/config/template.rs b/clients/native/src/client/config/template.rs index 8136f629c5..ca25e6c794 100644 --- a/clients/native/src/client/config/template.rs +++ b/clients/native/src/client/config/template.rs @@ -49,12 +49,6 @@ reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}' # Path to the database containing bandwidth credentials database_path = '{{ client.database_path }}' -# Ethereum private key. -eth_private_key = '{{ client.eth_private_key }}' - -# Addess to an Ethereum full node. -eth_endpoint = '{{ client.eth_endpoint }}' - ##### additional client config options ##### # A gateway specific, optional, base58 stringified shared key used for diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index b795001845..9e61d5f0e0 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -6,9 +6,7 @@ use client_core::client::inbound_messages::{ InputMessage, InputMessageReceiver, InputMessageSender, }; use client_core::client::key_manager::KeyManager; -use client_core::client::mix_traffic::{ - BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController, -}; +use client_core::client::mix_traffic::{BatchMixMessageSender, MixTrafficController}; use client_core::client::real_messages_control; use client_core::client::real_messages_control::RealMessagesController; use client_core::client::received_buffer::{ @@ -32,6 +30,7 @@ use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; use nymsphinx::anonymous_replies::ReplySurb; use nymsphinx::receiver::ReconstructedMessage; +use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; use crate::client::config::{Config, SocketType}; use crate::websocket; @@ -85,10 +84,11 @@ impl NymClient { &self, topology_accessor: TopologyAccessor, mix_tx: BatchMixMessageSender, + shutdown: ShutdownListener, ) { 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(), @@ -98,8 +98,14 @@ impl NymClient { mix_tx, self.as_mix_recipient(), topology_accessor, - ) - .start(); + ); + + if let Some(size) = self.config.get_base().get_use_extended_packet_size() { + log::debug!("Setting extended packet size: {:?}", size); + stream.set_custom_packet_size(size.into()); + } + + stream.start_with_shutdown(shutdown); } fn start_real_traffic_controller( @@ -109,17 +115,26 @@ impl NymClient { ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, 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 let Some(size) = self.config.get_base().get_use_extended_packet_size() { + log::debug!("Setting extended packet size: {:?}", size); + controller_config.set_custom_packet_size(size.into()); + } + info!("Starting real traffic stream..."); RealMessagesController::new( @@ -130,7 +145,7 @@ impl NymClient { topology_accessor, reply_key_storage, ) - .start(); + .start_with_shutdown(shutdown); } // buffer controlling all messages fetched from provider @@ -140,6 +155,7 @@ impl NymClient { query_receiver: ReceivedBufferRequestReceiver, mixnet_receiver: MixnetMessageReceiver, reply_key_storage: ReplyKeyStorage, + shutdown: ShutdownListener, ) { info!("Starting received messages buffer controller..."); ReceivedMessagesBufferController::new( @@ -148,13 +164,14 @@ impl NymClient { mixnet_receiver, reply_key_storage, ) - .start() + .start_with_shutdown(shutdown) } async fn start_gateway_client( &mut self, mixnet_message_sender: MixnetMessageSender, ack_sender: AcknowledgementSender, + shutdown: ShutdownListener, ) -> GatewayClient { let gateway_id = self.config.get_base().get_gateway_id(); if gateway_id.is_empty() { @@ -182,8 +199,6 @@ impl NymClient { let bandwidth_controller = BandwidthController::new( credential_storage::initialise_storage(self.config.get_base().get_database_path()) .await, - self.config.get_base().get_eth_endpoint(), - self.config.get_base().get_eth_private_key(), ) .expect("Could not create bandwidth controller"); @@ -197,11 +212,12 @@ impl NymClient { ack_sender, self.config.get_base().get_gateway_response_timeout(), Some(bandwidth_controller), + Some(shutdown), ); - if self.config.get_base().get_disabled_credentials_mode() { - gateway_client.set_disabled_credentials_mode(true) - } + gateway_client + .set_disabled_credentials_mode(self.config.get_base().get_disabled_credentials_mode()); + gateway_client .authenticate_and_start() .await @@ -212,7 +228,11 @@ impl NymClient { // 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) { + async fn start_topology_refresher( + &mut self, + topology_accessor: TopologyAccessor, + shutdown: ShutdownListener, + ) { let topology_refresher_config = TopologyRefresherConfig::new( self.config.get_base().get_validator_api_endpoints(), self.config.get_base().get_topology_refresh_rate(), @@ -234,7 +254,7 @@ impl NymClient { } info!("Starting topology refresher..."); - topology_refresher.start(); + topology_refresher.start_with_shutdown(shutdown); } // controller for sending sphinx packets to mixnet (either real traffic or cover traffic) @@ -242,12 +262,13 @@ impl NymClient { // 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, - ) { + shutdown: ShutdownListener, + ) -> BatchMixMessageSender { info!("Starting mix traffic controller..."); - MixTrafficController::new(mix_rx, gateway_client).start(); + let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client); + mix_traffic_controller.start_with_shutdown(shutdown); + mix_tx } fn start_websocket_listener( @@ -308,31 +329,32 @@ impl NymClient { /// blocking version of `start` method. Will run forever (or until SIGINT is sent) pub async fn run_forever(&mut self) { - self.start().await; - if let Err(e) = tokio::signal::ctrl_c().await { - error!( - "There was an error while capturing SIGINT - {:?}. We will terminate regardless", - e - ); - } + let shutdown = self.start().await; + wait_for_signal().await; println!( - "Received SIGINT - the client will terminate now (threads are not yet nicely stopped, if you see stack traces that's alright)." + "Received signal - the client will terminate now (threads are not yet nicely stopped, if you see stack traces that's alright)." ); + + log::info!("Sending shutdown"); + shutdown.signal_shutdown().ok(); + + // Some of these components have shutdown signalling implemented as part of socks5 work, + // but since it's not fully implemented (yet) for all the components of the native client, + // we don't try to wait and instead just stop immediately. + //log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); + //shutdown.wait_for_shutdown().await; + + log::info!("Stopping nym-client"); } - pub async fn start(&mut self) { + pub async fn start(&mut self) -> ShutdownNotifier { info!("Starting nym client"); // 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(); @@ -351,30 +373,51 @@ impl NymClient { ReplyKeyStorage::load(self.config.get_base().get_reply_encryption_key_store_path()) .expect("Failed to load reply key storage!"); + // Shutdown notifier for signalling tasks to stop + let shutdown = ShutdownNotifier::default(); + // 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()) + self.start_topology_refresher(shared_topology_accessor.clone(), shutdown.subscribe()) .await; self.start_received_messages_buffer_controller( received_buffer_request_receiver, mixnet_messages_receiver, reply_key_storage.clone(), + shutdown.subscribe(), ); let gateway_client = self - .start_gateway_client(mixnet_messages_sender, ack_sender) + .start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe()) .await; - self.start_mix_traffic_controller(sphinx_message_receiver, gateway_client); + // The 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. + // The MixTrafficController then sends the actual traffic + let sphinx_message_sender = + Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe()); + self.start_real_traffic_controller( shared_topology_accessor.clone(), reply_key_storage, ack_receiver, input_receiver, sphinx_message_sender.clone(), + shutdown.subscribe(), ); - self.start_cover_traffic_stream(shared_topology_accessor, sphinx_message_sender); + 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 => { @@ -399,5 +442,7 @@ impl NymClient { info!("Client startup finished!"); info!("The address of this client is: {}", self.as_mix_recipient()); + + shutdown } } diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 875aaa2721..208a026959 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use clap::Args; -use client_core::config::GatewayEndpoint; +use client_core::{config::GatewayEndpoint, error::ClientCoreError}; use config::NymConfig; use crate::{ @@ -10,9 +10,6 @@ use crate::{ commands::{override_config, OverrideConfig}, }; -#[cfg(all(feature = "eth", not(feature = "coconut")))] -use crate::commands::{DEFAULT_ETH_ENDPOINT, DEFAULT_ETH_PRIVATE_KEY}; - #[derive(Args, Clone)] pub(crate) struct Init { /// Id of the nym-mixnet-client we want to create config for. @@ -46,29 +43,10 @@ pub(crate) struct Init { fastmode: bool, /// Set this client to work in a enabled credentials mode that would attempt to use gateway - /// with bandwidth credential requirement. If this value is set, --eth-endpoint and - /// --eth-private_key don't need to be set. - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])] + /// with bandwidth credential requirement. + #[cfg(feature = "coconut")] + #[clap(long)] enabled_credentials_mode: bool, - - /// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 - /// tokens. If you don't want to set this value, use --enabled-credentials-mode instead - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap( - long, - default_value_if("enabled-credentials-mode", None, Some(DEFAULT_ETH_ENDPOINT)) - )] - eth_endpoint: String, - - /// Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't - /// want to set this value, use --enabled-credentials-mode instead") - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap( - long, - default_value_if("enabled-credentials-mode", None, Some(DEFAULT_ETH_PRIVATE_KEY)) - )] - eth_private_key: String, } impl From for OverrideConfig { @@ -79,14 +57,8 @@ impl From for OverrideConfig { port: init_config.port, fastmode: init_config.fastmode, - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] enabled_credentials_mode: init_config.enabled_credentials_mode, - - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_private_key: Some(init_config.eth_private_key), - - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_endpoint: Some(init_config.eth_endpoint), } } } @@ -121,7 +93,12 @@ pub(crate) async fn execute(args: &Init) { let override_config_fields = OverrideConfig::from(args.clone()); config = override_config(config, override_config_fields); - let gateway = setup_gateway(id, register_gateway, user_chosen_gateway_id, &config).await; + let gateway = setup_gateway(id, register_gateway, user_chosen_gateway_id, &config) + .await + .unwrap_or_else(|err| { + eprintln!("Failed to setup gateway\nError: {err}"); + std::process::exit(1) + }); config.get_base_mut().with_gateway_endpoint(gateway); let config_save_location = config.get_config_file_save_location(); @@ -139,7 +116,10 @@ pub(crate) async fn execute(args: &Init) { ); println!("Client configuration completed."); - client_core::init::show_address(config.get_base()); + client_core::init::show_address(config.get_base()).unwrap_or_else(|err| { + eprintln!("Failed to show address\nError: {err}"); + std::process::exit(1) + }); } async fn setup_gateway( @@ -147,7 +127,7 @@ async fn setup_gateway( register: bool, user_chosen_gateway_id: Option<&str>, config: &Config, -) -> GatewayEndpoint { +) -> Result { if register { // Get the gateway details by querying the validator-api. Either pick one at random or use // the chosen one if it's among the available ones. @@ -156,16 +136,16 @@ async fn setup_gateway( config.get_base().get_validator_api_endpoints(), user_chosen_gateway_id, ) - .await; + .await?; log::debug!("Querying gateway gives: {}", gateway); // Registering with gateway by setting up and writing shared keys to disk log::trace!("Registering gateway"); client_core::init::register_with_gateway_and_store_keys(gateway.clone(), config.get_base()) - .await; + .await?; println!("Saved all generated keys"); - gateway.into() + Ok(gateway.into()) } else if user_chosen_gateway_id.is_some() { // Just set the config, don't register or create any keys // This assumes that the user knows what they are doing, and that the existing keys are @@ -175,22 +155,22 @@ async fn setup_gateway( config.get_base().get_validator_api_endpoints(), user_chosen_gateway_id, ) - .await; + .await?; log::debug!("Querying gateway gives: {}", gateway); - gateway.into() + Ok(gateway.into()) } else { println!("Not registering gateway, will reuse existing config and keys"); - match Config::load_from_file(Some(id)) { - Ok(existing_config) => existing_config.get_base().get_gateway_endpoint().clone(), - Err(err) => { - panic!( - "Unable to configure gateway: {err}. \n - Seems like the client was already initialized but it was not possible to read \ - the existing configuration file. \n - CAUTION: Consider backing up your gateway keys and try force gateway registration, or \ - removing the existing configuration and starting over." - ) - } - } + let existing_config = Config::load_from_file(Some(id)).map_err(|err| { + log::error!( + "Unable to configure gateway: {err}. \n + Seems like the client was already initialized but it was not possible to read \ + the existing configuration file. \n + CAUTION: Consider backing up your gateway keys and try force gateway registration, or \ + removing the existing configuration and starting over." + ); + ClientCoreError::CouldNotLoadExistingGatewayConfiguration(err) + })?; + + Ok(existing_config.get_base().get_gateway_endpoint().clone()) } } diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index 06ee44ea13..fdbf7ff5bf 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -2,14 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::client::config::{Config, SocketType}; +use clap::CommandFactory; use clap::{Parser, Subcommand}; - -#[cfg(not(feature = "coconut"))] -pub(crate) const DEFAULT_ETH_ENDPOINT: &str = - "https://rinkeby.infura.io/v3/00000000000000000000000000000000"; -#[cfg(not(feature = "coconut"))] -pub(crate) const DEFAULT_ETH_PRIVATE_KEY: &str = - "0000000000000000000000000000000000000000000000000000000000000001"; +use completions::{fig_generate, ArgShell}; pub(crate) mod init; pub(crate) mod run; @@ -69,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. @@ -78,21 +79,19 @@ pub(crate) struct OverrideConfig { port: Option, fastmode: bool, - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] enabled_credentials_mode: bool, - - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_private_key: Option, - - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_endpoint: Option, } 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), } } @@ -117,27 +116,11 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi config = config.with_port(port); } - #[cfg(all(not(feature = "eth"), not(feature = "coconut")))] - { - config - .get_base_mut() - .with_eth_endpoint(DEFAULT_ETH_ENDPOINT.to_string()); - config - .get_base_mut() - .with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY.to_string()); - } - - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] { if args.enabled_credentials_mode { config.get_base_mut().with_disabled_credentials(false) } - if let Some(eth_endpoint) = args.eth_endpoint { - config.get_base_mut().with_eth_endpoint(eth_endpoint); - } - if let Some(eth_private_key) = args.eth_private_key { - config.get_base_mut().with_eth_private_key(eth_private_key); - } } if args.fastmode { diff --git a/clients/native/src/commands/run.rs b/clients/native/src/commands/run.rs index 1d2af52f8f..6fd09fbc4b 100644 --- a/clients/native/src/commands/run.rs +++ b/clients/native/src/commands/run.rs @@ -35,23 +35,10 @@ pub(crate) struct Run { port: Option, /// Set this client to work in a enabled credentials mode that would attempt to use gateway - /// with bandwidth credential requirement. If this value is set, --eth-endpoint and - /// --eth-private-key don't need to be set. - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])] + /// with bandwidth credential requirement. + #[cfg(feature = "coconut")] + #[clap(long)] enabled_credentials_mode: bool, - - /// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 - /// tokens. If you don't want to set this value, use --enabled-credentials-mode instead - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap(long)] - eth_endpoint: Option, - - /// Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't - /// want to set this value, use --enabled-credentials-mode instead") - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap(long)] - eth_private_key: Option, } impl From for OverrideConfig { @@ -61,15 +48,8 @@ impl From for OverrideConfig { disable_socket: run_config.disable_socket, port: run_config.port, fastmode: false, - - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] enabled_credentials_mode: run_config.enabled_credentials_mode, - - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_private_key: run_config.eth_private_key, - - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_endpoint: run_config.eth_endpoint, } } } 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/native/src/main.rs b/clients/native/src/main.rs index 13e82ec660..cc78423353 100644 --- a/clients/native/src/main.rs +++ b/clients/native/src/main.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use clap::{crate_version, Parser}; +use logging::setup_logging; use network_defaults::setup_env; pub mod client; @@ -34,25 +35,3 @@ fn banner() -> String { crate_version!() ) } - -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .filter_module("tungstenite", log::LevelFilter::Warn) - .filter_module("tokio_tungstenite", log::LevelFilter::Warn) - .filter_module("handlebars", log::LevelFilter::Warn) - .filter_module("sled", log::LevelFilter::Warn) - .init(); -} diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 008db6fe85..6aa35d14eb 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,27 +20,30 @@ 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 } -credentials = { path = "../../common/credentials", optional = true } -credential-storage = { path = "../../common/credential-storage" } config = { path = "../../common/config" } +completions = { path = "../../common/completions" } +credential-storage = { path = "../../common/credential-storage" } +credentials = { path = "../../common/credentials", optional = true } crypto = { path = "../../common/crypto" } +logging = { path = "../../common/logging"} gateway-client = { path = "../../common/client-libs/gateway-client" } gateway-requests = { path = "../../gateway/gateway-requests" } +network-defaults = { path = "../../common/network-defaults" } nymsphinx = { path = "../../common/nymsphinx" } ordered-buffer = { path = "../../common/socks5/ordered-buffer" } -socks5-requests = { path = "../../common/socks5/requests" } -topology = { path = "../../common/topology" } pemstore = { path = "../../common/pemstore" } proxy-helpers = { path = "../../common/socks5/proxy-helpers" } +socks5-requests = { path = "../../common/socks5/requests" } +task = { path = "../../common/task" } +topology = { path = "../../common/topology" } validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] } version-checker = { path = "../../common/version-checker" } -network-defaults = { path = "../../common/network-defaults" } [features] coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut", "credentials/coconut", "client-core/coconut"] diff --git a/clients/socks5/src/client/config/mod.rs b/clients/socks5/src/client/config/mod.rs index d3ef369109..4c43984f35 100644 --- a/clients/socks5/src/client/config/mod.rs +++ b/clients/socks5/src/client/config/mod.rs @@ -33,6 +33,10 @@ impl NymConfig for Config { .join("socks5-clients") } + fn try_default_root_directory() -> Option { + dirs::home_dir().map(|path| path.join(".nym").join("socks5-clients")) + } + fn root_directory(&self) -> PathBuf { self.base.get_nym_root_directory() } diff --git a/clients/socks5/src/client/config/template.rs b/clients/socks5/src/client/config/template.rs index 7165776317..4168d44e85 100644 --- a/clients/socks5/src/client/config/template.rs +++ b/clients/socks5/src/client/config/template.rs @@ -49,12 +49,6 @@ reply_encryption_key_store_path = '{{ client.reply_encryption_key_store_path }}' # Path to the database containing bandwidth credentials database_path = '{{ client.database_path }}' -# Ethereum private key. -eth_private_key = '{{ client.eth_private_key }}' - -# Addess to an Ethereum full node. -eth_endpoint = '{{ client.eth_endpoint }}' - ##### additional client config options ##### # A gateway specific, optional, base58 stringified shared key used for diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 6669a99430..59762ab749 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -1,14 +1,19 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +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, }; use client_core::client::key_manager::KeyManager; -use client_core::client::mix_traffic::{ - BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController, -}; +use client_core::client::mix_traffic::{BatchMixMessageSender, MixTrafficController}; use client_core::client::real_messages_control::RealMessagesController; use client_core::client::received_buffer::{ ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, @@ -29,12 +34,7 @@ use gateway_client::{ use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; - -use crate::client::config::Config; -use crate::socks::{ - authentication::{AuthenticationMethods, Authenticator, User}, - server::SphinxSocksServer, -}; +use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; pub mod config; @@ -84,10 +84,11 @@ impl NymClient { &self, topology_accessor: TopologyAccessor, mix_tx: BatchMixMessageSender, + shutdown: ShutdownListener, ) { 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(), @@ -97,8 +98,14 @@ impl NymClient { mix_tx, self.as_mix_recipient(), topology_accessor, - ) - .start(); + ); + + if let Some(size) = self.config.get_base().get_use_extended_packet_size() { + log::debug!("Setting extended packet size: {:?}", size); + stream.set_custom_packet_size(size.into()); + } + + stream.start_with_shutdown(shutdown); } fn start_real_traffic_controller( @@ -108,17 +115,26 @@ impl NymClient { ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, 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 let Some(size) = self.config.get_base().get_use_extended_packet_size() { + log::debug!("Setting extended packet size: {:?}", size); + controller_config.set_custom_packet_size(size.into()); + } + info!("Starting real traffic stream..."); RealMessagesController::new( @@ -129,7 +145,7 @@ impl NymClient { topology_accessor, reply_key_storage, ) - .start(); + .start_with_shutdown(shutdown); } // buffer controlling all messages fetched from provider @@ -139,6 +155,7 @@ impl NymClient { query_receiver: ReceivedBufferRequestReceiver, mixnet_receiver: MixnetMessageReceiver, reply_key_storage: ReplyKeyStorage, + shutdown: ShutdownListener, ) { info!("Starting received messages buffer controller..."); ReceivedMessagesBufferController::new( @@ -147,13 +164,14 @@ impl NymClient { mixnet_receiver, reply_key_storage, ) - .start() + .start_with_shutdown(shutdown); } async fn start_gateway_client( &mut self, mixnet_message_sender: MixnetMessageSender, ack_sender: AcknowledgementSender, + shutdown: ShutdownListener, ) -> GatewayClient { let gateway_id = self.config.get_base().get_gateway_id(); if gateway_id.is_empty() { @@ -181,8 +199,6 @@ impl NymClient { let bandwidth_controller = BandwidthController::new( credential_storage::initialise_storage(self.config.get_base().get_database_path()) .await, - self.config.get_base().get_eth_endpoint(), - self.config.get_base().get_eth_private_key(), ) .expect("Could not create bandwidth controller"); @@ -196,11 +212,12 @@ impl NymClient { ack_sender, self.config.get_base().get_gateway_response_timeout(), Some(bandwidth_controller), + Some(shutdown), ); - if self.config.get_base().get_disabled_credentials_mode() { - gateway_client.set_disabled_credentials_mode(true) - } + gateway_client + .set_disabled_credentials_mode(self.config.get_base().get_disabled_credentials_mode()); + gateway_client .authenticate_and_start() .await @@ -211,7 +228,11 @@ impl NymClient { // 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) { + async fn start_topology_refresher( + &mut self, + topology_accessor: TopologyAccessor, + shutdown: ShutdownListener, + ) { let topology_refresher_config = TopologyRefresherConfig::new( self.config.get_base().get_validator_api_endpoints(), self.config.get_base().get_topology_refresh_rate(), @@ -233,7 +254,7 @@ impl NymClient { } info!("Starting topology refresher..."); - topology_refresher.start(); + topology_refresher.start_with_shutdown(shutdown); } // controller for sending sphinx packets to mixnet (either real traffic or cover traffic) @@ -241,18 +262,20 @@ impl NymClient { // 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, - ) { + shutdown: ShutdownListener, + ) -> BatchMixMessageSender { info!("Starting mix traffic controller..."); - MixTrafficController::new(mix_rx, gateway_client).start(); + let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client); + mix_traffic_controller.start_with_shutdown(shutdown); + mix_tx } fn start_socks5_listener( &self, buffer_requester: ReceivedBufferRequestSender, msg_input: InputMessageSender, + shutdown: ShutdownListener, ) { info!("Starting socks5 listener..."); let auth_methods = vec![AuthenticationMethods::NoAuth as u8]; @@ -264,54 +287,63 @@ impl NymClient { authenticator, self.config.get_provider_mix_address(), self.as_mix_recipient(), + shutdown, ); tokio::spawn(async move { sphinx_socks.serve(msg_input, buffer_requester).await }); } /// blocking version of `start` method. Will run forever (or until SIGINT is sent) pub async fn run_forever(&mut self) { - self.start().await; - if let Err(e) = tokio::signal::ctrl_c().await { - error!( - "There was an error while capturing SIGINT - {:?}. We will terminate regardless", - e - ); - } + let mut shutdown = self.start().await; + wait_for_signal().await; - println!( - "Received SIGINT - the client will terminate now (threads are not yet nicely stopped, if you see stack traces that's alright)." - ); + log::info!("Sending shutdown"); + client_core::client::SHUTDOWN_HAS_BEEN_SIGNALLED.store(true, Ordering::Relaxed); + shutdown.signal_shutdown().ok(); + + log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); + shutdown.wait_for_shutdown().await; + + log::info!("Stopping nym-socks5-client"); } // Variant of `run_forever` that listends for remote control messages pub async fn run_and_listen(&mut self, mut receiver: Socks5ControlMessageReceiver) { - self.start().await; + let mut shutdown = self.start().await; tokio::select! { message = receiver.next() => { log::debug!("Received message: {:?}", message); match message { Some(Socks5ControlMessage::Stop) => { - log::info!("Shutting down"); - log::info!("Graceful shutdown of tasks not yet implemented, you might see (harmless) panics until then"); + log::info!("Received stop message"); + } + None => { + log::info!("Channel closed, stopping"); } - None => log::debug!("None"), } } + _ = tokio::signal::ctrl_c() => { + log::info!("Received SIGINT"); + }, } + + log::info!("Sending shutdown"); + client_core::client::SHUTDOWN_HAS_BEEN_SIGNALLED.store(true, Ordering::Relaxed); + shutdown.signal_shutdown().ok(); + + log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); + shutdown.wait_for_shutdown().await; + + log::info!("Stopping nym-socks5-client"); } - pub async fn start(&mut self) { + pub async fn start(&mut self) -> ShutdownNotifier { info!("Starting nym client"); // 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(); @@ -330,33 +362,61 @@ impl NymClient { ReplyKeyStorage::load(self.config.get_base().get_reply_encryption_key_store_path()) .expect("Failed to load reply key storage!"); + // Shutdown notifier for signalling tasks to stop + let shutdown = ShutdownNotifier::default(); + // 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()) + self.start_topology_refresher(shared_topology_accessor.clone(), shutdown.subscribe()) .await; self.start_received_messages_buffer_controller( received_buffer_request_receiver, mixnet_messages_receiver, reply_key_storage.clone(), + shutdown.subscribe(), ); let gateway_client = self - .start_gateway_client(mixnet_messages_sender, ack_sender) + .start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe()) .await; - self.start_mix_traffic_controller(sphinx_message_receiver, gateway_client); + // The 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. + // The MixTrafficController then sends the actual traffic + let sphinx_message_sender = + Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe()); + self.start_real_traffic_controller( shared_topology_accessor.clone(), reply_key_storage, ack_receiver, input_receiver, sphinx_message_sender.clone(), + shutdown.subscribe(), ); - self.start_cover_traffic_stream(shared_topology_accessor, sphinx_message_sender); - self.start_socks5_listener(received_buffer_request_sender, input_sender); + 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, + shutdown.subscribe(), + ); info!("Client startup finished!"); info!("The address of this client is: {}", self.as_mix_recipient()); + + shutdown } } diff --git a/clients/socks5/src/commands/init.rs b/clients/socks5/src/commands/init.rs index 92c72189d3..fb93e4e30c 100644 --- a/clients/socks5/src/commands/init.rs +++ b/clients/socks5/src/commands/init.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use clap::Args; -use client_core::config::GatewayEndpoint; +use client_core::{config::GatewayEndpoint, error::ClientCoreError}; use config::NymConfig; use crate::{ @@ -10,9 +10,6 @@ use crate::{ commands::{override_config, OverrideConfig}, }; -#[cfg(all(feature = "eth", not(feature = "coconut")))] -use crate::commands::{DEFAULT_ETH_ENDPOINT, DEFAULT_ETH_PRIVATE_KEY}; - #[derive(Args, Clone)] pub(crate) struct Init { /// Id of the nym-mixnet-client we want to create config for. @@ -46,29 +43,10 @@ pub(crate) struct Init { fastmode: bool, /// Set this client to work in a enabled credentials mode that would attempt to use gateway - /// with bandwidth credential requirement. If this value is set, --eth-endpoint and - /// --eth-private_key don't need to be set. - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])] + /// with bandwidth credential requirement. + #[cfg(feature = "coconut")] + #[clap(long)] enabled_credentials_mode: bool, - - /// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 - /// tokens. If you don't want to set this value, use --enabled-credentials-mode instead - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap( - long, - default_value_if("enabled-credentials-mode", None, Some(DEFAULT_ETH_ENDPOINT)) - )] - eth_endpoint: String, - - /// Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't - /// want to set this value, use --enabled-credentials-mode instead") - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap( - long, - default_value_if("enabled-credentials-mode", None, Some(DEFAULT_ETH_PRIVATE_KEY)) - )] - eth_private_key: String, } impl From for OverrideConfig { @@ -77,15 +55,8 @@ impl From for OverrideConfig { validators: init_config.validators, port: init_config.port, fastmode: init_config.fastmode, - - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] enabled_credentials_mode: init_config.enabled_credentials_mode, - - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_private_key: Some(init_config.eth_private_key), - - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_endpoint: Some(init_config.eth_endpoint), } } } @@ -121,7 +92,12 @@ pub(crate) async fn execute(args: &Init) { let override_config_fields = OverrideConfig::from(args.clone()); config = override_config(config, override_config_fields); - let gateway = setup_gateway(id, register_gateway, user_chosen_gateway_id, &config).await; + let gateway = setup_gateway(id, register_gateway, user_chosen_gateway_id, &config) + .await + .unwrap_or_else(|err| { + eprintln!("Failed to setup gateway\nError: {err}"); + std::process::exit(1) + }); config.get_base_mut().with_gateway_endpoint(gateway); let config_save_location = config.get_config_file_save_location(); @@ -139,7 +115,10 @@ pub(crate) async fn execute(args: &Init) { ); println!("Client configuration completed."); - client_core::init::show_address(config.get_base()); + client_core::init::show_address(config.get_base()).unwrap_or_else(|err| { + eprintln!("Failed to show address\nError: {err}"); + std::process::exit(1) + }); } async fn setup_gateway( @@ -147,7 +126,7 @@ async fn setup_gateway( register: bool, user_chosen_gateway_id: Option<&str>, config: &Config, -) -> GatewayEndpoint { +) -> Result { if register { // Get the gateway details by querying the validator-api. Either pick one at random or use // the chosen one if it's among the available ones. @@ -156,16 +135,16 @@ async fn setup_gateway( config.get_base().get_validator_api_endpoints(), user_chosen_gateway_id, ) - .await; + .await?; log::debug!("Querying gateway gives: {}", gateway); // Registering with gateway by setting up and writing shared keys to disk log::trace!("Registering gateway"); client_core::init::register_with_gateway_and_store_keys(gateway.clone(), config.get_base()) - .await; + .await?; println!("Saved all generated keys"); - gateway.into() + Ok(gateway.into()) } else if user_chosen_gateway_id.is_some() { // Just set the config, don't register or create any keys // This assumes that the user knows what they are doing, and that the existing keys are @@ -175,22 +154,21 @@ async fn setup_gateway( config.get_base().get_validator_api_endpoints(), user_chosen_gateway_id, ) - .await; + .await?; log::debug!("Querying gateway gives: {}", gateway); - gateway.into() + Ok(gateway.into()) } else { println!("Not registering gateway, will reuse existing config and keys"); - match Config::load_from_file(Some(id)) { - Ok(existing_config) => existing_config.get_base().get_gateway_endpoint().clone(), - Err(err) => { - panic!( - "Unable to configure gateway: {err}. \n - Seems like the client was already initialized but it was not possible to read \ - the existing configuration file. \n - CAUTION: Consider backing up your gateway keys and try force gateway registration, or \ - removing the existing configuration and starting over." - ) - } - } + let existing_config = Config::load_from_file(Some(id)).map_err(|err| { + log::error!( + "Unable to configure gateway: {err}. \n + Seems like the client was already initialized but it was not possible to read \ + the existing configuration file. \n + CAUTION: Consider backing up your gateway keys and try force gateway registration, or \ + removing the existing configuration and starting over." + ); + ClientCoreError::CouldNotLoadExistingGatewayConfiguration(err) + })?; + Ok(existing_config.get_base().get_gateway_endpoint().clone()) } } diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index cba489e00a..dbed90777f 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -2,20 +2,15 @@ // 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; pub(crate) mod run; pub(crate) mod upgrade; -#[cfg(not(feature = "coconut"))] -pub(crate) const DEFAULT_ETH_ENDPOINT: &str = - "https://rinkeby.infura.io/v3/00000000000000000000000000000000"; -#[cfg(not(feature = "coconut"))] -pub(crate) const DEFAULT_ETH_PRIVATE_KEY: &str = - "0000000000000000000000000000000000000000000000000000000000000001"; - fn long_version() -> String { format!( r#" @@ -70,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. @@ -78,21 +79,19 @@ pub(crate) struct OverrideConfig { port: Option, fastmode: bool, - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] enabled_credentials_mode: bool, - - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_private_key: Option, - - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_endpoint: Option, } 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), } } @@ -111,27 +110,11 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi config = config.with_port(port); } - #[cfg(all(not(feature = "eth"), not(feature = "coconut")))] - { - config - .get_base_mut() - .with_eth_endpoint(DEFAULT_ETH_ENDPOINT.to_string()); - config - .get_base_mut() - .with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY.to_string()); - } - - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] { if args.enabled_credentials_mode { config.get_base_mut().with_disabled_credentials(false) } - if let Some(eth_endpoint) = args.eth_endpoint { - config.get_base_mut().with_eth_endpoint(eth_endpoint); - } - if let Some(eth_private_key) = args.eth_private_key { - config.get_base_mut().with_eth_private_key(eth_private_key); - } } if args.fastmode { diff --git a/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs index 7d64c695ea..c3b596c916 100644 --- a/clients/socks5/src/commands/run.rs +++ b/clients/socks5/src/commands/run.rs @@ -39,23 +39,10 @@ pub(crate) struct Run { port: Option, /// Set this client to work in a enabled credentials mode that would attempt to use gateway - /// with bandwidth credential requirement. If this value is set, --eth-endpoint and - /// --eth-private-key don't need to be set. - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap(long, conflicts_with_all = &["eth-endpoint", "eth-private-key"])] + /// with bandwidth credential requirement. + #[cfg(feature = "coconut")] + #[clap(long)] enabled_credentials_mode: bool, - - /// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 - /// tokens. If you don't want to set this value, use --enabled-credentials-mode instead - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap(long)] - eth_endpoint: Option, - - /// Ethereum private key used for obtaining bandwidth tokens from ERC20 tokens. If you don't - /// want to set this value, use --enabled-credentials-mode instead - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap(long)] - eth_private_key: Option, } impl From for OverrideConfig { @@ -65,14 +52,8 @@ impl From for OverrideConfig { port: run_config.port, fastmode: false, - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] enabled_credentials_mode: run_config.enabled_credentials_mode, - - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_private_key: run_config.eth_private_key, - - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_endpoint: run_config.eth_endpoint, } } } 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/socks5/src/main.rs b/clients/socks5/src/main.rs index fb7a8bb607..21ab3f33c5 100644 --- a/clients/socks5/src/main.rs +++ b/clients/socks5/src/main.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use clap::{crate_version, Parser}; +use logging::setup_logging; use network_defaults::setup_env; pub mod client; @@ -34,23 +35,3 @@ fn banner() -> String { crate_version!() ) } - -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .filter_module("tungstenite", log::LevelFilter::Warn) - .filter_module("tokio_tungstenite", log::LevelFilter::Warn) - .init(); -} diff --git a/clients/socks5/src/socks/client.rs b/clients/socks5/src/socks/client.rs index 2c3dc578f2..71ad3514ec 100644 --- a/clients/socks5/src/socks/client.rs +++ b/clients/socks5/src/socks/client.rs @@ -20,6 +20,7 @@ use socks5_requests::{ConnectionId, Message, RemoteAddress, Request}; use std::io; use std::net::SocketAddr; use std::pin::Pin; +use task::ShutdownListener; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, ReadBuf}; use tokio::{self, net::TcpStream}; @@ -140,6 +141,7 @@ pub(crate) struct SocksClient { service_provider: Recipient, self_address: Recipient, started_proxy: bool, + shutdown_listener: ShutdownListener, } impl Drop for SocksClient { @@ -163,6 +165,7 @@ impl SocksClient { service_provider: Recipient, controller_sender: ControllerSender, self_address: Recipient, + shutdown_listener: ShutdownListener, ) -> Self { let connection_id = Self::generate_random(); SocksClient { @@ -176,6 +179,7 @@ impl SocksClient { service_provider, self_address, started_proxy: false, + shutdown_listener, } } @@ -250,6 +254,7 @@ impl SocksClient { conn_receiver, input_sender, connection_id, + self.shutdown_listener.clone(), ) .run(move |conn_id, read_data, socket_closed| { let provider_request = Request::new_send(conn_id, read_data, socket_closed); diff --git a/clients/socks5/src/socks/mixnet_responses.rs b/clients/socks5/src/socks/mixnet_responses.rs index 3146967711..5bd83bee85 100644 --- a/clients/socks5/src/socks/mixnet_responses.rs +++ b/clients/socks5/src/socks/mixnet_responses.rs @@ -1,16 +1,19 @@ -use client_core::client::received_buffer::ReconstructedMessagesReceiver; -use client_core::client::received_buffer::{ReceivedBufferMessage, ReceivedBufferRequestSender}; use futures::channel::mpsc; use futures::StreamExt; use log::*; + +use client_core::client::received_buffer::ReconstructedMessagesReceiver; +use client_core::client::received_buffer::{ReceivedBufferMessage, ReceivedBufferRequestSender}; use nymsphinx::receiver::ReconstructedMessage; use proxy_helpers::connection_controller::{ControllerCommand, ControllerSender}; use socks5_requests::Message; +use task::ShutdownListener; pub(crate) struct MixnetResponseListener { buffer_requester: ReceivedBufferRequestSender, mix_response_receiver: ReconstructedMessagesReceiver, controller_sender: ControllerSender, + shutdown: ShutdownListener, } impl Drop for MixnetResponseListener { @@ -25,6 +28,7 @@ impl MixnetResponseListener { pub(crate) fn new( buffer_requester: ReceivedBufferRequestSender, controller_sender: ControllerSender, + shutdown: ShutdownListener, ) -> Self { let (mix_response_sender, mix_response_receiver) = mpsc::unbounded(); buffer_requester @@ -35,6 +39,7 @@ impl MixnetResponseListener { buffer_requester, mix_response_receiver, controller_sender, + shutdown, } } @@ -54,6 +59,13 @@ impl MixnetResponseListener { return; } Ok(Message::Response(data)) => data, + Ok(Message::NetworkRequesterResponse(r)) => { + error!( + "Network requester failed on connection id {} with error: {}", + r.connection_id, r.network_requester_error + ); + return; + } }; self.controller_sender @@ -66,11 +78,25 @@ impl MixnetResponseListener { } pub(crate) async fn run(&mut self) { - while let Some(received_responses) = self.mix_response_receiver.next().await { - for reconstructed_message in received_responses { - self.on_message(reconstructed_message).await; + while !self.shutdown.is_shutdown() { + tokio::select! { + received_responses = self.mix_response_receiver.next() => match received_responses { + Some(received_responses) => { + for reconstructed_message in received_responses { + self.on_message(reconstructed_message).await; + } + }, + None => { + log::trace!("MixnetResponseListener: Stopping since channel closed"); + break; + } + }, + _ = self.shutdown.recv() => { + log::trace!("MixnetResponseListener: Received shutdown"); + } } } - error!("We should never see this message"); + assert!(self.shutdown.is_shutdown_poll()); + log::debug!("MixnetResponseListener: Exiting"); } } diff --git a/clients/socks5/src/socks/server.rs b/clients/socks5/src/socks/server.rs index afa6f88e02..c8feebbfb1 100644 --- a/clients/socks5/src/socks/server.rs +++ b/clients/socks5/src/socks/server.rs @@ -11,6 +11,7 @@ use log::*; use nymsphinx::addressing::clients::Recipient; use proxy_helpers::connection_controller::Controller; use std::net::SocketAddr; +use task::ShutdownListener; use tokio::net::TcpListener; /// A Socks5 server that listens for connections. @@ -19,6 +20,7 @@ pub struct SphinxSocksServer { listening_address: SocketAddr, service_provider: Recipient, self_address: Recipient, + shutdown: ShutdownListener, } impl SphinxSocksServer { @@ -28,6 +30,7 @@ impl SphinxSocksServer { authenticator: Authenticator, service_provider: Recipient, self_address: Recipient, + shutdown: ShutdownListener, ) -> Self { // hardcode ip as we (presumably) ONLY want to listen locally. If we change it, we can // just modify the config @@ -38,6 +41,7 @@ impl SphinxSocksServer { listening_address: format!("{}:{}", ip, port).parse().unwrap(), service_provider, self_address, + shutdown, } } @@ -52,62 +56,73 @@ impl SphinxSocksServer { info!("Serving Connections..."); // controller for managing all active connections - let (mut active_streams_controller, controller_sender) = Controller::new(); + let (mut active_streams_controller, controller_sender) = + Controller::new(self.shutdown.clone()); tokio::spawn(async move { active_streams_controller.run().await; }); // listener for mix messages - let mut mixnet_response_listener = - MixnetResponseListener::new(buffer_requester, controller_sender.clone()); - + let mut mixnet_response_listener = MixnetResponseListener::new( + buffer_requester, + controller_sender.clone(), + self.shutdown.clone(), + ); tokio::spawn(async move { mixnet_response_listener.run().await; }); loop { - if let Ok((stream, _remote)) = listener.accept().await { - // TODO Optimize this - let mut client = SocksClient::new( - stream, - self.authenticator.clone(), - input_sender.clone(), - self.service_provider, - controller_sender.clone(), - self.self_address, - ); + tokio::select! { + Ok((stream, _remote)) = listener.accept() => { + // TODO Optimize this + let mut client = SocksClient::new( + stream, + self.authenticator.clone(), + input_sender.clone(), + self.service_provider, + controller_sender.clone(), + self.self_address, + self.shutdown.clone(), + ); - tokio::spawn(async move { - { - match client.run().await { - Ok(_) => {} - Err(error) => { - error!("Error! {}", error); - let error_text = format!("{}", error); + tokio::spawn(async move { + { + match client.run().await { + Ok(_) => {} + Err(error) => { + error!("Error! {}", error); + let error_text = format!("{}", error); - let response: ResponseCode; + let response: ResponseCode; - if error_text.contains("Host") { - response = ResponseCode::HostUnreachable; - } else if error_text.contains("Network") { - response = ResponseCode::NetworkUnreachable; - } else if error_text.contains("ttl") { - response = ResponseCode::TtlExpired - } else { - response = ResponseCode::Failure + if error_text.contains("Host") { + response = ResponseCode::HostUnreachable; + } else if error_text.contains("Network") { + response = ResponseCode::NetworkUnreachable; + } else if error_text.contains("ttl") { + response = ResponseCode::TtlExpired + } else { + response = ResponseCode::Failure + } + + if client.error(response).await.is_err() { + warn!("Failed to send error code"); + }; + if client.shutdown().await.is_err() { + warn!("Failed to shutdown TcpStream"); + }; } - - if client.error(response).await.is_err() { - warn!("Failed to send error code"); - }; - if client.shutdown().await.is_err() { - warn!("Failed to shutdown TcpStream"); - }; - } - }; - // client gets dropped here - } - }); + }; + // client gets dropped here + } + }); + }, + _ = self.shutdown.recv() => { + log::trace!("SphinxSocksServer: Received shutdown"); + log::debug!("SphinxSocksServer: Exiting"); + return Ok(()); + } } } } diff --git a/clients/validator/tests/query/balance.test.ts b/clients/validator/tests/query/balance.test.ts index b9ed9e8737..254e54a745 100644 --- a/clients/validator/tests/query/balance.test.ts +++ b/clients/validator/tests/query/balance.test.ts @@ -4,8 +4,8 @@ import expect from 'expect'; describe('Query: balances', () => { it('can query for an account balance', async () => { const client = await ValidatorClient.connectForQuery( - 'https://rpc.nyx.nodes.guru/', 'https://validator.nymtech.net/api/', 'n', 'n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g', 'n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw', 'nym'); + 'https://rpc.nymtech.net/', 'https://validator.nymtech.net/api/', 'n', 'n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g', 'n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw', 'nym'); const balance = await client.getBalance('n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy'); expect(Number.parseFloat(balance.amount)).toBeGreaterThan(0); }).timeout(5000); -}) \ No newline at end of file +}) diff --git a/clients/webassembly/Cargo.toml b/clients/webassembly/Cargo.toml index 372551a29d..6f457bd2b9 100644 --- a/clients/webassembly/Cargo.toml +++ b/clients/webassembly/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "nym-client-wasm" authors = ["Dave Hrycyszyn ", "Jedrzej Stuczynski "] -version = "1.0.1" +version = "1.0.0" edition = "2021" keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"] license = "Apache-2.0" @@ -20,19 +20,21 @@ 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" } nymsphinx = { path = "../../common/nymsphinx" } topology = { path = "../../common/topology" } -gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm"] } +gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm", "coconut"] } validator-client = { path = "../../common/client-libs/validator-client", default-features = false } wasm-utils = { path = "../../common/wasm-utils" } diff --git a/clients/webassembly/README.md b/clients/webassembly/README.md index d18c0db8c8..d2bb4966cc 100644 --- a/clients/webassembly/README.md +++ b/clients/webassembly/README.md @@ -11,6 +11,7 @@ This client is part of the [Nym](https://nymtech.net/docs) project. It's written ## Security Status From a security point of view, this module is not yet complete. Key missing features include, but are not limited to: cover traffic, sending packets with delay according to Poisson distribution. + They should be implemented soon. You can build your applications, but don't rely on it for strong anonymity yet if your application needs cover traffic. ## Using it @@ -37,6 +38,6 @@ To be clear, this is not something that most JS developers need to worry about, If you're a Nym platform developer who's made changes to the Rust (or JS) files and wants to re-publish the package to NPM, here's how you do it: -1. `wasm-pack build --scope nymproject` builds the wasm binaries into the `pkg` directory (not in source control) -2. bump version numbers as necessary for SemVer -3. `wasm-pack publish --access=public` will publish your changed package to NPM +1. bump version numbers as necessary for SemVer +2. `wasm-pack build --scope nymproject` builds the wasm binaries into the `pkg` directory (not in source control) +3. `cd pkg && npm publish --access=public` will publish your changed package to NPM 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 e8d76eff17..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://sandbox-validator.nymtech.net/api"; //"http://localhost:8081"; - - 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().substr(11, 12); + 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().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 + ((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) + 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 9b7ff01b7d..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,6 +32,7 @@ "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" } @@ -41,6 +42,7 @@ "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" @@ -54,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" } @@ -63,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" @@ -80,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": "*" @@ -90,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": "*" } @@ -99,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": "*" } @@ -108,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": "*" @@ -115,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": "*" @@ -125,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": "*" @@ -137,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", @@ -153,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": "*", @@ -164,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": "*" @@ -231,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": "*" } @@ -249,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" @@ -258,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", @@ -287,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", @@ -306,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" } @@ -315,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" } @@ -323,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", @@ -346,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", @@ -359,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", @@ -371,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", @@ -385,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" @@ -392,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" @@ -402,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" }, @@ -414,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" }, @@ -430,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" @@ -453,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" }, @@ -468,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" @@ -490,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", @@ -506,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" }, @@ -523,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" }, @@ -538,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" }, @@ -559,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" @@ -569,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" }, @@ -587,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" } @@ -598,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", @@ -638,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", @@ -664,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" @@ -674,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" }, @@ -683,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": [ { @@ -696,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", @@ -714,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" } @@ -736,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" @@ -746,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": [ { @@ -758,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", @@ -771,6 +790,7 @@ "url": "https://paulmillr.com/funding/" } ], + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -792,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" }, @@ -804,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" } @@ -822,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", @@ -833,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" }, @@ -860,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", @@ -876,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" }, @@ -905,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" } @@ -921,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", @@ -950,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", @@ -973,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", @@ -999,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" }, @@ -1011,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" }, @@ -1029,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", @@ -1051,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", @@ -1080,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" } @@ -1090,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" }, @@ -1123,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" @@ -1138,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" } @@ -1148,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" @@ -1184,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" }, @@ -1195,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" } @@ -1209,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" @@ -1230,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" }, @@ -1242,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" } @@ -1251,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" } @@ -1258,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" } @@ -1268,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" } @@ -1284,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", @@ -1304,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", @@ -1346,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", @@ -1376,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" }, @@ -1387,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" } @@ -1409,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" }, @@ -1421,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" }, @@ -1430,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", @@ -1451,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" @@ -1461,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": [ { @@ -1470,6 +1507,7 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -1484,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" } @@ -1491,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" @@ -1527,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", @@ -1548,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" }, @@ -1557,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", @@ -1580,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" }, @@ -1591,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", @@ -1615,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" }, @@ -1642,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" }, @@ -1667,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", @@ -1678,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", @@ -1707,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", @@ -1732,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", @@ -1759,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" } @@ -1768,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" }, @@ -1780,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" } @@ -1789,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" @@ -1805,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" } @@ -1815,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" @@ -1826,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" }, @@ -1872,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" }, @@ -1881,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" }, @@ -1893,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" }, @@ -1908,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" }, @@ -1921,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" } @@ -1932,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" }, @@ -1944,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" } @@ -1971,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" }, @@ -1983,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" }, @@ -1992,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" @@ -2011,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" }, @@ -2023,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" }, @@ -2033,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" } @@ -2056,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", @@ -2067,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" } @@ -2100,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" }, @@ -2109,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" }, @@ -2137,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" } @@ -2158,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" @@ -2182,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" }, @@ -2194,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" } @@ -2203,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" }, @@ -2215,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" } @@ -2223,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" }, @@ -2239,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" }, @@ -2258,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" @@ -2276,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" } @@ -2293,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" } @@ -2324,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" }, @@ -2333,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" @@ -2349,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" } @@ -2360,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" }, @@ -2379,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" } @@ -2386,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" } @@ -2397,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" }, @@ -2412,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", @@ -2429,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" }, @@ -2444,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" }, @@ -2453,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" }, @@ -2468,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" @@ -2484,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" } @@ -2493,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" } @@ -2502,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" } @@ -2509,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" } @@ -2520,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" } @@ -2528,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" } @@ -2549,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" }, @@ -2568,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" }, @@ -2577,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", @@ -2591,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" @@ -2628,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" } @@ -2659,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" } @@ -2675,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", @@ -2696,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" } @@ -2708,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", @@ -2723,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" }, @@ -2735,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" }, @@ -2744,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" @@ -2763,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" } @@ -2770,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", @@ -2795,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" }, @@ -2807,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" } @@ -2816,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" } @@ -2825,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" @@ -2835,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" }, @@ -2864,6 +2944,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -2872,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", @@ -2902,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" }, @@ -2919,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", @@ -2943,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" } @@ -2959,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", @@ -2977,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", @@ -2992,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", @@ -3018,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" }, @@ -3039,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" }, @@ -3051,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" } @@ -3059,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" }, @@ -3075,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", @@ -3089,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" } @@ -3098,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" @@ -3108,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", @@ -3124,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", @@ -3135,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" }, @@ -3154,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", @@ -3172,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" }, @@ -3191,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" } @@ -3207,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" }, @@ -3231,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" } @@ -3240,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" }, @@ -3255,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" }, @@ -3267,15 +3365,15 @@ "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.12.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz", - "integrity": "sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.5.0", "commander": "^2.20.0", @@ -3291,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", @@ -3328,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", @@ -3344,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" } @@ -3352,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", @@ -3374,9 +3475,8 @@ }, "node_modules/terser/node_modules/source-map": { "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">= 8" } @@ -3385,13 +3485,15 @@ "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" }, @@ -3401,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" } @@ -3413,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" @@ -3424,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" } @@ -3435,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" } @@ -3442,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" } @@ -3467,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" @@ -3491,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", @@ -3544,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", @@ -3590,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", @@ -3619,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", @@ -3674,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" } @@ -3683,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" @@ -3696,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" } @@ -3705,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", @@ -3721,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" } @@ -3729,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", @@ -3754,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", @@ -3768,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" } @@ -3777,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" }, @@ -3791,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" }, @@ -3897,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": "*", @@ -3907,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": "*", @@ -3923,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": "*", @@ -3935,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": "*", @@ -3946,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": "*" @@ -3955,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": { @@ -3985,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": { @@ -4000,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", @@ -4019,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": "*" @@ -4174,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" @@ -4190,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": {} }, @@ -4209,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", @@ -4219,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": { @@ -4232,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", @@ -4278,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": { @@ -4294,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": { @@ -4306,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" @@ -4322,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": { @@ -4333,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", @@ -4351,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", @@ -4392,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", @@ -4411,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": { @@ -4433,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": { @@ -4472,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": { @@ -4489,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": { @@ -4526,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" @@ -4552,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": { @@ -4578,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": { @@ -4604,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", @@ -4633,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" @@ -4642,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", @@ -4658,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", @@ -4678,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 } } @@ -4687,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": { @@ -4714,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", @@ -4729,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" @@ -4739,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", @@ -4785,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": { @@ -4824,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": { @@ -4858,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", @@ -4897,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 } } @@ -4910,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", @@ -4940,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": { @@ -4973,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", @@ -4998,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": { @@ -5011,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": { @@ -5041,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", @@ -5058,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", @@ -5101,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": { @@ -5128,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": { @@ -5141,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", @@ -5152,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", @@ -5178,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": { @@ -5202,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", @@ -5246,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", @@ -5274,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": { @@ -5286,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" @@ -5304,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" @@ -5313,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": { @@ -5326,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": { @@ -5346,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": { @@ -5373,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", @@ -5399,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": { @@ -5427,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": { @@ -5445,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": { @@ -5460,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" @@ -5482,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": { @@ -5500,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", @@ -5548,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" @@ -5557,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" @@ -5573,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", @@ -5588,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": { @@ -5606,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": { @@ -5633,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", @@ -5643,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": { @@ -5655,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" @@ -5671,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" @@ -5717,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" @@ -5726,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", @@ -5755,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": { @@ -5773,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": { @@ -5805,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", @@ -5816,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" @@ -5825,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 } } @@ -5839,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", @@ -5855,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": { @@ -5882,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", @@ -5894,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 } } @@ -5935,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", @@ -5952,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", @@ -6038,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" @@ -6052,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", @@ -6073,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 } } @@ -6091,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", @@ -6106,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", @@ -6118,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": { @@ -6131,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", @@ -6143,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": { @@ -6185,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", @@ -6225,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" @@ -6256,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" @@ -6285,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": { @@ -6299,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" @@ -6335,8 +6246,6 @@ }, "terser": { "version": "5.12.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.12.1.tgz", - "integrity": "sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==", "dev": true, "requires": { "acorn": "^8.5.0", @@ -6347,16 +6256,12 @@ "dependencies": { "source-map": { "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", "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", @@ -6421,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": { @@ -6438,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": { @@ -6453,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", @@ -6495,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", @@ -6565,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", @@ -6593,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", @@ -6606,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", @@ -6701,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 6086e52425..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: { asyncWebAssembly: 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..2b8b8bd644 --- /dev/null +++ b/clients/webassembly/src/client/config.rs @@ -0,0 +1,164 @@ +// 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, ExtendedPacketSize, 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 { + // For now we just always use the (older) 32kb extended size + let use_extended_packet_size = debug + .use_extended_packet_size + .then(|| ExtendedPacketSize::Extended32); + + 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, + } + } +} + +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.is_some(), + } + } +} + +#[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 704a9b2825..1fb324dc39 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -1,47 +1,46 @@ -// 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::{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 rand::rngs::OsRng; -use received_processor::ReceivedMessagesProcessor; -use std::sync::Arc; -use std::time::Duration; -use topology::{gateway, nym_topology_from_bonds, 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 +50,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,64 +77,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, ); - if disabled_credentials_mode { - gateway_client.set_disabled_credentials_mode(true) + if let Some(size) = &self.config.debug.use_extended_packet_size { + stream.set_custom_packet_size(size.clone().into()); } - 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 let Some(size) = &self.config.debug.use_extended_packet_size { + controller_config.set_custom_packet_size(size.clone().into()); + } + + 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()) @@ -151,125 +216,166 @@ 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(gateway_client: GatewayClient) -> BatchMixMessageSender { + console_log!("Starting mix traffic controller..."); + let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client); + mix_traffic_controller.start(); + mix_tx + } + + // 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 + + // 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; + + // The 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. + // The MixTrafficController then sends the actual traffic + let sphinx_message_sender = Self::start_mix_traffic_controller(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) - .await - .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_bonds(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 2cd7388139..01557331fa 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -10,40 +10,46 @@ edition = "2021" # TODO: (for this and other crates), similarly to 'tokio', import only required "futures" modules rather than # the entire crate futures = "0.3" -json = "0.12.4" log = "0.4" thiserror = "1.0" url = "2.2" rand = { version = "0.7.3", features = ["wasm-bindgen"] } -secp256k1 = "0.20.3" -web3 = { version = "0.17.0", default-features = false } async-trait = { version = "0.1.51" } +tokio = { version = "1.21.2", features = ["macros"] } # internal +coconut-interface = { path = "../../coconut-interface", optional = true } credentials = { path = "../../credentials" } crypto = { path = "../../crypto" } gateway-requests = { path = "../../../gateway/gateway-requests" } +network-defaults = { path = "../../network-defaults" } nymsphinx = { path = "../../nymsphinx" } pemstore = { path = "../../pemstore" } -coconut-interface = { path = "../../coconut-interface", optional = true } -network-defaults = { path = "../../network-defaults" } 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] +version = "0.1.9" +features = ["net", "sync", "time"] + [target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite] version = "0.14" [target."cfg(not(target_arch = \"wasm32\"))".dependencies.credential-storage] path = "../../credential-storage" +[target."cfg(not(target_arch = \"wasm32\"))".dependencies.task] +path = "../../task" + # wasm-only dependencies [target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen] version = "0.2" @@ -55,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. @@ -73,5 +80,4 @@ features = ["js"] [features] coconut = ["gateway-requests/coconut", "coconut-interface", "validator-client", "credentials/coconut"] -wasm = ["web3/wasm", "web3/http", "web3/signing"] -default = ["web3/default"] +wasm = [] diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs index 9154ab7cc4..77916876f1 100644 --- a/common/client-libs/gateway-client/src/bandwidth.rs +++ b/common/client-libs/gateway-client/src/bandwidth.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::GatewayClientError; -use std::str::FromStr; #[cfg(target_arch = "wasm32")] use crate::wasm_storage::Storage; @@ -15,6 +14,8 @@ use crate::wasm_storage::StorageError; #[cfg(all(not(target_arch = "wasm32"), feature = "coconut"))] use credential_storage::error::StorageError; +#[cfg(feature = "coconut")] +use std::str::FromStr; #[cfg(feature = "coconut")] use { coconut_interface::Base58, @@ -23,65 +24,12 @@ use { }, }; -#[cfg(not(feature = "coconut"))] -use { - credentials::token::bandwidth::TokenCredential, - crypto::asymmetric::identity, - network_defaults::{ - eth_contract::ETH_ERC20_JSON_ABI, eth_contract::ETH_JSON_ABI, BANDWIDTH_VALUE, - ETH_BURN_FUNCTION_NAME, ETH_CONTRACT_ADDRESS, ETH_ERC20_APPROVE_FUNCTION_NAME, - ETH_ERC20_CONTRACT_ADDRESS, ETH_MIN_BLOCK_DEPTH, TOKENS_TO_BURN, UTOKENS_TO_BURN, - }, - pemstore::traits::PemStorableKeyPair, - rand::rngs::OsRng, - secp256k1::SecretKey, - web3::{ - contract::{Contract, Options}, - ethabi::Token, - signing::{Key, SecretKeyRef}, - transports::Http, - types::{Address, U256, U64}, - Web3, - }, -}; - -#[cfg(not(feature = "coconut"))] -pub fn eth_contract(web3: Web3) -> Contract { - Contract::from_json( - web3.eth(), - Address::from(ETH_CONTRACT_ADDRESS), - json::parse(ETH_JSON_ABI) - .expect("Invalid json abi") - .dump() - .as_bytes(), - ) - .expect("Invalid json abi") -} - -#[cfg(not(feature = "coconut"))] -pub fn eth_erc20_contract(web3: Web3) -> Contract { - Contract::from_json( - web3.eth(), - Address::from(ETH_ERC20_CONTRACT_ADDRESS), - json::parse(ETH_ERC20_JSON_ABI) - .expect("Invalid json abi") - .dump() - .as_bytes(), - ) - .expect("Invalid json abi") -} - #[derive(Clone)] pub struct BandwidthController { + #[allow(dead_code)] storage: St, #[cfg(feature = "coconut")] validator_endpoints: Vec, - #[cfg(not(feature = "coconut"))] - contract: Contract, - #[cfg(not(feature = "coconut"))] - erc20_contract: Contract, - #[cfg(not(feature = "coconut"))] - eth_private_key: SecretKey, } impl BandwidthController @@ -97,60 +45,8 @@ where } #[cfg(not(feature = "coconut"))] - pub fn new( - storage: St, - eth_endpoint: String, - eth_private_key: String, - ) -> Result { - // Fail early, on invalid url - let transport = - Http::new(ð_endpoint).map_err(|_| GatewayClientError::InvalidURL(eth_endpoint))?; - let web3 = web3::Web3::new(transport); - // Fail early, on invalid abi - let contract = eth_contract(web3.clone()); - let erc20_contract = eth_erc20_contract(web3); - let eth_private_key = secp256k1::SecretKey::from_str(ð_private_key) - .map_err(|_| GatewayClientError::InvalidEthereumPrivateKey)?; - - Ok(BandwidthController { - storage, - contract, - erc20_contract, - eth_private_key, - }) - } - - #[cfg(not(feature = "coconut"))] - async fn backup_keypair(&self, keypair: &identity::KeyPair) -> Result<(), GatewayClientError> { - self.storage - .insert_erc20_credential( - keypair.public_key().to_base58_string(), - keypair.private_key().to_base58_string(), - ) - .await?; - - Ok(()) - } - - #[cfg(not(feature = "coconut"))] - async fn restore_keypair(&self) -> Result { - let data = self.storage.get_next_erc20_credential().await?; - let public_key = identity::PublicKey::from_base58_string(data.public_key).unwrap(); - let private_key = identity::PrivateKey::from_base58_string(data.private_key).unwrap(); - - Ok(identity::KeyPair::from_keys(private_key, public_key)) - } - - #[cfg(not(feature = "coconut"))] - async fn mark_keypair_as_spent( - &self, - keypair: &identity::KeyPair, - ) -> Result<(), GatewayClientError> { - self.storage - .consume_erc20_credential(keypair.public_key().to_base58_string()) - .await?; - - Ok(()) + pub fn new(storage: St) -> Result { + Ok(BandwidthController { storage }) } #[cfg(feature = "coconut")] @@ -179,206 +75,4 @@ where &verification_key, )?) } - - #[cfg(not(feature = "coconut"))] - pub async fn prepare_token_credential( - &self, - gateway_identity: identity::PublicKey, - gateway_owner: String, - ) -> Result { - let kp = match self.restore_keypair().await { - Ok(kp) => kp, - Err(_) => { - let mut rng = OsRng; - let kp = identity::KeyPair::new(&mut rng); - self.backup_keypair(&kp).await?; - kp - } - }; - - let verification_key = *kp.public_key(); - let signed_verification_key = kp.private_key().sign(&verification_key.to_bytes()); - self.buy_token_credential(verification_key, signed_verification_key, gateway_owner) - .await?; - - self.mark_keypair_as_spent(&kp).await?; - - let message: Vec = verification_key - .to_bytes() - .iter() - .chain(gateway_identity.to_bytes().iter()) - .copied() - .collect(); - let signature = kp.private_key().sign(&message); - - Ok(TokenCredential::new( - verification_key, - gateway_identity, - BANDWIDTH_VALUE, - signature, - )) - } - - #[cfg(not(feature = "coconut"))] - pub async fn buy_token_credential( - &self, - verification_key: identity::PublicKey, - signed_verification_key: identity::Signature, - gateway_owner: String, - ) -> Result<(), GatewayClientError> { - let confirmations = if cfg!(debug_assertions) { - 1 - } else { - ETH_MIN_BLOCK_DEPTH - }; - // 15 seconds per confirmation block + 10 seconds of network overhead + 20 seconds of wait for kill - log::info!( - "Waiting for Ethereum transaction. This should take about {} seconds", - (confirmations + 1) * 15 + 30 - ); - let mut options = Options::default(); - let estimation = self - .erc20_contract - .estimate_gas( - ETH_ERC20_APPROVE_FUNCTION_NAME, - ( - Token::Address(Address::from(ETH_CONTRACT_ADDRESS)), - Token::Uint(U256::from(UTOKENS_TO_BURN)), - ), - SecretKeyRef::from(&self.eth_private_key).address(), - options.clone(), - ) - .await?; - options.gas = Some(estimation); - log::info!("Calling ERC20 approve in 10 seconds with an estimated gas of {}. Kill the process if you want to abort", estimation); - #[cfg(not(target_arch = "wasm32"))] - tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; - #[cfg(target_arch = "wasm32")] - if let Err(err) = fluvio_wasm_timer::Delay::new(std::time::Duration::from_secs(10)).await { - log::error!( - "the timer has gone away while waiting for possible kill! - {}", - err - ); - } - let recipt = self - .erc20_contract - .signed_call_with_confirmations( - ETH_ERC20_APPROVE_FUNCTION_NAME, - ( - Token::Address(Address::from(ETH_CONTRACT_ADDRESS)), - Token::Uint(U256::from(UTOKENS_TO_BURN)), - ), - options, - 1, // One confirmation is enough, as we'll be consuming the approved token next anyway - &self.eth_private_key, - ) - .await?; - if Some(U64::from(0u64)) == recipt.status { - return Err(GatewayClientError::BurnTokenError( - web3::Error::InvalidResponse(format!( - "Approve transaction status is 0 (failure): {:?}", - recipt.logs, - )), - )); - } else { - log::info!( - "Approved {} tokens for bandwidth use on Ethereum", - TOKENS_TO_BURN - ); - } - - let mut options = Options::default(); - let estimation = self - .contract - .estimate_gas( - ETH_BURN_FUNCTION_NAME, - ( - Token::Uint(U256::from(UTOKENS_TO_BURN)), - Token::Uint(U256::from(&verification_key.to_bytes())), - Token::Bytes(signed_verification_key.to_bytes().to_vec()), - Token::String(gateway_owner.clone()), - ), - SecretKeyRef::from(&self.eth_private_key).address(), - options.clone(), - ) - .await?; - options.gas = Some(estimation); - log::info!("Generating bandwidth on ETH contract in 10 seconds with an estimated gas of {}. \ - Kill the process if you want to abort. Keep in mind that if you abort now, you'll still have \ - some tokens approved for bandwidth spending from the previous action. \ - If you don't want that, you'll need to manually decreaseAllowance to revert the approval.", estimation); - #[cfg(not(target_arch = "wasm32"))] - tokio::time::sleep(tokio::time::Duration::from_secs(10)).await; - #[cfg(target_arch = "wasm32")] - if let Err(err) = fluvio_wasm_timer::Delay::new(std::time::Duration::from_secs(10)).await { - log::error!( - "the timer has gone away while waiting for possible kill! - {}", - err - ); - } - let recipt = self - .contract - .signed_call_with_confirmations( - ETH_BURN_FUNCTION_NAME, - ( - Token::Uint(U256::from(UTOKENS_TO_BURN)), - Token::Uint(U256::from(&verification_key.to_bytes())), - Token::Bytes(signed_verification_key.to_bytes().to_vec()), - Token::String(gateway_owner), - ), - options, - confirmations, - &self.eth_private_key, - ) - .await?; - if Some(U64::from(0u64)) == recipt.status { - Err(GatewayClientError::BurnTokenError( - web3::Error::InvalidResponse(format!( - "Transaction status is 0 (failure): {:?}", - recipt.logs, - )), - )) - } else { - log::info!( - "Bought bandwidth on Ethereum: {} MB", - BANDWIDTH_VALUE / 1024 / 1024 - ); - Ok(()) - } - } -} - -#[cfg(not(feature = "coconut"))] -#[cfg(test)] -mod tests { - use network_defaults::ETH_EVENT_NAME; - - use super::*; - - #[test] - fn parse_contract() { - let transport = - Http::new("https://rinkeby.infura.io/v3/00000000000000000000000000000000").unwrap(); - let web3 = web3::Web3::new(transport); - // test no panic occurs - eth_contract(web3); - } - - #[test] - fn parse_erc20_contract() { - let transport = - Http::new("https://rinkeby.infura.io/v3/00000000000000000000000000000000").unwrap(); - let web3 = web3::Web3::new(transport); - // test no panic occurs - eth_erc20_contract(web3); - } - - #[test] - fn check_event_name_constant_against_abi() { - let transport = - Http::new("https://rinkeby.infura.io/v3/00000000000000000000000000000000").unwrap(); - let web3 = web3::Web3::new(transport); - let contract = eth_contract(web3); - assert!(contract.abi().event(ETH_EVENT_NAME).is_ok()); - } } diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index c29a65b080..9e17f55ff8 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -15,8 +15,6 @@ use crate::wasm_storage::PersistentStorage; use coconut_interface::Credential; #[cfg(not(target_arch = "wasm32"))] use credential_storage::PersistentStorage; -#[cfg(not(feature = "coconut"))] -use credentials::token::bandwidth::TokenCredential; use crypto::asymmetric::identity; use futures::{FutureExt, SinkExt, StreamExt}; use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; @@ -30,13 +28,15 @@ use rand::rngs::OsRng; use std::convert::TryFrom; use std::sync::Arc; use std::time::Duration; +#[cfg(not(target_arch = "wasm32"))] +use task::ShutdownListener; use tungstenite::protocol::Message; #[cfg(not(target_arch = "wasm32"))] 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; @@ -65,6 +65,10 @@ pub struct GatewayClient { reconnection_attempts: usize, /// Delay between each subsequent reconnection attempt. reconnection_backoff: Duration, + + #[cfg(not(target_arch = "wasm32"))] + /// Listen to shutdown messages. + shutdown: Option, } impl GatewayClient { @@ -80,6 +84,7 @@ impl GatewayClient { ack_sender: AcknowledgementSender, response_timeout_duration: Duration, bandwidth_controller: Option>, + #[cfg(not(target_arch = "wasm32"))] shutdown: Option, ) -> Self { GatewayClient { authenticated: false, @@ -91,12 +96,19 @@ impl GatewayClient { local_identity, shared_key, connection: SocketState::NotConnected, - packet_router: PacketRouter::new(ack_sender, mixnet_message_sender), + packet_router: PacketRouter::new( + ack_sender, + mixnet_message_sender, + #[cfg(not(target_arch = "wasm32"))] + shutdown.clone(), + ), response_timeout_duration, bandwidth_controller, should_reconnect_on_failure: true, reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS, reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF, + #[cfg(not(target_arch = "wasm32"))] + shutdown, } } @@ -123,6 +135,7 @@ impl GatewayClient { gateway_owner: String, local_identity: Arc, response_timeout_duration: Duration, + #[cfg(not(target_arch = "wasm32"))] shutdown: Option, ) -> Self { use futures::channel::mpsc; @@ -130,7 +143,12 @@ impl GatewayClient { // perfectly fine here, because it's not meant to be used let (ack_tx, _) = mpsc::unbounded(); let (mix_tx, _) = mpsc::unbounded(); - let packet_router = PacketRouter::new(ack_tx, mix_tx); + let packet_router = PacketRouter::new( + ack_tx, + mix_tx, + #[cfg(not(target_arch = "wasm32"))] + shutdown.clone(), + ); GatewayClient { authenticated: false, @@ -148,6 +166,8 @@ impl GatewayClient { should_reconnect_on_failure: false, reconnection_attempts: DEFAULT_RECONNECTION_ATTEMPTS, reconnection_backoff: DEFAULT_RECONNECTION_BACKOFF, + #[cfg(not(target_arch = "wasm32"))] + shutdown, } } @@ -279,8 +299,33 @@ impl GatewayClient { let mut fused_timeout = timeout.fuse(); let mut fused_stream = conn.fuse(); + // 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 = self.shutdown.clone(); + async { + if let Some(mut s) = m_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(); + loop { futures::select! { + _ = shutdown => { + log::trace!("GatewayClient control response: Received shutdown"); + log::debug!("GatewayClient control response: Exiting"); + break Err(GatewayClientError::ConnectionClosedGatewayShutdown); + } _ = &mut fused_timeout => { break Err(GatewayClientError::Timeout); } @@ -291,14 +336,16 @@ impl GatewayClient { }; match ws_msg { Message::Binary(bin_msg) => { - self.packet_router.route_received(vec![bin_msg]); + if let Err(err) = self.packet_router.route_received(vec![bin_msg]) { + log::warn!("Route received failed: {:?}", err); + } } Message::Text(txt_msg) => { break ServerResponse::try_from(txt_msg).map_err(|_| GatewayClientError::MalformedResponse); } _ => (), } - } + } } } } @@ -505,30 +552,6 @@ impl GatewayClient { Ok(()) } - #[cfg(not(feature = "coconut"))] - async fn claim_token_bandwidth( - &mut self, - credential: TokenCredential, - ) -> Result<(), GatewayClientError> { - let mut rng = OsRng; - - let iv = IV::new_random(&mut rng); - - let msg = ClientControlRequest::new_enc_token_bandwidth_credential( - &credential, - self.shared_key.as_ref().unwrap(), - iv, - ) - .into(); - self.bandwidth_remaining = match self.send_websocket_message(msg).await? { - ServerResponse::Bandwidth { available_total } => Ok(available_total), - ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), - _ => Err(GatewayClientError::UnexpectedResponse), - }?; - - Ok(()) - } - async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> { let msg = ClientControlRequest::ClaimFreeTestnetBandwidth.into(); self.bandwidth_remaining = match self.send_websocket_message(msg).await? { @@ -567,17 +590,10 @@ impl GatewayClient { .prepare_coconut_credential() .await?; #[cfg(not(feature = "coconut"))] - let credential = self - .bandwidth_controller - .as_ref() - .unwrap() - .prepare_token_credential(self.gateway_identity, _gateway_owner) - .await?; + return self.try_claim_testnet_bandwidth().await; #[cfg(feature = "coconut")] return self.claim_coconut_bandwidth(credential).await; - #[cfg(not(feature = "coconut"))] - return self.claim_token_bandwidth(credential).await; } fn estimate_required_bandwidth(&self, packets: &[MixPacket]) -> i64 { @@ -723,6 +739,8 @@ impl GatewayClient { .as_ref() .expect("no shared key present even though we're authenticated!"), ), + #[cfg(not(target_arch = "wasm32"))] + self.shutdown.clone(), ) } _ => unreachable!(), diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index 0890dcd12a..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), @@ -64,6 +50,9 @@ pub enum GatewayClientError { #[error("Connection was abruptly closed")] ConnectionAbruptlyClosed, + #[error("Connection was abruptly closed as gateway was stopped")] + ConnectionClosedGatewayShutdown, + #[error("Received response was malformed")] MalformedResponse, @@ -93,6 +82,9 @@ pub enum GatewayClientError { #[error("Timed out")] Timeout, + + #[error("Failed to send mixnet message")] + MixnetMsgSenderFailedToSend, } impl GatewayClientError { 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 7d650ad695..6732d2202e 100644 --- a/common/client-libs/gateway-client/src/packet_router.rs +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -8,6 +8,10 @@ use futures::channel::mpsc; use log::*; use nymsphinx::addressing::nodes::MAX_NODE_ADDRESS_UNPADDED_LEN; use nymsphinx::params::packet_sizes::PacketSize; +#[cfg(not(target_arch = "wasm32"))] +use task::ShutdownListener; + +use crate::error::GatewayClientError; pub type MixnetMessageSender = mpsc::UnboundedSender>>; pub type MixnetMessageReceiver = mpsc::UnboundedReceiver>>; @@ -19,20 +23,28 @@ pub type AcknowledgementReceiver = mpsc::UnboundedReceiver>>; pub struct PacketRouter { ack_sender: AcknowledgementSender, mixnet_message_sender: MixnetMessageSender, + #[cfg(not(target_arch = "wasm32"))] + shutdown: Option, } impl PacketRouter { pub fn new( ack_sender: AcknowledgementSender, mixnet_message_sender: MixnetMessageSender, + #[cfg(not(target_arch = "wasm32"))] shutdown: Option, ) -> Self { PacketRouter { ack_sender, mixnet_message_sender, + #[cfg(not(target_arch = "wasm32"))] + shutdown, } } - pub fn route_received(&self, unwrapped_packets: Vec>) { + pub fn route_received( + &mut self, + unwrapped_packets: Vec>, + ) -> Result<(), GatewayClientError> { let mut received_messages = Vec::new(); let mut received_acks = Vec::new(); @@ -47,11 +59,22 @@ 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 + == PacketSize::ExtendedPacket8.plaintext_size() - ack_overhead { - warn!("received extended packet? Did not expect this..."); + trace!("routing extended8 packet"); + received_messages.push(received_packet); + } else if received_packet.len() + == PacketSize::ExtendedPacket16.plaintext_size() - ack_overhead + { + trace!("routing extended16 packet"); + received_messages.push(received_packet); + } else if received_packet.len() + == PacketSize::ExtendedPacket32.plaintext_size() - ack_overhead + { + trace!("routing extended32 packet"); received_messages.push(received_packet); } else { // this can happen if other clients are not padding their messages @@ -60,24 +83,29 @@ impl PacketRouter { } } - // due to how we are currently using it, those unwraps can't fail, but if we ever - // wanted to make `gateway-client` into some more generic library, we would probably need - // to catch that error or something. if !received_messages.is_empty() { trace!("routing 'real'"); - self.mixnet_message_sender - .unbounded_send(received_messages) - .unwrap(); + if let Err(err) = self.mixnet_message_sender.unbounded_send(received_messages) { + #[cfg(not(target_arch = "wasm32"))] + if let Some(shutdown) = &mut self.shutdown { + if shutdown.is_shutdown_poll() { + // This should ideally not happen, but it's ok + log::warn!("Failed to send mixnet message due to receiver task shutdown"); + return Err(GatewayClientError::MixnetMsgSenderFailedToSend); + } + } + // This should never happen during ordinary operation the way it's currently used. + // Abort to be on the safe side + panic!("Failed to send mixnet message: {:?}", err); + } } if !received_acks.is_empty() { trace!("routing acks"); - match self.ack_sender.unbounded_send(received_acks) { - Ok(_) => {} - Err(e) => { - error!("failed to send ack: {:?}", e); - } + if let Err(e) = self.ack_sender.unbounded_send(received_acks) { + error!("failed to send ack: {:?}", e); }; } + Ok(()) } } diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 5de854921c..df1b95c0ae 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -1,16 +1,18 @@ -// 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::*; use std::sync::Arc; +#[cfg(not(target_arch = "wasm32"))] +use task::ShutdownListener; use tungstenite::Message; #[cfg(not(target_arch = "wasm32"))] @@ -42,16 +44,15 @@ pub(crate) struct PartiallyDelegated { } impl PartiallyDelegated { - fn route_socket_message( - ws_msg: Message, - packet_router: &PacketRouter, - shared_key: &SharedKeys, - ) { - 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, }, @@ -60,32 +61,44 @@ impl PartiallyDelegated { "message received from the gateway was malformed! - {:?}", err ); - return; + 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]) + // 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 + ); + continue; + } + _ => continue, } - // 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. + } + plaintexts + } - // 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 - ), - _ => (), - }; + 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( conn: WsConn, packet_router: PacketRouter, shared_key: Arc, + #[cfg(not(target_arch = "wasm32"))] shutdown: Option, ) -> Self { // when called for, it NEEDS TO yield back the stream so that we could merge it and // read control request responses. @@ -95,20 +108,47 @@ 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 = { + async { + if let Some(mut s) = shutdown { + s.recv().await + } else { + std::future::pending::<()>().await + } + } + }; + #[cfg(not(target_arch = "wasm32"))] + tokio::pin!(shutdown); + + #[cfg(target_arch = "wasm32")] + let mut shutdown = std::future::pending::<()>(); let ret_err = loop { - futures::select! { - _ = fused_receiver => { + tokio::select! { + _ = &mut shutdown => { + log::trace!("GatewayClient listener: Received shutdown"); + log::debug!("GatewayClient listener: Exiting"); + return; + } + _ = &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 }; - Self::route_socket_message(ws_msg, &packet_router, shared_key.as_ref()); + + if let Err(err) = Self::route_socket_messages(ws_msgs, &mut packet_router, shared_key.as_ref()) { + log::warn!("Route socket messages failed: {:?}", err); + } } }; }; @@ -162,12 +202,10 @@ impl PartiallyDelegated { .expect("stream sender was somehow dropped without sending anything!"); if let Some(res) = receive_res { - if let Err(err) = res { - // the receiver got an error. most likely a network one. - return Err(err); - } else { - panic!("This should have NEVER happened - returned a stream before receiving notification") - } + let _res = res?; + panic!( + "This should have NEVER happened - returned a stream before receiving notification" + ) } // this call failing is incredibly unlikely, but not impossible. diff --git a/common/client-libs/gateway-client/src/wasm_storage.rs b/common/client-libs/gateway-client/src/wasm_storage.rs index 5948762ca9..53a9a1b30e 100644 --- a/common/client-libs/gateway-client/src/wasm_storage.rs +++ b/common/client-libs/gateway-client/src/wasm_storage.rs @@ -26,13 +26,6 @@ pub struct CoconutCredential { pub signature: String, } -pub struct ERC20Credential { - pub id: i64, - pub public_key: String, - pub private_key: String, - pub consumed: bool, -} - #[async_trait] pub trait Storage: Send + Sync { async fn insert_coconut_credential( @@ -47,16 +40,6 @@ pub trait Storage: Send + Sync { async fn get_next_coconut_credential(&self) -> Result; async fn remove_coconut_credential(&self, id: i64) -> Result<(), StorageError>; - - async fn insert_erc20_credential( - &self, - public_key: String, - private_key: String, - ) -> Result<(), StorageError>; - - async fn get_next_erc20_credential(&self) -> Result; - - async fn consume_erc20_credential(&self, public_key: String) -> Result<(), StorageError>; } #[async_trait] @@ -79,20 +62,4 @@ impl Storage for PersistentStorage { async fn remove_coconut_credential(&self, _id: i64) -> Result<(), StorageError> { Err(StorageError::WasmNotSupported) } - - async fn insert_erc20_credential( - &self, - _public_key: String, - _private_key: String, - ) -> Result<(), StorageError> { - Err(StorageError::WasmNotSupported) - } - - async fn get_next_erc20_credential(&self) -> Result { - Err(StorageError::WasmNotSupported) - } - - async fn consume_erc20_credential(&self, _public_key: String) -> Result<(), StorageError> { - Err(StorageError::WasmNotSupported) - } } 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..600268b69b 100644 --- a/common/client-libs/mixnet-client/src/client.rs +++ b/common/client-libs/mixnet-client/src/client.rs @@ -23,6 +23,7 @@ pub struct Config { maximum_reconnection_backoff: Duration, initial_connection_timeout: Duration, maximum_connection_buffer_size: usize, + use_legacy_version: bool, } impl Config { @@ -31,12 +32,14 @@ impl Config { maximum_reconnection_backoff: Duration, initial_connection_timeout: Duration, maximum_connection_buffer_size: usize, + use_legacy_version: bool, ) -> Self { Config { initial_reconnection_backoff, maximum_reconnection_backoff, initial_connection_timeout, maximum_connection_buffer_size, + use_legacy_version, } } } @@ -186,7 +189,7 @@ impl Client { address.into(), receiver, initial_connection_timeout, - &*current_reconnection_attempt, + ¤t_reconnection_attempt, ) .await }); @@ -201,7 +204,8 @@ impl SendWithoutResponse for Client { packet_mode: PacketMode, ) -> io::Result<()> { trace!("Sending packet to {:?}", address); - let framed_packet = FramedSphinxPacket::new(packet, packet_mode); + let framed_packet = + FramedSphinxPacket::new(packet, packet_mode, self.config.use_legacy_version); if let Some(sender) = self.conn_new.get_mut(&address) { if let Err(err) = sender.channel.try_send(framed_packet) { @@ -259,6 +263,7 @@ mod tests { maximum_reconnection_backoff: Duration::from_millis(300_000), initial_connection_timeout: Duration::from_millis(1_500), maximum_connection_buffer_size: 128, + use_legacy_version: false, }) } diff --git a/common/client-libs/mixnet-client/src/forwarder.rs b/common/client-libs/mixnet-client/src/forwarder.rs index ead3b25a41..d92f6ad790 100644 --- a/common/client-libs/mixnet-client/src/forwarder.rs +++ b/common/client-libs/mixnet-client/src/forwarder.rs @@ -24,12 +24,14 @@ impl PacketForwarder { maximum_reconnection_backoff: Duration, initial_connection_timeout: Duration, maximum_connection_buffer_size: usize, + use_legacy_version: bool, ) -> (PacketForwarder, MixForwardingSender) { let client_config = Config::new( initial_reconnection_backoff, maximum_reconnection_backoff, initial_connection_timeout, maximum_connection_buffer_size, + use_legacy_version, ); let (packet_sender, packet_receiver) = mpsc::unbounded(); diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 67c472710c..2b45232238 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -13,6 +13,7 @@ colored = "2.0" cw3 = "0.13.1" mixnet-contract-common = { path= "../../cosmwasm-smart-contracts/mixnet-contract" } vesting-contract-common = { path= "../../cosmwasm-smart-contracts/vesting-contract" } +contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common" } coconut-bandwidth-contract-common = { path= "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" } multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } vesting-contract = { path = "../../../contracts/vesting" } @@ -22,7 +23,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 d820611bf0..a4d0a22f05 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -2,35 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{validator_api, ValidatorClientError}; -use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixNodeBond}; +use mixnet_contract_common::mixnode::MixNodeDetails; +use mixnet_contract_common::MixId; +use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; use url::Url; use validator_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ - CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, - StakeSaturationResponse, + GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, + RewardEstimationResponse, StakeSaturationResponse, }; #[cfg(feature = "nymd-client")] -use validator_api_requests::models::{MixNodeBondAnnotated, UptimeResponse}; - +use crate::nymd::traits::MixnetQueryClient; #[cfg(feature = "nymd-client")] -use crate::nymd::{ - self, error::NymdError, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient, -}; - +use crate::nymd::{self, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient}; #[cfg(feature = "nymd-client")] use mixnet_contract_common::{ - mixnode::DelegationEvent, ContractStateParams, Delegation, IdentityKey, Interval, - MixnetContractVersion, MixnodeRewardingStatusResponse, RewardedSetNodeStatus, - RewardedSetUpdateDetails, + mixnode::MixNodeBond, + pending_events::{PendingEpochEvent, PendingIntervalEvent}, + Delegation, RewardedSetNodeStatus, UnbondedMixnode, }; #[cfg(feature = "nymd-client")] use network_defaults::NymNetworkDetails; #[cfg(feature = "nymd-client")] -use std::collections::{HashMap, HashSet}; +use validator_api_requests::models::MixNodeBondAnnotated; #[cfg(feature = "nymd-client")] #[must_use] @@ -191,12 +189,9 @@ impl Client { } } +// nymd wrappers #[cfg(feature = "nymd-client")] impl Client { - pub fn change_validator_api(&mut self, new_endpoint: Url) { - self.validator_api.change_url(new_endpoint) - } - // use case: somebody initialised client without a contract in order to upload and initialise one // and now they want to actually use it without making new client pub fn set_mixnet_contract_address(&mut self, mixnet_contract_address: cosmrs::AccountId) { @@ -208,203 +203,22 @@ impl Client { self.nymd.mixnet_contract_address().clone() } - pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_mixnodes().await?) - } - - pub async fn get_cached_mixnodes_detailed( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_mixnodes_detailed().await?) - } - - pub async fn get_cached_rewarded_mixnodes( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_rewarded_mixnodes().await?) - } - - pub async fn get_cached_rewarded_mixnodes_detailed( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_rewarded_mixnodes_detailed().await?) - } - - pub async fn get_cached_active_mixnodes( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_active_mixnodes().await?) - } - - pub async fn get_cached_active_mixnodes_detailed( - &self, - ) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_active_mixnodes_detailed().await?) - } - - pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_gateways().await?) - } - - pub async fn get_contract_settings(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_contract_settings().await?) - } - - pub async fn get_operator_rewards(&self, address: String) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_operator_rewards(address).await?.u128()) - } - - pub async fn get_delegator_rewards( - &self, - address: String, - mix_identity: IdentityKey, - proxy: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self - .nymd - .get_delegator_rewards(address, mix_identity, proxy) - .await? - .u128()) - } - - pub async fn get_pending_delegation_events( - &self, - owner_address: String, - proxy_address: Option, - ) -> Result, ValidatorClientError> - where - C: CosmWasmClient + Sync, - { - Ok(self - .nymd - .get_pending_delegation_events(owner_address, proxy_address) - .await?) - } - - pub async fn get_current_epoch(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_current_epoch().await?) - } - - pub async fn get_current_operator_cost(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_current_operator_cost().await?) - } - - pub async fn get_mixnet_contract_version(&self) -> Result - where - C: CosmWasmClient + Sync, - { - self.nymd.get_mixnet_contract_version().await - } - - pub async fn get_rewarding_status( - &self, - mix_identity: mixnet_contract_common::IdentityKey, - rewarding_interval_nonce: u32, - ) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self - .nymd - .get_rewarding_status(mix_identity, rewarding_interval_nonce) - .await?) - } - - pub async fn get_reward_pool(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_reward_pool().await?.u128()) - } - - pub async fn get_circulating_supply(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_circulating_supply().await?.u128()) - } - - pub async fn get_sybil_resistance_percent(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_sybil_resistance_percent().await?) - } - - pub async fn get_active_set_work_factor(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_active_set_work_factor().await?) - } - - pub async fn get_epochs_in_interval(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_epochs_in_interval().await?) - } - - pub async fn get_interval_reward_percent(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.nymd.get_interval_reward_percent().await?) - } - - pub async fn get_current_rewarded_set_update_details( - &self, - ) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self - .nymd - .query_current_rewarded_set_update_details() - .await?) - } - // basically handles paging for us - pub async fn get_all_nymd_rewarded_set_mixnode_identities( + pub async fn get_all_nymd_rewarded_set_mixnodes( &self, - ) -> Result, ValidatorClientError> + ) -> Result, ValidatorClientError> where - C: CosmWasmClient + Sync, + C: CosmWasmClient + Sync + Send, { let mut identities = Vec::new(); let mut start_after = None; - let mut height = None; loop { let mut paged_response = self .nymd - .get_rewarded_set_identities_paged( - start_after.take(), - self.rewarded_set_page_limit, - height, - ) + .get_rewarded_set_paged(start_after.take(), self.rewarded_set_page_limit) .await?; - identities.append(&mut paged_response.identities); - - if height.is_none() { - // keep using the same height (the first query happened at the most recent height) - height = Some(paged_response.at_height) - } + identities.append(&mut paged_response.nodes); if let Some(start_after_res) = paged_response.start_next_after { start_after = Some(start_after_res) @@ -416,83 +230,122 @@ impl Client { Ok(identities) } - pub async fn get_nymd_rewarded_and_active_sets( - &self, - ) -> Result, ValidatorClientError> + pub async fn get_all_nymd_mixnode_bonds(&self) -> Result, ValidatorClientError> where - C: CosmWasmClient + Sync, - { - let all_mixnodes = self.get_all_nymd_mixnodes().await?; - let rewarded_set_identities = self - .get_all_nymd_rewarded_set_mixnode_identities() - .await? - .into_iter() - .collect::>(); - - Ok(all_mixnodes - .into_iter() - .filter_map(|node| { - rewarded_set_identities - .get(node.identity()) - .map(|status| (node, *status)) - }) - .collect()) - } - - /// If you need both rewarded and the active set, consider using [Self::get_nymd_rewarded_and_active_sets] instead - pub async fn get_nymd_rewarded_set(&self) -> Result, ValidatorClientError> - where - C: CosmWasmClient + Sync, - { - let all_mixnodes = self.get_all_nymd_mixnodes().await?; - let rewarded_set_identities = self - .get_all_nymd_rewarded_set_mixnode_identities() - .await? - .into_iter() - .map(|(identity, _status)| identity) - .collect::>(); - - Ok(all_mixnodes - .into_iter() - .filter(|node| rewarded_set_identities.contains(node.identity())) - .collect()) - } - - /// If you need both rewarded and the active set, consider using [Self::get_nymd_rewarded_and_active_sets] instead - pub async fn get_nymd_active_set(&self) -> Result, ValidatorClientError> - where - C: CosmWasmClient + Sync, - { - let all_mixnodes = self.get_all_nymd_mixnodes().await?; - let active_set_identities = self - .get_all_nymd_rewarded_set_mixnode_identities() - .await? - .into_iter() - .filter_map(|(identity, status)| { - if status.is_active() { - Some(identity) - } else { - None - } - }) - .collect::>(); - - Ok(all_mixnodes - .into_iter() - .filter(|node| active_set_identities.contains(node.identity())) - .collect()) - } - - pub async fn get_all_nymd_mixnodes(&self) -> Result, ValidatorClientError> - where - C: CosmWasmClient + Sync, + C: CosmWasmClient + Sync + Send, { let mut mixnodes = Vec::new(); let mut start_after = None; loop { let mut paged_response = self .nymd - .get_mixnodes_paged(start_after.take(), self.mixnode_page_limit) + .get_mixnode_bonds_paged(self.mixnode_page_limit, start_after.take()) + .await?; + mixnodes.append(&mut paged_response.nodes); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(mixnodes) + } + + pub async fn get_all_nymd_mixnodes_detailed( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut mixnodes = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_mixnodes_detailed_paged(self.mixnode_page_limit, start_after.take()) + .await?; + mixnodes.append(&mut paged_response.nodes); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(mixnodes) + } + + pub async fn get_all_nymd_unbonded_mixnodes( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut mixnodes = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_unbonded_paged(self.mixnode_page_limit, start_after.take()) + .await?; + mixnodes.append(&mut paged_response.nodes); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(mixnodes) + } + + pub async fn get_all_nymd_unbonded_mixnodes_by_owner( + &self, + owner: &cosmrs::AccountId, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut mixnodes = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_unbonded_by_owner_paged(owner, self.mixnode_page_limit, start_after.take()) + .await?; + mixnodes.append(&mut paged_response.nodes); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(mixnodes) + } + + pub async fn get_all_nymd_unbonded_mixnodes_by_identity( + &self, + identity_key: String, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut mixnodes = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_unbonded_by_identity_paged( + identity_key.clone(), + self.mixnode_page_limit, + start_after.take(), + ) .await?; mixnodes.append(&mut paged_response.nodes); @@ -508,7 +361,7 @@ impl Client { pub async fn get_all_nymd_gateways(&self) -> Result, ValidatorClientError> where - C: CosmWasmClient + Sync, + C: CosmWasmClient + Sync + Send, { let mut gateways = Vec::new(); let mut start_after = None; @@ -531,18 +384,18 @@ impl Client { pub async fn get_all_nymd_single_mixnode_delegations( &self, - identity: IdentityKey, + mix_id: MixId, ) -> Result, ValidatorClientError> where - C: CosmWasmClient + Sync, + C: CosmWasmClient + Sync + Send, { let mut delegations = Vec::new(); let mut start_after = None; loop { let mut paged_response = self .nymd - .get_mix_delegations_paged( - identity.clone(), + .get_mixnode_delegations_paged( + mix_id, start_after.take(), self.mixnode_delegations_page_limit, ) @@ -564,7 +417,7 @@ impl Client { delegation_owner: &cosmrs::AccountId, ) -> Result, ValidatorClientError> where - C: CosmWasmClient + Sync, + C: CosmWasmClient + Sync + Send, { let mut delegations = Vec::new(); let mut start_after = None; @@ -589,10 +442,128 @@ impl Client { Ok(delegations) } - pub async fn get_mixnode_avg_uptimes( + pub async fn get_all_network_delegations(&self) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut delegations = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_all_network_delegations_paged( + start_after.take(), + self.mixnode_delegations_page_limit, + ) + .await?; + delegations.append(&mut paged_response.delegations); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(delegations) + } + + pub async fn get_all_nymd_pending_epoch_events( &self, - ) -> Result, ValidatorClientError> { - Ok(self.validator_api.get_mixnode_avg_uptimes().await?) + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut events = Vec::new(); + let mut start_after = None; + + loop { + let mut paged_response = self + .nymd + .get_pending_epoch_events_paged(start_after.take(), self.rewarded_set_page_limit) + .await?; + events.append(&mut paged_response.events); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(events) + } + + pub async fn get_all_nymd_pending_interval_events( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut events = Vec::new(); + let mut start_after = None; + + loop { + let mut paged_response = self + .nymd + .get_pending_interval_events_paged(start_after.take(), self.rewarded_set_page_limit) + .await?; + events.append(&mut paged_response.events); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(events) + } +} + +// validator-api wrappers +#[cfg(feature = "nymd-client")] +impl Client { + pub fn change_validator_api(&mut self, new_endpoint: Url) { + self.validator_api.change_url(new_endpoint) + } + + pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_mixnodes().await?) + } + + pub async fn get_cached_mixnodes_detailed( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_mixnodes_detailed().await?) + } + + pub async fn get_cached_rewarded_mixnodes( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_rewarded_mixnodes().await?) + } + + pub async fn get_cached_rewarded_mixnodes_detailed( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_rewarded_mixnodes_detailed().await?) + } + + pub async fn get_cached_active_mixnodes( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_active_mixnodes().await?) + } + + pub async fn get_cached_active_mixnodes_detailed( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_active_mixnodes_detailed().await?) + } + + pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_gateways().await?) } pub async fn blind_sign( @@ -630,17 +601,17 @@ impl ApiClient { pub async fn get_cached_active_mixnodes( &self, - ) -> Result, ValidatorClientError> { + ) -> Result, ValidatorClientError> { Ok(self.validator_api.get_active_mixnodes().await?) } pub async fn get_cached_rewarded_mixnodes( &self, - ) -> Result, ValidatorClientError> { + ) -> Result, ValidatorClientError> { Ok(self.validator_api.get_rewarded_mixnodes().await?) } - pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { + pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { Ok(self.validator_api.get_mixnodes().await?) } @@ -652,7 +623,7 @@ impl ApiClient { &self, identity: IdentityKeyRef<'_>, since: Option, - ) -> Result { + ) -> Result { Ok(self .validator_api .get_gateway_core_status_count(identity, since) @@ -661,39 +632,39 @@ impl ApiClient { pub async fn get_mixnode_core_status_count( &self, - identity: IdentityKeyRef<'_>, + mix_id: MixId, since: Option, - ) -> Result { + ) -> Result { Ok(self .validator_api - .get_mixnode_core_status_count(identity, since) + .get_mixnode_core_status_count(mix_id, since) .await?) } pub async fn get_mixnode_status( &self, - identity: IdentityKeyRef<'_>, + mix_id: MixId, ) -> Result { - Ok(self.validator_api.get_mixnode_status(identity).await?) + Ok(self.validator_api.get_mixnode_status(mix_id).await?) } pub async fn get_mixnode_reward_estimation( &self, - identity: IdentityKeyRef<'_>, + mix_id: MixId, ) -> Result { Ok(self .validator_api - .get_mixnode_reward_estimation(identity) + .get_mixnode_reward_estimation(mix_id) .await?) } pub async fn get_mixnode_stake_saturation( &self, - identity: IdentityKeyRef<'_>, + mix_id: MixId, ) -> Result { Ok(self .validator_api - .get_mixnode_stake_saturation(identity) + .get_mixnode_stake_saturation(mix_id) .await?) } diff --git a/common/client-libs/validator-client/src/connection_tester.rs b/common/client-libs/validator-client/src/connection_tester.rs index 33e698bb2c..dc1acb8cd9 100644 --- a/common/client-libs/validator-client/src/connection_tester.rs +++ b/common/client-libs/validator-client/src/connection_tester.rs @@ -1,11 +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; @@ -17,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 = @@ -45,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()) { @@ -80,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) @@ -92,7 +93,7 @@ fn extract_and_collect_results_into_map( } async fn test_nymd_connection( - network: Network, + network: NymNetworkDetails, url: &Url, client: &NymdClient, ) -> ConnectionResult { @@ -104,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(), @@ -161,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 } }; @@ -183,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 { @@ -217,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), @@ -239,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/cosmwasm_client/types.rs b/common/client-libs/validator-client/src/nymd/cosmwasm_client/types.rs index 3c28d8991f..b680a1a8f4 100644 --- a/common/client-libs/validator-client/src/nymd/cosmwasm_client/types.rs +++ b/common/client-libs/validator-client/src/nymd/cosmwasm_client/types.rs @@ -489,7 +489,7 @@ impl TryFrom for ContractCodeHistoryEntry { } } -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize)] pub struct GasInfo { /// GasWanted is the maximum units of work we allow this tx to perform. pub gas_wanted: Gas, @@ -645,7 +645,7 @@ impl InstantiateOptions { } } -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct InstantiateResult { /// The address of the newly instantiated contract pub contract_address: AccountId, @@ -658,7 +658,7 @@ pub struct InstantiateResult { pub gas_info: GasInfo, } -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct ChangeAdminResult { pub logs: Vec, @@ -668,7 +668,7 @@ pub struct ChangeAdminResult { pub gas_info: GasInfo, } -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct MigrateResult { pub logs: Vec, @@ -678,7 +678,7 @@ pub struct MigrateResult { pub gas_info: GasInfo, } -#[derive(Debug)] +#[derive(Debug, Serialize)] pub struct ExecuteResult { pub logs: Vec, diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index a279324ff8..481cb021be 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -10,25 +10,17 @@ use crate::nymd::error::NymdError; use crate::nymd::fee::DEFAULT_SIMULATED_GAS_MULTIPLIER; use crate::nymd::wallet::DirectSecp256k1HdWallet; use cosmrs::cosmwasm; +use cosmrs::rpc::endpoint::block::Response as BlockResponse; +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 mixnet_contract_common::mixnode::DelegationEvent; -use mixnet_contract_common::{ - ContractStateParams, Delegation, ExecuteMsg, Gateway, GatewayBond, GatewayBondResponse, - GatewayOwnershipResponse, IdentityKey, Interval, LayerDistribution, MixNode, MixNodeBond, - MixOwnershipResponse, MixnetContractVersion, MixnodeBondResponse, - MixnodeRewardingStatusResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse, - PagedMixDelegationsResponse, PagedMixnodeResponse, PagedRewardedSetResponse, QueryMsg, - RewardedSetUpdateDetails, -}; -use serde::Serialize; +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; @@ -50,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 network_defaults::{ChainDetails, NymNetworkDetails}; +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; @@ -155,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, @@ -163,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() @@ -212,6 +205,10 @@ impl NymdClient { &self.config } + pub fn current_chain_details(&self) -> &ChainDetails { + &self.config.chain_details + } + pub fn set_mixnet_contract_address(&mut self, address: AccountId) { self.config.mixnet_contract_address = Some(address); } @@ -282,6 +279,30 @@ impl NymdClient { self.simulated_gas_multiplier = multiplier; } + pub async fn query_contract_smart( + &self, + contract: &AccountId, + query_msg: &M, + ) -> Result + where + C: CosmWasmClient + Sync, + M: ?Sized + Serialize + Sync, + for<'a> T: Deserialize<'a>, + { + self.client.query_contract_smart(contract, query_msg).await + } + + pub async fn query_contract_raw( + &self, + contract: &AccountId, + query_data: Vec, + ) -> Result, NymdError> + where + C: CosmWasmClient + Sync, + { + self.client.query_contract_raw(contract, query_data).await + } + pub fn wrap_contract_execute_message( &self, contract_address: &AccountId, @@ -308,6 +329,13 @@ impl NymdClient { &self.client_address.as_ref().unwrap()[0] } + pub fn signer(&self) -> &DirectSecp256k1HdWallet + where + C: SigningCosmWasmClient, + { + self.client.signer() + } + pub fn gas_price(&self) -> &GasPrice where C: SigningCosmWasmClient, @@ -319,6 +347,12 @@ impl NymdClient { self.simulated_gas_multiplier } + // ============= + // CHAIN RELATED + // ============= + + // CHAIN QUERIES + pub async fn account_sequence(&self) -> Result where C: SigningCosmWasmClient + Sync, @@ -331,11 +365,26 @@ impl NymdClient { address: &AccountId, ) -> Result, NymdError> where - C: SigningCosmWasmClient + Sync, + C: CosmWasmClient + Sync, { self.client.get_account(address).await } + pub async fn get_account_public_key( + &self, + address: &AccountId, + ) -> Result, NymdError> + where + C: CosmWasmClient + Sync, + { + if let Some(account) = self.client.get_account(address).await? { + let base_account = account.try_get_base_account()?; + return Ok(base_account.pubkey); + } + + Ok(None) + } + pub async fn get_current_block_timestamp(&self) -> Result where C: CosmWasmClient + Sync, @@ -353,6 +402,13 @@ impl NymdClient { Ok(self.client.get_block(height).await?.block.header.time) } + pub async fn get_block(&self, height: Option) -> Result + where + C: CosmWasmClient + Sync, + { + self.client.get_block(height).await + } + pub async fn get_current_block_height(&self) -> Result where C: CosmWasmClient + Sync, @@ -397,6 +453,13 @@ impl NymdClient { self.client.get_balance(address, denom).await } + pub async fn get_all_balances(&self, address: &AccountId) -> Result, NymdError> + where + C: CosmWasmClient + Sync, + { + self.client.get_all_balances(address).await + } + pub async fn get_tx(&self, id: tx::Hash) -> Result where C: CosmWasmClient + Sync, @@ -404,6 +467,13 @@ impl NymdClient { self.client.get_tx(id).await } + pub async fn search_tx(&self, query: Query) -> Result, NymdError> + where + C: CosmWasmClient + Sync, + { + self.client.search_tx(query).await + } + pub async fn get_total_supply(&self) -> Result, NymdError> where C: CosmWasmClient + Sync, @@ -411,387 +481,6 @@ impl NymdClient { self.client.get_total_supply().await } - pub async fn get_contract_settings(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::StateParams {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_operator_rewards(&self, address: String) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::QueryOperatorReward { address }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .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 get_delegator_rewards( - &self, - address: String, - mix_identity: IdentityKey, - proxy: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::QueryDelegatorReward { - address, - mix_identity, - proxy, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_pending_delegation_events( - &self, - owner_address: String, - proxy_address: Option, - ) -> Result, NymdError> - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetPendingDelegationEvents { - owner_address, - proxy_address, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_current_epoch(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetCurrentEpoch {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_current_operator_cost(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetCurrentOperatorCost {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_mixnet_contract_version(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetContractVersion {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_rewarding_status( - &self, - mix_identity: mixnet_contract_common::IdentityKey, - interval_id: u32, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetRewardingStatus { - mix_identity, - interval_id, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn query_current_rewarded_set_height(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetCurrentRewardedSetHeight {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn query_current_rewarded_set_update_details( - &self, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetRewardedSetUpdateDetails {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_rewarded_set_identities_paged( - &self, - start_after: Option, - page_limit: Option, - height: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetRewardedSet { - height, - start_after, - limit: page_limit, - }; - - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_layer_distribution(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::LayerDistribution {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_reward_pool(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetRewardPool {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_circulating_supply(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetCirculatingSupply {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_sybil_resistance_percent(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetSybilResistancePercent {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_active_set_work_factor(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetActiveSetWorkFactor {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_interval_reward_percent(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetIntervalRewardPercent {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_epochs_in_interval(&self) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetEpochsInInterval {}; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - /// Checks whether there is a bonded mixnode associated with the provided client's address - pub async fn owns_mixnode(&self, address: &AccountId) -> Result, NymdError> - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::OwnsMixnode { - address: address.to_string(), - }; - let response: MixOwnershipResponse = self - .client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await?; - Ok(response.mixnode) - } - - /// Checks whether there is a bonded gateway associated with the provided client's address - pub async fn owns_gateway(&self, address: &AccountId) -> Result, NymdError> - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::OwnsGateway { - address: address.to_string(), - }; - let response: GatewayOwnershipResponse = self - .client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await?; - Ok(response.gateway) - } - - /// Checks whether there is a bonded mixnode associated with the provided identity key - pub async fn get_mixnode_bond( - &self, - identity: IdentityKey, - ) -> Result, NymdError> - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetMixnodeBond { identity }; - let response: MixnodeBondResponse = self - .client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await?; - Ok(response.mixnode) - } - - /// Checks whether there is a bonded gateway associated with the provided identity key - pub async fn get_gateway_bond( - &self, - identity: IdentityKey, - ) -> Result, NymdError> - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetGatewayBond { identity }; - let response: GatewayBondResponse = self - .client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await?; - Ok(response.gateway) - } - - pub async fn get_mixnodes_paged( - &self, - start_after: Option, - page_limit: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetMixNodes { - start_after, - limit: page_limit, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - pub async fn get_gateways_paged( - &self, - start_after: Option, - page_limit: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetGateways { - start_after, - limit: page_limit, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - /// Gets list of all delegations towards particular mixnode on particular page. - pub async fn get_mix_delegations_paged( - &self, - mix_identity: IdentityKey, - start_after: Option<(String, u64)>, - page_limit: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetMixnodeDelegations { - mix_identity: mix_identity.to_owned(), - start_after, - limit: page_limit, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - /// Gets list of all the mixnodes on which a particular address delegated. - pub async fn get_delegator_delegations_paged( - &self, - delegator: String, - start_after: Option, - page_limit: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetDelegatorDelegations { - delegator, - start_after, - limit: page_limit, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - - /// Checks value of delegation of given client towards particular mixnode. - pub async fn get_delegation_details( - &self, - mix_identity: IdentityKey, - delegator: &AccountId, - proxy: Option, - ) -> Result - where - C: CosmWasmClient + Sync, - { - let request = QueryMsg::GetDelegationDetails { - mix_identity, - delegator: delegator.to_string(), - proxy, - }; - self.client - .query_contract_smart(self.mixnet_contract_address(), &request) - .await - } - pub async fn simulate(&self, messages: I) -> Result where C: SigningCosmWasmClient + Sync, @@ -893,7 +582,7 @@ impl NymdClient { &self, contract_address: &AccountId, msg: &M, - fee: Fee, + fee: Option, memo: impl Into + Send + 'static, funds: Vec, ) -> Result @@ -901,6 +590,7 @@ impl NymdClient { C: SigningCosmWasmClient + Sync, M: ?Sized + Serialize + Sync, { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); self.client .execute(self.address(), contract_address, msg, fee, memo, funds) .await @@ -910,7 +600,7 @@ impl NymdClient { &self, contract_address: &AccountId, msgs: I, - fee: Fee, + fee: Option, memo: impl Into + Send + 'static, ) -> Result where @@ -918,6 +608,7 @@ impl NymdClient { I: IntoIterator)> + Send, M: Serialize, { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); self.client .execute_multiple(self.address(), contract_address, msgs, fee, memo) .await @@ -1006,86 +697,14 @@ impl NymdClient { .await } - #[execute("mixnet")] - fn _compound_reward( - &self, - operator: Option, - delegator: Option, - mix_identity: Option, - proxy: Option, - fee: Option, - ) -> (ExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - ( - ExecuteMsg::CompoundReward { - operator, - delegator, - mix_identity, - proxy, - }, - fee, - ) - } - - #[execute("mixnet")] - fn _compound_operator_reward(&self, fee: Option) -> (ExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - (ExecuteMsg::CompoundOperatorReward {}, fee) - } - - #[execute("mixnet")] - fn _claim_operator_reward(&self, fee: Option) -> (ExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - (ExecuteMsg::ClaimOperatorReward {}, fee) - } - - #[execute("mixnet")] - fn _compound_delegator_reward( - &self, - mix_identity: IdentityKey, - fee: Option, - ) -> (ExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - (ExecuteMsg::CompoundDelegatorReward { mix_identity }, fee) - } - - #[execute("mixnet")] - fn _claim_delegator_reward( - &self, - mix_identity: IdentityKey, - fee: Option, - ) -> (ExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - (ExecuteMsg::ClaimDelegatorReward { mix_identity }, fee) - } + // @DU: I don't want to touch them now, but for consistency sake those should be moved to + // `VestingSigningClient` #[execute("vesting")] - fn _vesting_compound_delegator_reward( + fn _vesting_withdraw_operator_reward( &self, - mix_identity: IdentityKey, fee: Option, ) -> (VestingExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - ( - VestingExecuteMsg::CompoundDelegatorReward { mix_identity }, - fee, - ) - } - - #[execute("vesting")] - fn _vesting_claim_operator_reward(&self, fee: Option) -> (VestingExecuteMsg, Option) where C: SigningCosmWasmClient + Sync, { @@ -1093,600 +712,30 @@ impl NymdClient { } #[execute("vesting")] - fn _vesting_compound_operator_reward( + fn _vesting_withdraw_delegator_reward( &self, + mix_id: MixId, fee: Option, ) -> (VestingExecuteMsg, Option) where C: SigningCosmWasmClient + Sync, { - (VestingExecuteMsg::CompoundOperatorReward {}, fee) + (VestingExecuteMsg::ClaimDelegatorReward { mix_id }, fee) } #[execute("vesting")] - fn _vesting_claim_delegator_reward( + fn _vesting_update_locked_pledge_cap( &self, - mix_identity: IdentityKey, + address: String, + cap: PledgeCap, fee: Option, ) -> (VestingExecuteMsg, Option) where C: SigningCosmWasmClient + Sync, { ( - VestingExecuteMsg::ClaimDelegatorReward { mix_identity }, + VestingExecuteMsg::UpdateLockedPledgeCap { address, cap }, fee, ) } - - #[execute("vesting")] - fn _vesting_update_locked_pledge_cap( - &self, - amount: Uint128, - fee: Option, - ) -> (VestingExecuteMsg, Option) - where - C: SigningCosmWasmClient + Sync, - { - (VestingExecuteMsg::UpdateLockedPledgeCap { amount }, fee) - } - - /// Announce a mixnode, paying a fee. - pub async fn bond_mixnode( - &self, - mixnode: MixNode, - owner_signature: String, - pledge: Coin, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::BondMixnode { - mix_node: mixnode, - owner_signature, - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Bonding mixnode from rust!", - vec![pledge], - ) - .await - } - - /// Announce a mixnode on behalf of the owner, paying a fee. - pub async fn bond_mixnode_on_behalf( - &self, - mixnode: MixNode, - owner: String, - owner_signature: String, - pledge: Coin, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::BondMixnodeOnBehalf { - mix_node: mixnode, - owner, - owner_signature, - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Bonding mixnode on behalf from rust!", - vec![pledge], - ) - .await - } - - /// Announce multiple mixnodes on behalf of other owners, paying a fee. - pub async fn bond_multiple_mixnodes_on_behalf( - &self, - mixnode_bonds_with_sigs: Vec<(MixNodeBond, String)>, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let reqs: Vec<(ExecuteMsg, Vec)> = mixnode_bonds_with_sigs - .into_iter() - .map(|(bond, owner_signature)| { - ( - ExecuteMsg::BondMixnodeOnBehalf { - mix_node: bond.mix_node, - owner: bond.owner.to_string(), - owner_signature, - }, - vec![bond.pledge_amount.into()], - ) - }) - .collect(); - - self.client - .execute_multiple( - self.address(), - self.mixnet_contract_address(), - reqs, - fee, - "Bonding multiple mixnodes on behalf from rust!", - ) - .await - } - - /// Unbond a mixnode, removing it from the network and reclaiming staked coins - pub async fn unbond_mixnode(&self, fee: Option) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UnbondMixnode {}; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Unbonding mixnode from rust!", - vec![], - ) - .await - } - - /// Unbond a mixnode on behalf of the owner, removing it from the network and reclaiming staked coins - pub async fn unbond_mixnode_on_behalf( - &self, - owner: String, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UnbondMixnodeOnBehalf { owner }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Unbonding mixnode on behalf from rust!", - vec![], - ) - .await - } - - /// Update the configuration of a mixnode. Right now, only possible for profit margin. - pub async fn update_mixnode_config( - &self, - profit_margin_percent: u8, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Updating mixnode configuration from rust!", - vec![], - ) - .await - } - - /// Delegates specified amount of stake to particular mixnode. - pub async fn delegate_to_mixnode( - &self, - mix_identity: &str, - amount: Coin, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::DelegateToMixnode { - mix_identity: mix_identity.to_string(), - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Delegating to mixnode from rust!", - vec![amount], - ) - .await - } - - /// Delegates specified amount of stake to particular mixnode on - /// behalf of a particular delegator. - pub async fn delegate_to_mixnode_on_behalf( - &self, - mix_identity: &str, - delegate: &str, - amount: Coin, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::DelegateToMixnodeOnBehalf { - mix_identity: mix_identity.to_string(), - delegate: delegate.to_string(), - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Delegating to mixnode on behalf from rust!", - vec![amount], - ) - .await - } - - /// Delegates specified amount of stake to multiple mixnodes on behalf of multiple delegators. - pub async fn delegate_to_multiple_mixnodes_on_behalf( - &self, - mixnode_delegations: Vec, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let reqs: Vec<(ExecuteMsg, Vec)> = mixnode_delegations - .into_iter() - .map(|delegation| { - ( - ExecuteMsg::DelegateToMixnodeOnBehalf { - mix_identity: delegation.node_identity(), - delegate: delegation.owner().to_string(), - }, - vec![delegation.amount().clone().into()], - ) - }) - .collect(); - - self.client - .execute_multiple( - self.address(), - self.mixnet_contract_address(), - reqs, - fee, - "Delegating to multiple mixnodes on behalf from rust!", - ) - .await - } - - /// Removes stake delegation from a particular mixnode. - pub async fn remove_mixnode_delegation( - &self, - mix_identity: &str, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UndelegateFromMixnode { - mix_identity: mix_identity.to_string(), - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Removing mixnode delegation from rust!", - vec![], - ) - .await - } - - /// Removes stake delegation from a particular mixnode on behalf of a particular delegator. - pub async fn remove_mixnode_delegation_on_behalf( - &self, - mix_identity: &str, - delegate: &str, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UndelegateFromMixnodeOnBehalf { - mix_identity: mix_identity.to_string(), - delegate: delegate.to_string(), - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Removing mixnode delegation on behalf from rust!", - vec![], - ) - .await - } - - /// Announce a gateway, paying a fee. - pub async fn bond_gateway( - &self, - gateway: Gateway, - owner_signature: String, - pledge: Coin, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::BondGateway { - gateway, - owner_signature, - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Bonding gateway from rust!", - vec![pledge], - ) - .await - } - - /// Announce a gateway on behalf of the owner, paying a fee. - pub async fn bond_gateway_on_behalf( - &self, - gateway: Gateway, - owner: String, - owner_signature: String, - pledge: Coin, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::BondGatewayOnBehalf { - gateway, - owner, - owner_signature, - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Bonding gateway on behalf from rust!", - vec![pledge], - ) - .await - } - - /// Announce multiple gateways on behalf of other owners, paying a fee. - pub async fn bond_multiple_gateways_on_behalf( - &self, - gateway_bonds_with_sigs: Vec<(GatewayBond, String)>, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let reqs: Vec<(ExecuteMsg, Vec)> = gateway_bonds_with_sigs - .into_iter() - .map(|(bond, owner_signature)| { - ( - ExecuteMsg::BondGatewayOnBehalf { - gateway: bond.gateway, - owner: bond.owner.to_string(), - owner_signature, - }, - vec![bond.pledge_amount.into()], - ) - }) - .collect(); - - self.client - .execute_multiple( - self.address(), - self.mixnet_contract_address(), - reqs, - fee, - "Bonding multiple gateways on behalf from rust!", - ) - .await - } - - /// Unbond a gateway, removing it from the network and reclaiming staked coins - pub async fn unbond_gateway(&self, fee: Option) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UnbondGateway {}; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Unbonding gateway from rust!", - vec![], - ) - .await - } - - /// Unbond a gateway on behalf of the owner, removing it from the - /// network and reclaiming staked coins - pub async fn unbond_gateway_on_behalf( - &self, - owner: String, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UnbondGatewayOnBehalf { owner }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Unbonding gateway on behalf from rust!", - vec![], - ) - .await - } - - pub async fn update_contract_settings( - &self, - new_params: ContractStateParams, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::UpdateContractStateParams(new_params); - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Updating contract state from rust!", - vec![], - ) - .await - } - - pub async fn advance_current_epoch(&self, fee: Option) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::AdvanceCurrentEpoch {}; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Advance current epoch", - vec![], - ) - .await - } - - pub async fn reconcile_delegations(&self, fee: Option) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::ReconcileDelegations {}; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Reconciling delegation events", - vec![], - ) - .await - } - - pub async fn checkpoint_mixnodes(&self, fee: Option) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::CheckpointMixnodes {}; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Snapshotting mixnodes", - vec![], - ) - .await - } - - pub async fn write_rewarded_set( - &self, - rewarded_set: Vec, - expected_active_set_size: u32, - fee: Option, - ) -> Result - where - C: SigningCosmWasmClient + Sync, - { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - - let req = ExecuteMsg::WriteRewardedSet { - rewarded_set, - expected_active_set_size, - }; - self.client - .execute( - self.address(), - self.mixnet_contract_address(), - &req, - fee, - "Writing rewarded set", - vec![], - ) - .await - } } 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 new file mode 100644 index 0000000000..675c15c5d1 --- /dev/null +++ b/common/client-libs/validator-client/src/nymd/traits/mixnet_query_client.rs @@ -0,0 +1,388 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub use crate::nymd::cosmwasm_client::client::CosmWasmClient; +use crate::nymd::error::NymdError; +use crate::nymd::NymdClient; +use async_trait::async_trait; +use cosmrs::AccountId; +use mixnet_contract_common::delegation::{MixNodeDelegationResponse, OwnerProxySubKey}; +use mixnet_contract_common::mixnode::{ + MixNodeDetails, MixnodeRewardingDetailsResponse, PagedMixnodesDetailsResponse, + PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, +}; +use mixnet_contract_common::reward_params::{Performance, RewardingParams}; +use mixnet_contract_common::rewarding::{ + EstimatedCurrentEpochRewardResponse, PendingRewardResponse, +}; +use mixnet_contract_common::{ + delegation, ContractBuildInformation, ContractState, ContractStateParams, + CurrentIntervalResponse, EpochEventId, GatewayBondResponse, GatewayOwnershipResponse, + IdentityKey, IntervalEventId, LayerDistribution, MixId, MixOwnershipResponse, + MixnodeDetailsResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, + PagedGatewayResponse, PagedMixNodeDelegationsResponse, PagedMixnodeBondsResponse, + PagedRewardedSetResponse, PendingEpochEventsResponse, PendingIntervalEventsResponse, + QueryMsg as MixnetQueryMsg, +}; +use serde::Deserialize; + +#[async_trait] +pub trait MixnetQueryClient { + async fn query_mixnet_contract(&self, query: MixnetQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>; + + // state/sys-params-related + + async fn get_mixnet_contract_version(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetContractVersion {}) + .await + } + + async fn get_rewarding_validator_address(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetRewardingValidatorAddress {}) + .await + } + + async fn get_mixnet_contract_settings(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetStateParams {}) + .await + } + + async fn get_mixnet_contract_state(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetState {}) + .await + } + + async fn get_rewarding_parameters(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetRewardingParams {}) + .await + } + + async fn get_current_interval_details(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetCurrentIntervalDetails {}) + .await + } + + async fn get_rewarded_set_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetRewardedSet { limit, start_after }) + .await + } + + // mixnode-related: + + async fn get_mixnode_bonds_paged( + &self, + limit: Option, + start_after: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetMixNodeBonds { limit, start_after }) + .await + } + + async fn get_mixnodes_detailed_paged( + &self, + limit: Option, + start_after: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetMixNodesDetailed { limit, start_after }) + .await + } + + async fn get_unbonded_paged( + &self, + limit: Option, + start_after: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodes { limit, start_after }) + .await + } + + async fn get_unbonded_by_owner_paged( + &self, + owner: &AccountId, + limit: Option, + start_after: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodesByOwner { + owner: owner.to_string(), + limit, + start_after, + }) + .await + } + + async fn get_unbonded_by_identity_paged( + &self, + identity_key: String, + limit: Option, + start_after: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodesByIdentityKey { + identity_key, + limit, + start_after, + }) + .await + } + + async fn get_owned_mixnode( + &self, + address: &AccountId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetOwnedMixnode { + address: address.to_string(), + }) + .await + } + + async fn get_mixnode_details( + &self, + mix_id: MixId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeDetails { mix_id }) + .await + } + + async fn get_mixnode_rewarding_details( + &self, + mix_id: MixId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeRewardingDetails { mix_id }) + .await + } + + async fn get_mixnode_stake_saturation( + &self, + mix_id: MixId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetStakeSaturation { mix_id }) + .await + } + + async fn get_unbonded_mixnode_information( + &self, + mix_id: MixId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodeInformation { mix_id }) + .await + } + + async fn get_layer_distribution(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetLayerDistribution {}) + .await + } + + // gateway-related: + + async fn get_gateways_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetGateways { start_after, limit }) + .await + } + + /// Checks whether there is a bonded gateway associated with the provided identity key + async fn get_gateway_bond( + &self, + identity: IdentityKey, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetGatewayBond { identity }) + .await + } + + /// Checks whether there is a bonded gateway associated with the provided client's address + async fn get_owned_gateway( + &self, + address: &AccountId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetOwnedGateway { + address: address.to_string(), + }) + .await + } + + // delegation-related: + + /// Gets list of all delegations towards particular mixnode on particular page. + async fn get_mixnode_delegations_paged( + &self, + mix_id: MixId, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeDelegations { + mix_id, + start_after, + limit, + }) + .await + } + + /// Gets list of all the mixnodes to which a particular address delegated. + async fn get_delegator_delegations_paged( + &self, + delegator: String, + start_after: Option<(MixId, OwnerProxySubKey)>, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetDelegatorDelegations { + delegator, + start_after, + limit, + }) + .await + } + + /// Checks value of delegation of given client towards particular mixnode. + async fn get_delegation_details( + &self, + mix_id: MixId, + delegator: &AccountId, + proxy: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetDelegationDetails { + mix_id, + delegator: delegator.to_string(), + proxy, + }) + .await + } + + /// Gets all the delegations on the entire network + async fn get_all_network_delegations_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetAllDelegations { start_after, limit }) + .await + } + + // rewards related + async fn get_pending_operator_reward( + &self, + operator: &AccountId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetPendingOperatorReward { + address: operator.to_string(), + }) + .await + } + + async fn get_pending_mixnode_operator_reward( + &self, + mix_id: MixId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetPendingMixNodeOperatorReward { mix_id }) + .await + } + + async fn get_pending_delegator_reward( + &self, + delegator: &AccountId, + mix_id: MixId, + proxy: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetPendingDelegatorReward { + address: delegator.to_string(), + mix_id, + proxy, + }) + .await + } + + // 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: MixId, + estimated_performance: Performance, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetEstimatedCurrentEpochOperatorReward { + mix_id, + estimated_performance, + }) + .await + } + + // given the provided performance, estimate the reward at the end of the current epoch + async fn get_estimated_current_epoch_delegator_reward( + &self, + delegator: &AccountId, + mix_id: MixId, + proxy: Option, + estimated_performance: Performance, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetEstimatedCurrentEpochDelegatorReward { + address: delegator.to_string(), + mix_id, + proxy, + estimated_performance, + }) + .await + } + + // interval-related + + async fn get_pending_epoch_events_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetPendingEpochEvents { start_after, limit }) + .await + } + + async fn get_pending_interval_events_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetPendingIntervalEvents { start_after, limit }) + .await + } + + async fn get_mixnode_details_by_identity( + &self, + mix_identity: IdentityKey, + ) -> Result, NymdError> { + self.query_mixnet_contract(MixnetQueryMsg::GetBondedMixnodeDetailsByIdentity { + mix_identity, + }) + .await + } +} + +#[async_trait] +impl MixnetQueryClient for NymdClient +where + C: CosmWasmClient + Sync + Send, +{ + async fn query_mixnet_contract(&self, query: MixnetQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>, + { + self.client + .query_contract_smart(self.mixnet_contract_address(), &query) + .await + } +} + +#[async_trait] +impl MixnetQueryClient for crate::Client +where + C: CosmWasmClient + Sync + Send, +{ + async fn query_mixnet_contract(&self, query: MixnetQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>, + { + self.nymd.query_mixnet_contract(query).await + } +} 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 new file mode 100644 index 0000000000..41fa7818a8 --- /dev/null +++ b/common/client-libs/validator-client/src/nymd/traits/mixnet_signing_client.rs @@ -0,0 +1,496 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nymd::coin::Coin; +pub use crate::nymd::cosmwasm_client::client::CosmWasmClient; +use crate::nymd::cosmwasm_client::types::ExecuteResult; +use crate::nymd::error::NymdError; +use crate::nymd::{Fee, NymdClient, SigningCosmWasmClient}; +use async_trait::async_trait; +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, MixId, MixNode, +}; + +#[async_trait] +pub trait MixnetSigningClient { + async fn execute_mixnet_contract( + &self, + fee: Option, + msg: MixnetExecuteMsg, + funds: Vec, + ) -> Result; + + // state/sys-params-related + + async fn update_rewarding_validator_address( + &self, + address: AccountId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateRewardingValidatorAddress { + address: address.to_string(), + }, + vec![], + ) + .await + } + + async fn update_contract_state_params( + &self, + updated_parameters: ContractStateParams, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateContractStateParams { updated_parameters }, + vec![], + ) + .await + } + + async fn update_active_set_size( + &self, + active_set_size: u32, + force_immediately: bool, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateActiveSetSize { + active_set_size, + force_immediately, + }, + vec![], + ) + .await + } + + async fn update_rewarding_parameters( + &self, + updated_params: IntervalRewardingParamsUpdate, + force_immediately: bool, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateRewardingParams { + updated_params, + force_immediately, + }, + vec![], + ) + .await + } + + async fn update_interval_config( + &self, + epochs_in_interval: u32, + epoch_duration_secs: u64, + force_immediately: bool, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateIntervalConfig { + epochs_in_interval, + epoch_duration_secs, + force_immediately, + }, + vec![], + ) + .await + } + + async fn advance_current_epoch( + &self, + new_rewarded_set: Vec, + expected_active_set_size: u32, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::AdvanceCurrentEpoch { + new_rewarded_set, + expected_active_set_size, + }, + vec![], + ) + .await + } + + async fn reconcile_epoch_events( + &self, + limit: Option, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::ReconcileEpochEvents { limit }, + vec![], + ) + .await + } + + // mixnode-related: + + async fn bond_mixnode( + &self, + mix_node: MixNode, + cost_params: MixNodeCostParams, + owner_signature: String, + pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::BondMixnode { + mix_node, + cost_params, + owner_signature, + }, + vec![pledge], + ) + .await + } + + async fn bond_mixnode_on_behalf( + &self, + owner: AccountId, + mix_node: MixNode, + cost_params: MixNodeCostParams, + owner_signature: String, + pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::BondMixnodeOnBehalf { + mix_node, + cost_params, + owner_signature, + owner: owner.to_string(), + }, + vec![pledge], + ) + .await + } + + async fn unbond_mixnode(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::UnbondMixnode {}, vec![]) + .await + } + + async fn unbond_mixnode_on_behalf( + &self, + owner: AccountId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UnbondMixnodeOnBehalf { + owner: owner.to_string(), + }, + vec![], + ) + .await + } + + async fn update_mixnode_cost_params( + &self, + new_costs: MixNodeCostParams, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateMixnodeCostParams { new_costs }, + vec![], + ) + .await + } + + async fn update_mixnode_cost_params_on_behalf( + &self, + owner: AccountId, + new_costs: MixNodeCostParams, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateMixnodeCostParamsOnBehalf { + new_costs, + owner: owner.to_string(), + }, + vec![], + ) + .await + } + + async fn update_mixnode_config( + &self, + new_config: MixNodeConfigUpdate, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateMixnodeConfig { new_config }, + vec![], + ) + .await + } + + async fn update_mixnode_config_on_behalf( + &self, + owner: AccountId, + new_config: MixNodeConfigUpdate, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UpdateMixnodeConfigOnBehalf { + new_config, + owner: owner.to_string(), + }, + vec![], + ) + .await + } + + // gateway-related: + + async fn bond_gateway( + &self, + gateway: Gateway, + owner_signature: String, + pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::BondGateway { + gateway, + owner_signature, + }, + vec![pledge], + ) + .await + } + + async fn bond_gateway_on_behalf( + &self, + owner: AccountId, + gateway: Gateway, + owner_signature: String, + pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::BondGatewayOnBehalf { + gateway, + owner_signature, + owner: owner.to_string(), + }, + vec![pledge], + ) + .await + } + + async fn unbond_gateway(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::UnbondGateway {}, vec![]) + .await + } + + async fn unbond_gateway_on_behalf( + &self, + owner: AccountId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UnbondGatewayOnBehalf { + owner: owner.to_string(), + }, + vec![], + ) + .await + } + + // delegation-related: + + async fn delegate_to_mixnode( + &self, + mix_id: MixId, + amount: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::DelegateToMixnode { mix_id }, + vec![amount], + ) + .await + } + + async fn delegate_to_mixnode_on_behalf( + &self, + delegate: AccountId, + mix_id: MixId, + amount: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::DelegateToMixnodeOnBehalf { + mix_id, + delegate: delegate.to_string(), + }, + vec![amount], + ) + .await + } + + async fn undelegate_from_mixnode( + &self, + mix_id: MixId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UndelegateFromMixnode { mix_id }, + vec![], + ) + .await + } + + async fn undelegate_to_mixnode_on_behalf( + &self, + delegate: AccountId, + mix_id: MixId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::UndelegateFromMixnodeOnBehalf { + mix_id, + delegate: delegate.to_string(), + }, + vec![], + ) + .await + } + + // reward-related + + async fn reward_mixnode( + &self, + mix_id: MixId, + performance: Performance, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::RewardMixnode { + mix_id, + performance, + }, + vec![], + ) + .await + } + + async fn withdraw_operator_reward(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::WithdrawOperatorReward {}, vec![]) + .await + } + + async fn withdraw_operator_reward_on_behalf( + &self, + owner: AccountId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::WithdrawOperatorRewardOnBehalf { + owner: owner.to_string(), + }, + vec![], + ) + .await + } + + async fn withdraw_delegator_reward( + &self, + mix_id: MixId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::WithdrawDelegatorReward { mix_id }, + vec![], + ) + .await + } + + async fn withdraw_delegator_reward_on_behalf( + &self, + owner: AccountId, + mix_id: MixId, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::WithdrawDelegatorRewardOnBehalf { + mix_id, + owner: owner.to_string(), + }, + vec![], + ) + .await + } +} + +#[async_trait] +impl MixnetSigningClient for NymdClient +where + C: SigningCosmWasmClient + Sync + Send, +{ + async fn execute_mixnet_contract( + &self, + fee: Option, + msg: MixnetExecuteMsg, + funds: Vec, + ) -> Result { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); + let memo = msg.default_memo(); + self.client + .execute( + self.address(), + self.mixnet_contract_address(), + &msg, + fee, + memo, + funds, + ) + .await + } +} + +#[async_trait] +impl MixnetSigningClient for crate::Client +where + C: SigningCosmWasmClient + Sync + Send, +{ + async fn execute_mixnet_contract( + &self, + fee: Option, + msg: MixnetExecuteMsg, + funds: Vec, + ) -> Result { + self.nymd.execute_mixnet_contract(fee, msg, funds).await + } +} diff --git a/common/client-libs/validator-client/src/nymd/traits/mod.rs b/common/client-libs/validator-client/src/nymd/traits/mod.rs index f1cda7066b..61a562a5b3 100644 --- a/common/client-libs/validator-client/src/nymd/traits/mod.rs +++ b/common/client-libs/validator-client/src/nymd/traits/mod.rs @@ -1,8 +1,10 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 mod coconut_bandwidth_query_client; mod coconut_bandwidth_signing_client; +mod mixnet_query_client; +mod mixnet_signing_client; mod multisig_query_client; mod multisig_signing_client; mod vesting_query_client; @@ -10,6 +12,8 @@ mod vesting_signing_client; pub use coconut_bandwidth_query_client::CoconutBandwidthQueryClient; pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient; +pub use mixnet_query_client::MixnetQueryClient; +pub use mixnet_signing_client::MixnetSigningClient; pub use multisig_query_client::MultisigQueryClient; pub use multisig_signing_client::MultisigSigningClient; pub use vesting_query_client::VestingQueryClient; 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 f79197a9f5..598818197a 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 @@ -6,14 +6,27 @@ pub use crate::nymd::cosmwasm_client::client::CosmWasmClient; use crate::nymd::error::NymdError; use crate::nymd::NymdClient; use async_trait::async_trait; +use contracts_common::ContractBuildInformation; use cosmwasm_std::{Coin as CosmWasmCoin, Timestamp}; +use mixnet_contract_common::MixId; +use serde::Deserialize; use vesting_contract::vesting::Account; use vesting_contract_common::{ - messages::QueryMsg as VestingQueryMsg, OriginalVestingResponse, Period, PledgeData, + messages::QueryMsg as VestingQueryMsg, AllDelegationsResponse, DelegationTimesResponse, + OriginalVestingResponse, Period, PledgeData, VestingDelegation, }; #[async_trait] pub trait VestingQueryClient { + async fn query_vesting_contract(&self, query: VestingQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>; + + async fn get_vesting_contract_version(&self) -> Result { + self.query_vesting_contract(VestingQueryMsg::GetContractVersion {}) + .await + } + async fn locked_coins( &self, address: &str, @@ -70,10 +83,50 @@ pub trait VestingQueryClient { &self, vesting_account_address: &str, ) -> Result; + + async fn get_delegation_timestamps( + &self, + address: &str, + mix_id: MixId, + ) -> Result; + + async fn get_all_vesting_delegations_paged( + &self, + start_after: Option<(u32, MixId, u64)>, + limit: Option, + ) -> Result; + + async fn get_all_vesting_delegations(&self) -> Result, NymdError> { + let mut delegations = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .get_all_vesting_delegations_paged(start_after.take(), None) + .await?; + delegations.append(&mut paged_response.delegations); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(delegations) + } } #[async_trait] impl VestingQueryClient for NymdClient { + async fn query_vesting_contract(&self, query: VestingQueryMsg) -> Result + where + for<'a> T: Deserialize<'a>, + { + self.client + .query_contract_smart(self.vesting_contract_address(), &query) + .await + } + async fn locked_coins( &self, vesting_account_address: &str, @@ -232,4 +285,29 @@ impl VestingQueryClient for NymdClient { .query_contract_smart(self.vesting_contract_address(), &request) .await } + + async fn get_delegation_timestamps( + &self, + address: &str, + mix_id: MixId, + ) -> Result { + let request = VestingQueryMsg::GetDelegationTimes { + address: address.to_string(), + mix_id, + }; + self.client + .query_contract_smart(self.vesting_contract_address(), &request) + .await + } + + async fn get_all_vesting_delegations_paged( + &self, + start_after: Option<(u32, MixId, u64)>, + limit: Option, + ) -> Result { + let request = VestingQueryMsg::GetAllDelegations { start_after, limit }; + self.client + .query_contract_smart(self.vesting_contract_address(), &request) + .await + } } 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 957a02e3e1..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 @@ -6,14 +6,29 @@ use crate::nymd::cosmwasm_client::types::ExecuteResult; use crate::nymd::error::NymdError; use crate::nymd::{Coin, Fee, NymdClient}; use async_trait::async_trait; -use mixnet_contract_common::{Gateway, IdentityKey, IdentityKeyRef, MixNode}; +use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; +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 { + async fn execute_vesting_contract( + &self, + fee: Option, + msg: VestingExecuteMsg, + funds: Vec, + ) -> Result; + + async fn vesting_update_mixnode_cost_params( + &self, + new_costs: MixNodeCostParams, + fee: Option, + ) -> Result; + async fn vesting_update_mixnode_config( &self, - profix_margin_percent: u8, + new_config: MixNodeConfigUpdate, fee: Option, ) -> Result; @@ -43,10 +58,12 @@ pub trait VestingSigningClient { async fn vesting_bond_mixnode( &self, mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: &str, pledge: Coin, fee: Option, ) -> Result; + async fn vesting_unbond_mixnode(&self, fee: Option) -> Result; async fn vesting_track_unbond_mixnode( @@ -65,21 +82,21 @@ pub trait VestingSigningClient { async fn vesting_track_undelegation( &self, address: &str, - mix_identity: IdentityKey, + mix_id: MixId, amount: Coin, fee: Option, ) -> Result; - async fn vesting_delegate_to_mixnode<'a>( + async fn vesting_delegate_to_mixnode( &self, - mix_identity: IdentityKeyRef<'a>, + mix_id: MixId, amount: Coin, fee: Option, ) -> Result; - async fn vesting_undelegate_from_mixnode<'a>( + async fn vesting_undelegate_from_mixnode( &self, - mix_identity: IdentityKeyRef<'a>, + mix_id: MixId, fee: Option, ) -> Result; @@ -89,21 +106,53 @@ pub trait VestingSigningClient { staking_address: Option, vesting_spec: Option, amount: Coin, + cap: Option, fee: Option, ) -> Result; } #[async_trait] impl VestingSigningClient for NymdClient { + async fn execute_vesting_contract( + &self, + fee: Option, + msg: VestingExecuteMsg, + funds: Vec, + ) -> Result { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); + let memo = msg.name().to_string(); + self.client + .execute( + self.address(), + self.vesting_contract_address(), + &msg, + fee, + memo, + funds, + ) + .await + } + + async fn vesting_update_mixnode_cost_params( + &self, + new_costs: MixNodeCostParams, + fee: Option, + ) -> Result { + self.execute_vesting_contract( + fee, + VestingExecuteMsg::UpdateMixnodeCostParams { new_costs }, + vec![], + ) + .await + } + async fn vesting_update_mixnode_config( &self, - profit_margin_percent: u8, + new_config: MixNodeConfigUpdate, fee: Option, ) -> Result { let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let req = VestingExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }; + let req = VestingExecuteMsg::UpdateMixnodeConfig { new_config }; self.client .execute( self.address(), @@ -203,26 +252,22 @@ impl VestingSigningClient for NymdClient async fn vesting_bond_mixnode( &self, mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: &str, pledge: Coin, fee: Option, ) -> Result { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let req = VestingExecuteMsg::BondMixnode { - mix_node, - owner_signature: owner_signature.to_string(), - amount: pledge.into(), - }; - self.client - .execute( - self.address(), - self.vesting_contract_address(), - &req, - fee, - "VestingContract::BondMixnode", - vec![], - ) - .await + self.execute_vesting_contract( + fee, + VestingExecuteMsg::BondMixnode { + mix_node, + cost_params, + owner_signature: owner_signature.to_string(), + amount: pledge.into(), + }, + vec![], + ) + .await } async fn vesting_unbond_mixnode(&self, fee: Option) -> Result { @@ -262,6 +307,7 @@ impl VestingSigningClient for NymdClient ) .await } + async fn withdraw_vested_coins( &self, amount: Coin, @@ -282,72 +328,54 @@ impl VestingSigningClient for NymdClient ) .await } + async fn vesting_track_undelegation( &self, address: &str, - mix_identity: IdentityKey, + mix_id: MixId, amount: Coin, fee: Option, ) -> Result { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let req = VestingExecuteMsg::TrackUndelegation { - owner: address.to_string(), - mix_identity, - amount: amount.into(), - }; - self.client - .execute( - self.address(), - self.vesting_contract_address(), - &req, - fee, - "VestingContract::TrackUndelegation", - vec![], - ) - .await - } - async fn vesting_delegate_to_mixnode<'a>( - &self, - mix_identity: IdentityKeyRef<'a>, - amount: Coin, - fee: Option, - ) -> Result { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let req = VestingExecuteMsg::DelegateToMixnode { - mix_identity: mix_identity.into(), - amount: amount.into(), - }; - self.client - .execute( - self.address(), - self.vesting_contract_address(), - &req, - fee, - "VestingContract::DelegateToMixnode", - vec![], - ) - .await + self.execute_vesting_contract( + fee, + VestingExecuteMsg::TrackUndelegation { + owner: address.to_string(), + mix_id, + amount: amount.into(), + }, + vec![], + ) + .await } - async fn vesting_undelegate_from_mixnode<'a>( + async fn vesting_delegate_to_mixnode( &self, - mix_identity: IdentityKeyRef<'a>, + mix_id: MixId, + amount: Coin, fee: Option, ) -> Result { - let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); - let req = VestingExecuteMsg::UndelegateFromMixnode { - mix_identity: mix_identity.into(), - }; - self.client - .execute( - self.address(), - self.vesting_contract_address(), - &req, - fee, - "VestingContract::UndelegateFromMixnode", - vec![], - ) - .await + self.execute_vesting_contract( + fee, + VestingExecuteMsg::DelegateToMixnode { + mix_id, + amount: amount.into(), + }, + vec![], + ) + .await + } + + async fn vesting_undelegate_from_mixnode( + &self, + mix_id: MixId, + fee: Option, + ) -> Result { + self.execute_vesting_contract( + fee, + VestingExecuteMsg::UndelegateFromMixnode { mix_id }, + vec![], + ) + .await } async fn create_periodic_vesting_account( @@ -356,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))); @@ -363,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 247ffcd743..c78aacde70 100644 --- a/common/client-libs/validator-client/src/nymd/wallet/mod.rs +++ b/common/client-libs/validator-client/src/nymd/wallet/mod.rs @@ -4,7 +4,7 @@ use crate::nymd::error::NymdError; use config::defaults; use cosmrs::bip32::{DerivationPath, XPrv}; -use cosmrs::crypto::secp256k1::SigningKey; +use cosmrs::crypto::secp256k1::{Signature, SigningKey}; use cosmrs::crypto::PublicKey; use cosmrs::tx::SignDoc; use cosmrs::{tx, AccountId}; @@ -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(); @@ -105,6 +105,17 @@ impl DirectSecp256k1HdWallet { self.secret.to_string() } + pub fn sign_raw_with_account( + &self, + signer: &AccountData, + message: &[u8], + ) -> Result { + signer + .private_key + .sign(message) + .map_err(|_| NymdError::SigningFailure) + } + pub fn sign_direct_with_account( &self, signer: &AccountData, @@ -196,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() { @@ -207,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/error.rs b/common/client-libs/validator-client/src/validator_api/error.rs index 02704e6229..9d7cd1d50d 100644 --- a/common/client-libs/validator-client/src/validator_api/error.rs +++ b/common/client-libs/validator-client/src/validator_api/error.rs @@ -1,4 +1,5 @@ use thiserror::Error; +use validator_api_requests::models::RequestError; #[derive(Error, Debug)] pub enum ValidatorAPIError { @@ -10,4 +11,7 @@ pub enum ValidatorAPIError { #[error("Request failed with error message - {0}")] GenericRequestFailure(String), + + #[error("The validator API has failed to resolve our request. It returned status code {status} and additional error message: {}", error.message())] + ApiRequestFailure { status: u16, error: RequestError }, } 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 1fa3a50862..0f4ebc60db 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -3,17 +3,20 @@ use crate::validator_api::error::ValidatorAPIError; use crate::validator_api::routes::{CORE_STATUS_COUNT, SINCE_ARG}; -use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixNodeBond}; +use mixnet_contract_common::mixnode::MixNodeDetails; +use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; +use reqwest::Response; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; use url::Url; use validator_api_requests::coconut::{ BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ - CoreNodeStatusResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, - MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, + GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, + InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeCoreStatusResponse, + MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RequestError, + RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; pub mod error; @@ -46,6 +49,19 @@ impl Client { &self.url } + async fn send_get_request( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result + where + K: AsRef, + V: AsRef, + { + let url = create_api_url(&self.url, path, params); + Ok(self.reqwest_client.get(url).send().await?) + } + async fn query_validator_api( &self, path: PathSegments<'_>, @@ -56,8 +72,36 @@ impl Client { K: AsRef, V: AsRef, { - let url = create_api_url(&self.url, path, params); - Ok(self.reqwest_client.get(url).send().await?.json().await?) + let res = self.send_get_request(path, params).await?; + if res.status().is_success() { + Ok(res.json().await?) + } else { + Err(ValidatorAPIError::GenericRequestFailure(res.text().await?)) + } + } + + // This works for endpoints returning Result, ErrorResponse> + async fn query_validator_api_fallible( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + ) -> Result + where + for<'a> T: Deserialize<'a>, + K: AsRef, + V: AsRef, + { + let res = self.send_get_request(path, params).await?; + let status = res.status(); + if res.status().is_success() { + Ok(res.json().await?) + } else { + let request_error: RequestError = res.json().await?; + Err(ValidatorAPIError::ApiRequestFailure { + status: status.as_u16(), + error: request_error, + }) + } } async fn post_validator_api( @@ -83,7 +127,7 @@ impl Client { } } - pub async fn get_mixnodes(&self) -> Result, ValidatorAPIError> { + pub async fn get_mixnodes(&self) -> Result, ValidatorAPIError> { self.query_validator_api(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS) .await } @@ -103,7 +147,7 @@ impl Client { .await } - pub async fn get_active_mixnodes(&self) -> Result, ValidatorAPIError> { + pub async fn get_active_mixnodes(&self) -> Result, ValidatorAPIError> { self.query_validator_api( &[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE], NO_PARAMS, @@ -126,7 +170,7 @@ impl Client { .await } - pub async fn get_rewarded_mixnodes(&self) -> Result, ValidatorAPIError> { + pub async fn get_rewarded_mixnodes(&self) -> Result, ValidatorAPIError> { self.query_validator_api( &[routes::API_VERSION, routes::MIXNODES, routes::REWARDED], NO_PARAMS, @@ -134,6 +178,74 @@ impl Client { .await } + pub async fn get_mixnode_report( + &self, + mix_id: MixId, + ) -> Result { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS, + routes::MIXNODE, + &mix_id.to_string(), + routes::REPORT, + ], + NO_PARAMS, + ) + .await + } + + pub async fn get_gateway_report( + &self, + identity: IdentityKeyRef<'_>, + ) -> Result { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS, + routes::GATEWAY, + identity, + routes::REPORT, + ], + NO_PARAMS, + ) + .await + } + + pub async fn get_mixnode_history( + &self, + mix_id: MixId, + ) -> Result { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS, + routes::MIXNODE, + &mix_id.to_string(), + routes::HISTORY, + ], + NO_PARAMS, + ) + .await + } + + pub async fn get_gateway_history( + &self, + identity: IdentityKeyRef<'_>, + ) -> Result { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS, + routes::GATEWAY, + identity, + routes::HISTORY, + ], + NO_PARAMS, + ) + .await + } + pub async fn get_rewarded_mixnodes_detailed( &self, ) -> Result, ValidatorAPIError> { @@ -149,28 +261,11 @@ impl Client { .await } - pub async fn get_probs_mixnode_rewarded( - &self, - mixnode_id: &str, - ) -> Result, ValidatorAPIError> { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::MIXNODES, - routes::REWARDED, - routes::INCLUSION_CHANCE, - mixnode_id, - ], - NO_PARAMS, - ) - .await - } - pub async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, since: Option, - ) -> Result { + ) -> Result { if let Some(since) = since { self.query_validator_api( &[ @@ -199,16 +294,16 @@ impl Client { pub async fn get_mixnode_core_status_count( &self, - identity: IdentityKeyRef<'_>, + mix_id: MixId, since: Option, - ) -> Result { + ) -> Result { if let Some(since) = since { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, + &mix_id.to_string(), CORE_STATUS_COUNT, ], &[(SINCE_ARG, since.to_string())], @@ -220,7 +315,8 @@ impl Client { routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, + &mix_id.to_string(), + CORE_STATUS_COUNT, ], NO_PARAMS, ) @@ -230,14 +326,14 @@ impl Client { pub async fn get_mixnode_status( &self, - identity: IdentityKeyRef<'_>, + mix_id: MixId, ) -> Result { self.query_validator_api( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, + &mix_id.to_string(), routes::STATUS, ], NO_PARAMS, @@ -247,14 +343,14 @@ impl Client { pub async fn get_mixnode_reward_estimation( &self, - identity: IdentityKeyRef<'_>, + mix_id: MixId, ) -> Result { - self.query_validator_api( + self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, + &mix_id.to_string(), routes::REWARD_ESTIMATION, ], NO_PARAMS, @@ -264,14 +360,14 @@ impl Client { pub async fn get_mixnode_stake_saturation( &self, - identity: IdentityKeyRef<'_>, + mix_id: MixId, ) -> Result { - self.query_validator_api( + self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, + &mix_id.to_string(), routes::STAKE_SATURATION, ], NO_PARAMS, @@ -281,14 +377,14 @@ impl Client { pub async fn get_mixnode_inclusion_probability( &self, - identity: IdentityKeyRef<'_>, + mix_id: MixId, ) -> Result { - self.query_validator_api( + self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, + &mix_id.to_string(), routes::INCLUSION_CHANCE, ], NO_PARAMS, @@ -298,27 +394,14 @@ impl Client { pub async fn get_mixnode_avg_uptime( &self, - identity: IdentityKeyRef<'_>, + mix_id: MixId, ) -> Result { - self.query_validator_api( + self.query_validator_api_fallible( &[ routes::API_VERSION, routes::STATUS_ROUTES, routes::MIXNODE, - identity, - routes::AVG_UPTIME, - ], - NO_PARAMS, - ) - .await - } - - pub async fn get_mixnode_avg_uptimes(&self) -> Result, ValidatorAPIError> { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::STATUS_ROUTES, - routes::MIXNODES, + &mix_id.to_string(), routes::AVG_UPTIME, ], NO_PARAMS, 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 8f1a125c80..64aa1b8cd9 100644 --- a/common/client-libs/validator-client/src/validator_api/routes.rs +++ b/common/client-libs/validator-client/src/validator_api/routes.rs @@ -10,7 +10,6 @@ pub const GATEWAYS: &str = "gateways"; pub const DETAILED: &str = "detailed"; pub const ACTIVE: &str = "active"; pub const REWARDED: &str = "rewarded"; - pub const COCONUT_ROUTES: &str = "coconut"; pub const BANDWIDTH: &str = "bandwidth"; @@ -28,6 +27,8 @@ pub const CORE_STATUS_COUNT: &str = "core-status-count"; pub const SINCE_ARG: &str = "since"; pub const STATUS: &str = "status"; +pub const REPORT: &str = "report"; +pub const HISTORY: &str = "history"; pub const REWARD_ESTIMATION: &str = "reward-estimation"; pub const AVG_UPTIME: &str = "avg_uptime"; pub const STAKE_SATURATION: &str = "stake-saturation"; diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml new file mode 100644 index 0000000000..35b38512fa --- /dev/null +++ b/common/commands/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "nym-cli-commands" +version = "1.0.0" +authors = ["Nym Technologies SA"] +edition = "2021" + +[dependencies] +base64 = "0.13.0" +bip39 = "1.0.1" +bs58 = "0.4" +comfy-table = "6.0.0" +cfg-if = "1.0.0" +clap = { version = "3.2", features = ["derive"] } +handlebars = "3.0.1" +humantime-serde = "1.0" +k256 = { version = "0.10", features = ["ecdsa", "sha256"] } +log = "0.4" +rand = {version = "0.6", features = ["std"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1" +thiserror = "1" +time = { version = "0.3.6", features = ["parsing", "formatting"] } +toml = "0.5.6" +url = "2.2" + +cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" } +cosmwasm-std = { version = "1.0.0" } + +validator-client = { path = "../client-libs/validator-client", features = ["nymd-client"] } +network-defaults = { path = "../network-defaults" } +mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" } +vesting-contract-common = { path = "../cosmwasm-smart-contracts/vesting-contract" } diff --git a/common/commands/README.md b/common/commands/README.md new file mode 100644 index 0000000000..c545e13b1d --- /dev/null +++ b/common/commands/README.md @@ -0,0 +1,13 @@ +# Common `clap` Command Crate + +This crate contains `clap` commands for common operations: + +- account creation and queries +- block queries +- cosmwasm uploads, instantiate, execution, query, etc +- mixnet actions and queries +- sign and verify messages +- query for transactions +- create vesting schedules and query for them + +For how to use this crate, please see the [Nym CLI](../../tools/nym-cli). \ No newline at end of file diff --git a/common/commands/src/coconut/mod.rs b/common/commands/src/coconut/mod.rs new file mode 100644 index 0000000000..6cfc921228 --- /dev/null +++ b/common/commands/src/coconut/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// TODO: add coconut commands here diff --git a/common/commands/src/context/errors.rs b/common/commands/src/context/errors.rs new file mode 100644 index 0000000000..5c3d24a2bb --- /dev/null +++ b/common/commands/src/context/errors.rs @@ -0,0 +1,18 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum ContextError { + #[error("mnemonic was not provided, pass as an argument or an env var called MNEMONIC")] + MnemonicNotProvided, + + #[error("failed to parse mnemonic - {0}")] + Bip39Error(#[from] bip39::Error), + + // there are lots of error that can occur in the nymd client, so just pass through their display details + // TODO: improve this to return known errors + #[error("failed to create client - {0}")] + NymdError(String), +} diff --git a/common/commands/src/context/mod.rs b/common/commands/src/context/mod.rs new file mode 100644 index 0000000000..856a03e62a --- /dev/null +++ b/common/commands/src/context/mod.rs @@ -0,0 +1,138 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use network_defaults::{ + setup_env, + var_names::{API_VALIDATOR, MIXNET_CONTRACT_ADDRESS, NYMD_VALIDATOR, VESTING_CONTRACT_ADDRESS}, + NymNetworkDetails, +}; +use validator_client::nymd::{self, AccountId, NymdClient, QueryNymdClient, SigningNymdClient}; +pub use validator_client::validator_api::Client as ValidatorApiClient; + +use crate::context::errors::ContextError; + +pub mod errors; + +pub type SigningClient = validator_client::nymd::NymdClient; +pub type QueryClient = validator_client::nymd::NymdClient; +pub type SigningClientWithValidatorAPI = validator_client::Client; +pub type QueryClientWithValidatorAPI = validator_client::Client; + +#[derive(Debug)] +pub struct ClientArgs { + pub config_env_file: Option, + pub nymd_url: Option, + pub validator_api_url: Option, + pub mnemonic: Option, + pub mixnet_contract_address: Option, + pub vesting_contract_address: Option, +} + +pub fn get_network_details(args: &ClientArgs) -> Result { + // let the network defaults crate handle setting up the env vars if the file arg is set, otherwise + // it will default to what is already in env vars, falling back to mainnet + setup_env(args.config_env_file.clone()); + + // override the env vars with user supplied arguments, if set + if let Some(nymd_url) = args.nymd_url.as_ref() { + std::env::set_var(NYMD_VALIDATOR, nymd_url); + } + if let Some(validator_api_url) = args.validator_api_url.as_ref() { + std::env::set_var(API_VALIDATOR, validator_api_url); + } + if let Some(mixnet_contract_address) = args.mixnet_contract_address.as_ref() { + std::env::set_var(MIXNET_CONTRACT_ADDRESS, mixnet_contract_address.to_string()); + } + if let Some(vesting_contract_address) = args.vesting_contract_address.as_ref() { + std::env::set_var( + VESTING_CONTRACT_ADDRESS, + vesting_contract_address.to_string(), + ); + } + + Ok(NymNetworkDetails::new_from_env()) +} + +pub fn create_signing_client( + args: ClientArgs, + network_details: &NymNetworkDetails, +) -> Result { + let client_config = nymd::Config::try_from_nym_network_details(network_details) + .expect("failed to construct valid validator client config with the provided network"); + + // get mnemonic + let mnemonic = match std::env::var("MNEMONIC") { + Ok(value) => bip39::Mnemonic::parse(value)?, + // env var MNEMONIC is not present, so try to fall back to arg --mnemonic ... + Err(_) => match args.mnemonic { + Some(value) => value, + None => return Err(ContextError::MnemonicNotProvided), // no env var or arg provided + }, + }; + + let nymd_url = network_details + .endpoints + .first() + .expect("network details are not defined") + .nymd_url + .as_str(); + + match NymdClient::connect_with_mnemonic(client_config, nymd_url, mnemonic, None) { + Ok(client) => Ok(client), + Err(e) => Err(ContextError::NymdError(format!("{:?}", e))), + } +} + +pub fn create_query_client( + network_details: &NymNetworkDetails, +) -> Result { + let client_config = nymd::Config::try_from_nym_network_details(network_details) + .expect("failed to construct valid validator client config with the provided network"); + + let nymd_url = network_details + .endpoints + .first() + .expect("network details are not defined") + .nymd_url + .as_str(); + + match NymdClient::connect(client_config, nymd_url) { + Ok(client) => Ok(client), + Err(e) => Err(ContextError::NymdError(format!("{:?}", e))), + } +} + +pub fn create_signing_client_with_validator_api( + args: ClientArgs, + network_details: &NymNetworkDetails, +) -> Result { + let client_config = validator_client::Config::try_from_nym_network_details(network_details) + .expect("failed to construct valid validator client config with the provided network"); + + // get mnemonic + let mnemonic = match std::env::var("MNEMONIC") { + Ok(value) => bip39::Mnemonic::parse(value)?, + // env var MNEMONIC is not present, so try to fall back to arg --mnemonic ... + Err(_) => match args.mnemonic { + Some(value) => value, + None => return Err(ContextError::MnemonicNotProvided), // no env var or arg provided + }, + }; + + match validator_client::client::Client::new_signing(client_config, mnemonic) { + Ok(client) => Ok(client), + Err(e) => Err(ContextError::NymdError(format!("{:?}", e))), + } +} + +pub fn create_query_client_with_validator_api( + network_details: &NymNetworkDetails, +) -> Result { + let client_config = validator_client::Config::try_from_nym_network_details(network_details) + .expect("failed to construct valid validator client config with the provided network"); + + match validator_client::client::Client::new_query(client_config) { + Ok(client) => Ok(client), + Err(e) => Err(ContextError::NymdError(format!("{:?}", e))), + } +} diff --git a/contracts/bandwidth-claim/src/support/mod.rs b/common/commands/src/lib.rs similarity index 59% rename from contracts/bandwidth-claim/src/support/mod.rs rename to common/commands/src/lib.rs index 3e1ec563d5..afca2be0f7 100644 --- a/contracts/bandwidth-claim/src/support/mod.rs +++ b/common/commands/src/lib.rs @@ -1,3 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod tests; + +pub mod coconut; +pub mod context; +pub mod utils; +pub mod validator; diff --git a/common/commands/src/utils.rs b/common/commands/src/utils.rs new file mode 100644 index 0000000000..2e042da300 --- /dev/null +++ b/common/commands/src/utils.rs @@ -0,0 +1,49 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::error::Error; +use std::fmt::Display; + +use cosmwasm_std::{Coin as CosmWasmCoin, Decimal}; +use log::error; +use validator_client::nymd::Coin; + +pub fn pretty_coin(coin: &Coin) -> String { + let amount = Decimal::from_ratio(coin.amount, 1_000_000u128); + let denom = if coin.denom.starts_with('u') { + &coin.denom[1..] + } else { + &coin.denom + }; + format!("{} {}", amount, denom) +} + +pub fn pretty_cosmwasm_coin(coin: &CosmWasmCoin) -> String { + let amount = Decimal::from_ratio(coin.amount, 1_000_000u128); + let denom = if coin.denom.starts_with('u') { + &coin.denom[1..] + } else { + &coin.denom + }; + format!("{} {}", amount, denom) +} + +pub fn pretty_decimal_with_denom(value: Decimal, denom: &str) -> String { + // TODO: we might have to truncate the value here (that's why I moved it to separate function) + format!("{} {}", value, denom) +} + +pub fn show_error(e: E) +where + E: Display, +{ + error!("{}", e); +} + +pub fn show_error_passthrough(e: E) -> E +where + E: Error + Display, +{ + error!("{}", e); + e +} diff --git a/common/commands/src/validator/account/balance.rs b/common/commands/src/validator/account/balance.rs new file mode 100644 index 0000000000..eb8e2dfa8f --- /dev/null +++ b/common/commands/src/validator/account/balance.rs @@ -0,0 +1,72 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use log::{error, info}; + +use validator_client::nymd::AccountId; + +use crate::context::QueryClient; +use crate::utils::{pretty_coin, show_error}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap(help = "The account address to get the balance for")] + pub address: Option, + + #[clap(long)] + #[clap(help = "Optional currency to show balance for")] + pub denom: Option, + + #[clap(long, requires = "denom")] + #[clap(help = "Optionally hide the denom")] + pub hide_denom: bool, + + #[clap(long)] + #[clap(help = "Show as a raw value")] + pub raw: bool, +} + +pub async fn query_balance( + args: Args, + client: &QueryClient, + address_from_mnemonic: Option, +) { + if args.address.is_none() && address_from_mnemonic.is_none() { + error!("Please specify an account address or a mnemonic to get the balance for"); + return; + } + + let address = args + .address + .unwrap_or_else(|| address_from_mnemonic.expect("please provide a mnemonic")); + + info!("Getting balance for {}...", address); + + match client.get_all_balances(&address).await { + Ok(coins) => { + if coins.is_empty() { + println!("No balance"); + return; + } + + let denom = args.denom.unwrap_or_default(); + + for coin in coins { + if denom.is_empty() || denom.eq_ignore_ascii_case(&coin.denom) { + if args.raw { + if !args.hide_denom { + println!("{}", coin); + } else { + println!("{}", coin.amount); + } + } else { + println!("{}", pretty_coin(&coin)); + } + } + } + } + Err(e) => show_error(e), + } +} diff --git a/common/commands/src/validator/account/create.rs b/common/commands/src/validator/account/create.rs new file mode 100644 index 0000000000..3003b9a9d5 --- /dev/null +++ b/common/commands/src/validator/account/create.rs @@ -0,0 +1,24 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use validator_client::nymd::wallet::DirectSecp256k1HdWallet; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + // allowed values are 12, 18 or 24 + pub word_count: Option, +} + +pub fn create_account(args: Args, prefix: &str) { + let word_count = args.word_count.unwrap_or(24); + let mnemonic = bip39::Mnemonic::generate(word_count).expect("failed to generate mnemonic!"); + + let wallet = + DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic).expect("failed to build wallet!"); + + // Output address and mnemonics into separate lines for easier parsing + println!("{}", wallet.mnemonic()); + println!("{}", wallet.try_derive_accounts().unwrap()[0].address()); +} diff --git a/common/commands/src/validator/account/mod.rs b/common/commands/src/validator/account/mod.rs new file mode 100644 index 0000000000..b3cecc7c9a --- /dev/null +++ b/common/commands/src/validator/account/mod.rs @@ -0,0 +1,28 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod balance; +pub mod create; +pub mod pubkey; +pub mod send; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct Account { + #[clap(subcommand)] + pub command: Option, +} + +#[derive(Debug, Subcommand)] +pub enum AccountCommands { + /// Create a new mnemonic - note, this account does not appear on the chain until the account id is used in a transaction + Create(crate::validator::account::create::Args), + /// Gets the balance of an account + Balance(crate::validator::account::balance::Args), + /// Gets the public key of an account + PubKey(crate::validator::account::pubkey::Args), + /// Sends tokens to another account + Send(crate::validator::account::send::Args), +} diff --git a/common/commands/src/validator/account/pubkey.rs b/common/commands/src/validator/account/pubkey.rs new file mode 100644 index 0000000000..83d15f25e3 --- /dev/null +++ b/common/commands/src/validator/account/pubkey.rs @@ -0,0 +1,89 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use log::{error, info}; + +use validator_client::nymd::wallet::DirectSecp256k1HdWallet; +use validator_client::nymd::AccountId; + +use crate::context::QueryClient; +use crate::utils::show_error; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap( + help = "Optionally, show the public key for this account address, otherwise generate the account address from the mnemonic" + )] + pub address: Option, + + #[clap(long)] + #[clap(help = "If set, get the public key from the mnemonic, rather than querying for it")] + pub from_mnemonic: bool, +} + +pub async fn get_pubkey( + args: Args, + client: &QueryClient, + mnemonic: Option, + address_from_mnemonic: Option, +) { + if args.address.is_none() && address_from_mnemonic.is_none() { + error!("Please specify an account address or a mnemonic to get the balance for"); + return; + } + + let address = args + .address + .unwrap_or_else(|| address_from_mnemonic.expect("please provide a mnemonic")); + + if args.from_mnemonic { + let prefix = client + .current_chain_details() + .bech32_account_prefix + .as_str(); + get_pubkey_from_mnemonic(address, prefix, mnemonic.expect("mnemonic not set")); + return; + } + + get_pubkey_from_chain(address, client).await; +} + +pub fn get_pubkey_from_mnemonic(address: AccountId, prefix: &str, mnemonic: bip39::Mnemonic) { + match DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic) { + Ok(wallet) => match wallet.try_derive_accounts() { + Ok(accounts) => match accounts.iter().find(|a| *a.address() == address) { + Some(account) => { + println!("{}", account.public_key().to_string()); + } + None => { + error!("Could not derive key that matches {}", address) + } + }, + Err(e) => { + error!("Failed to derive accounts. {}", e); + } + }, + Err(e) => show_error(e), + } +} + +pub async fn get_pubkey_from_chain(address: AccountId, client: &QueryClient) { + info!("Getting public key for address {} from chain...", address); + match client.get_account_details(&address).await { + Ok(Some(account)) => { + if let Ok(base_account) = account.try_get_base_account() { + if let Some(pubkey) = base_account.pubkey { + println!("{}", pubkey.to_string()); + } else { + println!("No account associated with address {}", address); + } + } + } + Ok(None) => { + println!("No account associated with address {}", address); + } + Err(e) => show_error(e), + } +} diff --git a/common/commands/src/validator/account/send.rs b/common/commands/src/validator/account/send.rs new file mode 100644 index 0000000000..90816258bd --- /dev/null +++ b/common/commands/src/validator/account/send.rs @@ -0,0 +1,66 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use log::info; +use serde_json::json; + +use validator_client::nymd::{AccountId, Coin}; + +use crate::context::SigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser, help = "The recipient account address")] + pub recipient: AccountId, + + #[clap( + value_parser, + help = "Amount to transfer in micro denomination (e.g. unym or unyx)" + )] + pub amount: u128, + + #[clap(long, help = "Override the denomination")] + pub denom: Option, + + #[clap(long)] + pub memo: Option, +} + +pub async fn send(args: Args, client: &SigningClient) { + let memo = args + .memo + .unwrap_or_else(|| "Sending tokens with nym-cli".to_owned()); + let denom = args + .denom + .unwrap_or_else(|| client.current_chain_details().mix_denom.base.clone()); + + let coin = Coin { + denom, + amount: args.amount, + }; + + info!( + "Sending {} {} from {} to {}...", + coin.amount, + coin.denom, + client.address(), + args.recipient + ); + + let res = client + .send(&args.recipient, vec![coin], memo, None) + .await + .expect("failed to send tokens!"); + + info!("Sending result: {}", json!(res)); + + println!(); + println!( + "Nodesguru: https://nym.explorers.guru/transaction/{}", + &res.hash + ); + println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash); + println!("Transaction result code: {}", &res.tx_result.code.value()); + println!("Transaction hash: {}", &res.hash); +} diff --git a/common/commands/src/validator/block/block_time.rs b/common/commands/src/validator/block/block_time.rs new file mode 100644 index 0000000000..a35e7ca597 --- /dev/null +++ b/common/commands/src/validator/block/block_time.rs @@ -0,0 +1,23 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; + +use crate::context::QueryClient; +use crate::utils::show_error; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap(help = "The block height")] + pub height: u32, +} + +pub async fn query_for_block_time(args: Args, client: &QueryClient) { + match client.get_block_timestamp(Some(args.height)).await { + Ok(res) => { + println!("{}", res.to_rfc3339()) + } + Err(e) => show_error(e), + } +} diff --git a/common/commands/src/validator/block/current_height.rs b/common/commands/src/validator/block/current_height.rs new file mode 100644 index 0000000000..a7aa7fb743 --- /dev/null +++ b/common/commands/src/validator/block/current_height.rs @@ -0,0 +1,19 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; + +use crate::context::QueryClient; +use crate::utils::show_error; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn query_current_block_height(client: &QueryClient) { + match client.get_current_block_height().await { + Ok(res) => { + println!("Current block height:\n{}", res.value()) + } + Err(e) => show_error(e), + } +} diff --git a/common/commands/src/validator/block/get.rs b/common/commands/src/validator/block/get.rs new file mode 100644 index 0000000000..12a612b0c8 --- /dev/null +++ b/common/commands/src/validator/block/get.rs @@ -0,0 +1,24 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; + +use crate::context::QueryClient; +use crate::utils::show_error; +use serde_json::json; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap(help = "The block height")] + pub height: u32, +} + +pub async fn query_for_block(args: Args, client: &QueryClient) { + match client.get_block(Some(args.height)).await { + Ok(res) => { + println!("{}", json!(res)) + } + Err(e) => show_error(e), + } +} diff --git a/common/commands/src/validator/block/mod.rs b/common/commands/src/validator/block/mod.rs new file mode 100644 index 0000000000..07798ced63 --- /dev/null +++ b/common/commands/src/validator/block/mod.rs @@ -0,0 +1,25 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod block_time; +pub mod current_height; +pub mod get; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct Block { + #[clap(subcommand)] + pub command: Option, +} + +#[derive(Debug, Subcommand)] +pub enum BlockCommands { + /// Gets a block's details and prints as JSON + Get(crate::validator::block::get::Args), + /// Gets the block time at a height + Time(crate::validator::block::block_time::Args), + /// Gets the current block height + CurrentHeight(crate::validator::block::current_height::Args), +} diff --git a/common/commands/src/validator/cosmwasm/execute_contract.rs b/common/commands/src/validator/cosmwasm/execute_contract.rs new file mode 100644 index 0000000000..f387b83903 --- /dev/null +++ b/common/commands/src/validator/cosmwasm/execute_contract.rs @@ -0,0 +1,60 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use cosmrs::AccountId; +use log::{error, info}; +use serde_json::{json, Value}; +use validator_client::nymd::Coin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap(help = "The address of contract to execute")] + pub contract_address: AccountId, + + #[clap(value_parser)] + #[clap(help = "JSON encoded method arguments")] + pub json_args: String, + + #[clap(long)] + pub memo: Option, + + #[clap( + value_parser, + requires = "fundsDenom", + help = "Amount to supply as funds in micro denomination (e.g. unym or unyx)" + )] + pub funds: Option, + + #[clap(long, requires = "funds", help = "Set the denomination for the funds")] + pub funds_denom: Option, +} + +pub async fn execute(args: Args, client: SigningClient) { + info!("Starting contract method execution!"); + + let json_args: Value = + serde_json::from_str(&args.json_args).expect("Unable to parse JSON args"); + + let memo = args + .memo + .unwrap_or_else(|| "nym-cli execute contract method".to_owned()); + + let funds = match args.funds { + Some(funds) => vec![Coin::new( + funds, + args.funds_denom.expect("denom for funds not set"), + )], + None => vec![], + }; + + match client + .execute(&args.contract_address, &json_args, None, memo, funds) + .await + { + Ok(res) => info!("SUCCESS ✅\n{}", json!(res)), + Err(e) => error!("FAILURE ❌\n{}", e), + } +} diff --git a/common/commands/src/validator/cosmwasm/init_contract.rs b/common/commands/src/validator/cosmwasm/init_contract.rs new file mode 100644 index 0000000000..c624a81a94 --- /dev/null +++ b/common/commands/src/validator/cosmwasm/init_contract.rs @@ -0,0 +1,78 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use cosmrs::{AccountId, Coin as CosmosCoin}; +use log::info; +use network_defaults::NymNetworkDetails; +use validator_client::nymd::cosmwasm_client::types::{ContractCodeId, InstantiateOptions}; +use validator_client::nymd::Coin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + pub code_id: ContractCodeId, + + #[clap(long)] + pub memo: Option, + + #[clap(long)] + pub label: Option, + + #[clap(long)] + pub init_message: String, + + #[clap(long)] + pub admin: Option, + + #[clap( + long, + requires = "fundsDenom", + help = "Amount to supply as funds in micro denomination (e.g. unym or unyx)" + )] + pub funds: Option, + + #[clap(long, requires = "funds", help = "Set the denomination for the funds")] + pub funds_denom: Option, +} + +pub async fn init(args: Args, client: SigningClient, network_details: &NymNetworkDetails) { + info!("Starting contract instantiation!"); + + let memo = args + .memo + .unwrap_or_else(|| "contract instantiation".to_owned()); + let label = args + .label + .unwrap_or_else(|| "Nym mixnet smart contract".to_owned()); + + let funds: Vec = match args.funds { + Some(funds) => vec![Coin::new( + funds, + args.funds_denom + .unwrap_or_else(|| network_details.chain_details.mix_denom.base.to_string()), + ) + .into()], + None => vec![], + }; + + // by default we make ourselves an admin, let me know if you don't like that behaviour + let opts = Some(InstantiateOptions { + funds, + admin: Some(args.admin.unwrap_or_else(|| client.address().clone())), + }); + + let msg: serde_json::Value = + serde_json::from_str(&args.init_message).expect("failed to parse init message"); + + // the EmptyMsg{} argument is equivalent to `--init-message='{}'` + let res = client + .instantiate(args.code_id, &msg, label, memo, opts, None) + .await + .expect("failed to instantiate the contract!"); + + info!("Init result: {:?}", res); + + println!("{}", res.contract_address) +} diff --git a/common/commands/src/validator/cosmwasm/migrate_contract.rs b/common/commands/src/validator/cosmwasm/migrate_contract.rs new file mode 100644 index 0000000000..fa21062ee7 --- /dev/null +++ b/common/commands/src/validator/cosmwasm/migrate_contract.rs @@ -0,0 +1,51 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use crate::utils::show_error_passthrough; +use clap::Parser; +use cosmrs::AccountId; +use log::info; +use validator_client::nymd::cosmwasm_client::types::{ContractCodeId, EmptyMsg}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + pub contract_address: AccountId, + + #[clap(long)] + pub code_id: ContractCodeId, + + #[clap(long)] + pub memo: Option, + + #[clap(long)] + pub init_message: Option, +} + +pub async fn migrate(args: Args, client: SigningClient) { + println!("Starting contract migration!"); + + let memo = args.memo.unwrap_or_else(|| "contract migration".to_owned()); + let contract_address = args.contract_address; + + // the EmptyMsg{} argument is equivalent to `--init-message='{}'` + let res = if let Some(raw_msg) = args.init_message { + let msg: serde_json::Value = + serde_json::from_str(&raw_msg).expect("failed to parse init message"); + + client + .migrate(&contract_address, args.code_id, &msg, memo, None) + .await + .map_err(show_error_passthrough) + .expect("failed to migrate the contract!") + } else { + client + .migrate(&contract_address, args.code_id, &EmptyMsg {}, memo, None) + .await + .map_err(show_error_passthrough) + .expect("failed to migrate the contract!") + }; + + info!("Migrate result: {:?}", res); +} diff --git a/common/commands/src/validator/cosmwasm/mod.rs b/common/commands/src/validator/cosmwasm/mod.rs new file mode 100644 index 0000000000..9ee1fa182f --- /dev/null +++ b/common/commands/src/validator/cosmwasm/mod.rs @@ -0,0 +1,28 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod execute_contract; +pub mod init_contract; +pub mod migrate_contract; +pub mod upload_contract; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct Cosmwasm { + #[clap(subcommand)] + pub command: Option, +} + +#[derive(Debug, Subcommand)] +pub enum CosmwasmCommands { + /// Upload a smart contract WASM blob + Upload(crate::validator::cosmwasm::upload_contract::Args), + /// Init a WASM smart contract + Init(crate::validator::cosmwasm::init_contract::Args), + /// Migrate a WASM smart contract + Migrate(crate::validator::cosmwasm::migrate_contract::Args), + /// Execute a WASM smart contract method + Execute(crate::validator::cosmwasm::execute_contract::Args), +} diff --git a/common/commands/src/validator/cosmwasm/upload_contract.rs b/common/commands/src/validator/cosmwasm/upload_contract.rs new file mode 100644 index 0000000000..96d01741ee --- /dev/null +++ b/common/commands/src/validator/cosmwasm/upload_contract.rs @@ -0,0 +1,37 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use std::io::Read; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub wasm_path: PathBuf, + + #[clap(long)] + pub memo: Option, +} + +pub async fn upload(args: Args, client: SigningClient) { + info!("Starting contract upload!"); + + let mut file = std::fs::File::open(args.wasm_path).expect("failed to open the wasm blob"); + let mut data = Vec::new(); + + file.read_to_end(&mut data).unwrap(); + + let memo = args.memo.unwrap_or_else(|| "contract upload".to_owned()); + + let res = client + .upload(data, memo, None) + .await + .expect("failed to upload the contract!"); + + info!("Upload result: {:?}", res); + + println!("{}", res.code_id) +} diff --git a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs new file mode 100644 index 0000000000..50b4880c6e --- /dev/null +++ b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs @@ -0,0 +1,50 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +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, + + #[clap(long)] + pub identity_key: Option, + + #[clap(long)] + pub amount: u128, +} + +pub async fn delegate_to_mixnode(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!("Starting delegation to mixnode"); + + let mix_id = match args.mix_id { + Some(mix_id) => mix_id, + None => { + let identity_key = args + .identity_key + .expect("either mix_id or mix_identity has to be specified"); + let node_details = client + .get_mixnode_details_by_identity(identity_key) + .await + .expect("contract query failed") + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + + let coin = Coin::new(args.amount, denom); + + let res = client + .delegate_to_mixnode(mix_id, coin.into(), None) + .await + .expect("failed to delegate to mixnode!"); + + info!("delegating to mixnode: {:?}", res); +} diff --git a/common/commands/src/validator/mixnet/delegators/mod.rs b/common/commands/src/validator/mixnet/delegators/mod.rs new file mode 100644 index 0000000000..61e87f119d --- /dev/null +++ b/common/commands/src/validator/mixnet/delegators/mod.rs @@ -0,0 +1,35 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod rewards; + +pub mod delegate_to_mixnode; +pub mod query_for_delegations; +pub mod undelegate_from_mixnode; +pub mod vesting_delegate_to_mixnode; +pub mod vesting_undelegate_from_mixnode; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetDelegators { + #[clap(subcommand)] + pub command: MixnetDelegatorsCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetDelegatorsCommands { + /// Lists current delegations + List(query_for_delegations::Args), + /// Manage rewards from delegations + Rewards(rewards::MixnetDelegatorsReward), + /// Delegate to a mixnode + Delegate(delegate_to_mixnode::Args), + /// Undelegate from a mixnode + Undelegate(undelegate_from_mixnode::Args), + /// Delegate to a mixnode with locked tokens + DelegateVesting(vesting_delegate_to_mixnode::Args), + /// Undelegate from a mixnode (when originally using locked tokens) + UndelegateVesting(vesting_undelegate_from_mixnode::Args), +} diff --git a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs new file mode 100644 index 0000000000..d816e65a0c --- /dev/null +++ b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs @@ -0,0 +1,130 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use log::info; + +use crate::context::SigningClientWithValidatorAPI; +use crate::utils::{pretty_cosmwasm_coin, show_error_passthrough}; + +use comfy_table::Table; +use cosmwasm_std::Addr; +use mixnet_contract_common::{Delegation, PendingEpochEvent, PendingEpochEventKind}; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn execute(_args: Args, client: SigningClientWithValidatorAPI) { + info!( + "Getting delegations for account {}...", + client.nymd.address() + ); + + let delegations = client + .get_all_delegator_delegations(client.nymd.address()) + .await + .map_err(show_error_passthrough); + + let mixnet_contract_events = client + .get_all_nymd_pending_epoch_events() + .await + .map_err(show_error_passthrough); + + if let Ok(res) = delegations { + println!(); + if res.is_empty() { + println!("This account has not delegated any tokens to mixnodes"); + } else { + println!("Delegations:"); + print_delegations(res, &client).await; + } + } + if let Ok(res) = mixnet_contract_events { + if !res.is_empty() { + println!(); + println!("Pending delegations (liquid tokens):"); + print_delegation_events(res, &client).await; + } + } +} + +async fn to_iso_timestamp(block_height: u32, client: &SigningClientWithValidatorAPI) -> String { + match client.nymd.get_block_timestamp(Some(block_height)).await { + Ok(res) => res.to_rfc3339(), + Err(_e) => "-".to_string(), + } +} + +async fn print_delegations(delegations: Vec, client: &SigningClientWithValidatorAPI) { + let mut table = Table::new(); + + table.set_header(vec!["Timestamp", "Mix Id", "Delegation", "Proxy"]); + + for delegation in delegations { + table.add_row(vec![ + to_iso_timestamp(delegation.height as u32, client).await, + delegation.mix_id.to_string(), + pretty_cosmwasm_coin(&delegation.amount), + delegation + .proxy + .map(Addr::into_string) + .unwrap_or_else(|| "-".into()), + ]); + } + + println!("{table}"); +} + +async fn print_delegation_events( + events: Vec, + client: &SigningClientWithValidatorAPI, +) { + let mut table = Table::new(); + + table.set_header(vec![ + "Timestamp", + "Mix id", + "Delegation", + "Event Type", + "Proxy", + ]); + + for event in events { + match event.event.kind { + PendingEpochEventKind::Delegate { + owner, + mix_id, + amount, + proxy, + } => { + if owner.as_str() == client.nymd.address().as_ref() { + table.add_row(vec![ + "not-sure-if-applicable".into(), + mix_id.to_string(), + pretty_cosmwasm_coin(&amount), + "Delegate".to_string(), + proxy.map(Addr::into_string).unwrap_or_else(|| "-".into()), + ]); + } + } + PendingEpochEventKind::Undelegate { + owner, + mix_id, + proxy, + } => { + if owner.as_str() == client.nymd.address().as_ref() { + table.add_row(vec![ + "not-sure-if-applicable".into(), + mix_id.to_string(), + "-".to_string(), + "Undelegate".to_string(), + proxy.map(Addr::into_string).unwrap_or_else(|| "-".into()), + ]); + } + } + _ => {} + } + } + + println!("{table}"); +} 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 new file mode 100644 index 0000000000..1858b7cebb --- /dev/null +++ b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs @@ -0,0 +1,43 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use mixnet_contract_common::MixId; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub mix_id: Option, + + #[clap(long)] + pub identity_key: Option, +} + +pub async fn claim_delegator_reward(args: Args, client: SigningClient) { + info!("Claim delegator reward"); + + let mix_id = match args.mix_id { + Some(mix_id) => mix_id, + None => { + let identity_key = args + .identity_key + .expect("either mix_id or mix_identity has to be specified"); + let node_details = client + .get_mixnode_details_by_identity(identity_key) + .await + .expect("contract query failed") + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + + let res = client + .withdraw_delegator_reward(mix_id, None) + .await + .expect("failed to claim delegator-reward"); + + info!("Claiming delegator reward: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/delegators/rewards/mod.rs b/common/commands/src/validator/mixnet/delegators/rewards/mod.rs new file mode 100644 index 0000000000..05fe7e98b5 --- /dev/null +++ b/common/commands/src/validator/mixnet/delegators/rewards/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod claim_delegator_reward; +pub mod vesting_claim_delegator_reward; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetDelegatorsReward { + #[clap(subcommand)] + pub command: MixnetDelegatorsRewardCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetDelegatorsRewardCommands { + /// Claim rewards accumulated during the delegation of unlocked tokens + Claim(claim_delegator_reward::Args), + /// Claim rewards accumulated during the delegation of locked tokens + VestingClaim(vesting_claim_delegator_reward::Args), +} 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 new file mode 100644 index 0000000000..6eca5cbf7a --- /dev/null +++ b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs @@ -0,0 +1,43 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use mixnet_contract_common::MixId; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub mix_id: Option, + + #[clap(long)] + pub identity_key: Option, +} + +pub async fn vesting_claim_delegator_reward(args: Args, client: SigningClient) { + info!("Claim vesting delegator reward"); + + let mix_id = match args.mix_id { + Some(mix_id) => mix_id, + None => { + let identity_key = args + .identity_key + .expect("either mix_id or mix_identity has to be specified"); + let node_details = client + .get_mixnode_details_by_identity(identity_key) + .await + .expect("contract query failed") + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + + let res = client + .withdraw_delegator_reward_on_behalf(client.address().clone(), mix_id, None) + .await + .expect("failed to claim vesting delegator-reward"); + + info!("Claiming vesting delegator reward: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs new file mode 100644 index 0000000000..063d4a523f --- /dev/null +++ b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs @@ -0,0 +1,43 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use mixnet_contract_common::MixId; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub mix_id: Option, + + #[clap(long)] + pub identity_key: Option, +} + +pub async fn undelegate_from_mixnode(args: Args, client: SigningClient) { + info!("removing stake from mix-node"); + + let mix_id = match args.mix_id { + Some(mix_id) => mix_id, + None => { + let identity_key = args + .identity_key + .expect("either mix_id or mix_identity has to be specified"); + let node_details = client + .get_mixnode_details_by_identity(identity_key) + .await + .expect("contract query failed") + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + + let res = client + .undelegate_from_mixnode(mix_id, None) + .await + .expect("failed to remove stake from mixnode!"); + + info!("removing stake from mixnode: {:?}", res) +} 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 new file mode 100644 index 0000000000..ba33a49037 --- /dev/null +++ b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs @@ -0,0 +1,53 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use log::info; + +use mixnet_contract_common::{Coin, MixId}; +use validator_client::nymd::traits::MixnetQueryClient; +use validator_client::nymd::VestingSigningClient; + +use crate::context::SigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub mix_id: Option, + + #[clap(long)] + pub identity_key: Option, + + #[clap(long)] + pub amount: u128, +} + +pub async fn vesting_delegate_to_mixnode(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!("Starting vesting delegation to mixnode"); + + let mix_id = match args.mix_id { + Some(mix_id) => mix_id, + None => { + let identity_key = args + .identity_key + .expect("either mix_id or mix_identity has to be specified"); + let node_details = client + .get_mixnode_details_by_identity(identity_key) + .await + .expect("contract query failed") + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + + let coin = Coin::new(args.amount, denom); + + let res = client + .vesting_delegate_to_mixnode(mix_id, coin.into(), None) + .await + .expect("failed to delegate to mixnode!"); + + info!("vesting delegating to mixnode: {:?}", res); +} 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 new file mode 100644 index 0000000000..59fb6b6c3d --- /dev/null +++ b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs @@ -0,0 +1,45 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use log::info; +use mixnet_contract_common::MixId; +use validator_client::nymd::traits::MixnetQueryClient; +use validator_client::nymd::VestingSigningClient; + +use crate::context::SigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub mix_id: Option, + + #[clap(long)] + pub identity_key: Option, +} + +pub async fn vesting_undelegate_from_mixnode(args: Args, client: SigningClient) { + info!("removing stake from vesting mix-node"); + + let mix_id = match args.mix_id { + Some(mix_id) => mix_id, + None => { + let identity_key = args + .identity_key + .expect("either mix_id or mix_identity has to be specified"); + let node_details = client + .get_mixnode_details_by_identity(identity_key) + .await + .expect("contract query failed") + .expect("mixnode with the specified identity doesnt exist"); + node_details.mix_id() + } + }; + + let res = client + .vesting_undelegate_from_mixnode(mix_id, None) + .await + .expect("failed to remove stake from vesting account on mixnode!"); + + info!("removing stake from vesting mixnode: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/mod.rs b/common/commands/src/validator/mixnet/mod.rs new file mode 100644 index 0000000000..9429ea6118 --- /dev/null +++ b/common/commands/src/validator/mixnet/mod.rs @@ -0,0 +1,25 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod delegators; +pub mod operators; +pub mod query; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct Mixnet { + #[clap(subcommand)] + pub command: MixnetCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetCommands { + /// Query the mixnet directory + Query(query::MixnetQuery), + /// Manage your delegations + Delegators(delegators::MixnetDelegators), + /// Manage a mixnode or gateway you operate + Operators(operators::MixnetOperators), +} diff --git a/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs new file mode 100644 index 0000000000..e9482dde34 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/gateway/bond_gateway.rs @@ -0,0 +1,78 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::{info, warn}; +use mixnet_contract_common::Coin; +use network_defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; +use validator_client::nymd::traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: String, + + #[clap(long)] + pub signature: String, + + #[clap(long)] + pub mix_port: Option, + + #[clap(long)] + pub clients_port: Option, + + #[clap(long)] + pub location: Option, + + #[clap(long)] + pub sphinx_key: String, + + #[clap(long)] + pub identity_key: String, + + #[clap(long)] + pub version: String, + + #[clap( + long, + help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub amount: u128, + + #[clap(short, long)] + pub force: bool, +} + +pub async fn bond_gateway(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!("Starting gateway bonding!"); + + // if we're trying to bond less than 1 token + if args.amount < 1_000_000 && !args.force { + warn!("You're trying to bond only {}{} which is less than 1 full token. Are you sure that's what you want? If so, run with `--force` or `-f` flag", args.amount, denom); + return; + } + + let gateway = mixnet_contract_common::Gateway { + host: args.host, + mix_port: args.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT), + clients_port: args.clients_port.unwrap_or(DEFAULT_CLIENT_LISTENING_PORT), + location: args + .location + .unwrap_or_else(|| "secret gateway location".to_owned()), + sphinx_key: args.sphinx_key, + identity_key: args.identity_key, + version: args.version, + }; + + let coin = Coin::new(args.amount, denom); + + let res = client + .bond_gateway(gateway, args.signature, coin.into(), None) + .await + .expect("failed to bond gateway!"); + + info!("Bonding result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/gateway/mod.rs b/common/commands/src/validator/mixnet/operators/gateway/mod.rs new file mode 100644 index 0000000000..af658a7876 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/gateway/mod.rs @@ -0,0 +1,28 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod bond_gateway; +pub mod unbond_gateway; +pub mod vesting_bond_gateway; +pub mod vesting_unbond_gateway; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsGateway { + #[clap(subcommand)] + pub command: MixnetOperatorsGatewayCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsGatewayCommands { + /// Bond to a gateway + Bond(bond_gateway::Args), + /// Unbound from a gateway + Unbound(unbond_gateway::Args), + /// Bond to a gateway with locked tokens + VestingBond(vesting_bond_gateway::Args), + /// Unbound from a gateway (when originally using locked tokens) + VestingUnbound(vesting_unbond_gateway::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs new file mode 100644 index 0000000000..90cae1623c --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/gateway/unbond_gateway.rs @@ -0,0 +1,21 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use validator_client::nymd::traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn unbond_gateway(client: SigningClient) { + info!("Starting gateway unbonding!"); + + let res = client + .unbond_gateway(None) + .await + .expect("failed to unbond gateway!"); + + info!("Unbonding result: {:?}", res) +} 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 new file mode 100644 index 0000000000..6c26b09eeb --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/gateway/vesting_bond_gateway.rs @@ -0,0 +1,76 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::{info, warn}; +use mixnet_contract_common::{Coin, Gateway}; +use network_defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; +use validator_client::nymd::VestingSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: String, + + #[clap(long)] + pub signature: String, + + #[clap(long)] + pub mix_port: Option, + + #[clap(long)] + pub clients_port: Option, + + #[clap(long)] + pub location: Option, + + #[clap(long)] + pub sphinx_key: String, + + #[clap(long)] + pub identity_key: String, + + #[clap(long)] + pub version: String, + + #[clap( + long, + help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub amount: u128, + + #[clap(short, long)] + pub force: bool, +} + +pub async fn vesting_bond_gateway(client: SigningClient, args: Args, denom: &str) { + info!("Starting vesting gateway bonding!"); + + // if we're trying to bond less than 1 token + if args.amount < 1_000_000 && !args.force { + warn!("You're trying to bond only {}{} which is less than 1 full token. Are you sure that's what you want? If so, run with `--force` or `-f` flag", args.amount, denom); + return; + } + + let gateway = Gateway { + host: args.host, + mix_port: args.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT), + clients_port: args.clients_port.unwrap_or(DEFAULT_CLIENT_LISTENING_PORT), + location: args + .location + .unwrap_or_else(|| "secret gateway location".to_owned()), + sphinx_key: args.sphinx_key, + identity_key: args.identity_key, + version: args.version, + }; + + let coin = Coin::new(args.amount, denom); + + let res = client + .vesting_bond_gateway(gateway, &args.signature, coin.into(), None) + .await + .expect("failed to bond gateway!"); + + info!("Vesting bonding gateway result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs b/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs new file mode 100644 index 0000000000..0c0eae2fe6 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/gateway/vesting_unbond_gateway.rs @@ -0,0 +1,21 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use validator_client::nymd::traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn vesting_unbond_gateway(client: SigningClient) { + info!("Starting vesting gateway unbonding!"); + + let res = client + .unbond_gateway(None) + .await + .expect("failed to unbond vesting gateway!"); + + info!("Unbonding vesting result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs new file mode 100644 index 0000000000..4d672a5322 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs @@ -0,0 +1,104 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use cosmwasm_std::Uint128; +use log::{info, warn}; + +use mixnet_contract_common::{Coin, MixNodeCostParams, Percent}; +use network_defaults::{ + DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, +}; +use validator_client::nymd::traits::MixnetSigningClient; +use validator_client::nymd::CosmWasmCoin; + +use crate::context::SigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: String, + + #[clap(long)] + pub signature: String, + + #[clap(long)] + pub mix_port: Option, + + #[clap(long)] + pub verloc_port: Option, + + #[clap(long)] + pub http_api_port: Option, + + #[clap(long)] + pub sphinx_key: String, + + #[clap(long)] + pub identity_key: String, + + #[clap(long)] + pub version: String, + + #[clap(long)] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, + + #[clap( + long, + help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub amount: u128, + + #[clap(short, long)] + pub force: bool, +} + +pub async fn bond_mixnode(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!("Starting mixnode bonding!"); + + // if we're trying to bond less than 1 token + if args.amount < 1_000_000 && !args.force { + warn!("You're trying to bond only {}{} which is less than 1 full token. Are you sure that's what you want? If so, run with `--force` or `-f` flag", args.amount, denom); + return; + } + + let mixnode = mixnet_contract_common::MixNode { + host: args.host, + mix_port: args.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT), + verloc_port: args.verloc_port.unwrap_or(DEFAULT_VERLOC_LISTENING_PORT), + http_api_port: args + .http_api_port + .unwrap_or(DEFAULT_HTTP_API_LISTENING_PORT), + sphinx_key: args.sphinx_key, + identity_key: args.identity_key, + version: args.version, + }; + + let coin = Coin::new(args.amount, denom); + + let cost_params = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent.unwrap_or(10) as u64, + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + }, + }; + + let res = client + .bond_mixnode(mixnode, cost_params, args.signature, coin.into(), None) + .await + .expect("failed to bond mixnode!"); + + info!("Bonding result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/keys/decode_mixnode_key.rs b/common/commands/src/validator/mixnet/operators/mixnode/keys/decode_mixnode_key.rs new file mode 100644 index 0000000000..5938a672c4 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/keys/decode_mixnode_key.rs @@ -0,0 +1,17 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(short, long)] + pub key: String, +} + +pub fn decode_mixnode_key(args: Args) { + let b64_decoded = base64::decode(args.key).expect("failed to decode base64 string"); + let b58_encoded = bs58::encode(&b64_decoded).into_string(); + + println!("{}", b58_encoded) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/keys/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/keys/mod.rs new file mode 100644 index 0000000000..3f2b566a12 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/keys/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod decode_mixnode_key; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsMixnodeKeys { + #[clap(subcommand)] + pub command: MixnetOperatorsMixnodeKeysCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsMixnodeKeysCommands { + /// Decode a mixnode key + DecodeMixnodeKey(decode_mixnode_key::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs new file mode 100644 index 0000000000..e608c4c36f --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs @@ -0,0 +1,37 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod bond_mixnode; +pub mod keys; +pub mod rewards; +pub mod settings; +pub mod unbond_mixnode; +pub mod vesting_bond_mixnode; +pub mod vesting_unbond_mixnode; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsMixnode { + #[clap(subcommand)] + pub command: MixnetOperatorsMixnodeCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsMixnodeCommands { + /// Operations for mixnode keys + Keys(keys::MixnetOperatorsMixnodeKeys), + /// Manage your mixnode operator rewards + Rewards(rewards::MixnetOperatorsMixnodeRewards), + /// Manage your mixnode settings stored in the directory + Settings(settings::MixnetOperatorsMixnodeSettings), + /// Bond to a mixnode + Bond(bond_mixnode::Args), + /// Unbound from a mixnode + Unbound(unbond_mixnode::Args), + /// Bond to a mixnode with locked tokens + BondVesting(vesting_bond_mixnode::Args), + /// Unbound from a mixnode (when originally using locked tokens) + UnboundVesting(vesting_unbond_mixnode::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs b/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs new file mode 100644 index 0000000000..773f4cb3d7 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/rewards/claim_operator_reward.rs @@ -0,0 +1,21 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use validator_client::nymd::traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn claim_operator_reward(_args: Args, client: SigningClient) { + info!("Claim operator reward"); + + let res = client + .withdraw_operator_reward(None) + .await + .expect("failed to claim operator reward"); + + info!("Claiming operator reward: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/rewards/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/rewards/mod.rs new file mode 100644 index 0000000000..d3e4fc988c --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/rewards/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod claim_operator_reward; +pub mod vesting_claim_operator_reward; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsMixnodeRewards { + #[clap(subcommand)] + pub command: MixnetOperatorsMixnodeRewardsCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsMixnodeRewardsCommands { + /// Claim rewards + Claim(claim_operator_reward::Args), + /// Claim rewards for a mixnode bonded with locked tokens + VestingClaim(vesting_claim_operator_reward::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs b/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs new file mode 100644 index 0000000000..d4895e83f2 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/rewards/vesting_claim_operator_reward.rs @@ -0,0 +1,24 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use validator_client::nymd::traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub gas: Option, +} + +pub async fn vesting_claim_operator_reward(client: SigningClient) { + info!("Claim vesting operator reward"); + + let res = client + .withdraw_operator_reward_on_behalf(client.address().clone(), None) + .await + .expect("failed to claim vesting operator reward"); + + info!("Claiming vesting operator reward: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/mod.rs new file mode 100644 index 0000000000..9d0c018cf9 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/mod.rs @@ -0,0 +1,26 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod update_config; +pub mod vesting_update_config; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsMixnodeSettings { + #[clap(subcommand)] + pub command: MixnetOperatorsMixnodeSettingsCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsMixnodeSettingsCommands { + /// Update mixnode configuration + UpdateConfig(update_config::Args), + /// Update mixnode configuration for a mixnode bonded with locked tokens + VestingUpdateConfig(vesting_update_config::Args), + /// Update mixnode cost parameters + UpdateCostParameters, + /// Update mixnode cost parameters for a mixnode bonded with locked tokens + VestingUpdateCostParameters, +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs new file mode 100644 index 0000000000..84cf9f2f4e --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_config.rs @@ -0,0 +1,68 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use mixnet_contract_common::MixNodeConfigUpdate; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: Option, + + #[clap(long)] + pub mix_port: Option, + + #[clap(long)] + pub verloc_port: Option, + + #[clap(long)] + pub http_api_port: Option, + + #[clap(long)] + pub version: Option, +} + +pub async fn update_config(args: Args, client: SigningClient) { + info!("Update mix node config!"); + + let current_details = match client + .get_owned_mixnode(client.address()) + .await + .expect("failed to query the chain for mixnode details") + .mixnode_details + { + Some(details) => details, + None => { + log::warn!("this operator does not own a mixnode to update"); + return; + } + }; + + let update = MixNodeConfigUpdate { + host: args + .host + .unwrap_or(current_details.bond_information.mix_node.host), + mix_port: args + .mix_port + .unwrap_or(current_details.bond_information.mix_node.mix_port), + verloc_port: args + .verloc_port + .unwrap_or(current_details.bond_information.mix_node.verloc_port), + http_api_port: args + .http_api_port + .unwrap_or(current_details.bond_information.mix_node.http_api_port), + version: args + .version + .unwrap_or(current_details.bond_information.mix_node.version), + }; + + let res = client + .update_mixnode_config(update, None) + .await + .expect("updating mix-node config"); + + info!("mixnode config updated: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs new file mode 100644 index 0000000000..bca3642c97 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/vesting_update_config.rs @@ -0,0 +1,69 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use mixnet_contract_common::MixNodeConfigUpdate; +use validator_client::nymd::traits::MixnetQueryClient; +use validator_client::nymd::VestingSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: Option, + + #[clap(long)] + pub mix_port: Option, + + #[clap(long)] + pub verloc_port: Option, + + #[clap(long)] + pub http_api_port: Option, + + #[clap(long)] + pub version: Option, +} + +pub async fn vesting_update_config(client: SigningClient, args: Args) { + info!("Update vesting mix node config!"); + + let current_details = match client + .get_owned_mixnode(client.address()) + .await + .expect("failed to query the chain for mixnode details") + .mixnode_details + { + Some(details) => details, + None => { + log::warn!("this operator does not own a mixnode to update"); + return; + } + }; + + let update = MixNodeConfigUpdate { + host: args + .host + .unwrap_or(current_details.bond_information.mix_node.host), + mix_port: args + .mix_port + .unwrap_or(current_details.bond_information.mix_node.mix_port), + verloc_port: args + .verloc_port + .unwrap_or(current_details.bond_information.mix_node.verloc_port), + http_api_port: args + .http_api_port + .unwrap_or(current_details.bond_information.mix_node.http_api_port), + version: args + .version + .unwrap_or(current_details.bond_information.mix_node.version), + }; + + let res = client + .vesting_update_mixnode_config(update, None) + .await + .expect("updating vesting mix-node config"); + + info!("mixnode config updated: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs new file mode 100644 index 0000000000..04fa9a55cd --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/unbond_mixnode.rs @@ -0,0 +1,22 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use log::info; +use validator_client::nymd::traits::MixnetSigningClient; + +use crate::context::SigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn unbond_mixnode(_args: Args, client: SigningClient) { + info!("Starting mixnode unbonding!"); + + let res = client + .unbond_mixnode(None) + .await + .expect("failed to unbond mixnode!"); + + info!("Unbonding result: {:?}", res) +} 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 new file mode 100644 index 0000000000..6f20996460 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs @@ -0,0 +1,103 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use cosmwasm_std::Uint128; +use log::{info, warn}; +use mixnet_contract_common::{Coin, MixNodeCostParams}; +use mixnet_contract_common::{MixNode, Percent}; +use network_defaults::{ + DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, +}; +use validator_client::nymd::{CosmWasmCoin, VestingSigningClient}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: String, + + #[clap(long)] + pub signature: String, + + #[clap(long)] + pub mix_port: Option, + + #[clap(long)] + pub verloc_port: Option, + + #[clap(long)] + pub http_api_port: Option, + + #[clap(long)] + pub sphinx_key: String, + + #[clap(long)] + pub identity_key: String, + + #[clap(long)] + pub version: String, + + #[clap(long)] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, + + #[clap( + long, + help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub amount: u128, + + #[clap(long)] + pub gas: Option, + + #[clap(short, long)] + pub force: bool, +} + +pub async fn vesting_bond_mixnode(client: SigningClient, args: Args, denom: &str) { + info!("Starting vesting mixnode bonding!"); + + // if we're trying to bond less than 1 token + if args.amount < 1_000_000 && !args.force { + warn!("You're trying to bond only {}{} which is less than 1 full token. Are you sure that's what you want? If so, run with `--force` or `-f` flag", args.amount, denom); + return; + } + + let mixnode = MixNode { + host: args.host, + mix_port: args.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT), + verloc_port: args.verloc_port.unwrap_or(DEFAULT_VERLOC_LISTENING_PORT), + http_api_port: args + .http_api_port + .unwrap_or(DEFAULT_HTTP_API_LISTENING_PORT), + sphinx_key: args.sphinx_key, + identity_key: args.identity_key, + version: args.version, + }; + + let coin = Coin::new(args.amount, denom); + + let cost_params = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent.unwrap_or(10) as u64, + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + }, + }; + + let res = client + .vesting_bond_mixnode(mixnode, cost_params, &args.signature, coin.into(), None) + .await + .expect("failed to bond vesting mixnode!"); + + info!("Bonding vesting result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_unbond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_unbond_mixnode.rs new file mode 100644 index 0000000000..0824cc9342 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_unbond_mixnode.rs @@ -0,0 +1,24 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use validator_client::nymd::VestingSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub gas: Option, +} + +pub async fn vesting_unbond_mixnode(client: SigningClient) { + info!("Starting vesting mixnode unbonding!"); + + let res = client + .vesting_unbond_mixnode(None) + .await + .expect("failed to unbond vesting mixnode!"); + + info!("Unbonding vesting result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mod.rs b/common/commands/src/validator/mixnet/operators/mod.rs new file mode 100644 index 0000000000..a38479ba65 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod gateway; +pub mod mixnode; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperators { + #[clap(subcommand)] + pub command: MixnetOperatorsCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsCommands { + /// Manage your mixnode + Mixnode(mixnode::MixnetOperatorsMixnode), + /// Manage your gateway + Gateway(gateway::MixnetOperatorsGateway), +} diff --git a/common/commands/src/validator/mixnet/query/mod.rs b/common/commands/src/validator/mixnet/query/mod.rs new file mode 100644 index 0000000000..6d86e6c7b9 --- /dev/null +++ b/common/commands/src/validator/mixnet/query/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod query_all_gateways; +pub mod query_all_mixnodes; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetQuery { + #[clap(subcommand)] + pub command: MixnetQueryCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetQueryCommands { + /// Query mixnodes + Mixnodes(query_all_mixnodes::Args), + /// Query gateways + Gateways(query_all_gateways::Args), +} diff --git a/common/commands/src/validator/mixnet/query/query_all_gateways.rs b/common/commands/src/validator/mixnet/query/query_all_gateways.rs new file mode 100644 index 0000000000..679ac08bcd --- /dev/null +++ b/common/commands/src/validator/mixnet/query/query_all_gateways.rs @@ -0,0 +1,52 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use comfy_table::Table; + +use crate::context::QueryClientWithValidatorAPI; +use crate::utils::{pretty_cosmwasm_coin, show_error}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap(help = "Optionally, the gateway to display")] + pub identity_key: Option, +} + +pub async fn query(args: Args, client: &QueryClientWithValidatorAPI) { + match client.validator_api.get_gateways().await { + Ok(res) => match args.identity_key { + Some(identity_key) => { + let node = res.iter().find(|node| { + node.gateway + .identity_key + .to_string() + .eq_ignore_ascii_case(&identity_key) + }); + println!( + "{}", + ::serde_json::to_string_pretty(&node).expect("json formatting error") + ); + } + None => { + let mut table = Table::new(); + + table.set_header(vec!["Identity Key", "Owner", "Host", "Bond", "Version"]); + for node in res { + table.add_row(vec![ + node.gateway.identity_key.to_string(), + node.owner.to_string(), + node.gateway.host.to_string(), + pretty_cosmwasm_coin(&node.pledge_amount), + node.gateway.version, + ]); + } + + println!("The gateways in the directory are:"); + println!("{table}"); + } + }, + Err(e) => show_error(e), + } +} diff --git a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs new file mode 100644 index 0000000000..4ec6b25b7b --- /dev/null +++ b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs @@ -0,0 +1,64 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use comfy_table::Table; + +use crate::context::QueryClientWithValidatorAPI; +use crate::utils::{pretty_decimal_with_denom, show_error}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap(help = "Optionally, the mixnode to display")] + pub identity_key: Option, +} + +pub async fn query(args: Args, client: &QueryClientWithValidatorAPI) { + match client.validator_api.get_mixnodes().await { + Ok(res) => match args.identity_key { + Some(identity_key) => { + let node = res.iter().find(|node| { + node.bond_information + .mix_node + .identity_key + .to_string() + .eq_ignore_ascii_case(&identity_key) + }); + println!( + "{}", + ::serde_json::to_string_pretty(&node).expect("json formatting error") + ); + } + None => { + let mut table = Table::new(); + + table.set_header(vec![ + "Mix id", + "Identity Key", + "Owner", + "Host", + "Bond", + "Total Delegations", + "Version", + ]); + for node in res { + let denom = &node.bond_information.original_pledge().denom; + table.add_row(vec![ + node.mix_id().to_string(), + node.bond_information.mix_node.identity_key.clone(), + node.bond_information.owner.clone().into_string(), + node.bond_information.mix_node.host.clone(), + pretty_decimal_with_denom(node.rewarding_details.operator, denom), + pretty_decimal_with_denom(node.rewarding_details.delegates, denom), + node.bond_information.mix_node.version, + ]); + } + + println!("The mixnodes in the directory are:"); + println!("{table}"); + } + }, + Err(e) => show_error(e), + } +} diff --git a/common/commands/src/validator/mod.rs b/common/commands/src/validator/mod.rs new file mode 100644 index 0000000000..ebc295569f --- /dev/null +++ b/common/commands/src/validator/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod account; +pub mod block; +pub mod cosmwasm; +pub mod mixnet; +pub mod signature; +pub mod transactions; +pub mod vesting; diff --git a/common/commands/src/validator/signature/errors.rs b/common/commands/src/validator/signature/errors.rs new file mode 100644 index 0000000000..2369e06d66 --- /dev/null +++ b/common/commands/src/validator/signature/errors.rs @@ -0,0 +1,16 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +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), + + #[error("{0}")] + CosmrsError(#[from] cosmrs::ErrorReport), +} diff --git a/common/commands/src/validator/signature/helpers.rs b/common/commands/src/validator/signature/helpers.rs new file mode 100644 index 0000000000..1c2d05c9e1 --- /dev/null +++ b/common/commands/src/validator/signature/helpers.rs @@ -0,0 +1,114 @@ +use std::str::FromStr; + +use cosmrs::crypto::secp256k1::{Signature, VerifyingKey}; +use cosmrs::crypto::PublicKey; +use k256::ecdsa::signature::Verifier; + +use crate::validator::signature::errors::Errors; + +pub fn secp256k1_verify_with_public_key( + public_key_as_bytes: &[u8], + signature_as_hex: String, + message: String, +) -> Result<(), k256::ecdsa::signature::Error> { + let verifying_key = VerifyingKey::from_sec1_bytes(public_key_as_bytes)?; + let signature = Signature::from_str(&signature_as_hex)?; + let message_as_bytes = message.into_bytes(); + verifying_key.verify(&message_as_bytes, &signature) +} + +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)?; + 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)] +mod test_secp256k1 { + use crate::validator::signature::helpers::{ + secp256k1_verify_with_public_key, secp256k1_verify_with_public_key_json, + }; + use cosmrs::crypto::PublicKey; + + #[test] + fn test_verify_with_json_public_key_with_valid_signature() { + let json_public_key = r#"{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A4FdhUMasPmNhRZjtpKlmjNbq7EEUgPxfdI+E3vSajvc"}"#; + let signature_as_hex = "E3AA5AC0DA1B7DEBB7808000F719D8ACB9A0BE10AFA2756A788516268EB246A1257EC1097C5E364EF916145B01641DEDFE955994CB340BDAFA99A65BCA3F6F28".to_string(); + let message = "test 1234".to_string(); + + let public_key = PublicKey::from_json(json_public_key).unwrap(); + let public_key_bytes = public_key.to_bytes(); + + let result = secp256k1_verify_with_public_key(&public_key_bytes, signature_as_hex, message); + + assert!(result.is_ok()); + } + + #[test] + fn test_verify_with_json_public_key_with_invalid_signature() { + let json_public_key = r#"{"@type":"/cosmos.crypto.secp256k1.PubKey","key":"A4FdhUMasPmNhRZjtpKlmjNbq7EEUgPxfdI+E3vSajvc"}"#; + let signature_as_hex = "E3AA5AC0DA1B7DEBB7808000F719D8ACB9A0BE10AFA2756A788516268EB246A1257EC1097C5E364EF916145B01641DEDFE955994CB340BDAFA99A65BCA3F6F28".to_string(); + let message = "abcdef".to_string(); + + let public_key = PublicKey::from_json(json_public_key).unwrap(); + let public_key_bytes = public_key.to_bytes(); + + let result = secp256k1_verify_with_public_key(&public_key_bytes, signature_as_hex, message); + + assert!(result.is_err()); + } + + #[test] + fn test_valid_json_public_key_succeeds() { + 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, + account_id, + account_prefix, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_json_public_key_fails_with_error() { + 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, + account_id, + account_prefix, + ); + + assert!(result.is_err()); + } +} diff --git a/common/commands/src/validator/signature/mod.rs b/common/commands/src/validator/signature/mod.rs new file mode 100644 index 0000000000..3782e977f4 --- /dev/null +++ b/common/commands/src/validator/signature/mod.rs @@ -0,0 +1,24 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod errors; +pub mod helpers; +pub mod sign; +pub mod verify; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct Signature { + #[clap(subcommand)] + pub command: Option, +} + +#[derive(Debug, Subcommand)] +pub enum SignatureCommands { + /// Sign a message + Sign(crate::validator::signature::sign::Args), + /// Verify a message + Verify(crate::validator::signature::verify::Args), +} diff --git a/common/commands/src/validator/signature/sign.rs b/common/commands/src/validator/signature/sign.rs new file mode 100644 index 0000000000..d26302aeba --- /dev/null +++ b/common/commands/src/validator/signature/sign.rs @@ -0,0 +1,68 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::utils::show_error; +use clap::Parser; +use cosmrs::crypto::PublicKey; +use log::error; +use serde::Serialize; +use serde_json::json; +use validator_client::nymd::wallet::DirectSecp256k1HdWallet; + +#[derive(Debug, Serialize)] +pub struct SignatureOutputJson { + pub account_id: String, + pub public_key: PublicKey, + pub signature: String, +} + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap(help = "The message to sign")] + pub message: String, +} + +pub fn sign(args: Args, prefix: &str, mnemonic: Option) { + if args.message.trim().is_empty() { + error!("Message is empty or contains only whitespace"); + return; + } + + if mnemonic.is_none() { + error!( + "Please provide the mnemonic as an argument or using the MNEMONIC environment variable" + ); + return; + } + + match DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.expect("mnemonic not set")) { + Ok(wallet) => match wallet.try_derive_accounts() { + Ok(accounts) => match accounts.first() { + Some(account) => { + let msg = args.message.into_bytes(); + match wallet.sign_raw_with_account(account, &msg) { + Ok(signature) => { + let output = SignatureOutputJson { + account_id: account.address().to_string(), + public_key: account.public_key(), + signature: signature.to_string(), + }; + println!("{}", json!(output)); + } + Err(e) => { + error!("Failed to sign message. {}", e); + } + } + } + None => { + error!("Could not derive an account key from the mnemonic",) + } + }, + Err(e) => { + error!("Failed to derive accounts. {}", e); + } + }, + Err(e) => show_error(e), + } +} diff --git a/common/commands/src/validator/signature/verify.rs b/common/commands/src/validator/signature/verify.rs new file mode 100644 index 0000000000..b29f93ca9a --- /dev/null +++ b/common/commands/src/validator/signature/verify.rs @@ -0,0 +1,89 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use clap::Parser; +use cosmrs::crypto::PublicKey; +use log::{error, info}; +use serde_json::json; + +use validator_client::nymd::AccountId; + +use crate::context::QueryClient; +use crate::validator::signature::helpers::secp256k1_verify_with_public_key; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap( + help = "The public key of the account, or the account id to query for a public key (NOTE: the account must have signed a message stored on the chain for the public key record to exist)" + )] + pub public_key_or_address: String, + + #[clap(value_parser)] + #[clap(help = "The signature to verify as hex")] + pub signature_as_hex: String, + + #[clap(value_parser)] + #[clap(help = "The message to verify as a string")] + pub message: String, +} + +pub async fn verify(args: Args, client: &QueryClient) { + if args.public_key_or_address.trim().is_empty() { + error!("Please ensure the public key or address is not empty or whitespace"); + return; + } + + let public_key = match AccountId::from_str(&args.public_key_or_address) { + Ok(address) => { + info!("Found account address instead of public key, so looking up public key for {} from chain", address); + match client.get_account_public_key(&address).await.ok() { + Some(public_key) => { + if let Some(k) = public_key { + info!("Found public key {}", json!(k)); + } + public_key + } + None => { + error!( + "Address {} does not have a public key recorded on the chain. This is probably because the account has never signed a transaction.", + address + ); + None + } + } + } + Err(_) => match PublicKey::from_json(&args.public_key_or_address) { + Ok(parsed) => Some(parsed), + Err(e) => { + error!("Public key should be JSON. Unable to parse: {}", e); + None + } + }, + }; + + match public_key { + Some(public_key) => { + if public_key.type_url() != PublicKey::SECP256K1_TYPE_URL { + error!("Sorry, we only support secp256k1 public keys at the moment"); + return; + } + + match secp256k1_verify_with_public_key( + &public_key.to_bytes(), + args.signature_as_hex, + args.message, + ) { + Ok(()) => println!("SUCCESS ✅ signature verified"), + Err(e) => { + error!("FAILURE ❌ Signature verification failed: {}", e); + } + } + } + None => { + error!("Unable to verify, as unable to get the public key"); + } + } +} diff --git a/common/commands/src/validator/transactions/get_transaction.rs b/common/commands/src/validator/transactions/get_transaction.rs new file mode 100644 index 0000000000..3d1b53e7b2 --- /dev/null +++ b/common/commands/src/validator/transactions/get_transaction.rs @@ -0,0 +1,28 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use std::str::FromStr; + +use crate::context::QueryClient; +use crate::utils::show_error; +use cosmrs::tx::Hash; +use serde_json::json; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap(help = "The transaction hash")] + pub tx_hash: String, +} + +pub async fn get(args: Args, client: &QueryClient) { + let hash = Hash::from_str(&args.tx_hash).expect("could not parse transaction hash"); + + match client.get_tx(hash).await { + Ok(res) => { + println!("{}", json!(res)) + } + Err(e) => show_error(e), + } +} diff --git a/common/commands/src/validator/transactions/mod.rs b/common/commands/src/validator/transactions/mod.rs new file mode 100644 index 0000000000..d78cdf07b0 --- /dev/null +++ b/common/commands/src/validator/transactions/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod get_transaction; +pub mod query_transactions; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct Transactions { + #[clap(subcommand)] + pub command: Option, +} + +#[derive(Debug, Subcommand)] +pub enum TransactionsCommands { + /// Get a transaction by hash or block height + Get(crate::validator::transactions::get_transaction::Args), + /// Query for transactions + Query(crate::validator::transactions::query_transactions::Args), +} diff --git a/common/commands/src/validator/transactions/query_transactions.rs b/common/commands/src/validator/transactions/query_transactions.rs new file mode 100644 index 0000000000..b8b341ec57 --- /dev/null +++ b/common/commands/src/validator/transactions/query_transactions.rs @@ -0,0 +1,30 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use clap::Parser; +use cosmrs::rpc::query::Query; +use serde_json::json; + +use crate::context::QueryClient; +use crate::utils::show_error; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap(help = "The query to execute")] + pub query: String, +} + +pub async fn query(args: Args, client: &QueryClient) { + match Query::from_str(&args.query) { + Ok(query) => match client.search_tx(query).await { + Ok(res) => { + println!("{}", json!(res)) + } + Err(e) => show_error(e), + }, + Err(e) => show_error(e), + } +} diff --git a/common/commands/src/validator/vesting/balance.rs b/common/commands/src/validator/vesting/balance.rs new file mode 100644 index 0000000000..45b2f7866d --- /dev/null +++ b/common/commands/src/validator/vesting/balance.rs @@ -0,0 +1,55 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use cosmrs::AccountId; +use log::info; + +use validator_client::nymd::{Coin, VestingQueryClient}; + +use crate::context::SigningClient; +use crate::utils::show_error; +use crate::utils::{pretty_coin, pretty_cosmwasm_coin}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap(help = "Optionally, the account address to get the balance for")] + pub address: Option, +} + +pub async fn balance(args: Args, client: SigningClient) { + let account_id = args.address.unwrap_or_else(|| client.address().clone()); + let vesting_address = account_id.to_string(); + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!( + "Getting vesting schedule information for {}...", + &vesting_address + ); + + let original_vesting = client.original_vesting(&vesting_address).await; + + match original_vesting { + Ok(res) => { + let spendable_coins = client + .spendable_coins(&vesting_address, None) + .await + .unwrap_or_else(|_| Coin::new(0u128, denom)); + let liquid_account_balance = client + .get_balance(&account_id, denom.to_string()) + .await + .unwrap_or(None) + .unwrap_or_else(|| Coin::new(0u128, denom)); + + println!( + "Account {} has\n{} vested with\n{} available to be withdrawn to the main account (balance {})", + &account_id, + pretty_cosmwasm_coin(&res.amount), + pretty_coin(&spendable_coins), + pretty_coin(&liquid_account_balance), + ); + } + Err(e) => show_error(e), + } +} diff --git a/common/commands/src/validator/vesting/create_vesting_schedule.rs b/common/commands/src/validator/vesting/create_vesting_schedule.rs new file mode 100644 index 0000000000..c7c133673d --- /dev/null +++ b/common/commands/src/validator/vesting/create_vesting_schedule.rs @@ -0,0 +1,91 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::str::FromStr; + +use clap::Parser; +use log::info; + +use mixnet_contract_common::Coin; +use network_defaults::NymNetworkDetails; +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; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub periods_seconds: Option, + + #[clap(long)] + pub number_of_periods: Option, + + #[clap(long)] + pub start_time: Option, + + #[clap(long)] + pub address: String, + + #[clap(long)] + pub amount: u64, + + #[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) { + info!("Creating vesting schedule!"); + + let vesting = VestingSpecification::new( + args.start_time, + args.periods_seconds, + args.number_of_periods, + ); + + let denom = network_details.chain_details.mix_denom.base.to_string(); + + let coin = Coin::new(args.amount.into(), &denom); + + let res = client + .create_periodic_vesting_account( + &args.address, + args.staking_address, + Some(vesting), + coin.into(), + args.pledge_cap, + None, + ) + .await + .expect("creating vesting schedule for the user!"); + + //send 1 coin + let coin_amount: u64 = 1_000_000; + + let coin = CosmosCoin { + denom: Denom::from_str(&denom).unwrap(), + amount: coin_amount.into(), + }; + + let send_coin_response = client + .send( + &AccountId::from_str(&args.address).unwrap(), + vec![coin.into()], + "payment made :)", + None, + ) + .await + .unwrap(); + + info!("Vesting result: {:?}", res); + info!("Coin send result: {:?}", send_coin_response); +} diff --git a/common/commands/src/validator/vesting/mod.rs b/common/commands/src/validator/vesting/mod.rs new file mode 100644 index 0000000000..807ba08438 --- /dev/null +++ b/common/commands/src/validator/vesting/mod.rs @@ -0,0 +1,28 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod balance; +pub mod create_vesting_schedule; +pub mod query_vesting_schedule; +pub mod withdraw_vested; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct VestingSchedule { + #[clap(subcommand)] + pub command: Option, +} + +#[derive(Debug, Subcommand)] +pub enum VestingScheduleCommands { + /// Creates a vesting schedule + Create(crate::validator::vesting::create_vesting_schedule::Args), + /// Query for vesting schedule + Query(crate::validator::vesting::query_vesting_schedule::Args), + /// Get the amount that has vested and is free for withdrawal, delegation or bonding + VestedBalance(crate::validator::vesting::balance::Args), + /// Withdraw vested tokens (note: the available amount excludes anything delegated or bonded before or after vesting) + WithdrawVested(crate::validator::vesting::withdraw_vested::Args), +} diff --git a/common/commands/src/validator/vesting/query_vesting_schedule.rs b/common/commands/src/validator/vesting/query_vesting_schedule.rs new file mode 100644 index 0000000000..23118c1bf5 --- /dev/null +++ b/common/commands/src/validator/vesting/query_vesting_schedule.rs @@ -0,0 +1,149 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use cosmrs::AccountId; +use cosmwasm_std::Coin as CosmWasmCoin; +use log::info; + +use validator_client::nymd::{Coin, VestingQueryClient}; + +use crate::context::SigningClient; +use crate::utils::show_error; +use crate::utils::{pretty_coin, pretty_cosmwasm_coin}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap(help = "Optionally, the account address to get the balance for")] + pub address: Option, +} + +pub async fn query(args: Args, client: SigningClient) { + let account_id = args.address.unwrap_or_else(|| client.address().clone()); + let vesting_address = account_id.to_string(); + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!( + "Getting vesting schedule information for {}...", + &vesting_address + ); + + let liquid_account_balance = client + .get_balance(&account_id, denom.to_string()) + .await + .unwrap_or(None) + .unwrap_or_else(|| Coin::new(0u128, denom)); + let original_vesting = client.original_vesting(&vesting_address).await; + let start_time = client.vesting_start_time(&vesting_address).await; + let end_time = client.vesting_end_time(&vesting_address).await; + let vested_coins = client.vested_coins(&vesting_address, None).await; + let spendable_coins = client.spendable_coins(&vesting_address, None).await; + let locked_coins = client.locked_coins(&vesting_address, None).await; + + // TODO: get better copy text for what these are + let vesting_coins = client.vesting_coins(&vesting_address, None).await; + let delegated_vesting = client.delegated_vesting(&vesting_address, None).await; + let delegated_free = client.delegated_free(&vesting_address, None).await; + + original_vesting.as_ref().map_or_else(show_error, |res| { + println!( + "Amount: {} ({})", + pretty_cosmwasm_coin(&res.amount), + res.amount + ); + println!("No of periods: {}", res.number_of_periods); + println!( + "Duration each: {}", + time::Duration::seconds(res.period_duration as i64) + ); + }); + + start_time.as_ref().map_or_else(show_error, |res| { + println!( + "Start date: {}", + time::OffsetDateTime::from_unix_timestamp(res.seconds() as i64) + .expect("unable to parse vesting start timestamp") + .date() + ); + }); + + end_time.map_or_else(show_error, |res| { + println!( + "End date: {}", + time::OffsetDateTime::from_unix_timestamp(res.seconds() as i64) + .expect("unable to parse vesting end timestamp") + .date() + ); + }); + + vested_coins.map_or_else(show_error, |res| { + println!("Vested balance: {} ({})", pretty_coin(&res), res); + }); + + if let Ok(res) = original_vesting { + if let Ok(start) = start_time { + let amount_in_each_period = res.amount.amount.u128() / res.number_of_periods as u128; + let coin_in_each_period = CosmWasmCoin::new(amount_in_each_period, denom); + println!(); + println!("Vesting schedule:"); + for period in 1..(res.number_of_periods as u64 + 1) { + let date = time::OffsetDateTime::from_unix_timestamp( + (start.seconds() + period * res.period_duration) as i64, + ) + .expect("unable to parse vesting start timestamp") + .date(); + let amount_in_vested = + period as u128 * res.amount.amount.u128() / res.number_of_periods as u128; + let coin_in_vested = CosmWasmCoin::new(amount_in_vested, denom); + println!( + "{}. {} {} => {}", + period, + date, + pretty_cosmwasm_coin(&coin_in_each_period), + pretty_cosmwasm_coin(&coin_in_vested), + ); + } + } + } + + spendable_coins.map_or_else(show_error, |res| { + println!(); + println!("This account has the following vested tokens available either to be withdrawn to the main account, or to be delegated:"); + println!("Spendable coins: {} ({})", pretty_coin(&res), res); + }); + + locked_coins.map_or_else(show_error, |res| { + println!(); + if res.amount > 0 { + println!("This account has delegated more than the current cap, so the following balance is unavailable for bonding or delegation:"); + println!("Locked balance: {} ({})", pretty_coin(&res), res); + } else { + println!("This account is not capped and can use the spendable balance for bonding or delegations:"); + println!("Locked balance: {} ({})", pretty_coin(&res), res); + } + }); + + println!(); + println!("The following are shown for information (more help text will follow soon):"); + vesting_coins.map_or_else(show_error, |res| { + println!("Vesting coins: {} ({})", pretty_coin(&res), res); + }); + delegated_vesting.map_or_else(show_error, |res| { + println!("Delegated vesting: {} ({})", pretty_coin(&res), res); + }); + delegated_free.map_or_else(show_error, |res| { + println!("Delegation free: {} ({})", pretty_coin(&res), res); + }); + + println!(); + println!( + "The main account {} also has a regular balance of:", + &account_id + ); + println!( + "{} ({})", + pretty_coin(&liquid_account_balance), + &liquid_account_balance + ); +} diff --git a/common/commands/src/validator/vesting/withdraw_vested.rs b/common/commands/src/validator/vesting/withdraw_vested.rs new file mode 100644 index 0000000000..972c5290d9 --- /dev/null +++ b/common/commands/src/validator/vesting/withdraw_vested.rs @@ -0,0 +1,112 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use log::info; + +use validator_client::nymd::{Coin, VestingQueryClient, VestingSigningClient}; + +use crate::context::SigningClient; +use crate::utils::show_error; +use crate::utils::{pretty_coin, pretty_cosmwasm_coin}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(value_parser)] + #[clap(help = "Amount to transfer in micro denomination (e.g. unym or unyx)")] + pub amount: u128, +} + +pub async fn execute(args: Args, client: SigningClient) { + let account_id = client.address(); + let vesting_address = account_id.to_string(); + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!( + "Getting vesting schedule information for {}...", + &vesting_address + ); + + let original_vesting = client.original_vesting(&vesting_address).await; + + match original_vesting { + Ok(res) => { + let spendable_coins = client + .spendable_coins(&vesting_address, None) + .await + .unwrap_or_else(|_| Coin::new(0u128, denom)); + let liquid_account_balance = client + .get_balance(account_id, denom.to_string()) + .await + .unwrap_or(None) + .unwrap_or_else(|| Coin::new(0u128, denom)); + + println!( + "Account {} has\n{} vested with {} available to be withdrawn to the main account (balance {})", + &account_id, + pretty_cosmwasm_coin(&res.amount), + pretty_coin(&spendable_coins), + pretty_coin(&liquid_account_balance), + ); + println!(); + + // execute withdraw + + let amount = Coin { + amount: args.amount, + denom: denom.to_string(), + }; + + info!( + "Withdrawing {} ({}) from {}...", + pretty_coin(&amount), + &amount, + &account_id + ); + + match client.withdraw_vested_coins(amount, None).await { + Ok(res) => { + println!(); + println!("SUCCESS ✅"); + println!( + "Nodesguru: https://nym.explorers.guru/transaction/{}", + &res.transaction_hash + ); + println!( + "Mintscan: https://www.mintscan.io/nyx/txs/{}", + &res.transaction_hash + ); + println!("Transaction hash: {}", &res.transaction_hash); + println!("Gas used: {}", &res.gas_info.gas_used); + println!(); + } + Err(e) => show_error(e), + } + + // query for balances again + let res = client + .original_vesting(&vesting_address) + .await + .expect("vesting account does not exist"); + let spendable_coins = client + .spendable_coins(&vesting_address, None) + .await + .unwrap_or_else(|_| Coin::new(0u128, denom)); + + let liquid_account_balance = client + .get_balance(account_id, denom.to_string()) + .await + .unwrap_or(None) + .unwrap_or_else(|| Coin::new(0u128, denom)); + + println!( + "After withdrawal, account {} has\n{} vested with {} available to be withdrawn to the main account (balance {})", + &account_id, + pretty_cosmwasm_coin(&res.amount), + pretty_coin(&spendable_coins), + pretty_coin(&liquid_account_balance), + ); + } + Err(e) => show_error(e), + } +} 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/Cargo.toml b/common/config/Cargo.toml index da8774932b..c0db6fe9e4 100644 --- a/common/config/Cargo.toml +++ b/common/config/Cargo.toml @@ -9,7 +9,6 @@ edition = "2021" [dependencies] cfg-if = "1.0.0" handlebars = "3.0.1" -humantime-serde = "1.0" log = "0.4" serde = { version = "1.0", features = ["derive"] } toml = "0.5.6" diff --git a/common/config/src/lib.rs b/common/config/src/lib.rs index 299208eebc..1f72896d9c 100644 --- a/common/config/src/lib.rs +++ b/common/config/src/lib.rs @@ -41,6 +41,30 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned { Self::default_config_directory(id).join(Self::config_file_name()) } + // We provide a second set of functions that tries to not panic. + + fn try_default_root_directory() -> Option; + + fn try_default_config_directory(id: Option<&str>) -> Option { + if let Some(id) = id { + Self::try_default_root_directory().map(|d| d.join(id).join("config")) + } else { + Self::try_default_root_directory().map(|d| d.join("config")) + } + } + + fn try_default_data_directory(id: Option<&str>) -> Option { + if let Some(id) = id { + Self::try_default_root_directory().map(|d| d.join(id).join("data")) + } else { + Self::try_default_root_directory().map(|d| d.join("data")) + } + } + + fn try_default_config_file_path(id: Option<&str>) -> Option { + Self::try_default_config_directory(id).map(|d| d.join(Self::config_file_name())) + } + fn root_directory(&self) -> PathBuf; fn config_directory(&self) -> PathBuf; fn data_directory(&self) -> PathBuf; @@ -64,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/coconut-bandwidth-contract/src/spend_credential.rs b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/spend_credential.rs index 8aef32f1a1..28842576c6 100644 --- a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/spend_credential.rs +++ b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/spend_credential.rs @@ -37,7 +37,7 @@ impl SpendCredentialData { } } -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] pub enum SpendCredentialStatus { InProgress, Spent, diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml index a7e383db64..4f98451100 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -8,3 +8,9 @@ edition = "2021" [dependencies] cosmwasm-std = "1.0.0" +serde = { version = "1.0", features = ["derive"] } +schemars = "0.8" +thiserror = "1" + +[dev-dependencies] +serde_json = "1.0.0" diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/events.rs b/common/cosmwasm-smart-contracts/contracts-common/src/events.rs index d9b5b363ca..06cea03f22 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/events.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/events.rs @@ -28,3 +28,26 @@ pub fn may_find_attribute(event: &Event, key: &str) -> Option { } None } + +pub trait OptionallyAddAttribute { + fn add_optional_attribute( + self, + key: impl Into, + value: Option>, + ) -> Self; +} + +impl OptionallyAddAttribute for Event { + fn add_optional_attribute( + self, + key: impl Into, + value: Option>, + ) -> Self { + if let Some(value) = value { + self.add_attribute(key, value) + } else { + // TODO: perhaps if value doesn't exist, we should emit explicit 'null'? + self + } + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs index 58d367a2f5..53bf334611 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs @@ -2,3 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 pub mod events; +pub mod types; + +pub use types::*; diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs new file mode 100644 index 0000000000..a4dda07148 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -0,0 +1,198 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +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 zero() -> Self { + Self(Decimal::zero()) + } + + pub fn hundred() -> Self { + Self(Decimal::one()) + } + + 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 +#[derive(Debug, Serialize, Deserialize)] +pub struct ContractBuildInformation { + // VERGEN_BUILD_TIMESTAMP + /// Provides the build timestamp, for example `2021-02-23T20:14:46.558472672+00:00`. + pub build_timestamp: String, + + // VERGEN_BUILD_SEMVER + /// Provides the build version, for example `0.1.0-9-g46f83e1`. + pub build_version: String, + + // VERGEN_GIT_SHA + /// Provides the hash of the commit that was used for the build, for example `46f83e112520533338245862d366f6a02cef07d4`. + pub commit_sha: String, + + // VERGEN_GIT_COMMIT_TIMESTAMP + /// Provides the timestamp of the commit that was used for the build, for example `2021-02-23T08:08:02-05:00`. + pub commit_timestamp: String, + + // VERGEN_GIT_BRANCH + /// Provides the name of the git branch that was used for the build, for example `master`. + pub commit_branch: String, + + // VERGEN_RUSTC_SEMVER + /// Provides the rustc version that was used for the build, for example `1.52.0-nightly`. + pub rustc_version: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn percent_serde() { + let valid_value = Percent::from_percentage_value(80).unwrap(); + let serialized = serde_json::to_string(&valid_value).unwrap(); + + let deserialized: Percent = serde_json::from_str(&serialized).unwrap(); + assert_eq!(valid_value, deserialized); + + let invalid_values = vec!["\"42\"", "\"1.1\"", "\"1.00000001\"", "\"foomp\"", "\"1a\""]; + for invalid_value in invalid_values { + assert!(serde_json::from_str::<'_, Percent>(invalid_value).is_err()) + } + assert_eq!( + serde_json::from_str::<'_, Percent>("\"0.95\"").unwrap(), + Percent::from_percentage_value(95).unwrap() + ) + } + + #[test] + fn percent_to_absolute_integer() { + let p = serde_json::from_str::<'_, Percent>("\"0.0001\"").unwrap(); + assert_eq!(p.round_to_integer(), 0); + + let p = serde_json::from_str::<'_, Percent>("\"0.0099\"").unwrap(); + assert_eq!(p.round_to_integer(), 0); + + let p = serde_json::from_str::<'_, Percent>("\"0.0199\"").unwrap(); + assert_eq!(p.round_to_integer(), 1); + + let p = serde_json::from_str::<'_, Percent>("\"0.45123\"").unwrap(); + assert_eq!(p.round_to_integer(), 45); + + let p = serde_json::from_str::<'_, Percent>("\"0.999999999\"").unwrap(); + assert_eq!(p.round_to_integer(), 99); + + let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap(); + assert_eq!(p.round_to_integer(), 100); + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index 20fc4ecb16..1f3e72af8a 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -8,23 +8,26 @@ rust-version = "1.62" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bs58 = "0.4.0" cosmwasm-std = "1.0.0" - serde = { version = "1.0", features = ["derive"] } serde_repr = "0.1" schemars = "0.8" thiserror = "1.0" -fixed = { version = "1.1", features = ["serde"] } -az = "1.1" +contracts-common = { path = "../contracts-common" } +serde_json = "1.0.0" +humantime-serde = "1.1.1" + +# TO CHECK WHETHER STILL NEEDED: log = "0.4.14" time = { version = "0.3.6", features = ["parsing", "formatting"] } -ts-rs = "6.1.2" - -contracts-common = { path = "../contracts-common" } +ts-rs = { version = "6.1.2", optional = true } [dev-dependencies] +rand_chacha = "0.3" time = { version = "0.3.5", features = ["serde", "macros"] } [features] default = [] -generate-ts = [] +contract-testing = [] +generate-ts = ['ts-rs'] diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs new file mode 100644 index 0000000000..2c46abf8ce --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs @@ -0,0 +1,25 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::Decimal; + +// I'm still not 100% sure how to feel about existence of this file +// This is equivalent of representing our display coin with 6 decimal places. +// I'm using this one as opposed to "Decimal::one()", as this provides us with higher accuracy +// whilst providing no noticable drawbacks. +pub const UNIT_DELEGATION_BASE: Decimal = + Decimal::raw(1_000_000_000 * 1_000_000_000_000_000_000u128); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unit_delegation_didnt_change() { + // a sanity check test to make sure Decimal's `DECIMAL_FRACTIONAL` internal implementation hasn't changed + assert_eq!( + UNIT_DELEGATION_BASE, + Decimal::one() * Decimal::from_atomics(1_000_000_000u32, 0).unwrap() + ) + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs index d0d4372fa4..ce144ebad9 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs @@ -1,151 +1,133 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 // due to code generated by JsonSchema #![allow(clippy::field_reassign_with_default)] -use crate::{Addr, IdentityKey}; -use cosmwasm_std::{Coin, Uint128}; +use crate::{Addr, MixId}; +use cosmwasm_std::{Coin, Decimal}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::fmt::Display; -use std::hash::{Hash, Hasher}; -type OwnerAddressBytes = Vec; -type BlockHeight = u64; +// 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 = (MixId, OwnerProxySubKey); -pub fn generate_storage_key(address: &Addr, proxy: Option<&Addr>) -> Vec { +pub fn generate_owner_storage_subkey(address: &Addr, proxy: Option<&Addr>) -> String { if let Some(proxy) = &proxy { - address + let key_bytes = address .as_bytes() .iter() .zip(proxy.as_bytes()) .map(|(x, y)| x ^ y) - .collect() + .collect::>(); + bs58::encode(key_bytes).into_string() } else { - address.as_bytes().to_vec() + address.clone().into_string() } } #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct Delegation { + /// Address of the owner of this delegation. pub owner: Addr, - pub node_identity: IdentityKey, + + /// Id of the MixNode that this delegation was performed against. + 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. + pub cumulative_reward_ratio: Decimal, + + /// Original delegation amount. Note that it is never mutated as delegation accumulates rewards. pub amount: Coin, - pub block_height: u64, - pub proxy: Option, // proxy address used to delegate the funds on behalf of another address -} -impl Eq for Delegation {} + /// Block height where this delegation occurred. + pub height: u64, -#[allow(clippy::derive_hash_xor_eq)] -impl Hash for Delegation { - fn hash(&self, state: &mut H) { - self.owner.hash(state); - self.node_identity.hash(state); - self.block_height.hash(state); - self.proxy.hash(state); - } + /// Proxy address used to delegate the funds on behalf of another address + pub proxy: Option, } impl Delegation { pub fn new( owner: Addr, - node_identity: IdentityKey, + mix_id: MixId, + cumulative_reward_ratio: Decimal, amount: Coin, - block_height: BlockHeight, + height: u64, proxy: Option, ) -> Self { Delegation { owner, - node_identity, + mix_id, + cumulative_reward_ratio, amount, - block_height, + height, proxy, } } - pub fn storage_key(&self) -> (IdentityKey, OwnerAddressBytes, BlockHeight) { - ( - self.node_identity(), - self.proxy_storage_key(), - self.block_height(), - ) + pub fn generate_storage_key( + mix_id: MixId, + owner_address: &Addr, + proxy: Option<&Addr>, + ) -> StorageKey { + (mix_id, generate_owner_storage_subkey(owner_address, proxy)) } - pub fn event_storage_key(&self) -> (OwnerAddressBytes, BlockHeight, IdentityKey) { - ( - self.proxy_storage_key(), - self.block_height(), - self.node_identity(), - ) + // 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( + mix_id: MixId, + owner_proxy_subkey: OwnerProxySubKey, + ) -> StorageKey { + (mix_id, owner_proxy_subkey) } - pub fn proxy_storage_key(&self) -> OwnerAddressBytes { - generate_storage_key(&self.owner, self.proxy.as_ref()) + pub fn dec_amount(&self) -> Decimal { + // the unwrap here is fine as we're guaranteed our base coin amount is going to fit in a Decimal + // with 0 decimal places + Decimal::from_atomics(self.amount.amount, 0).unwrap() } - pub fn proxy(&self) -> Option<&Addr> { - self.proxy.as_ref() + pub fn proxy_storage_key(&self) -> OwnerProxySubKey { + generate_owner_storage_subkey(&self.owner, self.proxy.as_ref()) } - pub fn increment_amount(&mut self, amount: Uint128, at_height: Option) { - self.amount.amount += amount; - if let Some(at_height) = at_height { - self.block_height = at_height; - } - } - - pub fn amount(&self) -> &Coin { - &self.amount - } - - pub fn node_identity(&self) -> IdentityKey { - self.node_identity.clone() - } - - pub fn owner(&self) -> Addr { - self.owner.clone() - } - - pub fn block_height(&self) -> u64 { - self.block_height + pub fn storage_key(&self) -> StorageKey { + Self::generate_storage_key(self.mix_id, &self.owner, self.proxy.as_ref()) } } -impl Display for Delegation { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{} delegated towards {} by {} at block {}", - self.amount, self.node_identity, self.owner, self.block_height - ) - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] -pub struct PagedMixDelegationsResponse { +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct PagedMixNodeDelegationsResponse { pub delegations: Vec, - pub start_next_after: Option<(String, u64)>, + pub start_next_after: Option, } -impl PagedMixDelegationsResponse { - pub fn new(delegations: Vec, start_next_after: Option<(String, u64)>) -> Self { - PagedMixDelegationsResponse { +impl PagedMixNodeDelegationsResponse { + pub fn new(delegations: Vec, start_next_after: Option) -> Self { + PagedMixNodeDelegationsResponse { delegations, start_next_after, } } } -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct PagedDelegatorDelegationsResponse { pub delegations: Vec, - pub start_next_after: Option, + pub start_next_after: Option<(MixId, OwnerProxySubKey)>, } impl PagedDelegatorDelegationsResponse { - pub fn new(delegations: Vec, start_next_after: Option) -> Self { + pub fn new( + delegations: Vec, + start_next_after: Option<(MixId, OwnerProxySubKey)>, + ) -> Self { PagedDelegatorDelegationsResponse { delegations, start_next_after, @@ -153,20 +135,32 @@ impl PagedDelegatorDelegationsResponse { } } -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] -pub struct PagedAllDelegationsResponse { - pub delegations: Vec, - pub start_next_after: Option<(IdentityKey, Vec, u64)>, +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct MixNodeDelegationResponse { + pub delegation: Option, + pub mixnode_still_bonded: bool, } -impl PagedAllDelegationsResponse { - pub fn new( - delegations: Vec, - start_next_after: Option<(IdentityKey, Vec, u64)>, - ) -> Self { - PagedAllDelegationsResponse { - delegations, - start_next_after: start_next_after.map(|(id, addr, height)| (id, addr, height)), +impl MixNodeDelegationResponse { + pub fn new(delegation: Option, mixnode_still_bonded: bool) -> Self { + MixNodeDelegationResponse { + delegation, + mixnode_still_bonded, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct PagedAllDelegationsResponse { + pub delegations: Vec, + pub start_next_after: Option, +} + +impl PagedAllDelegationsResponse { + pub fn new(delegations: Vec, start_next_after: Option) -> Self { + PagedAllDelegationsResponse { + delegations, + start_next_after, } } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index 1cc501a3a0..e0d369f933 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -1,23 +1,136 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::MixId; +use cosmwasm_std::{Addr, Coin, Decimal}; use thiserror::Error; #[derive(Error, Debug, PartialEq)] pub enum MixnetContractError { - #[error("Overflow Error")] - OverflowError(#[from] cosmwasm_std::OverflowError), - #[error("reward_blockstamp field not set, set_reward_blockstamp must be called before attempting to issue rewards")] - BlockstampNotSet, - #[error("{source}")] - TryFromIntError { - #[from] - source: std::num::TryFromIntError, - }, - #[error("Error casting from U128")] - CastError, #[error("{source}")] StdErr { #[from] source: cosmwasm_std::StdError, }, - #[error("Division by zero at {}", line!())] - DivisionByZero, + + #[error("Attempted to subtract decimals with overflow ({minuend}.sub({subtrahend}))")] + OverflowDecimalSubtraction { + minuend: Decimal, + subtrahend: Decimal, + }, + + #[error("Attempted to subtract with overflow ({minuend}.sub({subtrahend}))")] + OverflowSubtraction { minuend: u64, subtrahend: u64 }, + + #[error("Not enough funds sent for node pledge. (received {received}, minimum {minimum})")] + InsufficientPledge { received: Coin, minimum: Coin }, + + #[error("Not enough funds sent for node delegation. (received {received}, minimum {minimum})")] + InsufficientDelegation { received: Coin, minimum: Coin }, + + #[error("Mixnode ({mix_id}) does not exist")] + MixNodeBondNotFound { mix_id: MixId }, + + #[error("{owner} does not seem to own any mixnodes")] + NoAssociatedMixNodeBond { owner: Addr }, + + #[error("{owner} does not seem to own any gateways")] + NoAssociatedGatewayBond { owner: Addr }, + + #[error("This address has already bonded a mixnode")] + AlreadyOwnsMixnode, + + #[error("This address has already bonded a gateway")] + AlreadyOwnsGateway, + + #[error("Gateway with this identity already exists. Its owner is {owner}")] + DuplicateGateway { owner: Addr }, + + #[error("Unauthorized")] + Unauthorized, + + #[error("No tokens were sent for the bonding")] + NoBondFound, + + #[error("No funds were provided for the delegation")] + EmptyDelegation, + + #[error("Wrong coin denomination. Received: {received}, expected: {expected}")] + WrongDenom { received: String, expected: String }, + + #[error("Received multiple coin types during staking")] + MultipleDenoms, + + #[error("Proxy address mismatch, expected {existing}, got {incoming}")] + ProxyMismatch { existing: String, incoming: String }, + + #[error("Failed to recover ed25519 public key from its base58 representation - {0}")] + MalformedEd25519IdentityKey(String), + + #[error("Failed to recover ed25519 signature from its base58 representation - {0}")] + MalformedEd25519Signature(String), + + #[error("Provided ed25519 signature did not verify correctly")] + InvalidEd25519Signature, + + #[error("Can't perform the specified action as the current epoch is still progress. It started at {epoch_start} and finishes at {epoch_end}, while the current block time is {current_block_time}")] + EpochInProgress { + current_block_time: u64, + epoch_start: i64, + epoch_end: i64, + }, + + #[error("Mixnode {mix_id} has already been rewarded during the current rewarding epoch ({absolute_epoch_id})")] + MixnodeAlreadyRewarded { + mix_id: MixId, + absolute_epoch_id: u32, + }, + + #[error("Mixnode {mix_id} hasn't been selected to the rewarding set in this epoch ({absolute_epoch_id})")] + MixnodeNotInRewardedSet { + mix_id: MixId, + absolute_epoch_id: u32, + }, + + #[error("Mixnode {mix_id} is currently in the process of unbonding")] + MixnodeIsUnbonding { mix_id: MixId }, + + #[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 }, + + #[error( + "Could not find any delegation information associated with mixnode {mix_id} for {address} (proxy: {proxy:?})" + )] + NoMixnodeDelegationFound { + mix_id: MixId, + address: String, + proxy: Option, + }, + + #[error("Provided message to update rewarding params did not contain any updates")] + EmptyParamsChangeMsg, + + #[error("Provided active set size is bigger than the rewarded set")] + InvalidActiveSetSize, + + #[error("Provided rewarded set size is smaller than the active set")] + InvalidRewardedSetSize, + + #[error("Provided active set size is zero")] + ZeroActiveSet, + + #[error("Provided rewarded set size is zero")] + ZeroRewardedSet, + + #[error("Received unexpected value for the active set. Got: {received}, expected: {expected}")] + UnexpectedActiveSetSize { received: u32, expected: u32 }, + + #[error("Received unexpected value for the rewarded set. Got: {received}, expected at most: {expected}")] + UnexpectedRewardedSetSize { received: u32, expected: u32 }, + + #[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 a2ed8ffe35..bba85e3ccd 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -1,32 +1,90 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::mixnode::NodeRewardResult; -use crate::{ContractStateParams, IdentityKeyRef, Interval, Layer}; -use cosmwasm_std::{Addr, Coin, Event, Uint128}; +use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; +use crate::reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate}; +use crate::rewarding::RewardDistribution; +use crate::{BlockHeight, ContractStateParams, IdentityKeyRef, Interval, Layer, MixId}; pub use contracts_common::events::*; -// FIXME: This should becoma an Enum -// event types -pub const DELEGATION_EVENT_TYPE: &str = "delegation"; -pub const PENDING_DELEGATION_EVENT_TYPE: &str = "pending_delegation"; -pub const RECONCILE_DELEGATION_EVENT_TYPE: &str = "reconcile_delegation"; -pub const UNDELEGATION_EVENT_TYPE: &str = "undelegation"; -pub const PENDING_UNDELEGATION_EVENT_TYPE: &str = "pending_undelegation"; -pub const GATEWAY_BONDING_EVENT_TYPE: &str = "gateway_bonding"; -pub const GATEWAY_UNBONDING_EVENT_TYPE: &str = "gateway_unbonding"; -pub const MIXNODE_BONDING_EVENT_TYPE: &str = "mixnode_bonding"; -pub const MIXNODE_UNBONDING_EVENT_TYPE: &str = "mixnode_unbonding"; -pub const SETTINGS_UPDATE_EVENT_TYPE: &str = "settings_update"; -pub const OPERATOR_REWARDING_EVENT_TYPE: &str = "mix_rewarding"; -pub const MIX_DELEGATORS_REWARDING_EVENT_TYPE: &str = "mix_delegators_rewarding"; -pub const CHANGE_REWARDED_SET_EVENT_TYPE: &str = "change_rewarded_set"; -pub const ADVANCE_INTERVAL_EVENT_TYPE: &str = "advance_interval"; -pub const ADVANCE_EPOCH_EVENT_TYPE: &str = "advance_epoch"; -pub const COMPOUND_DELEGATOR_REWARD_EVENT_TYPE: &str = "compound_delegator_reward"; -pub const CLAIM_DELEGATOR_REWARD_EVENT_TYPE: &str = "claim_delegator_reward"; -pub const COMPOUND_OPERATOR_REWARD_EVENT_TYPE: &str = "compound_operator_reward"; -pub const CLAIM_OPERATOR_REWARD_EVENT_TYPE: &str = "claim_operator_reward"; -pub const SNAPSHOT_MIXNODES_EVENT: &str = "snapshot_mixnodes"; +use cosmwasm_std::{Addr, Coin, Decimal, Event}; + +pub const EVENT_VERSION_PREFIX: &str = "v2_"; + +pub enum MixnetEventType { + MixnodeBonding, + GatewayBonding, + GatewayUnbonding, + PendingMixnodeUnbonding, + MixnodeUnbonding, + MixnodeConfigUpdate, + PendingMixnodeCostParamsUpdate, + MixnodeCostParamsUpdate, + MixnodeRewarding, + WithdrawDelegatorReward, + WithdrawOperatorReward, + PendingActiveSetUpdate, + ActiveSetUpdate, + PendingIntervalRewardingParamsUpdate, + IntervalRewardingParamsUpdate, + PendingDelegation, + PendingUndelegation, + Delegation, + DelegationOnUnbonding, + Undelegation, + ContractSettingsUpdate, + RewardingValidatorUpdate, + AdvanceEpoch, + ExecutePendingEpochEvents, + ExecutePendingIntervalEvents, + ReconcilePendingEvents, + PendingIntervalConfigUpdate, + IntervalConfigUpdate, +} + +impl From for String { + fn from(typ: MixnetEventType) -> Self { + typ.to_string() + } +} + +impl ToString for MixnetEventType { + fn to_string(&self) -> String { + let event_name = match self { + MixnetEventType::MixnodeBonding => "mixnode_bonding", + MixnetEventType::GatewayBonding => "gateway_bonding", + MixnetEventType::GatewayUnbonding => "gateway_unbonding", + MixnetEventType::PendingMixnodeUnbonding => "pending_mixnode_unbonding", + MixnetEventType::MixnodeConfigUpdate => "mixnode_config_update", + MixnetEventType::MixnodeUnbonding => "mixnode_unbonding", + MixnetEventType::PendingMixnodeCostParamsUpdate => "pending_mixnode_cost_params_update", + MixnetEventType::MixnodeCostParamsUpdate => "mixnode_cost_params_update", + MixnetEventType::MixnodeRewarding => "mix_rewarding", + MixnetEventType::WithdrawDelegatorReward => "withdraw_delegator_reward", + MixnetEventType::WithdrawOperatorReward => "withdraw_operator_reward", + MixnetEventType::PendingActiveSetUpdate => "pending_active_set_update", + MixnetEventType::ActiveSetUpdate => "active_set_update", + MixnetEventType::PendingIntervalRewardingParamsUpdate => { + "pending_interval_rewarding_params_update" + } + MixnetEventType::IntervalRewardingParamsUpdate => "interval_rewarding_params_update", + MixnetEventType::PendingDelegation => "pending_delegation", + MixnetEventType::PendingUndelegation => "pending_undelegation", + MixnetEventType::Delegation => "delegation", + MixnetEventType::Undelegation => "undelegation", + MixnetEventType::ContractSettingsUpdate => "settings_update", + MixnetEventType::RewardingValidatorUpdate => "rewarding_validator_address_update", + MixnetEventType::AdvanceEpoch => "advance_epoch", + MixnetEventType::ExecutePendingEpochEvents => "execute_pending_epoch_events", + MixnetEventType::ExecutePendingIntervalEvents => "execute_pending_interval_events", + MixnetEventType::ReconcilePendingEvents => "reconcile_pending_events", + MixnetEventType::PendingIntervalConfigUpdate => "pending_interval_config_update", + MixnetEventType::IntervalConfigUpdate => "interval_config_update", + MixnetEventType::DelegationOnUnbonding => "delegation_on_unbonding_node", + }; + + format!("{}{}", EVENT_VERSION_PREFIX, event_name) + } +} // attributes that are used in multiple places pub const OWNER_KEY: &str = "owner"; @@ -38,198 +96,194 @@ 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 MIX_ID_KEY: &str = "mix_id"; pub const NODE_IDENTITY_KEY: &str = "identity"; pub const ASSIGNED_LAYER_KEY: &str = "assigned_layer"; // settings change pub const OLD_MINIMUM_MIXNODE_PLEDGE_KEY: &str = "old_minimum_mixnode_pledge"; pub const OLD_MINIMUM_GATEWAY_PLEDGE_KEY: &str = "old_minimum_gateway_pledge"; -pub const OLD_MIXNODE_REWARDED_SET_SIZE_KEY: &str = "old_mixnode_rewarded_set_size"; -pub const OLD_MIXNODE_ACTIVE_SET_SIZE_KEY: &str = "old_mixnode_active_set_size"; -pub const OLD_ACTIVE_SET_WORK_FACTOR_KEY: &str = "old_active_set_work_factor"; +pub const OLD_MINIMUM_DELEGATION_KEY: &str = "old_minimum_delegation"; pub const NEW_MINIMUM_MIXNODE_PLEDGE_KEY: &str = "new_minimum_mixnode_pledge"; pub const NEW_MINIMUM_GATEWAY_PLEDGE_KEY: &str = "new_minimum_gateway_pledge"; -pub const NEW_MIXNODE_REWARDED_SET_SIZE_KEY: &str = "new_mixnode_rewarded_set_size"; -pub const NEW_MIXNODE_ACTIVE_SET_SIZE_KEY: &str = "new_mixnode_active_set_size"; +pub const NEW_MINIMUM_DELEGATION_KEY: &str = "new_minimum_delegation"; + +pub const OLD_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "old_rewarding_validator_address"; +pub const NEW_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "new_rewarding_validator_address"; + +pub const UPDATED_MIXNODE_CONFIG_KEY: &str = "updated_mixnode_config"; +pub const UPDATED_MIXNODE_COST_PARAMS_KEY: &str = "updated_mixnode_cost_params"; // rewarding -pub const INTERVAL_ID_KEY: &str = "interval_id"; -pub const TOTAL_MIXNODE_REWARD_KEY: &str = "total_node_reward"; +pub const INTERVAL_KEY: &str = "interval_details"; pub const OPERATOR_REWARD_KEY: &str = "operator_reward"; -pub const TOTAL_PLEDGE_KEY: &str = "pledge"; -pub const TOTAL_DELEGATIONS_KEY: &str = "delegated"; -pub const LAMBDA_KEY: &str = "lambda"; -pub const SIGMA_KEY: &str = "sigma"; -pub const DISTRIBUTED_DELEGATION_REWARDS_KEY: &str = "distributed_delegation_rewards"; -pub const FURTHER_DELEGATIONS_TO_REWARD_KEY: &str = "further_delegations"; +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 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_UPTIME_VALUE: &str = "zero_uptime"; +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 CHECKPOINT_MIXNODES_EVENT: &str = "checkpoint_mixnodes"; -pub const RECONCILIATION_ERROR_EVENT: &str = "reconciliation_error"; -pub fn new_checkpoint_mixnodes_event(block_height: u64) -> Event { - Event::new(CHECKPOINT_MIXNODES_EVENT) - .add_attribute(BLOCK_HEIGHT_KEY, format!("{}", block_height)) -} - -pub fn new_error_event(err: String) -> Event { - Event::new(RECONCILIATION_ERROR_EVENT).add_attribute("error", err) -} +// 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_identity: IdentityKeyRef<'_>, + mix_id: MixId, + unit_reward: Decimal, ) -> Event { - let mut event = Event::new(DELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - 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_identity) + .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: MixId, +) -> Event { + Event::new(MixnetEventType::Delegation) + .add_attribute(DELEGATOR_KEY, delegator) + .add_optional_attribute(PROXY_KEY, proxy.as_ref()) + .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } pub fn new_pending_delegation_event( delegator: &Addr, proxy: &Option, amount: &Coin, - mix_identity: IdentityKeyRef<'_>, + mix_id: MixId, ) -> Event { - let mut event = - Event::new(PENDING_DELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event - .add_attribute(AMOUNT_KEY, amount.to_string()) - .add_attribute(DELEGATION_TARGET_KEY, mix_identity) -} - -pub fn new_reconcile_delegation_event( - delegator: &Addr, - proxy: &Option, - amount: &Coin, - mix_identity: IdentityKeyRef<'_>, -) -> Event { - let mut event = - Event::new(RECONCILE_DELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event - .add_attribute(AMOUNT_KEY, amount.to_string()) - .add_attribute(DELEGATION_TARGET_KEY, mix_identity) -} - -pub fn new_compound_operator_reward_event(owner: &Addr, amount: Uint128) -> Event { - let event = Event::new(COMPOUND_OPERATOR_REWARD_EVENT_TYPE).add_attribute(OWNER_KEY, owner); - event.add_attribute(AMOUNT_KEY, amount.to_string()) -} - -pub fn new_claim_operator_reward_event(owner: &Addr, amount: Uint128) -> Event { - let event = Event::new(CLAIM_OPERATOR_REWARD_EVENT_TYPE).add_attribute(OWNER_KEY, owner); - event.add_attribute(AMOUNT_KEY, amount.to_string()) -} - -pub fn new_compound_delegator_reward_event( - delegator: &Addr, - proxy: &Option, - amount: Uint128, - mix_identity: IdentityKeyRef<'_>, -) -> Event { - let mut event = - Event::new(COMPOUND_DELEGATOR_REWARD_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event - .add_attribute(AMOUNT_KEY, amount.to_string()) - .add_attribute(DELEGATION_TARGET_KEY, mix_identity) + Event::new(MixnetEventType::PendingDelegation) .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()) } -pub fn new_claim_delegator_reward_event( +pub fn new_withdraw_operator_reward_event( + owner: &Addr, + proxy: &Option, + amount: Coin, + 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(MIX_ID_KEY, mix_id.to_string()) +} + +pub fn new_withdraw_delegator_reward_event( delegator: &Addr, proxy: &Option, - amount: Uint128, - mix_identity: IdentityKeyRef<'_>, + amount: Coin, + mix_id: MixId, ) -> Event { - let mut event = - Event::new(CLAIM_DELEGATOR_REWARD_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event - .add_attribute(AMOUNT_KEY, amount.to_string()) - .add_attribute(DELEGATION_TARGET_KEY, mix_identity) + Event::new(MixnetEventType::WithdrawDelegatorReward) .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()) +} + +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()) +} + +pub fn new_pending_active_set_update_event( + new_size: u32, + approximate_time_remaining_secs: i64, +) -> Event { + Event::new(MixnetEventType::PendingActiveSetUpdate) + .add_attribute(ACTIVE_SET_SIZE_KEY, new_size.to_string()) + .add_attribute( + APPROXIMATE_TIME_LEFT_SECS_KEY, + approximate_time_remaining_secs.to_string(), + ) +} + +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(), + ) + .add_attribute( + UPDATED_INTERVAL_REWARDING_PARAMS_KEY, + updated.to_inline_json(), + ) +} + +pub fn new_pending_rewarding_params_update_event( + update: IntervalRewardingParamsUpdate, + approximate_time_remaining_secs: i64, +) -> Event { + Event::new(MixnetEventType::PendingIntervalRewardingParamsUpdate) + .add_attribute( + INTERVAL_REWARDING_PARAMS_UPDATE_KEY, + update.to_inline_json(), + ) + .add_attribute( + APPROXIMATE_TIME_LEFT_SECS_KEY, + approximate_time_remaining_secs.to_string(), + ) } pub fn new_undelegation_event( + created_at: BlockHeight, delegator: &Addr, proxy: &Option, - mix_identity: IdentityKeyRef<'_>, - amount: Uint128, + mix_id: MixId, ) -> Event { - let mut event = Event::new(UNDELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event - .add_attribute(AMOUNT_KEY, amount.to_string()) - .add_attribute(DELEGATION_TARGET_KEY, mix_identity) + 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(MIX_ID_KEY, mix_id.to_string()) } pub fn new_pending_undelegation_event( delegator: &Addr, proxy: &Option, - mix_identity: IdentityKeyRef<'_>, + mix_id: MixId, ) -> Event { - let mut event = - Event::new(PENDING_UNDELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event.add_attribute(DELEGATION_TARGET_KEY, mix_identity) + Event::new(MixnetEventType::PendingUndelegation) + .add_attribute(DELEGATOR_KEY, delegator) + .add_optional_attribute(PROXY_KEY, proxy.as_ref()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) } pub fn new_gateway_bonding_event( @@ -238,16 +292,11 @@ pub fn new_gateway_bonding_event( amount: &Coin, identity: IdentityKeyRef<'_>, ) -> Event { - let mut event = Event::new(GATEWAY_BONDING_EVENT_TYPE) + Event::new(MixnetEventType::GatewayBonding) .add_attribute(OWNER_KEY, owner) - .add_attribute(NODE_IDENTITY_KEY, identity); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event.add_attribute(AMOUNT_KEY, amount.to_string()) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_optional_attribute(PROXY_KEY, proxy.as_ref()) + .add_attribute(AMOUNT_KEY, amount.to_string()) } pub fn new_gateway_unbonding_event( @@ -256,16 +305,11 @@ pub fn new_gateway_unbonding_event( amount: &Coin, identity: IdentityKeyRef<'_>, ) -> Event { - let mut event = Event::new(GATEWAY_UNBONDING_EVENT_TYPE) + Event::new(MixnetEventType::GatewayUnbonding) .add_attribute(OWNER_KEY, owner) - .add_attribute(NODE_IDENTITY_KEY, identity); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - - // coin implements Display trait and we use that implementation here - event.add_attribute(AMOUNT_KEY, amount.to_string()) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_optional_attribute(PROXY_KEY, proxy.as_ref()) + .add_attribute(AMOUNT_KEY, amount.to_string()) } pub fn new_mixnode_bonding_event( @@ -273,55 +317,96 @@ pub fn new_mixnode_bonding_event( proxy: &Option, amount: &Coin, identity: IdentityKeyRef<'_>, + mix_id: MixId, assigned_layer: Layer, ) -> Event { - let mut event = Event::new(MIXNODE_BONDING_EVENT_TYPE) - .add_attribute(OWNER_KEY, owner) - .add_attribute(NODE_IDENTITY_KEY, identity); - - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } - // coin implements Display trait and we use that implementation here - event + Event::new(MixnetEventType::MixnodeBonding) + .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()) .add_attribute(ASSIGNED_LAYER_KEY, assigned_layer) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_mixnode_unbonding_event( +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, - amount: &Coin, identity: IdentityKeyRef<'_>, + mix_id: MixId, ) -> Event { - let mut event = Event::new(MIXNODE_UNBONDING_EVENT_TYPE) + Event::new(MixnetEventType::PendingMixnodeUnbonding) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(OWNER_KEY, owner) - .add_attribute(NODE_IDENTITY_KEY, identity); + .add_optional_attribute(PROXY_KEY, proxy.as_ref()) +} - if let Some(proxy) = proxy { - event = event.add_attribute(PROXY_KEY, proxy) - } +pub fn new_mixnode_config_update_event( + mix_id: MixId, + owner: &Addr, + proxy: &Option, + update: &MixNodeConfigUpdate, +) -> Event { + Event::new(MixnetEventType::MixnodeConfigUpdate) + .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()) +} - // coin implements Display trait and we use that implementation here - event.add_attribute(AMOUNT_KEY, amount.to_string()) +pub fn new_mixnode_pending_cost_params_update_event( + mix_id: MixId, + owner: &Addr, + proxy: &Option, + new_costs: &MixNodeCostParams, +) -> Event { + Event::new(MixnetEventType::PendingMixnodeCostParamsUpdate) + .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( + created_at: BlockHeight, + mix_id: MixId, + new_costs: &MixNodeCostParams, +) -> Event { + Event::new(MixnetEventType::MixnodeCostParamsUpdate) + .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()) +} + +pub fn new_rewarding_validator_address_update_event(old: Addr, new: Addr) -> Event { + Event::new(MixnetEventType::RewardingValidatorUpdate) + .add_attribute(OLD_REWARDING_VALIDATOR_ADDRESS_KEY, old) + .add_attribute(NEW_REWARDING_VALIDATOR_ADDRESS_KEY, new) } pub fn new_settings_update_event( old_params: &ContractStateParams, new_params: &ContractStateParams, ) -> Event { - let mut event = Event::new(SETTINGS_UPDATE_EVENT_TYPE); + let mut event = Event::new(MixnetEventType::ContractSettingsUpdate); if old_params.minimum_mixnode_pledge != new_params.minimum_mixnode_pledge { event = event .add_attribute( OLD_MINIMUM_MIXNODE_PLEDGE_KEY, - old_params.minimum_mixnode_pledge, + old_params.minimum_mixnode_pledge.to_string(), ) .add_attribute( NEW_MINIMUM_MIXNODE_PLEDGE_KEY, - new_params.minimum_mixnode_pledge, + new_params.minimum_mixnode_pledge.to_string(), ) } @@ -329,128 +414,130 @@ pub fn new_settings_update_event( event = event .add_attribute( OLD_MINIMUM_GATEWAY_PLEDGE_KEY, - old_params.minimum_gateway_pledge, + old_params.minimum_gateway_pledge.to_string(), ) .add_attribute( NEW_MINIMUM_GATEWAY_PLEDGE_KEY, - new_params.minimum_gateway_pledge, + new_params.minimum_gateway_pledge.to_string(), ) } - if old_params.mixnode_rewarded_set_size != new_params.mixnode_rewarded_set_size { - event = event - .add_attribute( - OLD_MIXNODE_REWARDED_SET_SIZE_KEY, - old_params.mixnode_rewarded_set_size.to_string(), - ) - .add_attribute( - NEW_MIXNODE_REWARDED_SET_SIZE_KEY, - new_params.mixnode_rewarded_set_size.to_string(), - ) - } - - if old_params.mixnode_active_set_size != new_params.mixnode_active_set_size { - event = event - .add_attribute( - OLD_MIXNODE_ACTIVE_SET_SIZE_KEY, - old_params.mixnode_active_set_size.to_string(), - ) - .add_attribute( - NEW_MIXNODE_ACTIVE_SET_SIZE_KEY, - new_params.mixnode_active_set_size.to_string(), - ) + if old_params.minimum_mixnode_delegation != new_params.minimum_mixnode_delegation { + if let Some(ref old) = old_params.minimum_mixnode_delegation { + event = event.add_attribute(OLD_MINIMUM_DELEGATION_KEY, old.to_string()) + } else { + event = event.add_attribute(OLD_MINIMUM_DELEGATION_KEY, "None") + } + if let Some(ref new) = new_params.minimum_mixnode_delegation { + event = event.add_attribute(NEW_MINIMUM_DELEGATION_KEY, new.to_string()) + } else { + event = event.add_attribute(NEW_MINIMUM_DELEGATION_KEY, "None") + } } event } -pub fn new_not_found_mix_operator_rewarding_event( - interval_id: u32, - identity: IdentityKeyRef<'_>, -) -> Event { - Event::new(OPERATOR_REWARDING_EVENT_TYPE) - .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) - .add_attribute(NODE_IDENTITY_KEY, identity) +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(MIX_ID_KEY, mix_id.to_string()) .add_attribute(NO_REWARD_REASON_KEY, BOND_NOT_FOUND_VALUE) } -pub fn new_too_fresh_bond_mix_operator_rewarding_event( - interval_id: u32, - identity: IdentityKeyRef<'_>, -) -> Event { - Event::new(OPERATOR_REWARDING_EVENT_TYPE) - .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) - .add_attribute(NODE_IDENTITY_KEY, identity) - .add_attribute(NO_REWARD_REASON_KEY, BOND_TOO_FRESH_VALUE) -} - -pub fn new_zero_uptime_mix_operator_rewarding_event( - interval_id: u32, - identity: IdentityKeyRef<'_>, -) -> Event { - Event::new(OPERATOR_REWARDING_EVENT_TYPE) - .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) - .add_attribute(NODE_IDENTITY_KEY, identity) - .add_attribute(NO_REWARD_REASON_KEY, ZERO_UPTIME_VALUE) -} - -#[allow(clippy::too_many_arguments)] -pub fn new_mix_operator_rewarding_event( - interval_id: u32, - identity: IdentityKeyRef<'_>, - node_reward_result: NodeRewardResult, - node_pledge: Uint128, - node_delegation: Uint128, -) -> Event { - Event::new(OPERATOR_REWARDING_EVENT_TYPE) - .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) - .add_attribute(NODE_IDENTITY_KEY, identity) - .add_attribute(TOTAL_PLEDGE_KEY, node_pledge) - .add_attribute(TOTAL_DELEGATIONS_KEY, node_delegation) +pub fn new_zero_uptime_mix_operator_rewarding_event(interval: Interval, mix_id: MixId) -> Event { + Event::new(MixnetEventType::MixnodeRewarding) .add_attribute( - TOTAL_MIXNODE_REWARD_KEY, - node_reward_result.reward().to_string(), + INTERVAL_KEY, + interval.current_epoch_absolute_id().to_string(), ) - .add_attribute(LAMBDA_KEY, node_reward_result.lambda().to_string()) - .add_attribute(SIGMA_KEY, node_reward_result.sigma().to_string()) + .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NO_REWARD_REASON_KEY, ZERO_PERFORMANCE_VALUE) } -pub fn new_mix_delegators_rewarding_event( - interval_id: u32, - identity: IdentityKeyRef<'_>, - delegation_rewards_distributed: Uint128, - further_delegations: bool, +pub fn new_mix_rewarding_event( + interval: Interval, + mix_id: MixId, + reward_distribution: RewardDistribution, + prior_delegates: Decimal, + prior_unit_reward: Decimal, ) -> Event { - Event::new(MIX_DELEGATORS_REWARDING_EVENT_TYPE) - .add_attribute(INTERVAL_ID_KEY, interval_id.to_string()) - .add_attribute(NODE_IDENTITY_KEY, identity) + Event::new(MixnetEventType::MixnodeRewarding) .add_attribute( - DISTRIBUTED_DELEGATION_REWARDS_KEY, - delegation_rewards_distributed, + INTERVAL_KEY, + interval.current_epoch_absolute_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(), ) .add_attribute( - FURTHER_DELEGATIONS_TO_REWARD_KEY, - further_delegations.to_string(), + DELEGATES_REWARD_KEY, + reward_distribution.delegates.to_string(), ) } -// note that when this event is emitted, we'll know the current block height -pub fn new_change_rewarded_set_event( - active_set_size: u32, - rewarded_set_size: u32, - nodes_in_rewarded_set: u32, +pub fn new_advance_epoch_event(interval: Interval, rewarded_nodes: u32) -> Event { + Event::new(MixnetEventType::AdvanceEpoch) + .add_attribute( + NEW_CURRENT_EPOCH_KEY, + interval.current_epoch_absolute_id().to_string(), + ) + .add_attribute(REWARDED_SET_NODES_KEY, rewarded_nodes.to_string()) +} + +pub fn new_pending_epoch_events_execution_event(executed: u32) -> Event { + Event::new(MixnetEventType::ExecutePendingEpochEvents) + .add_attribute(EVENTS_EXECUTED_KEY, executed.to_string()) +} + +pub fn new_pending_interval_events_execution_event(executed: u32) -> Event { + Event::new(MixnetEventType::ExecutePendingIntervalEvents) + .add_attribute(EVENTS_EXECUTED_KEY, executed.to_string()) +} + +pub fn new_reconcile_pending_events() -> Event { + Event::new(MixnetEventType::ReconcilePendingEvents) +} + +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(CHANGE_REWARDED_SET_EVENT_TYPE) - .add_attribute(ACTIVE_SET_SIZE_KEY, active_set_size.to_string()) - .add_attribute(REWARDED_SET_SIZE_KEY, rewarded_set_size.to_string()) - .add_attribute(NODES_IN_REWARDED_SET_KEY, nodes_in_rewarded_set.to_string()) + 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(), + ) + .add_attribute(NEW_EPOCHS_IN_INTERVAL, epochs_in_interval.to_string()) + .add_attribute( + UPDATED_INTERVAL_REWARDING_PARAMS_KEY, + updated_rewarding_params.to_inline_json(), + ) } -pub fn new_advance_interval_event(interval: Interval) -> Event { - Event::new(ADVANCE_INTERVAL_EVENT_TYPE) - .add_attribute(NEW_CURRENT_INTERVAL_KEY, interval.to_string()) -} - -pub fn new_advance_epoch_event(interval: Interval) -> Event { - Event::new(ADVANCE_EPOCH_EVENT_TYPE).add_attribute(NEW_CURRENT_EPOCH_KEY, interval.to_string()) +pub fn new_pending_interval_config_update_event( + epochs_in_interval: u32, + epoch_duration_secs: u64, + approximate_time_remaining_secs: i64, +) -> Event { + Event::new(MixnetEventType::PendingIntervalConfigUpdate) + .add_attribute( + NEW_EPOCHS_DURATION_SECS_KEY, + epoch_duration_secs.to_string(), + ) + .add_attribute(NEW_EPOCHS_IN_INTERVAL, epochs_in_interval.to_string()) + .add_attribute( + APPROXIMATE_TIME_LEFT_SECS_KEY, + approximate_time_remaining_secs.to_string(), + ) } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs index 502d43dc13..ebb5464064 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs @@ -1,3 +1,6 @@ +// Copyright 2021-2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + // due to code generated by JsonSchema #![allow(clippy::field_reassign_with_default)] 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 862851ca6c..efedb88b8a 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -1,12 +1,13 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::pending_events::{PendingEpochEvent, PendingIntervalEvent}; +use crate::{EpochId, IntervalId}; use cosmwasm_std::Env; use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::convert::TryInto; use std::fmt::{Display, Formatter}; use std::time::Duration; use time::OffsetDateTime; @@ -59,12 +60,27 @@ pub(crate) mod string_rfc3339_offset_date_time { } } -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize)] +#[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: u32, + 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")] - start: OffsetDateTime, - length: Duration, + // note: the `ts-rs failed to parse this attribute. It will be ignored.` warning emitted during + // compilation is fine (I guess). `ts-rs` can't handle `with` serde attribute, but that's okay + // since we explicitly specified this field should correspond to typescript's string + 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, } impl JsonSchema for Interval { @@ -81,20 +97,46 @@ impl JsonSchema for Interval { let object_validation = schema_object.object(); object_validation .properties - .insert("id".to_owned(), gen.subschema_for::()); + .insert("id".to_owned(), gen.subschema_for::()); object_validation.required.insert("id".to_owned()); + object_validation + .properties + .insert("epochs_in_interval".to_owned(), gen.subschema_for::()); + object_validation + .required + .insert("epochs_in_interval".to_owned()); + // PrimitiveDateTime does not implement JsonSchema. However it has a custom // serialization to string, so we just specify the schema to be String. + object_validation.properties.insert( + "current_epoch_start".to_owned(), + gen.subschema_for::(), + ); object_validation - .properties - .insert("start".to_owned(), gen.subschema_for::()); - object_validation.required.insert("start".to_owned()); + .required + .insert("current_epoch_start".to_owned()); + + object_validation.properties.insert( + "current_epoch_id".to_owned(), + gen.subschema_for::(), + ); + object_validation + .required + .insert("current_epoch_id".to_owned()); object_validation .properties - .insert("length".to_owned(), gen.subschema_for::()); - object_validation.required.insert("length".to_owned()); + .insert("epoch_length".to_owned(), gen.subschema_for::()); + object_validation.required.insert("epoch_length".to_owned()); + + object_validation.properties.insert( + "total_elapsed_epochs".to_owned(), + gen.subschema_for::(), + ); + object_validation + .required + .insert("total_elapsed_epochs".to_owned()); Schema::Object(schema_object) } @@ -102,328 +144,451 @@ impl JsonSchema for Interval { impl Interval { /// Initialize epoch in the contract with default values. - pub fn init_epoch(env: Env) -> Self { + pub fn init_interval(epochs_in_interval: u32, epoch_length: Duration, env: &Env) -> Self { Interval { id: 0, + epochs_in_interval, // I really don't see a way for this to fail, unless the blockchain is lying to us - start: OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64) - .expect("Invalid timestamp from env.block.time"), - length: Duration::from_secs(3600), + current_epoch_start: OffsetDateTime::from_unix_timestamp( + env.block.time.seconds() as i64 + ) + .expect("Invalid timestamp from env.block.time"), + current_epoch_id: 0, + epoch_length, + total_elapsed_epochs: 0, } } - pub fn is_over(&self, env: Env) -> bool { - self.end_unix_timestamp() <= env.block.time.seconds() as i64 + pub const fn current_epoch_id(&self) -> EpochId { + self.current_epoch_id } - pub fn in_progress(&self, env: Env) -> bool { - let block_time = env.block.time.seconds() as i64; - self.start_unix_timestamp() <= block_time && block_time < self.end_unix_timestamp() - } - - pub fn update_duration(&mut self, secs: u64) { - self.length = Duration::from_secs(secs); - } - - pub const fn length_secs(&self) -> u64 { - self.length.as_secs() - } - - /// Returns the next interval. - #[must_use] - pub fn next(&self) -> Self { - Interval { - id: self.id + 1, - start: self.end(), - length: self.length, - } - } - - pub fn next_on_chain(&self, env: Env) -> Self { - let start = self - .end() - .max(OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64).unwrap()); - Interval { - id: self.id + 1, - start, - length: self.length, - } - } - - /// Returns the last interval. - pub fn previous(&self) -> Option { - if self.id > 0 { - Some(Interval { - id: self.id - 1, - start: self.start - self.length, - length: self.length, - }) - } else { - None - } - } - - /// Determines whether the provided datetime is contained within the interval - /// - /// # Arguments - /// - /// * `datetime`: specified datetime - pub fn contains(&self, datetime: OffsetDateTime) -> bool { - self.start <= datetime && datetime <= self.end() - } - - /// Determines whether the provided unix timestamp is contained within the interval - /// - /// # Arguments - /// - /// * `timestamp`: specified timestamp - pub fn contains_timestamp(&self, timestamp: i64) -> bool { - self.start_unix_timestamp() <= timestamp && timestamp <= self.end_unix_timestamp() - } - - /// Returns new instance of [Interval] such that the provided datetime would be within - /// its duration. - /// - /// # Arguments - /// - /// * `now`: current datetime - pub fn current(&self, now: OffsetDateTime) -> Option { - let mut candidate = *self; - - if now > self.start { - loop { - if candidate.contains(now) { - return Some(candidate); - } - candidate = candidate.next(); - } - } else { - loop { - if candidate.contains(now) { - return Some(candidate); - } - candidate = candidate.previous()?; - } - } - } - - /// Returns new instance of [Interval] such that the provided unix timestamp would be within - /// its duration. - /// - /// # Arguments - /// - /// * `now_unix`: current unix time - pub fn current_with_timestamp(&self, now_unix: i64) -> Option { - let mut candidate = *self; - - if now_unix > self.start_unix_timestamp() { - loop { - if candidate.contains_timestamp(now_unix) { - return Some(candidate); - } - candidate = candidate.next(); - } - } else { - loop { - if candidate.contains_timestamp(now_unix) { - return Some(candidate); - } - candidate = candidate.previous()?; - } - } - } - - /// Checks whether this interval has already finished - /// - /// # Arguments - /// - /// * `now`: current datetime - pub fn has_elapsed(&self, now: OffsetDateTime) -> bool { - self.end() < now - } - - /// Returns id of this interval - pub const fn id(&self) -> u32 { + pub const fn current_interval_id(&self) -> IntervalId { self.id } - /// Determines amount of time left until this interval finishes. - /// - /// # Arguments - /// - /// * `now`: current datetime - pub fn until_end(&self, now: OffsetDateTime) -> Option { - let remaining = self.end() - now; - if remaining.is_negative() { - None + pub const fn epochs_in_interval(&self) -> u32 { + self.epochs_in_interval + } + + pub fn force_change_epochs_in_interval(&mut self, epochs_in_interval: u32) { + self.epochs_in_interval = epochs_in_interval; + if self.current_epoch_id >= epochs_in_interval { + // we have to go to the next interval as we can't + // have the same (interval, epoch) combo as we had in the past + self.id += self.current_epoch_id / epochs_in_interval; + self.current_epoch_id %= epochs_in_interval; + } + } + + pub fn change_epoch_length(&mut self, epoch_length: Duration) { + self.epoch_length = epoch_length + } + + pub const fn total_elapsed_epochs(&self) -> u32 { + self.total_elapsed_epochs + } + + pub const fn current_epoch_absolute_id(&self) -> u32 { + // since we count epochs starting from 0, if n epochs have elapsed, the current one has absolute id of n + self.total_elapsed_epochs + } + + #[inline] + pub fn is_current_epoch_over(&self, env: &Env) -> bool { + self.current_epoch_end_unix_timestamp() <= env.block.time.seconds() as i64 + } + + pub fn secs_until_current_epoch_end(&self, env: &Env) -> i64 { + if self.is_current_epoch_over(env) { + 0 } else { - remaining.try_into().ok() + self.current_epoch_end_unix_timestamp() - env.block.time.seconds() as i64 + } + } + + #[inline] + pub fn is_current_interval_over(&self, env: &Env) -> bool { + self.current_interval_end_unix_timestamp() <= env.block.time.seconds() as i64 + } + + pub fn secs_until_current_interval_end(&self, env: &Env) -> i64 { + if self.is_current_interval_over(env) { + 0 + } else { + self.current_interval_end_unix_timestamp() - env.block.time.seconds() as i64 + } + } + + pub fn current_epoch_in_progress(&self, env: &Env) -> bool { + let block_time = env.block.time.seconds() as i64; + self.current_epoch_start_unix_timestamp() <= block_time + && block_time < self.current_epoch_end_unix_timestamp() + } + + pub fn update_epoch_duration(&mut self, secs: u64) { + self.epoch_length = Duration::from_secs(secs); + } + + pub const fn epoch_length_secs(&self) -> u64 { + self.epoch_length.as_secs() + } + + /// Returns the next epoch. If if would result in advancing the interval, + /// the relevant changes are applied. + #[must_use] + pub fn advance_epoch(&self) -> Self { + // remember we start from 0th epoch, so if we're supposed to have 100 epochs in interval, + // epoch 99 is going to be the last one + if self.current_epoch_id == self.epochs_in_interval - 1 { + Interval { + id: self.id + 1, + epochs_in_interval: self.epochs_in_interval, + current_epoch_start: self.current_epoch_end(), + current_epoch_id: 0, + epoch_length: self.epoch_length, + total_elapsed_epochs: self.total_elapsed_epochs + 1, + } + } else { + Interval { + id: self.id, + epochs_in_interval: self.epochs_in_interval, + current_epoch_start: self.current_epoch_end(), + current_epoch_id: self.current_epoch_id + 1, + epoch_length: self.epoch_length, + total_elapsed_epochs: self.total_elapsed_epochs + 1, + } } } /// Returns the starting datetime of this interval. - pub const fn start(&self) -> OffsetDateTime { - self.start + pub const fn current_epoch_start(&self) -> OffsetDateTime { + self.current_epoch_start } /// Returns the length of this interval. - pub const fn length(&self) -> Duration { - self.length + pub const fn epoch_length(&self) -> Duration { + self.epoch_length } - /// Returns the ending datetime of this interval. - pub fn end(&self) -> OffsetDateTime { - self.start + self.length + /// Returns the ending datetime of the current epoch. + pub fn current_epoch_end(&self) -> OffsetDateTime { + self.current_epoch_start + self.epoch_length } - /// Returns the unix timestamp of the start of this interval. - pub const fn start_unix_timestamp(&self) -> i64 { - self.start().unix_timestamp() + pub fn epochs_until_interval_end(&self) -> u32 { + self.epochs_in_interval - self.current_epoch_id } - /// Returns the unix timestamp of the end of this interval. - pub fn end_unix_timestamp(&self) -> i64 { - self.end().unix_timestamp() + /// Returns the ending datetime of the current interval. + pub fn current_interval_end(&self) -> OffsetDateTime { + self.current_epoch_start + self.epochs_until_interval_end() * self.epoch_length + } + + /// Returns the unix timestamp of the start of the current epoch. + pub const fn current_epoch_start_unix_timestamp(&self) -> i64 { + self.current_epoch_start().unix_timestamp() + } + + /// Returns the unix timestamp of the end of the current epoch. + #[inline] + pub fn current_epoch_end_unix_timestamp(&self) -> i64 { + self.current_epoch_end().unix_timestamp() + } + + /// Returns the unix timestamp of the end of the current interval. + #[inline] + pub fn current_interval_end_unix_timestamp(&self) -> i64 { + self.current_interval_end().unix_timestamp() } } impl Display for Interval { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - let length = self.length().as_secs(); + let length = self.epoch_length_secs(); let full_hours = length / 3600; let rem = length % 3600; write!( f, - "Interval {}: {} - {} ({}h {}s)", + "Interval {}: epoch {}/{} (current epoch begun at: {}; epoch lengths: {}h {}s)", self.id, - self.start(), - self.end(), + self.current_epoch_id + 1, + self.epochs_in_interval, + self.current_epoch_start, full_hours, rem ) } } +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)] +pub struct CurrentIntervalResponse { + pub interval: Interval, + pub current_blocktime: u64, + pub is_current_interval_over: bool, + pub is_current_epoch_over: bool, +} + +impl CurrentIntervalResponse { + pub fn new(interval: Interval, env: Env) -> Self { + CurrentIntervalResponse { + interval, + current_blocktime: env.block.time.seconds(), + is_current_interval_over: interval.is_current_interval_over(&env), + is_current_epoch_over: interval.is_current_epoch_over(&env), + } + } + + pub fn time_until_current_epoch_end(&self) -> Duration { + if self.is_current_epoch_over { + Duration::from_secs(0) + } else { + let remaining_secs = + self.interval.current_epoch_end_unix_timestamp() - self.current_blocktime as i64; + // this should never be negative, but better safe than sorry and guard ourselves against that case + if remaining_secs <= 0 { + Duration::from_secs(0) + } else { + Duration::from_secs(remaining_secs as u64) + } + } + } +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct PendingEpochEventsResponse { + pub seconds_until_executable: i64, + pub events: Vec, + pub start_next_after: Option, +} + +impl PendingEpochEventsResponse { + pub fn new( + seconds_until_executable: i64, + events: Vec, + start_next_after: Option, + ) -> Self { + PendingEpochEventsResponse { + seconds_until_executable, + events, + start_next_after, + } + } +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct PendingIntervalEventsResponse { + pub seconds_until_executable: i64, + pub events: Vec, + pub start_next_after: Option, +} + +impl PendingIntervalEventsResponse { + pub fn new( + seconds_until_executable: i64, + events: Vec, + start_next_after: Option, + ) -> Self { + PendingIntervalEventsResponse { + seconds_until_executable, + events, + start_next_after, + } + } +} + #[cfg(test)] mod tests { use super::*; + use cosmwasm_std::testing::mock_env; + use rand_chacha::rand_core::{RngCore, SeedableRng}; #[test] - fn previous() { + fn advancing_epoch() { + // just advancing epoch let interval = Interval { - id: 1, - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), + id: 0, + epochs_in_interval: 100, + current_epoch_start: time::macros::datetime!(2021-08-23 12:00 UTC), + current_epoch_id: 23, + epoch_length: Duration::from_secs(60 * 60), + total_elapsed_epochs: 0, }; let expected = Interval { id: 0, - start: time::macros::datetime!(2021-08-22 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), + epochs_in_interval: 100, + current_epoch_start: time::macros::datetime!(2021-08-23 13:00 UTC), + current_epoch_id: 24, + epoch_length: Duration::from_secs(60 * 60), + total_elapsed_epochs: 1, }; - assert_eq!(expected, interval.previous().unwrap()); + assert_eq!(expected, interval.advance_epoch()); - let genesis_interval = Interval { - id: 0, - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), - }; - assert!(genesis_interval.previous().is_none()); - } - - #[test] - fn next() { + // results in advancing interval let interval = Interval { id: 0, - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), + epochs_in_interval: 100, + current_epoch_start: time::macros::datetime!(2021-08-23 12:00 UTC), + current_epoch_id: 99, + epoch_length: Duration::from_secs(60 * 60), + total_elapsed_epochs: 42, }; let expected = Interval { id: 1, - start: time::macros::datetime!(2021-08-24 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), + epochs_in_interval: 100, + current_epoch_start: time::macros::datetime!(2021-08-23 13:00 UTC), + current_epoch_id: 0, + epoch_length: Duration::from_secs(60 * 60), + total_elapsed_epochs: 43, }; - assert_eq!(expected, interval.next()) + assert_eq!(expected, interval.advance_epoch()) } #[test] - fn checking_for_datetime_inclusion() { + fn checking_for_epoch_ends() { + let env = mock_env(); + + // epoch just begun let interval = Interval { - id: 100, - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), + id: 0, + epochs_in_interval: 100, + current_epoch_start: OffsetDateTime::from_unix_timestamp( + env.block.time.seconds() as i64 - 100, + ) + .unwrap(), + current_epoch_id: 23, + epoch_length: Duration::from_secs(60 * 60), + total_elapsed_epochs: 0, }; + assert!(!interval.is_current_epoch_over(&env)); - // it must contain its own boundaries - assert!(interval.contains(interval.start)); - assert!(interval.contains(interval.end())); + // current time == current epoch start + let mut interval = interval; + interval.current_epoch_start = + OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64).unwrap(); + assert!(!interval.is_current_epoch_over(&env)); - let in_the_midle = interval.start + Duration::from_secs(interval.length.as_secs() / 2); - assert!(interval.contains(in_the_midle)); + // epoch HASN'T yet begun (weird edge case, but can happen if we decide to manually adjust things) + let mut interval = interval; + interval.current_epoch_start = + OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64 + 100).unwrap(); + assert!(!interval.is_current_epoch_over(&env)); - assert!(!interval.contains(interval.next().end())); - assert!(!interval.contains(interval.previous().unwrap().start())); + // current_time = EXACTLY end of the epoch + let mut interval = interval; + interval.current_epoch_start = + OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64).unwrap() + - interval.epoch_length; + assert!(interval.is_current_epoch_over(&env)); + + // revert time a bit more + interval.current_epoch_start -= Duration::from_secs(42); + assert!(interval.is_current_epoch_over(&env)); + + // revert by A LOT -> epoch still should be in finished state + interval.current_epoch_start -= Duration::from_secs(5 * 31 * 60 * 60); + assert!(interval.is_current_epoch_over(&env)); } #[test] - fn determining_current_interval() { - let first_interval = Interval { - id: 100, - start: time::macros::datetime!(2021-08-23 12:00 UTC), - length: Duration::from_secs(24 * 60 * 60), + fn interval_end() { + let mut interval = Interval { + id: 0, + epochs_in_interval: 100, + current_epoch_start: time::macros::datetime!(2021-08-23 12:00 UTC), + current_epoch_id: 99, + epoch_length: Duration::from_secs(60 * 60), + total_elapsed_epochs: 0, }; - // interval just before - let fake_now = first_interval.start - Duration::from_secs(123); - assert_eq!(first_interval.previous(), first_interval.current(fake_now)); - - // this interval (start boundary) assert_eq!( - first_interval, - first_interval.current(first_interval.start).unwrap() + interval.current_epoch_start + interval.epoch_length, + interval.current_interval_end() ); - // this interval (in the middle) - let fake_now = first_interval.start + Duration::from_secs(123); - assert_eq!(first_interval, first_interval.current(fake_now).unwrap()); - - // this interval (end boundary) + interval.current_epoch_id -= 1; assert_eq!( - first_interval, - first_interval.current(first_interval.end()).unwrap() + interval.current_epoch_start + 2 * interval.epoch_length, + interval.current_interval_end() ); - // next interval - let fake_now = first_interval.end() + Duration::from_secs(123); + interval.current_epoch_id -= 10; assert_eq!( - first_interval.next(), - first_interval.current(fake_now).unwrap() + interval.current_epoch_start + 12 * interval.epoch_length, + interval.current_interval_end() ); - // few intervals in the past - let fake_now = first_interval.start() - - first_interval.length - - first_interval.length - - first_interval.length; + interval.current_epoch_id = 0; assert_eq!( - first_interval - .previous() - .unwrap() - .previous() - .unwrap() - .previous() - .unwrap(), - first_interval.current(fake_now).unwrap() + interval.current_epoch_start + interval.epochs_in_interval * interval.epoch_length, + interval.current_interval_end() ); + } - // few intervals in the future - let fake_now = first_interval.end() - + first_interval.length - + first_interval.length - + first_interval.length; - assert_eq!( - first_interval.next().next().next(), - first_interval.current(fake_now).unwrap() - ); + #[test] + fn checking_for_interval_ends() { + let env = mock_env(); + + let epoch_length = Duration::from_secs(60 * 60); + + let mut interval = Interval { + id: 0, + epochs_in_interval: 100, + current_epoch_start: OffsetDateTime::from_unix_timestamp( + env.block.time.seconds() as i64 + ) + .unwrap(), + current_epoch_id: 98, + epoch_length, + total_elapsed_epochs: 0, + }; + + // current epoch just started (we still have to finish 2 epochs) + assert!(!interval.is_current_interval_over(&env)); + + // still need to finish the 99th epoch + interval.current_epoch_start -= epoch_length; + assert!(!interval.is_current_interval_over(&env)); + + // it JUST finished + interval.current_epoch_start -= epoch_length; + assert!(interval.is_current_interval_over(&env)); + + // nobody updated the interval data, but the current one should still be in finished state + interval.current_epoch_start -= 10 * epoch_length; + assert!(interval.is_current_interval_over(&env)); + } + + #[test] + fn getting_current_full_epoch_id() { + let env = mock_env(); + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let epoch_length = Duration::from_secs(60 * 60); + + let mut interval = Interval::init_interval(100, epoch_length, &env); + + // normal situation + for i in 0u32..2000 { + assert_eq!(interval.current_epoch_absolute_id(), i); + interval = interval.advance_epoch(); + } + + let mut interval = Interval::init_interval(100, epoch_length, &env); + + for i in 0u32..2000 { + // every few epochs decide to change epochs in interval + if i % 7 == 0 { + let new_epochs_in_interval = (rng.next_u32() % 200) + 42; + interval.force_change_epochs_in_interval(new_epochs_in_interval) + } + + // make sure full epoch id is always monotonically increasing + assert_eq!(interval.current_epoch_absolute_id(), i); + + interval = interval.advance_epoch(); + } } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs index bd0c7feeea..200b4c5a00 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs @@ -1,36 +1,41 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +mod constants; pub mod delegation; pub mod error; pub mod events; pub mod gateway; +pub mod helpers; mod interval; pub mod mixnode; mod msg; +pub mod pending_events; pub mod reward_params; +pub mod rewarding; mod types; -pub const MIXNODE_DELEGATORS_PAGE_LIMIT: usize = 250; - -pub use cosmwasm_std::{Addr, Coin}; +pub use contracts_common::types::*; +pub use cosmwasm_std::{Addr, Coin, Decimal, Fraction}; pub use delegation::{ Delegation, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, - PagedMixDelegationsResponse, + PagedMixNodeDelegationsResponse, }; pub use gateway::{ Gateway, GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, PagedGatewayResponse, }; -pub use interval::Interval; +pub use interval::{ + CurrentIntervalResponse, Interval, PendingEpochEventsResponse, PendingIntervalEventsResponse, +}; pub use mixnode::{ - Layer, MixNode, MixNodeBond, MixOwnershipResponse, MixnodeBondResponse, PagedMixnodeResponse, - RewardedSetNodeStatus, + Layer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeCostParams, MixNodeDetails, + MixNodeRewarding, MixOwnershipResponse, MixnodeDetailsResponse, PagedMixnodeBondsResponse, + RewardedSetNodeStatus, UnbondedMixnode, }; pub use msg::*; +pub use pending_events::{ + PendingEpochEvent, PendingEpochEventData, PendingEpochEventKind, PendingIntervalEvent, + PendingIntervalEventData, PendingIntervalEventKind, +}; +pub use reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate, RewardingParams}; pub use types::*; - -pub type U128 = fixed::types::U75F53; - -fixed::const_fixed_from_int! { - const ONE: U128 = 1; -} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 752719e9c9..5eb98bcbb3 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -1,18 +1,19 @@ +// Copyright 2021-2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + // due to code generated by JsonSchema #![allow(clippy::field_reassign_with_default)] +use crate::constants::UNIT_DELEGATION_BASE; use crate::error::MixnetContractError; -use crate::reward_params::RewardParams; -use crate::{Delegation, IdentityKey, SphinxKey}; -use crate::{ONE, U128}; -use az::CheckedCast; -use cosmwasm_std::{coin, Addr, Coin, Uint128}; -use log::error; +use crate::reward_params::{NodeRewardParams, RewardingParams}; +use crate::rewarding::helpers::truncate_reward; +use crate::rewarding::RewardDistribution; +use crate::{Delegation, EpochId, IdentityKey, MixId, Percent, SphinxKey}; +use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -use std::cmp::Ordering; -use std::fmt::Display; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( @@ -31,102 +32,504 @@ impl RewardedSetNodeStatus { } } -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub enum DelegationEvent { - Delegate(Delegation), - Undelegate(PendingUndelegate), +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct MixNodeDetails { + pub bond_information: MixNodeBond, + + pub rewarding_details: MixNodeRewarding, } -impl DelegationEvent { - pub fn delegation_amount(&self) -> Option { - match self { - DelegationEvent::Delegate(delegation) => Some(delegation.amount.clone()), - // I think it would be nice to also expose an amount here to know how much we're undelegating - DelegationEvent::Undelegate(_) => None, +impl MixNodeDetails { + pub fn new(bond_information: MixNodeBond, rewarding_details: MixNodeRewarding) -> Self { + MixNodeDetails { + bond_information, + rewarding_details, } } + + pub fn mix_id(&self) -> MixId { + self.bond_information.mix_id + } + + pub fn is_unbonding(&self) -> bool { + self.bond_information.is_unbonding + } + + pub fn original_pledge(&self) -> &Coin { + &self.bond_information.original_pledge + } + + pub fn pending_operator_reward(&self) -> Coin { + let pledge = self.original_pledge(); + self.rewarding_details.pending_operator_reward(pledge) + } + + pub fn pending_detailed_operator_reward(&self) -> Decimal { + let pledge = self.original_pledge(); + self.rewarding_details + .pending_detailed_operator_reward(pledge) + } + + pub fn total_stake(&self) -> Decimal { + self.rewarding_details.node_bond() + } } -#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] -pub struct PendingUndelegate { - mix_identity: IdentityKey, - delegate: Addr, - proxy: Option, - block_height: u64, +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct MixNodeRewarding { + /// Information provided by the operator that influence the cost function. + pub cost_params: MixNodeCostParams, + + /// Total pledge and compounded reward earned by the node operator. + pub operator: Decimal, + + /// Total delegation and compounded reward earned by all node delegators. + pub delegates: Decimal, + + /// Cumulative reward earned by the "unit delegation" since the block 0. + pub total_unit_reward: Decimal, + + /// Value of the theoretical "unit delegation" that has delegated to this mixnode at block 0. + 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. + 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) + pub unique_delegations: u32, } -impl PendingUndelegate { - pub fn new( - mix_identity: IdentityKey, - delegate: Addr, - proxy: Option, - block_height: u64, +impl MixNodeRewarding { + pub fn initialise_new( + cost_params: MixNodeCostParams, + initial_pledge: &Coin, + current_epoch: EpochId, ) -> Self { - Self { - mix_identity, - delegate, - proxy, - block_height, + MixNodeRewarding { + cost_params, + operator: Decimal::from_atomics(initial_pledge.amount, 0).unwrap(), + delegates: Decimal::zero(), + total_unit_reward: Decimal::zero(), + unit_delegation: UNIT_DELEGATION_BASE, + last_rewarded_epoch: current_epoch, + unique_delegations: 0, } } - pub fn mix_identity(&self) -> IdentityKey { - self.mix_identity.clone() + /// Determines whether this node is still bonded. This is performed via a simple check, + /// if there are no tokens left associated with the operator, it means they have unbonded + /// and those params only exist for the purposes of calculating rewards for delegators that + /// have not yet removed their tokens. + pub fn still_bonded(&self) -> bool { + self.operator != Decimal::zero() } - pub fn delegate(&self) -> Addr { - self.delegate.clone() + pub fn pending_operator_reward(&self, original_pledge: &Coin) -> Coin { + let reward_with_pledge = truncate_reward(self.operator, &original_pledge.denom); + Coin { + denom: reward_with_pledge.denom, + amount: reward_with_pledge.amount - original_pledge.amount, + } } - pub fn proxy(&self) -> Option { - self.proxy.clone() + pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> Decimal { + let initial_dec = Decimal::from_atomics(original_pledge.amount, 0).unwrap(); + if initial_dec > self.operator { + panic!( + "seems slashing has occurred while it has not been implemented nor accounted for!" + ) + } + self.operator - initial_dec } - pub fn block_height(&self) -> u64 { - self.block_height + pub fn operator_pledge_with_reward(&self, denom: impl Into) -> Coin { + truncate_reward(self.operator, denom) } - pub fn proxy_storage_key(&self) -> Vec { - if let Some(proxy) = &self.proxy { - self.delegate() - .as_bytes() - .iter() - .zip(proxy.as_bytes()) - .map(|(x, y)| x ^ y) - .collect() + pub fn pending_delegator_reward(&self, delegation: &Delegation) -> Coin { + let delegator_reward = self.determine_delegation_reward(delegation); + truncate_reward(delegator_reward, &delegation.amount.denom) + } + + pub fn withdraw_operator_reward(&mut self, original_pledge: &Coin) -> Coin { + let initial_dec = Decimal::from_atomics(original_pledge.amount, 0).unwrap(); + if initial_dec > self.operator { + panic!( + "seems slashing has occurred while it has not been implemented nor accounted for!" + ) + } + let diff = self.operator - initial_dec; + self.operator = initial_dec; + + truncate_reward(diff, &original_pledge.denom) + } + + pub fn withdraw_delegator_reward( + &mut self, + delegation: &mut Delegation, + ) -> Result { + let reward = self.determine_delegation_reward(delegation); + self.decrease_delegates_decimal(reward)?; + + delegation.cumulative_reward_ratio = self.full_reward_ratio(); + Ok(truncate_reward(reward, &delegation.amount.denom)) + } + + pub fn node_bond(&self) -> Decimal { + self.operator + self.delegates + } + + /// Saturation over the tokens pledged by the node operator. + pub fn pledge_saturation(&self, reward_params: &RewardingParams) -> Decimal { + // make sure our saturation is never greater than 1 + if self.operator > reward_params.interval.stake_saturation_point { + Decimal::one() } else { - self.delegate().as_bytes().to_vec() + self.operator / reward_params.interval.stake_saturation_point } } - pub fn storage_key(&self) -> (IdentityKey, Vec) { - (self.mix_identity(), self.proxy_storage_key()) + /// Saturation over all the tokens staked over this node. + pub fn bond_saturation(&self, reward_params: &RewardingParams) -> Decimal { + // make sure our saturation is never greater than 1 + if self.node_bond() > reward_params.interval.stake_saturation_point { + Decimal::one() + } else { + self.node_bond() / reward_params.interval.stake_saturation_point + } } - pub fn delegation_key(&self, block_height: u64) -> (IdentityKey, Vec, u64) { - (self.mix_identity(), self.proxy_storage_key(), block_height) + pub fn uncapped_bond_saturation(&self, reward_params: &RewardingParams) -> Decimal { + self.node_bond() / reward_params.interval.stake_saturation_point } - pub fn event_storage_key(&self) -> (Vec, u64, IdentityKey) { - ( - self.proxy_storage_key(), - self.block_height(), - self.mix_identity(), - ) + pub fn node_reward( + &self, + reward_params: &RewardingParams, + node_params: NodeRewardParams, + ) -> Decimal { + let work = if node_params.in_active_set { + reward_params.active_node_work() + } else { + reward_params.standby_node_work() + }; + + let alpha = reward_params.interval.sybil_resistance; + + reward_params.interval.epoch_reward_budget + * node_params.performance.value() + * self.bond_saturation(reward_params) + * (work + + alpha.value() * self.pledge_saturation(reward_params) + / reward_params.dec_rewarded_set_size()) + / (Decimal::one() + alpha.value()) + } + + pub fn determine_reward_split( + &self, + node_reward: Decimal, + node_performance: Percent, + // I don't like this argument here, makes things look, idk, messy... + epochs_in_interval: u32, + ) -> RewardDistribution { + let node_cost = + self.cost_params.epoch_operating_cost(epochs_in_interval) * node_performance.value(); + + // check if profit is positive + if node_reward > node_cost { + let profit = node_reward - node_cost; + let profit_margin = self.cost_params.profit_margin_percent.value(); + let one = Decimal::one(); + + let operator_share = self.operator / self.node_bond(); + + let operator = profit * (profit_margin + (one - profit_margin) * operator_share); + let delegates = profit - operator; + + debug_assert_eq!(operator + delegates + node_cost, node_reward); + + RewardDistribution { + operator: operator + node_cost, + delegates, + } + } else { + RewardDistribution { + operator: node_reward, + delegates: Decimal::zero(), + } + } + } + + pub fn calculate_epoch_reward( + &self, + reward_params: &RewardingParams, + node_params: NodeRewardParams, + epochs_in_interval: u32, + ) -> RewardDistribution { + let node_reward = self.node_reward(reward_params, node_params); + self.determine_reward_split(node_reward, node_params.performance, epochs_in_interval) + } + + pub fn distribute_rewards( + &mut self, + distribution: RewardDistribution, + absolute_epoch_id: EpochId, + ) { + let unit_delegation_reward = distribution.delegates + * self.delegator_share(self.unit_delegation + self.total_unit_reward); + + self.operator += distribution.operator; + self.delegates += distribution.delegates; + + // self.current_period_reward += unit_delegation_reward; + self.total_unit_reward += unit_delegation_reward; + self.last_rewarded_epoch = absolute_epoch_id; + } + + pub fn epoch_rewarding( + &mut self, + reward_params: &RewardingParams, + node_params: NodeRewardParams, + epochs_in_interval: u32, + absolute_epoch_id: EpochId, + ) { + let reward_distribution = + self.calculate_epoch_reward(reward_params, node_params, epochs_in_interval); + self.distribute_rewards(reward_distribution, absolute_epoch_id) + } + + 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 + self.unit_delegation; + + (ending_ratio - starting_ratio) * delegation.dec_amount() / adjust + } + + // this updates `unique_delegations` field + pub fn add_base_delegation(&mut self, amount: Uint128) { + self.increase_delegates_uint128(amount); + self.unique_delegations += 1; + } + + pub fn increase_delegates_uint128(&mut self, amount: Uint128) { + // the unwrap here is fine as the value is guaranteed to fit under provided constraints + self.delegates += Decimal::from_atomics(amount, 0).unwrap() + } + + // this updates `unique_delegations` field + // special care must be taken when calling this method as the caller has to ensure + // the corresponding delegation has not accumulated any rewards + pub fn remove_delegation_uint128( + &mut self, + amount: Uint128, + ) -> Result<(), MixnetContractError> { + self.decrease_delegates_uint128(amount)?; + self.decrement_unique_delegations() + } + + pub fn decrease_delegates_uint128( + &mut self, + amount: Uint128, + ) -> Result<(), MixnetContractError> { + let amount_dec = Decimal::from_atomics(amount, 0).unwrap(); + self.decrease_delegates_decimal(amount_dec) + } + + fn decrement_unique_delegations(&mut self) -> Result<(), MixnetContractError> { + if self.unique_delegations == 0 { + return Err(MixnetContractError::OverflowSubtraction { + minuend: 0, + subtrahend: 1, + }); + } + self.unique_delegations -= 1; + Ok(()) + } + + // this updates `unique_delegations` field + pub fn remove_delegation_decimal( + &mut self, + amount: Decimal, + ) -> Result<(), MixnetContractError> { + self.decrease_delegates_decimal(amount)?; + self.decrement_unique_delegations()?; + + // 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.unique_delegations == 0 { + self.operator += self.delegates; + self.delegates = Decimal::zero(); + } + Ok(()) + } + + pub fn undelegate(&mut self, delegation: &Delegation) -> Result { + let reward = self.determine_delegation_reward(delegation); + let full_amount = reward + delegation.dec_amount(); + self.remove_delegation_decimal(full_amount)?; + Ok(truncate_reward(full_amount, &delegation.amount.denom)) + } + + pub fn decrease_delegates_decimal( + &mut self, + amount: Decimal, + ) -> Result<(), MixnetContractError> { + if self.delegates < amount { + return Err(MixnetContractError::OverflowDecimalSubtraction { + minuend: self.delegates, + subtrahend: amount, + }); + } + + self.delegates -= amount; + Ok(()) + } + + pub fn decrease_operator_decimal( + &mut self, + amount: Decimal, + ) -> Result<(), MixnetContractError> { + if self.operator < amount { + return Err(MixnetContractError::OverflowDecimalSubtraction { + minuend: self.operator, + subtrahend: amount, + }); + } + + self.operator -= amount; + Ok(()) + } + + pub fn full_reward_ratio(&self) -> Decimal { + self.total_unit_reward //+ self.current_period_reward + } + + pub fn delegator_share(&self, amount: Decimal) -> Decimal { + if self.delegates.is_zero() { + Decimal::zero() + } else { + amount / self.delegates + } } } -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema)] +// operator information + data assigned by the contract(s) +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct MixNodeBond { + /// Unique id assigned to the bonded mixnode. + pub mix_id: MixId, + + /// Address of the owner of this mixnode. + pub owner: Addr, + + /// Original amount pledged by the operator of this node. + pub original_pledge: Coin, + + /// Layer assigned to this mixnode. + pub layer: Layer, + + /// Information provided by the operator for the purposes of bonding. + pub mix_node: MixNode, + + /// Entity who bonded this mixnode on behalf of the owner. + /// If exists, it's most likely the address of the vesting contract. + pub proxy: Option, + + /// Block height at which this mixnode has been bonded. + pub bonding_height: u64, + + /// Flag to indicate whether this node is in the process of unbonding, + /// that will conclude upon the epoch finishing. + pub is_unbonding: bool, +} + +impl MixNodeBond { + pub fn new( + mix_id: MixId, + owner: Addr, + original_pledge: Coin, + layer: Layer, + mix_node: MixNode, + proxy: Option, + bonding_height: u64, + ) -> Self { + MixNodeBond { + mix_id, + owner, + original_pledge, + layer, + mix_node, + proxy, + bonding_height, + is_unbonding: false, + } + } + + pub fn identity(&self) -> &str { + &self.mix_node.identity_key + } + + pub fn original_pledge(&self) -> &Coin { + &self.original_pledge + } + + pub fn owner(&self) -> &Addr { + &self.owner + } + + pub fn mix_node(&self) -> &MixNode { + &self.mix_node + } +} + +// information provided by the operator +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/Mixnode.ts") +)] pub struct MixNode { + /// Network address of this mixnode, for example 1.1.1.1:1234 or foo.mixnode.com pub host: String, + pub mix_port: u16, + pub verloc_port: u16, + pub http_api_port: u16, + + /// Base58-encoded x25519 public key used for sphinx key derivation. pub sphinx_key: SphinxKey, - /// Base58 encoded ed25519 EdDSA public key. + + /// Base58-encoded ed25519 EdDSA public key. pub identity_key: IdentityKey, + pub version: String, - pub profit_margin_percent: u8, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct MixNodeCostParams { + pub profit_margin_percent: Percent, + + /// Operating cost of the associated mixnode per the entire interval. + pub interval_operating_cost: Coin, +} + +impl MixNodeCostParams { + pub fn to_inline_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) + } +} + +impl MixNodeCostParams { + pub fn epoch_operating_cost(&self, epochs_in_interval: u32) -> Decimal { + Decimal::from_ratio(self.interval_operating_cost.amount, epochs_in_interval) + } } #[derive( @@ -144,7 +547,6 @@ pub struct MixNode { )] #[repr(u8)] pub enum Layer { - Gateway = 0, One = 1, Two = 2, Three = 3, @@ -152,496 +554,101 @@ pub enum Layer { impl From for String { fn from(layer: Layer) -> Self { - if layer == Layer::Gateway { - "gateway".to_string() - } else { - (layer as u8).to_string() - } + (layer as u8).to_string() } } -// cosmwasm's limited serde doesn't work with U128 directly -#[allow(non_snake_case)] -pub mod fixed_U128_as_string { - use super::U128; - use serde::de::Error; - use serde::Deserialize; - use std::str::FromStr; +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/UnbondedMixnode.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct UnbondedMixnode { + pub identity_key: IdentityKey, - pub fn serialize(val: &U128, serializer: S) -> Result - where - S: serde::Serializer, - { - let s = (*val).to_string(); - serializer.serialize_str(&s) - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - U128::from_str(&s).map_err(|err| { - D::Error::custom(format!( - "failed to deserialize U128 with its string representation - {}", - err - )) - }) - } -} - -// everything required to reward delegator of given mixnode -#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)] -pub struct DelegatorRewardParams { - reward_params: RewardParams, - - // to be completely honest I don't understand all consequences of using `#[schemars(with = "String")]` - // for U128 here, but it seems that CosmWasm is using the same attribute for their Uint128 - #[schemars(with = "String")] - #[serde(with = "fixed_U128_as_string")] - sigma: U128, - #[schemars(with = "String")] - #[serde(with = "fixed_U128_as_string")] - profit_margin: U128, - #[schemars(with = "String")] - #[serde(with = "fixed_U128_as_string")] - node_profit: U128, -} - -impl DelegatorRewardParams { - pub fn new( - sigma: U128, - profit_margin: U128, - node_profit: U128, - reward_params: RewardParams, - ) -> Self { - DelegatorRewardParams { - sigma, - profit_margin, - node_profit, - reward_params, - } - } - - pub fn determine_delegation_reward(&self, delegation_amount: Uint128) -> u128 { - if self.sigma == 0 { - return 0; - } - - // change all values into their fixed representations - let delegation_amount = U128::from_num(delegation_amount.u128()); - let staking_supply = U128::from_num(self.reward_params.staking_supply()); - - let scaled_delegation_amount = delegation_amount / staking_supply; - - // Div by zero checked above - let delegator_reward = - (ONE - self.profit_margin) * (scaled_delegation_amount / self.sigma) * self.node_profit; - - let reward = delegator_reward.max(U128::ZERO); - if let Some(int_reward) = reward.checked_cast() { - int_reward - } else { - error!( - "Could not cast delegator reward ({}) to u128, returning 0", - reward, - ); - 0u128 - } - } - - pub fn node_reward_params(&self) -> RewardParams { - self.reward_params - } -} - -#[derive(Debug, Clone, JsonSchema, PartialEq, Eq, Serialize, Deserialize, Copy)] -pub struct StoredNodeRewardResult { - reward: Uint128, - - #[schemars(with = "String")] - #[serde(with = "fixed_U128_as_string")] - lambda: U128, - - #[schemars(with = "String")] - #[serde(with = "fixed_U128_as_string")] - sigma: U128, -} - -impl StoredNodeRewardResult { - pub fn reward(&self) -> Uint128 { - self.reward - } - - pub fn lambda(&self) -> U128 { - self.lambda - } - - pub fn sigma(&self) -> U128 { - self.sigma - } -} - -impl TryFrom for StoredNodeRewardResult { - type Error = MixnetContractError; - - fn try_from(node_reward_result: NodeRewardResult) -> Result { - Ok(StoredNodeRewardResult { - reward: Uint128::new( - node_reward_result - .reward() - .checked_cast() - .ok_or(MixnetContractError::CastError)?, - ), - lambda: node_reward_result.lambda(), - sigma: node_reward_result.sigma(), - }) - } -} - -#[derive(Debug, Copy, Clone)] -pub struct NodeRewardResult { - reward: U128, - lambda: U128, - sigma: U128, -} - -impl NodeRewardResult { - pub fn reward(&self) -> U128 { - self.reward - } - - pub fn lambda(&self) -> U128 { - self.lambda - } - - pub fn sigma(&self) -> U128 { - self.sigma - } -} - -pub struct RewardEstimate { - pub total_node_reward: u64, - pub operator_reward: u64, - pub delegators_reward: u64, - pub node_profit: u64, - pub operator_cost: u64, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct MixNodeBond { - pub pledge_amount: Coin, - pub total_delegation: Coin, + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub owner: Addr, - pub layer: Layer, - pub block_height: u64, - pub mix_node: MixNode, + + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] pub proxy: Option, - pub accumulated_rewards: Option, + + #[cfg_attr(feature = "generate-ts", ts(type = "number"))] + pub unbonding_height: u64, } -impl MixNodeBond { - pub fn new( - pledge_amount: Coin, - owner: Addr, - layer: Layer, - block_height: u64, - mix_node: MixNode, - proxy: Option, - ) -> Self { - MixNodeBond { - total_delegation: coin(0, &pledge_amount.denom), - pledge_amount, - owner, - layer, - block_height, - mix_node, - proxy, - accumulated_rewards: None, - } - } - - pub fn accumulated_rewards(&self) -> Uint128 { - self.accumulated_rewards.unwrap_or_else(Uint128::zero) - } - - pub fn profit_margin(&self) -> U128 { - U128::from_num(self.mix_node.profit_margin_percent) / U128::from_num(100) - } - - pub fn identity(&self) -> &String { - &self.mix_node.identity_key - } - - pub fn pledge_amount(&self) -> Coin { - self.pledge_amount.clone() - } - - pub fn owner(&self) -> &Addr { - &self.owner - } - - pub fn mix_node(&self) -> &MixNode { - &self.mix_node - } - - // Takes into account accumulated rewards as well as current pledge and delegation amounts - pub fn total_bond(&self) -> Option { - if self.pledge_amount.denom != self.total_delegation.denom { - None - } else { - Some( - self.pledge_amount.amount.u128() - + self.total_delegation.amount.u128() - + self.accumulated_rewards().u128(), - ) - } - } - - pub fn total_delegation(&self) -> Coin { - self.total_delegation.clone() - } - - pub fn stake_saturation(&self, staking_supply: u128, rewarded_set_size: u32) -> U128 { - self.total_bond_to_staking_supply(staking_supply) * U128::from_num(rewarded_set_size) - } - - // TODO: There is an effect here when adding accumulted rewards to the total bond, ie accumulated rewards will not - // affect lambda, but will affect sigma, in turn over time, if left unclaimed operator rewards will not compound, but - // behave similarly to delegations. - // The question is should this be taken into account when calculating operator rewards? - pub fn pledge_to_staking_supply(&self, staking_supply: u128) -> U128 { - U128::from_num(self.pledge_amount().amount.u128()) / U128::from_num(staking_supply) - } - - pub fn total_bond_to_staking_supply(&self, staking_supply: u128) -> U128 { - U128::from_num(self.pledge_amount().amount.u128() + self.total_delegation().amount.u128()) - / U128::from_num(staking_supply) - } - - pub fn lambda_ticked(&self, params: &RewardParams) -> U128 { - // Ratio of a bond to the token circulating supply - self.lambda(params).min(params.one_over_k()) - } - - pub fn lambda(&self, params: &RewardParams) -> U128 { - // Ratio of a bond to the token circulating supply - self.pledge_to_staking_supply(params.staking_supply()) - } - - pub fn sigma_ticked(&self, params: &RewardParams) -> U128 { - // Ratio of a delegation to the the token circulating supply - self.sigma(params).min(params.one_over_k()) - } - - pub fn sigma(&self, params: &RewardParams) -> U128 { - // Ratio of a delegation to the the token circulating supply - self.total_bond_to_staking_supply(params.staking_supply()) - } - - pub fn estimate_reward( - &self, - base_operator_cost: u64, - params: &RewardParams, - ) -> Result { - let total_node_reward = self - .reward(params) - .reward() - .checked_to_num::() - .unwrap_or_default(); - let node_profit = self - .node_profit(params, base_operator_cost) - .checked_to_num::() - .unwrap_or_default(); - let operator_cost = params - .node - .operator_cost(base_operator_cost) - .checked_to_num::() - .unwrap_or_default(); - let operator_reward = self.operator_reward(params, base_operator_cost); - // Total reward has to be the sum of operator and delegator rewards - let delegators_reward = node_profit.saturating_sub(operator_reward); - - Ok(RewardEstimate { - total_node_reward: total_node_reward.try_into()?, - operator_reward: operator_reward.try_into()?, - delegators_reward: delegators_reward.try_into()?, - node_profit: node_profit.try_into()?, - operator_cost: operator_cost.try_into()?, - }) - } - - // keybase://chat/nymtech#dev-core/14473 - pub fn reward(&self, params: &RewardParams) -> NodeRewardResult { - let lambda_ticked = self.lambda_ticked(params); - let sigma_ticked = self.sigma_ticked(params); - - let reward = params.performance() - * params.epoch_reward_pool() - * (sigma_ticked * params.omega() - + params.alpha() * lambda_ticked * sigma_ticked * params.rewarded_set_size()) - / (ONE + params.alpha()); - - // we only need regular lambda and sigma to calculate operator and delegator rewards - NodeRewardResult { - reward, - lambda: self.lambda(params), - sigma: self.sigma(params), - } - } - - pub fn node_profit(&self, params: &RewardParams, base_operator_cost: u64) -> U128 { - self.reward(params) - .reward() - .saturating_sub(params.node.operator_cost(base_operator_cost)) - } - - pub fn operator_reward(&self, params: &RewardParams, base_operator_cost: u64) -> u128 { - let reward = self.reward(params); - if reward.sigma == 0u128 { - return 0; - } - - let profit = reward - .reward - .saturating_sub(params.node.operator_cost(base_operator_cost)); - - let operator_base_reward = reward - .reward - .min(params.node.operator_cost(base_operator_cost)); - // Div by zero checked above - let operator_reward = (self.profit_margin() - + (ONE - self.profit_margin()) * reward.lambda / reward.sigma) - * profit; - - let reward = (operator_reward + operator_base_reward).max(U128::from_num(0)); - - if let Some(int_reward) = reward.checked_cast() { - int_reward - } else { - error!( - "Could not cast reward ({}) to u128, returning 0 - mixnode {}", - reward, - self.identity() - ); - 0u128 - } - } - - pub fn sigma_ratio(&self, params: &RewardParams) -> U128 { - if self.total_bond_to_staking_supply(params.staking_supply()) < params.one_over_k() { - self.total_bond_to_staking_supply(params.staking_supply()) - } else { - params.one_over_k() - } - } - - pub fn reward_delegation( - &self, - delegation_amount: Uint128, - params: &RewardParams, - base_operator_cost: u64, - ) -> u128 { - let reward_params = DelegatorRewardParams::new( - self.sigma(params), - self.profit_margin(), - self.node_profit(params, base_operator_cost), - params.to_owned(), - ); - reward_params.determine_delegation_reward(delegation_amount) - } +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/MixNodeConfigUpdate.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct MixNodeConfigUpdate { + pub host: String, + pub mix_port: u16, + pub verloc_port: u16, + pub http_api_port: u16, + pub version: String, } -impl PartialOrd for MixNodeBond { - fn partial_cmp(&self, other: &Self) -> Option { - // first remove invalid cases - if self.pledge_amount.denom != self.total_delegation.denom { - return None; - } - - if other.pledge_amount.denom != other.total_delegation.denom { - return None; - } - - if self.pledge_amount.denom != other.pledge_amount.denom { - return None; - } - - // try to order by total bond + delegation - let total_cmp = (self.pledge_amount.amount + self.total_delegation.amount) - .partial_cmp(&(self.pledge_amount.amount + self.total_delegation.amount))?; - - if total_cmp != Ordering::Equal { - return Some(total_cmp); - } - - // then if those are equal, prefer higher bond over delegation - let pledge_cmp = self - .pledge_amount - .amount - .partial_cmp(&other.pledge_amount.amount)?; - if pledge_cmp != Ordering::Equal { - return Some(pledge_cmp); - } - - // then look at delegation (I'm not sure we can get here, but better safe than sorry) - let delegation_cmp = self - .total_delegation - .amount - .partial_cmp(&other.total_delegation.amount)?; - if delegation_cmp != Ordering::Equal { - return Some(delegation_cmp); - } - - // then check block height - let height_cmp = self.block_height.partial_cmp(&other.block_height)?; - if height_cmp != Ordering::Equal { - return Some(height_cmp); - } - - // finally go by the rest of the fields in order. It doesn't really matter at this point - // but we should be deterministic. - let owner_cmp = self.owner.partial_cmp(&other.owner)?; - if owner_cmp != Ordering::Equal { - return Some(owner_cmp); - } - - let layer_cmp = self.layer.partial_cmp(&other.layer)?; - if layer_cmp != Ordering::Equal { - return Some(layer_cmp); - } - - self.mix_node.partial_cmp(&other.mix_node) - } -} - -impl Display for MixNodeBond { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "amount: {} {}, owner: {}, identity: {}", - self.pledge_amount.amount, - self.pledge_amount.denom, - self.owner, - self.mix_node.identity_key - ) +impl MixNodeConfigUpdate { + pub fn to_inline_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct PagedMixnodeResponse { +pub struct PagedMixnodeBondsResponse { pub nodes: Vec, pub per_page: usize, - pub start_next_after: Option, + pub start_next_after: Option, } -impl PagedMixnodeResponse { +impl PagedMixnodeBondsResponse { + pub fn new(nodes: Vec, per_page: usize, start_next_after: Option) -> Self { + PagedMixnodeBondsResponse { + nodes, + per_page, + start_next_after, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct PagedMixnodesDetailsResponse { + pub nodes: Vec, + pub per_page: usize, + pub start_next_after: Option, +} + +impl PagedMixnodesDetailsResponse { pub fn new( - nodes: Vec, + nodes: Vec, per_page: usize, - start_next_after: Option, + start_next_after: Option, ) -> Self { - PagedMixnodeResponse { + PagedMixnodesDetailsResponse { + nodes, + per_page, + start_next_after, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct PagedUnbondedMixnodesResponse { + pub nodes: Vec<(MixId, UnbondedMixnode)>, + pub per_page: usize, + pub start_next_after: Option, +} + +impl PagedUnbondedMixnodesResponse { + pub fn new( + nodes: Vec<(MixId, UnbondedMixnode)>, + per_page: usize, + start_next_after: Option, + ) -> Self { + PagedUnbondedMixnodesResponse { nodes, per_page, start_next_after, @@ -652,114 +659,30 @@ impl PagedMixnodeResponse { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixOwnershipResponse { pub address: Addr, - pub mixnode: Option, + pub mixnode_details: Option, } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct MixnodeBondResponse { - pub identity: IdentityKey, - pub mixnode: Option, +pub struct MixnodeDetailsResponse { + pub mix_id: MixId, + pub mixnode_details: Option, } -#[cfg(test)] -mod tests { - use super::*; - - fn mixnode_fixture() -> MixNode { - MixNode { - host: "1.1.1.1".to_string(), - mix_port: 123, - verloc_port: 456, - http_api_port: 789, - sphinx_key: "sphinxkey".to_string(), - identity_key: "identitykey".to_string(), - version: "0.11.0".to_string(), - profit_margin_percent: 10, - } - } - - #[test] - fn mixnode_bond_partial_ord() { - let _150foos = Coin::new(150, "foo"); - let _50foos = Coin::new(50, "foo"); - let _0foos = Coin::new(0, "foo"); - - let mix1 = MixNodeBond { - pledge_amount: _150foos.clone(), - total_delegation: _50foos.clone(), - owner: Addr::unchecked("foo1"), - layer: Layer::One, - block_height: 100, - mix_node: mixnode_fixture(), - proxy: None, - accumulated_rewards: Some(Uint128::zero()), - }; - - let mix2 = MixNodeBond { - pledge_amount: _150foos.clone(), - total_delegation: _50foos.clone(), - owner: Addr::unchecked("foo2"), - layer: Layer::One, - block_height: 120, - mix_node: mixnode_fixture(), - proxy: None, - accumulated_rewards: Some(Uint128::zero()), - }; - - let mix3 = MixNodeBond { - pledge_amount: _50foos, - total_delegation: _150foos.clone(), - owner: Addr::unchecked("foo3"), - layer: Layer::One, - block_height: 120, - mix_node: mixnode_fixture(), - proxy: None, - accumulated_rewards: Some(Uint128::zero()), - }; - - let mix4 = MixNodeBond { - pledge_amount: _150foos.clone(), - total_delegation: _0foos.clone(), - owner: Addr::unchecked("foo4"), - layer: Layer::One, - block_height: 120, - mix_node: mixnode_fixture(), - proxy: None, - accumulated_rewards: Some(Uint128::zero()), - }; - - let mix5 = MixNodeBond { - pledge_amount: _0foos, - total_delegation: _150foos, - owner: Addr::unchecked("foo5"), - layer: Layer::One, - block_height: 120, - mix_node: mixnode_fixture(), - proxy: None, - accumulated_rewards: Some(Uint128::zero()), - }; - - // summary: - // mix1: 150bond + 50delegation, foo1, 100 - // mix2: 150bond + 50delegation, foo2, 120 - // mix3: 50bond + 150delegation, foo3, 120 - // mix4: 150bond + 0delegation, foo4, 120 - // mix5: 0bond + 150delegation, foo5, 120 - - // highest total bond+delegation is used - // then bond followed by delegation - // finally just the rest of the fields - - // mix1 has higher total than mix4 or mix5 - assert!(mix1 > mix4); - assert!(mix1 > mix5); - - // mix1 has the same total as mix3, however, mix1 has more tokens in bond - assert!(mix1 > mix3); - // same case for mix4 and mix5 - assert!(mix4 > mix5); - - // same bond and delegation, so it's just ordered by height - assert!(mix1 < mix2); - } +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct MixnodeRewardingDetailsResponse { + pub mix_id: MixId, + pub rewarding_details: Option, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct UnbondedMixnodeResponse { + pub mix_id: MixId, + pub unbonded_info: Option, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct StakeSaturationResponse { + 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 6d71de8887..c2046af9b0 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -1,160 +1,354 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::reward_params::NodeRewardParams; -use crate::ContractStateParams; +use crate::delegation::OwnerProxySubKey; +use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; +use crate::reward_params::{ + IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams, +}; +use crate::{delegation, ContractStateParams, MixId, Percent}; use crate::{Gateway, IdentityKey, MixNode}; +use cosmwasm_std::Decimal; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::time::Duration; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] pub struct InstantiateMsg { pub rewarding_validator_address: String, - pub mixnet_denom: String, + pub vesting_contract_address: String, + + pub rewarding_denom: String, + pub epochs_in_interval: u32, + pub epoch_duration: Duration, + pub initial_rewarding_params: InitialRewardingParams, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] +pub struct InitialRewardingParams { + pub initial_reward_pool: Decimal, + pub initial_staking_supply: Decimal, + + pub staking_supply_scale_factor: Percent, + pub sybil_resistance: Percent, + pub active_set_work_factor: Decimal, + pub interval_pool_emission: Percent, + + pub rewarded_set_size: u32, + pub active_set_size: u32, +} + +impl InitialRewardingParams { + pub fn into_rewarding_params(self, epochs_in_interval: u32) -> RewardingParams { + let epoch_reward_budget = self.initial_reward_pool + / Decimal::from_atomics(epochs_in_interval, 0).unwrap() + * self.interval_pool_emission; + let stake_saturation_point = + self.initial_staking_supply / Decimal::from_atomics(self.rewarded_set_size, 0).unwrap(); + + RewardingParams { + interval: IntervalRewardParams { + reward_pool: self.initial_reward_pool, + staking_supply: self.initial_staking_supply, + staking_supply_scale_factor: self.staking_supply_scale_factor, + epoch_reward_budget, + stake_saturation_point, + sybil_resistance: self.sybil_resistance, + active_set_work_factor: self.active_set_work_factor, + interval_pool_emission: self.interval_pool_emission, + }, + rewarded_set_size: self.rewarded_set_size, + active_set_size: self.active_set_size, + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] pub enum ExecuteMsg { + // state/sys-params-related UpdateRewardingValidatorAddress { address: String, }, - InitEpoch {}, - ReconcileDelegations {}, - CheckpointMixnodes {}, - CompoundOperatorRewardOnBehalf { - owner: String, + UpdateContractStateParams { + updated_parameters: ContractStateParams, }, - CompoundDelegatorRewardOnBehalf { - owner: String, - mix_identity: IdentityKey, + UpdateActiveSetSize { + active_set_size: u32, + force_immediately: bool, }, - CompoundOperatorReward {}, - CompoundDelegatorReward { - mix_identity: IdentityKey, + UpdateRewardingParams { + updated_params: IntervalRewardingParamsUpdate, + force_immediately: bool, }, - CompoundReward { - operator: Option, - delegator: Option, - mix_identity: Option, - proxy: Option, + UpdateIntervalConfig { + epochs_in_interval: u32, + epoch_duration_secs: u64, + force_immediately: bool, }, + AdvanceCurrentEpoch { + new_rewarded_set: Vec, + expected_active_set_size: u32, + }, + ReconcileEpochEvents { + limit: Option, + }, + + // mixnode-related: BondMixnode { mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, }, - UnbondMixnode {}, - UpdateMixnodeConfig { - profit_margin_percent: u8, - }, - UpdateMixnodeConfigOnBehalf { - profit_margin_percent: u8, - owner: String, - }, - BondGateway { - gateway: Gateway, - owner_signature: String, - }, - UnbondGateway {}, - UpdateContractStateParams(ContractStateParams), - - DelegateToMixnode { - mix_identity: IdentityKey, - }, - - UndelegateFromMixnode { - mix_identity: IdentityKey, - }, - - RewardMixnode { - identity: IdentityKey, - // percentage value in range 0-100 - params: NodeRewardParams, - }, - // RewardNextMixDelegators { - // mix_identity: IdentityKey, - // // id of the current rewarding interval - // interval_id: u32, - // }, - DelegateToMixnodeOnBehalf { - mix_identity: IdentityKey, - delegate: String, - }, - UndelegateFromMixnodeOnBehalf { - mix_identity: IdentityKey, - delegate: String, - }, BondMixnodeOnBehalf { mix_node: MixNode, - owner: String, + cost_params: MixNodeCostParams, owner_signature: String, + owner: String, }, + UnbondMixnode {}, UnbondMixnodeOnBehalf { owner: String, }, + UpdateMixnodeCostParams { + new_costs: MixNodeCostParams, + }, + UpdateMixnodeCostParamsOnBehalf { + new_costs: MixNodeCostParams, + owner: String, + }, + UpdateMixnodeConfig { + new_config: MixNodeConfigUpdate, + }, + UpdateMixnodeConfigOnBehalf { + new_config: MixNodeConfigUpdate, + owner: String, + }, + + // gateway-related: + BondGateway { + gateway: Gateway, + owner_signature: String, + }, BondGatewayOnBehalf { gateway: Gateway, owner: String, owner_signature: String, }, + UnbondGateway {}, UnbondGatewayOnBehalf { owner: String, }, - WriteRewardedSet { - rewarded_set: Vec, - expected_active_set_size: u32, + + // delegation-related: + DelegateToMixnode { + mix_id: MixId, }, - // AdvanceCurrentInterval {}, - AdvanceCurrentEpoch {}, - ClaimOperatorReward {}, - ClaimOperatorRewardOnBehalf { + DelegateToMixnodeOnBehalf { + mix_id: MixId, + delegate: String, + }, + UndelegateFromMixnode { + mix_id: MixId, + }, + UndelegateFromMixnodeOnBehalf { + mix_id: MixId, + delegate: String, + }, + + // reward-related + RewardMixnode { + mix_id: MixId, + performance: Performance, + }, + WithdrawOperatorReward {}, + WithdrawOperatorRewardOnBehalf { owner: String, }, - ClaimDelegatorReward { - mix_identity: IdentityKey, + WithdrawDelegatorReward { + mix_id: MixId, }, - ClaimDelegatorRewardOnBehalf { - mix_identity: IdentityKey, + WithdrawDelegatorRewardOnBehalf { + mix_id: MixId, owner: String, }, + + // testing-only + #[cfg(feature = "contract-testing")] + TestingResolveAllPendingEvents { + limit: Option, + }, +} + +impl ExecuteMsg { + pub fn default_memo(&self) -> String { + match self { + ExecuteMsg::UpdateRewardingValidatorAddress { address } => { + format!("updating rewarding validator to {}", address) + } + ExecuteMsg::UpdateContractStateParams { .. } => { + "updating mixnet state parameters".into() + } + ExecuteMsg::UpdateActiveSetSize { + active_set_size, + force_immediately, + } => format!( + "updating active set size to {}. forced: {}", + active_set_size, force_immediately + ), + ExecuteMsg::UpdateRewardingParams { + force_immediately, .. + } => format!( + "updating mixnet rewarding parameters. forced: {}", + force_immediately + ), + ExecuteMsg::UpdateIntervalConfig { + force_immediately, .. + } => format!( + "updating mixnet interval configuration. forced: {}", + force_immediately + ), + ExecuteMsg::AdvanceCurrentEpoch { .. } => "advancing current epoch".into(), + ExecuteMsg::ReconcileEpochEvents { .. } => "reconciling epoch events".into(), + ExecuteMsg::BondMixnode { mix_node, .. } => { + format!("bonding mixnode {}", mix_node.identity_key) + } + ExecuteMsg::BondMixnodeOnBehalf { mix_node, .. } => { + format!("bonding mixnode {} on behalf", mix_node.identity_key) + } + ExecuteMsg::UnbondMixnode { .. } => "unbonding mixnode".into(), + ExecuteMsg::UnbondMixnodeOnBehalf { .. } => "unbonding mixnode on behalf".into(), + ExecuteMsg::UpdateMixnodeCostParams { .. } => "updating mixnode cost parameters".into(), + ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { .. } => { + "updating mixnode cost parameters on behalf".into() + } + ExecuteMsg::UpdateMixnodeConfig { .. } => "updating mixnode configuration".into(), + ExecuteMsg::UpdateMixnodeConfigOnBehalf { .. } => { + "updating mixnode configuration on behalf".into() + } + ExecuteMsg::BondGateway { gateway, .. } => { + format!("bonding gateway {}", gateway.identity_key) + } + ExecuteMsg::BondGatewayOnBehalf { gateway, .. } => { + format!("bonding gateway {} on behalf", gateway.identity_key) + } + ExecuteMsg::UnbondGateway { .. } => "unbonding gateway".into(), + ExecuteMsg::UnbondGatewayOnBehalf { .. } => "unbonding gateway on behalf".into(), + ExecuteMsg::DelegateToMixnode { mix_id } => format!("delegating to mixnode {}", mix_id), + ExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, .. } => { + format!("delegating to mixnode {} on behalf", mix_id) + } + ExecuteMsg::UndelegateFromMixnode { mix_id } => { + format!("removing delegation from mixnode {}", mix_id) + } + ExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, .. } => { + format!("removing delegation from mixnode {} on behalf", mix_id) + } + ExecuteMsg::RewardMixnode { + mix_id, + performance, + } => format!( + "rewarding mixnode {} for performance {}", + mix_id, performance + ), + ExecuteMsg::WithdrawOperatorReward { .. } => "withdrawing operator reward".into(), + ExecuteMsg::WithdrawOperatorRewardOnBehalf { .. } => { + "withdrawing operator reward on behalf".into() + } + ExecuteMsg::WithdrawDelegatorReward { mix_id } => { + format!("withdrawing delegator reward from mixnode {}", mix_id) + } + ExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, .. } => format!( + "withdrawing delegator reward from mixnode {} on behalf", + mix_id + ), + #[cfg(feature = "contract-testing")] + ExecuteMsg::TestingResolveAllPendingEvents { .. } => { + "resolving all pending events".into() + } + } + } } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { - GetBlacklistedNodes {}, - GetCurrentOperatorCost {}, - GetRewardingValidatorAddress {}, - GetAllDelegationKeys {}, - DebugGetAllDelegationValues {}, + // state/sys-params-related GetContractVersion {}, - GetMixNodes { + GetRewardingValidatorAddress {}, + GetStateParams {}, + GetState {}, + GetRewardingParams {}, + GetCurrentIntervalDetails {}, + GetRewardedSet { limit: Option, - start_after: Option, + start_after: Option, }, + + // mixnode-related: + GetMixNodeBonds { + limit: Option, + start_after: Option, + }, + GetMixNodesDetailed { + limit: Option, + start_after: Option, + }, + GetUnbondedMixNodes { + limit: Option, + start_after: Option, + }, + GetUnbondedMixNodesByOwner { + owner: String, + limit: Option, + start_after: Option, + }, + GetUnbondedMixNodesByIdentityKey { + identity_key: String, + limit: Option, + start_after: Option, + }, + GetOwnedMixnode { + address: String, + }, + GetMixnodeDetails { + mix_id: MixId, + }, + GetMixnodeRewardingDetails { + mix_id: MixId, + }, + GetStakeSaturation { + mix_id: MixId, + }, + GetUnbondedMixNodeInformation { + mix_id: MixId, + }, + GetBondedMixnodeDetailsByIdentity { + mix_identity: IdentityKey, + }, + GetLayerDistribution {}, + + // gateway-related: GetGateways { start_after: Option, limit: Option, }, - OwnsMixnode { - address: String, - }, - OwnsGateway { - address: String, - }, - GetMixnodeBond { - identity: IdentityKey, - }, GetGatewayBond { identity: IdentityKey, }, - StateParams {}, + GetOwnedGateway { + address: String, + }, + + // delegation-related: // gets all [paged] delegations associated with particular mixnode GetMixnodeDelegations { - mix_identity: IdentityKey, + 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<(String, u64)>, + start_after: Option, limit: Option, }, // gets all [paged] delegations associated with particular delegator @@ -162,82 +356,56 @@ 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, + start_after: Option<(MixId, OwnerProxySubKey)>, limit: Option, }, // gets delegation associated with particular mixnode, delegator pair GetDelegationDetails { - mix_identity: IdentityKey, + mix_id: MixId, delegator: String, proxy: Option, }, - LayerDistribution {}, - GetRewardPool {}, - GetCirculatingSupply {}, - GetStakingSupply {}, - GetIntervalRewardPercent {}, - GetSybilResistancePercent {}, - GetActiveSetWorkFactor {}, - GetRewardingStatus { - mix_identity: IdentityKey, - interval_id: u32, - }, - GetRewardedSet { - height: Option, - start_after: Option, + // gets all delegations in the system + GetAllDelegations { + start_after: Option, limit: Option, }, - GetRewardedSetUpdateDetails {}, - GetCurrentRewardedSetHeight {}, - GetRewardedSetRefreshBlocks {}, - GetCurrentEpoch {}, - GetEpochsInInterval {}, - QueryOperatorReward { + + // rewards related + GetPendingOperatorReward { address: String, }, - QueryDelegatorReward { + GetPendingMixNodeOperatorReward { + mix_id: MixId, + }, + GetPendingDelegatorReward { address: String, - mix_identity: IdentityKey, + mix_id: MixId, proxy: Option, }, - GetPendingDelegationEvents { - owner_address: String, - proxy_address: Option, + // given the provided performance, estimate the reward at the end of the current epoch + GetEstimatedCurrentEpochOperatorReward { + mix_id: MixId, + estimated_performance: Performance, }, - GetCheckpointsForMixnode { - mix_identity: IdentityKey, + GetEstimatedCurrentEpochDelegatorReward { + address: String, + mix_id: MixId, + proxy: Option, + estimated_performance: Performance, }, - GetMixnodeAtHeight { - mix_identity: IdentityKey, - height: u64, + + // interval-related + GetPendingEpochEvents { + limit: Option, + start_after: Option, + }, + GetPendingIntervalEvents { + limit: Option, + start_after: Option, }, } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct MigrateMsg { - pub mixnet_denom: String, - nodes_to_remove: Option>, -} - -impl MigrateMsg { - pub fn nodes_to_remove(&self) -> Vec { - self.nodes_to_remove.clone().unwrap_or_default() - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -pub struct NodeToRemove { - owner: String, - proxy: Option, -} - -impl NodeToRemove { - pub fn owner(&self) -> &str { - &self.owner - } - - pub fn proxy(&self) -> Option<&String> { - self.proxy.as_ref() - } -} +pub struct MigrateMsg {} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs new file mode 100644 index 0000000000..fce8bd8cf0 --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs @@ -0,0 +1,107 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mixnode::MixNodeCostParams; +use crate::reward_params::IntervalRewardingParamsUpdate; +use crate::{BlockHeight, EpochEventId, IntervalEventId, MixId}; +use cosmwasm_std::{Addr, Coin}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PendingEpochEvent { + pub id: EpochEventId, + pub event: PendingEpochEventData, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +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: MixId, + amount: Coin, + proxy: Option, + }, + Undelegate { + owner: Addr, + mix_id: MixId, + proxy: Option, + }, + UnbondMixnode { + 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 { + id: data.0, + event: data.1, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PendingIntervalEvent { + pub id: EpochEventId, + pub event: PendingIntervalEventData, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct PendingIntervalEventData { + pub created_at: BlockHeight, + pub kind: PendingIntervalEventKind, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum PendingIntervalEventKind { + ChangeMixCostParams { + mix_id: MixId, + new_costs: MixNodeCostParams, + }, + + UpdateRewardingParams { + update: IntervalRewardingParamsUpdate, + }, + UpdateIntervalConfig { + epochs_in_interval: u32, + epoch_duration_secs: u64, + }, +} + +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 { + id: data.0, + event: data.1, + } + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs index c4bafa0b90..c8e38fbe5b 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -1,288 +1,277 @@ -use crate::{error::MixnetContractError, mixnode::StoredNodeRewardResult, ONE, U128}; -use az::CheckedCast; -use cosmwasm_std::Uint128; +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{error::MixnetContractError, Percent}; +use cosmwasm_std::Decimal; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, JsonSchema, PartialEq, Eq, Serialize, Deserialize, Copy)] -pub struct NodeEpochRewards { - params: NodeRewardParams, - result: StoredNodeRewardResult, - epoch_id: u32, +pub type Performance = Percent; + +/// Parameters required by the mix-mining reward distribution that do not change during an interval. +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/IntervalRewardParams.ts") +)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema)] +pub struct IntervalRewardParams { + /// Current value of the rewarding pool. + /// It is expected to be constant throughout the interval. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub reward_pool: Decimal, + + /// Current value of the staking supply. + /// It is expected to be constant throughout the interval. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub staking_supply: Decimal, + + /// Defines the percentage of stake needed to reach saturation for all of the nodes in the rewarded set. + /// Also known as `beta`. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub staking_supply_scale_factor: Percent, + + // computed values + /// Current value of the computed reward budget per epoch, per node. + /// It is expected to be constant throughout the interval. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub epoch_reward_budget: Decimal, + + /// Current value of the stake saturation point. + /// It is expected to be constant throughout the interval. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub stake_saturation_point: Decimal, + + // constants(-ish) + // default: 30% + /// Current value of the sybil resistance percent (`alpha`). + /// It is not really expected to be changing very often. + /// As a matter of fact, unless there's a very specific reason, it should remain constant. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub sybil_resistance: Percent, + + // default: 10 + /// Current active set work factor. + /// It is not really expected to be changing very often. + /// As a matter of fact, unless there's a very specific reason, it should remain constant. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub active_set_work_factor: Decimal, + + // default: 2% + /// Current maximum interval pool emission. + /// Assuming all nodes in the rewarded set are fully saturated and have 100% performance, + /// this % of the reward pool would get distributed in rewards to all operators and its delegators. + /// It is not really expected to be changing very often. + /// As a matter of fact, unless there's a very specific reason, it should remain constant. + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub interval_pool_emission: Percent, } -impl NodeEpochRewards { - pub fn new(params: NodeRewardParams, result: StoredNodeRewardResult, epoch_id: u32) -> Self { - Self { - params, - result, - epoch_id, - } - } - - pub fn epoch_id(&self) -> u32 { - self.epoch_id - } - - pub fn sigma(&self) -> U128 { - self.result.sigma() - } - - pub fn lambda(&self) -> U128 { - self.result.lambda() - } - - pub fn params(&self) -> NodeRewardParams { - self.params - } - - pub fn reward(&self) -> Uint128 { - self.result.reward() - } - - pub fn operator_cost(&self, base_operator_cost: u64) -> U128 { - self.params.operator_cost(base_operator_cost) - } - - pub fn node_profit(&self, base_operator_cost: u64) -> U128 { - let reward = U128::from_num(self.reward().u128()); - // if operating cost is higher then the reward node profit is 0 - reward.saturating_sub(self.operator_cost(base_operator_cost)) - } - - pub fn operator_reward( - &self, - profit_margin: U128, - base_operator_cost: u64, - ) -> Result { - let reward = self.node_profit(base_operator_cost); - let operator_base_reward = reward.min(self.operator_cost(base_operator_cost)); - let div_by_zero_check = if let Some(value) = self.lambda().checked_div(self.sigma()) { - value - } else { - return Err(MixnetContractError::DivisionByZero); - }; - let operator_reward = (profit_margin + (ONE - profit_margin) * div_by_zero_check) * reward; - - let reward = (operator_reward + operator_base_reward).max(U128::from_num(0u128)); - - if let Some(int_reward) = reward.checked_cast() { - Ok(Uint128::new(int_reward)) - } else { - Err(MixnetContractError::CastError) - } - } - - pub fn delegation_reward( - &self, - delegation_amount: Uint128, - profit_margin: U128, - base_operator_cost: u64, - epoch_reward_params: EpochRewardParams, - ) -> Result { - // change all values into their fixed representations - let delegation_amount = U128::from_num(delegation_amount.u128()); - let staking_supply = U128::from_num(epoch_reward_params.staking_supply()); - - let scaled_delegation_amount = delegation_amount / staking_supply; - - let check_div_by_zero = - if let Some(value) = scaled_delegation_amount.checked_div(self.sigma()) { - value - } else { - return Err(MixnetContractError::DivisionByZero); - }; - - let delegator_reward = - (ONE - profit_margin) * check_div_by_zero * self.node_profit(base_operator_cost); - - let reward = delegator_reward.max(U128::ZERO); - if let Some(int_reward) = reward.checked_cast() { - Ok(Uint128::new(int_reward)) - } else { - Err(MixnetContractError::CastError) - } +impl IntervalRewardParams { + pub fn to_inline_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } -#[derive(Debug, Clone, JsonSchema, PartialEq, Eq, Serialize, Deserialize, Copy)] -pub struct EpochRewardParams { - epoch_reward_pool: Uint128, - rewarded_set_size: Uint128, - active_set_size: Uint128, - #[serde(alias = "circulating_supply")] - staking_supply: Uint128, - sybil_resistance_percent: u8, - active_set_work_factor: u8, +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/RewardingParams.ts") +)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema)] +pub struct RewardingParams { + /// Parameters that should remain unchanged throughout an interval. + pub interval: IntervalRewardParams, + + // while the active set size can change between epochs to accommodate for bandwidth demands, + // the active set size should be unchanged between epochs and should only be adjusted between + // intervals. However, it makes more sense to keep both of those values together as they're + // very strongly related to each other. + pub rewarded_set_size: u32, + pub active_set_size: u32, } -impl EpochRewardParams { - pub fn new( - epoch_reward_pool: u128, - rewarded_set_size: u128, - active_set_size: u128, - staking_supply: u128, - sybil_resistance_percent: u8, - active_set_work_factor: u8, - ) -> EpochRewardParams { - EpochRewardParams { - epoch_reward_pool: Uint128::new(epoch_reward_pool), - rewarded_set_size: Uint128::new(rewarded_set_size), - active_set_size: Uint128::new(active_set_size), - staking_supply: Uint128::new(staking_supply), - sybil_resistance_percent, - active_set_work_factor, +impl RewardingParams { + pub fn active_node_work(&self) -> Decimal { + self.interval.active_set_work_factor * self.standby_node_work() + } + + pub fn standby_node_work(&self) -> Decimal { + let f = self.interval.active_set_work_factor; + let k = self.dec_rewarded_set_size(); + let one = Decimal::one(); + + // nodes in reserve + let k_r = self.dec_standby_set_size(); + + one / (f * k - (f - one) * k_r) + } + + pub fn dec_rewarded_set_size(&self) -> Decimal { + // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal + // with 0 decimal places + Decimal::from_atomics(self.rewarded_set_size, 0).unwrap() + } + + pub fn dec_active_set_size(&self) -> Decimal { + // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal + // with 0 decimal places + Decimal::from_atomics(self.active_set_size, 0).unwrap() + } + + fn dec_standby_set_size(&self) -> Decimal { + // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal + // with 0 decimal places + Decimal::from_atomics(self.rewarded_set_size - self.active_set_size, 0).unwrap() + } + + pub fn apply_epochs_in_interval_change(&mut self, new_epochs_in_interval: u32) { + self.interval.epoch_reward_budget = self.interval.reward_pool + / Decimal::from_atomics(new_epochs_in_interval, 0).unwrap() + * self.interval.interval_pool_emission; + } + + pub fn try_change_active_set_size( + &mut self, + new_active_set_size: u32, + ) -> Result<(), MixnetContractError> { + if new_active_set_size == 0 { + return Err(MixnetContractError::ZeroActiveSet); } - } - // technically it's identical to what would have been derived with a Default implementation, - // however, I prefer to be explicit about it, as a `Default::default` value makes no sense - // apart from the `ValidatorCacheInner` context, where this value is not going to be touched anyway - // (it's guarded behind an `initialised` flag) - pub fn new_empty() -> Self { - EpochRewardParams { - epoch_reward_pool: Uint128::new(0), - staking_supply: Uint128::new(0), - sybil_resistance_percent: 0, - rewarded_set_size: Uint128::new(0), - active_set_size: Uint128::new(0), - active_set_work_factor: 0, + if new_active_set_size > self.rewarded_set_size { + return Err(MixnetContractError::InvalidActiveSetSize); } + + self.active_set_size = new_active_set_size; + Ok(()) } - pub fn rewarded_set_size(&self) -> u128 { - self.rewarded_set_size.u128() - } + pub fn try_apply_updates( + &mut self, + updates: IntervalRewardingParamsUpdate, + epochs_in_interval: u32, + ) -> Result<(), MixnetContractError> { + if !updates.contains_updates() { + return Err(MixnetContractError::EmptyParamsChangeMsg); + } - pub fn active_set_size(&self) -> u128 { - self.active_set_size.u128() - } + let mut recompute_epoch_budget = false; + let mut recompute_saturation_point = false; - pub fn staking_supply(&self) -> u128 { - self.staking_supply.u128() - } + if let Some(reward_pool) = updates.reward_pool { + recompute_epoch_budget = true; + self.interval.reward_pool = reward_pool; + } - pub fn epoch_reward_pool(&self) -> u128 { - self.epoch_reward_pool.u128() - } + if let Some(staking_supply) = updates.staking_supply { + recompute_saturation_point = true; + self.interval.staking_supply = staking_supply; + } - pub fn sybil_resistance_percent(&self) -> u8 { - self.sybil_resistance_percent - } + if let Some(staking_supply_scale_factor) = updates.staking_supply_scale_factor { + self.interval.staking_supply_scale_factor = staking_supply_scale_factor + } - pub fn active_set_work_factor(&self) -> u8 { - self.active_set_work_factor + if let Some(sybil_resistance_percent) = updates.sybil_resistance_percent { + self.interval.sybil_resistance = sybil_resistance_percent; + } + + if let Some(active_set_work_factor) = updates.active_set_work_factor { + self.interval.active_set_work_factor = active_set_work_factor; + } + + if let Some(interval_pool_emission) = updates.interval_pool_emission { + recompute_epoch_budget = true; + self.interval.interval_pool_emission = interval_pool_emission; + } + + if let Some(rewarded_set_size) = updates.rewarded_set_size { + if rewarded_set_size == 0 { + return Err(MixnetContractError::ZeroRewardedSet); + } + if rewarded_set_size < self.active_set_size { + return Err(MixnetContractError::InvalidRewardedSetSize); + } + + recompute_saturation_point = true; + self.rewarded_set_size = rewarded_set_size; + } + + if recompute_epoch_budget { + self.interval.epoch_reward_budget = self.interval.reward_pool + / Decimal::from_atomics(epochs_in_interval, 0).unwrap() + * self.interval.interval_pool_emission; + } + + if recompute_saturation_point { + self.interval.stake_saturation_point = self.interval.staking_supply + / Decimal::from_atomics(self.rewarded_set_size, 0).unwrap(); + } + + Ok(()) } } -#[derive(Debug, Clone, JsonSchema, PartialEq, Eq, Serialize, Deserialize, Copy)] +// TODO: possibly refactor this +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema)] pub struct NodeRewardParams { - reward_blockstamp: u64, - uptime: Uint128, - in_active_set: bool, + pub performance: Percent, + pub in_active_set: bool, } impl NodeRewardParams { - pub fn new(reward_blockstamp: u64, uptime: u128, in_active_set: bool) -> NodeRewardParams { + pub fn new(performance: Percent, in_active_set: bool) -> Self { NodeRewardParams { - reward_blockstamp, - uptime: Uint128::new(uptime), + performance, in_active_set, } } - - pub fn operator_cost(&self, base_operator_cost: u64) -> U128 { - self.performance() * U128::from_num(base_operator_cost) - } - - pub fn uptime(&self) -> Uint128 { - self.uptime - } - - pub fn performance(&self) -> U128 { - U128::from_num(self.uptime.u128()) / U128::from_num(100) - } - - pub fn set_reward_blockstamp(&mut self, blockstamp: u64) { - self.reward_blockstamp = blockstamp; - } } -#[derive(Debug, Clone, JsonSchema, PartialEq, Eq, Serialize, Deserialize, Copy)] -pub struct RewardParams { - pub epoch: EpochRewardParams, - pub node: NodeRewardParams, +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/IntervalRewardingParamsUpdate.ts") +)] +#[derive( + Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema, +)] +pub struct IntervalRewardingParamsUpdate { + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub reward_pool: Option, + + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub staking_supply: Option, + + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub staking_supply_scale_factor: Option, + + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub sybil_resistance_percent: Option, + + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub active_set_work_factor: Option, + + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub interval_pool_emission: Option, + + pub rewarded_set_size: Option, } -impl RewardParams { - pub fn new(epoch: EpochRewardParams, node: NodeRewardParams) -> RewardParams { - RewardParams { epoch, node } +impl IntervalRewardingParamsUpdate { + pub fn contains_updates(&self) -> bool { + // essentially at least a single field has to be a `Some` + self.reward_pool.is_some() + || self.staking_supply.is_some() + || self.staking_supply_scale_factor.is_some() + || self.sybil_resistance_percent.is_some() + || self.active_set_work_factor.is_some() + || self.interval_pool_emission.is_some() + || self.rewarded_set_size.is_some() } - pub fn omega(&self) -> U128 { - // As per keybase://chat/nymtech#tokeneconomics/1179 - let denom = self.active_set_work_factor() * U128::from_num(self.rewarded_set_size()) - - (self.active_set_work_factor() - ONE) * U128::from_num(self.idle_nodes().u128()); - - if denom == 0 { - return U128::ZERO; - } - - // Div by zero checked above - if self.in_active_set() { - // work_active = factor / (factor * self.network.k[month] - (factor - 1) * idle_nodes) - self.active_set_work_factor() / denom * self.rewarded_set_size() - } else { - // work_idle = 1 / (factor * self.network.k[month] - (factor - 1) * idle_nodes) - ONE / denom * self.rewarded_set_size() - } - } - - pub fn idle_nodes(&self) -> Uint128 { - self.epoch.rewarded_set_size - self.epoch.active_set_size - } - - pub fn active_set_work_factor(&self) -> U128 { - U128::from_num(self.epoch.active_set_work_factor) - } - - pub fn in_active_set(&self) -> bool { - self.node.in_active_set - } - - pub fn performance(&self) -> U128 { - self.node.performance() - } - - pub fn set_reward_blockstamp(&mut self, blockstamp: u64) { - self.node.reward_blockstamp = blockstamp; - } - - pub fn epoch_reward_pool(&self) -> u128 { - self.epoch.epoch_reward_pool.u128() - } - - pub fn rewarded_set_size(&self) -> u128 { - self.epoch.rewarded_set_size.u128() - } - - pub fn staking_supply(&self) -> u128 { - self.epoch.staking_supply.u128() - } - - pub fn reward_blockstamp(&self) -> u64 { - self.node.reward_blockstamp - } - - pub fn uptime(&self) -> Uint128 { - self.node.uptime() - } - - pub fn one_over_k(&self) -> U128 { - ONE / U128::from_num(self.epoch.rewarded_set_size.u128()) - } - - pub fn alpha(&self) -> U128 { - U128::from_num(self.epoch.sybil_resistance_percent) / U128::from_num(100) + pub fn to_inline_json(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs new file mode 100644 index 0000000000..4d9ca75459 --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/helpers.rs @@ -0,0 +1,20 @@ +// 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 +/// never attempt to reward more than the owner is due +/// for example it truncates "23.9" into "23" +pub fn truncate_reward(reward: Decimal, denom: impl Into) -> Coin { + let amount = truncate_reward_amount(reward); + Coin { + denom: denom.into(), + amount, + } +} + +pub fn truncate_reward_amount(reward: Decimal) -> Uint128 { + truncate_decimal(reward) +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs new file mode 100644 index 0000000000..e50df76434 --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs @@ -0,0 +1,70 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Coin, Decimal}; +use schemars::JsonSchema; +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, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)] +pub struct RewardDistribution { + pub operator: Decimal, + pub delegates: Decimal, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct PendingRewardResponse { + pub amount_staked: Option, + pub amount_earned: Option, + pub amount_earned_detailed: Option, + + /// The associated mixnode is still fully bonded, meaning it is neither unbonded + /// nor in the process of unbonding that would have finished at the epoch transition. + pub mixnode_still_fully_bonded: bool, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq)] +pub struct EstimatedCurrentEpochRewardResponse { + pub original_stake: Option, + + pub current_stake_value: Option, + pub current_stake_value_detailed_amount: Option, + + pub estimation: Option, + pub detailed_estimation_amount: Option, +} + +impl EstimatedCurrentEpochRewardResponse { + pub fn empty_response() -> Self { + EstimatedCurrentEpochRewardResponse { + original_stake: None, + current_stake_value: None, + current_stake_value_detailed_amount: None, + estimation: None, + detailed_estimation_amount: None, + } + } +} 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..5fe1c46175 --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs @@ -0,0 +1,757 @@ +// 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 + .system_rewarding_params + .interval + .staking_supply_scale_factor + * 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, + staking_supply_scale_factor: old.staking_supply_scale_factor, + 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 + staking_supply_scale_factor: Percent::hundred(), + 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(), + staking_supply_scale_factor: Percent::hundred(), + 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 c93acd80a4..41914e62fb 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -1,16 +1,28 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::mixnode::DelegatorRewardParams; +use crate::error::MixnetContractError; use crate::{Layer, RewardedSetNodeStatus}; -use cosmwasm_std::{Addr, Uint128}; +use cosmwasm_std::Addr; +use cosmwasm_std::Coin; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::fmt::{self, Display, Formatter}; +use std::ops::Index; + +// type aliases for better reasoning about available data +pub type IdentityKey = String; +pub type IdentityKeyRef<'a> = &'a str; +pub type SphinxKey = String; +pub type SphinxKeyRef<'a> = &'a str; +pub type EpochId = u32; +pub type IntervalId = u32; +pub type MixId = u32; +pub type BlockHeight = u64; +pub type EpochEventId = u32; +pub type IntervalEventId = u32; #[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] pub struct LayerDistribution { - pub gateways: u64, pub layer1: u64, pub layer2: u64, pub layer3: u64, @@ -25,121 +37,88 @@ impl LayerDistribution { ]; layers.iter().min_by_key(|x| x.1).unwrap().0 } -} -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -pub struct ContractStateParams { - // so currently interval_length is being unused and validator API performs rewarding - // based on its own interval length config value. I guess that's fine for time being - // however, in the future, the contract constant should be controlling it instead. - // pub interval_length: u32, // length of a rewarding interval/interval, expressed in hours - pub minimum_mixnode_pledge: Uint128, // minimum amount a mixnode must pledge to get into the system - pub minimum_gateway_pledge: Uint128, // minimum amount a gateway must pledge to get into the system + pub fn increment_layer_count(&mut self, layer: Layer) { + match layer { + Layer::One => self.layer1 += 1, + Layer::Two => self.layer2 += 1, + Layer::Three => self.layer3 += 1, + } + } - // number of mixnode that are going to get rewarded during current rewarding interval (k_m) - // based on overall demand for private bandwidth- - pub mixnode_rewarded_set_size: u32, + pub fn decrement_layer_count(&mut self, layer: Layer) -> Result<(), MixnetContractError> { + match layer { + Layer::One => { + self.layer1 = + self.layer1 + .checked_sub(1) + .ok_or(MixnetContractError::OverflowSubtraction { + minuend: self.layer1, + subtrahend: 1, + })? + } + Layer::Two => { + self.layer2 = + self.layer2 + .checked_sub(1) + .ok_or(MixnetContractError::OverflowSubtraction { + minuend: self.layer2, + subtrahend: 1, + })? + } + Layer::Three => { + self.layer3 = + self.layer3 + .checked_sub(1) + .ok_or(MixnetContractError::OverflowSubtraction { + minuend: self.layer3, + subtrahend: 1, + })? + } + } - // subset of rewarded mixnodes that are actively receiving mix traffic - // used to handle shorter-term (e.g. hourly) fluctuations of demand - pub mixnode_active_set_size: u32, - pub staking_supply: Uint128, -} - -impl Display for ContractStateParams { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Contract state parameters: [ ")?; - write!( - f, - "minimum mixnode pledge: {}; ", - self.minimum_mixnode_pledge - )?; - write!( - f, - "minimum gateway pledge: {}; ", - self.minimum_gateway_pledge - )?; - write!( - f, - "mixnode rewarded set size: {}", - self.mixnode_rewarded_set_size - )?; - write!( - f, - "mixnode active set size: {}", - self.mixnode_active_set_size - ) + Ok(()) } } -#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)] -pub struct RewardingResult { - pub node_reward: Uint128, +impl Index for LayerDistribution { + type Output = u64; + + fn index(&self, index: Layer) -> &Self::Output { + match index { + Layer::One => &self.layer1, + Layer::Two => &self.layer2, + Layer::Three => &self.layer3, + } + } } -#[derive(Debug, Serialize, Deserialize)] -pub struct PendingDelegatorRewarding { - // keep track of the running rewarding results so we'd known how much was the operator and its delegators rewarded - pub running_results: RewardingResult, +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ContractState { + pub owner: Addr, // only the owner account can update state + pub rewarding_validator_address: Addr, - pub next_start: Addr, - - pub rewarding_params: DelegatorRewardParams, + /// Address of the vesting contract to which the mixnet contract would be sending all + /// track-related messages. + pub vesting_contract_address: Addr, + pub rewarding_denom: String, + pub params: ContractStateParams, } -#[derive(Debug, Serialize, Deserialize)] -pub enum RewardingStatus { - Complete(RewardingResult), - PendingNextDelegatorPage(PendingDelegatorRewarding), +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct ContractStateParams { + /// Minimum amount a delegator must stake in orders for his delegation to get accepted. + pub minimum_mixnode_delegation: Option, + + /// Minimum amount a mixnode must pledge to get into the system. + pub minimum_mixnode_pledge: Coin, + + /// Minimum amount a gateway must pledge to get into the system. + pub minimum_gateway_pledge: Coin, } -#[derive(Debug, Serialize, Deserialize)] -pub struct MixnodeRewardingStatusResponse { - pub status: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct MixnetContractVersion { - // VERGEN_BUILD_TIMESTAMP - pub build_timestamp: String, - - // VERGEN_BUILD_SEMVER - pub build_version: String, - - // VERGEN_GIT_SHA - pub commit_sha: String, - - // VERGEN_GIT_COMMIT_TIMESTAMP - pub commit_timestamp: String, - - // VERGEN_GIT_BRANCH - pub commit_branch: String, - - // VERGEN_RUSTC_SEMVER - pub rustc_version: String, -} - -// type aliases for better reasoning about available data -pub type IdentityKey = String; -pub type IdentityKeyRef<'a> = &'a str; -pub type SphinxKey = String; - #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct PagedRewardedSetResponse { - pub identities: Vec<(IdentityKey, RewardedSetNodeStatus)>, - pub start_next_after: Option, - pub at_height: u64, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] -pub struct RewardedSetUpdateDetails { - pub refresh_rate_blocks: u64, - pub last_refreshed_block: u64, - pub current_height: u64, -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] -pub struct IntervalRewardedSetHeightsResponse { - pub interval_id: u32, - pub heights: Vec, + pub nodes: Vec<(MixId, RewardedSetNodeStatus)>, + pub start_next_after: Option, } diff --git a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml index 2452f6eeb1..c2fa50a159 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/vesting-contract/Cargo.toml @@ -3,16 +3,14 @@ name = "vesting-contract-common" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [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" -cw-storage-plus = "0.13.4" -config = { path = "../../config" } -ts-rs = "6.1.2" +log = "0.4" +ts-rs = {version = "6.1.2", optional = true} [features] -generate-ts = [] +generate-ts = ["ts-rs"] diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs index 93ca6814dd..896ead2c9e 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/events.rs @@ -16,6 +16,8 @@ pub const VESTING_GATEWAY_UNBONDING_EVENT_TYPE: &str = "vesting_gateway_unbondin pub const VESTING_MIXNODE_BONDING_EVENT_TYPE: &str = "vesting_mixnode_bonding"; pub const VESTING_MIXNODE_UNBONDING_EVENT_TYPE: &str = "vesting_mixnode_unbonding"; pub const VESTING_UPDATE_MIXNODE_CONFIG_EVENT_TYPE: &str = "vesting_update_mixnode_config"; +pub const VESTING_UPDATE_MIXNODE_COST_PARAMS_EVENT_TYPE: &str = + "vesting_update_mixnode_cost_params"; pub const TRACK_MIXNODE_UNBOND_EVENT_TYPE: &str = "track_mixnode_unbond"; pub const TRACK_GATEWAY_UNBOND_EVENT_TYPE: &str = "track_gateway_unbond"; @@ -114,6 +116,10 @@ pub fn new_vesting_update_mixnode_config_event() -> Event { Event::new(VESTING_UPDATE_MIXNODE_CONFIG_EVENT_TYPE) } +pub fn new_vesting_update_mixnode_cost_params_event() -> Event { + Event::new(VESTING_UPDATE_MIXNODE_COST_PARAMS_EVENT_TYPE) +} + pub fn new_vesting_mixnode_unbonding_event() -> Event { Event::new(VESTING_MIXNODE_UNBONDING_EVENT_TYPE) } diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs index da29e02cae..4d494510b5 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs @@ -1,6 +1,11 @@ +use std::str::FromStr; + // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{Coin, Timestamp}; +use contracts_common::Percent; +use cosmwasm_std::{Addr, Coin, Timestamp, Uint128}; +use log::warn; +use mixnet_contract_common::MixId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -9,10 +14,6 @@ pub use messages::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg}; pub mod events; pub mod messages; -pub fn one_ucoin(denom: String) -> Coin { - Coin::new(1, denom) -} - #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", @@ -45,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, @@ -73,3 +114,60 @@ impl OriginalVestingResponse { } } } + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, JsonSchema)] +pub struct VestingDelegation { + pub account_id: u32, + pub mix_id: MixId, + pub block_timestamp: u64, + pub amount: Uint128, +} + +impl VestingDelegation { + pub fn storage_key(&self) -> (u32, MixId, u64) { + (self.account_id, self.mix_id, self.block_timestamp) + } +} + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, JsonSchema)] +pub struct DelegationTimesResponse { + pub owner: Addr, + pub account_id: u32, + 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, 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 a9dd627557..696df628aa 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -1,8 +1,13 @@ -use cosmwasm_std::{Coin, Timestamp, Uint128}; -use mixnet_contract_common::{Gateway, IdentityKey, MixNode}; +use cosmwasm_std::{Coin, Timestamp}; +use mixnet_contract_common::{ + mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, + 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 { @@ -13,7 +18,7 @@ pub struct InitMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub struct MigrateMsg { - pub mix_denom: String, + pub v2_mixnet_contract_address: String, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, Default)] @@ -58,40 +63,41 @@ pub enum ExecuteMsg { }, ClaimOperatorReward {}, ClaimDelegatorReward { - mix_identity: String, + mix_id: MixId, }, - CompoundDelegatorReward { - mix_identity: String, + UpdateMixnodeCostParams { + new_costs: MixNodeCostParams, }, - CompoundOperatorReward {}, UpdateMixnodeConfig { - profit_margin_percent: u8, + new_config: MixNodeConfigUpdate, }, UpdateMixnetAddress { address: String, }, DelegateToMixnode { - mix_identity: IdentityKey, + mix_id: MixId, amount: Coin, }, UndelegateFromMixnode { - mix_identity: IdentityKey, + mix_id: MixId, }, CreateAccount { owner_address: String, staking_address: Option, vesting_spec: Option, + cap: Option, }, WithdrawVestedCoins { amount: Coin, }, TrackUndelegation { owner: String, - mix_identity: IdentityKey, + mix_id: MixId, amount: Coin, }, BondMixnode { mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, amount: Coin, }, @@ -117,13 +123,44 @@ pub enum ExecuteMsg { to_address: Option, }, UpdateLockedPledgeCap { - amount: Uint128, + address: String, + cap: PledgeCap, }, } +impl ExecuteMsg { + pub fn name(&self) -> &str { + match self { + ExecuteMsg::TrackReward { .. } => "VestingExecuteMsg::TrackReward", + ExecuteMsg::ClaimOperatorReward { .. } => "VestingExecuteMsg::ClaimOperatorReward", + ExecuteMsg::ClaimDelegatorReward { .. } => "VestingExecuteMsg::ClaimDelegatorReward", + ExecuteMsg::UpdateMixnodeConfig { .. } => "VestingExecuteMsg::UpdateMixnodeConfig", + ExecuteMsg::UpdateMixnodeCostParams { .. } => { + "VestingExecuteMsg::UpdateMixnodeCostParams" + } + ExecuteMsg::UpdateMixnetAddress { .. } => "VestingExecuteMsg::UpdateMixnetAddress", + ExecuteMsg::DelegateToMixnode { .. } => "VestingExecuteMsg::DelegateToMixnode", + ExecuteMsg::UndelegateFromMixnode { .. } => "VestingExecuteMsg::UndelegateFromMixnode", + ExecuteMsg::CreateAccount { .. } => "VestingExecuteMsg::CreateAccount", + ExecuteMsg::WithdrawVestedCoins { .. } => "VestingExecuteMsg::WithdrawVestedCoins", + ExecuteMsg::TrackUndelegation { .. } => "VestingExecuteMsg::TrackUndelegation", + ExecuteMsg::BondMixnode { .. } => "VestingExecuteMsg::BondMixnode", + ExecuteMsg::UnbondMixnode { .. } => "VestingExecuteMsg::UnbondMixnode", + ExecuteMsg::TrackUnbondMixnode { .. } => "VestingExecuteMsg::TrackUnbondMixnode", + ExecuteMsg::BondGateway { .. } => "VestingExecuteMsg::BondGateway", + ExecuteMsg::UnbondGateway { .. } => "VestingExecuteMsg::UnbondGateway", + ExecuteMsg::TrackUnbondGateway { .. } => "VestingExecuteMsg::TrackUnbondGateway", + ExecuteMsg::TransferOwnership { .. } => "VestingExecuteMsg::TransferOwnership", + ExecuteMsg::UpdateStakingAddress { .. } => "VestingExecuteMsg::UpdateStakingAddress", + ExecuteMsg::UpdateLockedPledgeCap { .. } => "VestingExecuteMsg::UpdateLockedPledgeCap", + } + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { + GetContractVersion {}, LockedCoins { vesting_account_address: String, block_time: Option, @@ -169,5 +206,12 @@ pub enum QueryMsg { GetCurrentVestingPeriod { address: String, }, - GetLockedPledgeCap {}, + GetDelegationTimes { + address: String, + mix_id: MixId, + }, + GetAllDelegations { + start_after: Option<(u32, MixId, u64)>, + limit: Option, + }, } diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index 8b797fb74a..4eacccd531 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -7,14 +7,13 @@ edition = "2021" [dependencies] async-trait = { version = "0.1.51" } -nymcoconut = { path = "../nymcoconut" } 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/credential-storage/src/erc20.rs b/common/credential-storage/src/erc20.rs index 1c1fd85814..74559cfd30 100644 --- a/common/credential-storage/src/erc20.rs +++ b/common/credential-storage/src/erc20.rs @@ -17,55 +17,4 @@ impl ERC20CredentialManager { pub(crate) fn new(connection_pool: sqlx::SqlitePool) -> Self { ERC20CredentialManager { connection_pool } } - - /// Inserts provided signature into the database. - /// - /// # Arguments - /// - /// * `public_key`: Base58 representation of a public key. - /// * `private_key`: Base58 representation of a private key. - pub(crate) async fn insert_erc20_credential( - &self, - public_key: String, - private_key: String, - ) -> Result<(), sqlx::Error> { - sqlx::query!( - "INSERT INTO erc20_credentials(public_key, private_key, consumed) VALUES (?, ?, ?)", - public_key, - private_key, - false, - ) - .execute(&self.connection_pool) - .await?; - Ok(()) - } - - /// Tries to retrieve one of the stored, unused credentials. - pub(crate) async fn get_next_erc20_credential(&self) -> Result { - sqlx::query_as!( - ERC20Credential, - "SELECT * FROM erc20_credentials WHERE consumed = false" - ) - .fetch_one(&self.connection_pool) - .await - } - - /// Mark a credential as being consumed. - pub(crate) async fn consume_erc20_credential( - &self, - public_key: String, - ) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - UPDATE erc20_credentials - SET consumed = true - WHERE public_key = ? - "#, - public_key - ) - .execute(&self.connection_pool) - .await?; - - Ok(()) - } } diff --git a/common/credential-storage/src/lib.rs b/common/credential-storage/src/lib.rs index 05cd5e7988..7a4ecb5358 100644 --- a/common/credential-storage/src/lib.rs +++ b/common/credential-storage/src/lib.rs @@ -4,18 +4,16 @@ */ use crate::coconut::CoconutCredentialManager; -use crate::erc20::ERC20CredentialManager; use crate::error::StorageError; use crate::storage::Storage; -use crate::models::{CoconutCredential, ERC20Credential}; +use crate::models::CoconutCredential; use async_trait::async_trait; use log::{debug, error}; use sqlx::ConnectOptions; use std::path::{Path, PathBuf}; mod coconut; -mod erc20; pub mod error; mod models; pub mod storage; @@ -24,7 +22,6 @@ pub mod storage; #[derive(Clone)] pub struct PersistentStorage { coconut_credential_manager: CoconutCredentialManager, - erc20_credential_manager: ERC20CredentialManager, } impl PersistentStorage { @@ -60,7 +57,6 @@ impl PersistentStorage { Ok(PersistentStorage { coconut_credential_manager: CoconutCredentialManager::new(connection_pool.clone()), - erc20_credential_manager: ERC20CredentialManager::new(connection_pool), }) } } @@ -104,36 +100,6 @@ impl Storage for PersistentStorage { Ok(()) } - - async fn insert_erc20_credential( - &self, - public_key: String, - private_key: String, - ) -> Result<(), StorageError> { - self.erc20_credential_manager - .insert_erc20_credential(public_key, private_key) - .await?; - - Ok(()) - } - - async fn get_next_erc20_credential(&self) -> Result { - let credential = self - .erc20_credential_manager - .get_next_erc20_credential() - .await?; - - Ok(credential) - } - - async fn consume_erc20_credential(&self, public_key: String) -> Result<(), StorageError> { - let credential = self - .erc20_credential_manager - .consume_erc20_credential(public_key) - .await?; - - Ok(credential) - } } pub async fn initialise_storage(path: PathBuf) -> PersistentStorage { diff --git a/common/credential-storage/src/models.rs b/common/credential-storage/src/models.rs index ee480a2034..9684df056f 100644 --- a/common/credential-storage/src/models.rs +++ b/common/credential-storage/src/models.rs @@ -10,11 +10,3 @@ pub struct CoconutCredential { pub binding_number: String, pub signature: String, } - -pub struct ERC20Credential { - #[allow(dead_code)] - pub id: i64, - pub public_key: String, - pub private_key: String, - pub consumed: bool, -} diff --git a/common/credential-storage/src/storage.rs b/common/credential-storage/src/storage.rs index be15632ca1..892e561442 100644 --- a/common/credential-storage/src/storage.rs +++ b/common/credential-storage/src/storage.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; -use crate::models::{CoconutCredential, ERC20Credential}; +use crate::models::CoconutCredential; use crate::StorageError; #[async_trait] @@ -31,22 +31,4 @@ pub trait Storage: Send + Sync { /// /// * `signature`: Coconut credential in the form of a signature. async fn remove_coconut_credential(&self, id: i64) -> Result<(), StorageError>; - - /// Inserts provided signature into the database. - /// - /// # Arguments - /// - /// * `public_key`: Base58 representation of a public key. - /// * `private_key`: Base58 representation of a private key. - async fn insert_erc20_credential( - &self, - public_key: String, - private_key: String, - ) -> Result<(), StorageError>; - - /// Tries to retrieve one of the stored, unused credential data. - async fn get_next_erc20_credential(&self) -> Result; - - /// Mark a credential as being consumed. - async fn consume_erc20_credential(&self, public_key: String) -> Result<(), StorageError>; } diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index c0b7d29bcc..a8e749f2e0 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -26,7 +26,6 @@ subtle-encoding = { version = "0.5", features = ["bech32-preview"]} # internal nymsphinx-types = { path = "../nymsphinx/types" } pemstore = { path = "../../common/pemstore" } -config = { path="../../common/config" } [dev-dependencies] rand_chacha = "0.2" 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/inclusion-probability/Cargo.toml b/common/inclusion-probability/Cargo.toml new file mode 100644 index 0000000000..73eedb75a0 --- /dev/null +++ b/common/inclusion-probability/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "inclusion-probability" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4.17" +rand = "0.8.5" +thiserror = "1.0.32" diff --git a/common/inclusion-probability/src/error.rs b/common/inclusion-probability/src/error.rs new file mode 100644 index 0000000000..72a02a2a6a --- /dev/null +++ b/common/inclusion-probability/src/error.rs @@ -0,0 +1,11 @@ +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("The list of cumulative stake was unexpectedly empty")] + EmptyListCumulStake, + #[error("Sample point was unexpectedly out of bounds")] + SamplePointOutOfBounds, + #[error("Norm computation failed on different size arrays")] + NormDifferenceSizeArrays, + #[error("Computed probabilities are fewer than input number of nodes")] + ResultsShorterThanInput, +} diff --git a/common/inclusion-probability/src/lib.rs b/common/inclusion-probability/src/lib.rs new file mode 100644 index 0000000000..8894dc4764 --- /dev/null +++ b/common/inclusion-probability/src/lib.rs @@ -0,0 +1,426 @@ +//! Active set inclusion probability simulator + +use std::time::{Duration, Instant}; + +use error::Error; +use rand::Rng; + +mod error; + +const TOLERANCE_L2_NORM: f64 = 1e-4; +const TOLERANCE_MAX_NORM: f64 = 1e-4; + +pub struct SelectionProbability { + pub active_set_probability: Vec, + pub reserve_set_probability: Vec, + pub samples: u64, + pub time: Duration, + pub delta_l2: f64, + pub delta_max: f64, +} + +pub fn simulate_selection_probability_mixnodes( + list_stake_for_mixnodes: &[u128], + active_set_size: usize, + reserve_set_size: usize, + max_samples: u64, + max_time: Duration, + rng: &mut R, +) -> Result +where + R: Rng + ?Sized, +{ + log::trace!("Simulating mixnode active set selection probability"); + + // In case the active set size is larger than the number of bonded mixnodes, they all have 100% + // chance we don't have to go through with the simulation + if list_stake_for_mixnodes.len() <= active_set_size { + return Ok(SelectionProbability { + active_set_probability: vec![1.0; list_stake_for_mixnodes.len()], + reserve_set_probability: vec![0.0; list_stake_for_mixnodes.len()], + samples: 0, + time: Duration::ZERO, + delta_l2: 0.0, + delta_max: 0.0, + }); + } + + // Total number of existing (registered) nodes + let num_mixnodes = list_stake_for_mixnodes.len(); + + // Cumulative stake ordered by node index + let list_cumul = cumul_sum(list_stake_for_mixnodes); + + // The computed probabilities + let mut active_set_probability = vec![0.0; num_mixnodes]; + let mut reserve_set_probability = vec![0.0; num_mixnodes]; + + // Number sufficiently large to have a good approximation of selection probability + let mut samples = 0; + let mut delta_l2; + let mut delta_max; + + // Make sure we bound the time we allow it to run + let start_time = Instant::now(); + + loop { + samples += 1; + let mut sample_active_mixnodes = Vec::new(); + let mut sample_reserve_mixnodes = Vec::new(); + let mut list_cumul_temp = list_cumul.clone(); + + let active_set_probability_previous = active_set_probability.clone(); + + // Select the active nodes for the epoch (hour) + while sample_active_mixnodes.len() < active_set_size + && sample_active_mixnodes.len() < list_cumul_temp.len() + { + let candidate = sample_candidate(&list_cumul_temp, rng)?; + + if !sample_active_mixnodes.contains(&candidate) { + sample_active_mixnodes.push(candidate); + remove_mixnode_from_cumul_stake(candidate, &mut list_cumul_temp); + } + } + + // Select the reserve nodes for the epoch (hour) + while sample_reserve_mixnodes.len() < reserve_set_size + && sample_reserve_mixnodes.len() + sample_active_mixnodes.len() < list_cumul_temp.len() + { + let candidate = sample_candidate(&list_cumul_temp, rng)?; + + if !sample_reserve_mixnodes.contains(&candidate) + && !sample_active_mixnodes.contains(&candidate) + { + sample_reserve_mixnodes.push(candidate); + remove_mixnode_from_cumul_stake(candidate, &mut list_cumul_temp); + } + } + + // Sum up nodes being in active or reserve set + for active_mixnodes in sample_active_mixnodes { + active_set_probability[active_mixnodes] += 1.0; + } + for reserve_mixnodes in sample_reserve_mixnodes { + reserve_set_probability[reserve_mixnodes] += 1.0; + } + + // Convergence critera only on active set. + // We devide by samples to get the average, that is not really part of the delta + // computation. + delta_l2 = + l2_diff(&active_set_probability, &active_set_probability_previous)? / (samples as f64); + delta_max = + max_diff(&active_set_probability, &active_set_probability_previous)? / (samples as f64); + if samples > 10 && delta_l2 < TOLERANCE_L2_NORM && delta_max < TOLERANCE_MAX_NORM + || samples >= max_samples + { + break; + } + + // Stop if we run out of time + if start_time.elapsed() > max_time { + log::debug!("Simulation ran out of time, stopping"); + break; + } + } + + // Divide occurrences with the number of samples once we're done to get the probabilities. + active_set_probability + .iter_mut() + .for_each(|x| *x /= samples as f64); + reserve_set_probability + .iter_mut() + .for_each(|x| *x /= samples as f64); + + // Some sanity checks of the output + if active_set_probability.len() != num_mixnodes || reserve_set_probability.len() != num_mixnodes + { + return Err(Error::ResultsShorterThanInput); + } + + Ok(SelectionProbability { + active_set_probability, + reserve_set_probability, + samples, + time: start_time.elapsed(), + delta_l2, + delta_max, + }) +} + +// Compute the cumulative sum +fn cumul_sum<'a>(list: impl IntoIterator) -> Vec { + let mut list_cumul = Vec::new(); + let mut cumul = 0; + for entry in list { + cumul += entry; + list_cumul.push(cumul); + } + list_cumul +} + +fn sample_candidate(list_cumul: &[u128], rng: &mut R) -> Result +where + R: Rng + ?Sized, +{ + use rand::distributions::{Distribution, Uniform}; + let uniform = Uniform::from(0..*list_cumul.last().ok_or(Error::EmptyListCumulStake)?); + let r = uniform.sample(rng); + + let candidate = list_cumul + .iter() + .enumerate() + .find(|(_, x)| *x >= &r) + .ok_or(Error::SamplePointOutOfBounds)? + .0; + + Ok(candidate) +} + +// Update list of cumulative stake to reflect eliminating the picked node +fn remove_mixnode_from_cumul_stake(candidate: usize, list_cumul_stake: &mut [u128]) { + let prob_candidate = if candidate == 0 { + list_cumul_stake[0] + } else { + list_cumul_stake[candidate] - list_cumul_stake[candidate - 1] + }; + + for cumul in list_cumul_stake.iter_mut().skip(candidate) { + *cumul -= prob_candidate; + } +} + +// Compute the difference in l2-norm +fn l2_diff(v1: &[f64], v2: &[f64]) -> Result { + if v1.len() != v2.len() { + return Err(Error::NormDifferenceSizeArrays); + } + Ok(v1 + .iter() + .zip(v2) + .map(|(&i1, &i2)| (i1 - i2).powi(2)) + .sum::() + .sqrt()) +} + +// Compute the difference in max-norm +fn max_diff(v1: &[f64], v2: &[f64]) -> Result { + if v1.len() != v2.len() { + return Err(Error::NormDifferenceSizeArrays); + } + Ok(v1 + .iter() + .zip(v2) + .map(|(x, y)| (x - y).abs()) + .fold(f64::NEG_INFINITY, f64::max)) +} + +#[cfg(test)] +mod tests { + use rand::{rngs::StdRng, SeedableRng}; + + use super::*; + + fn test_rng() -> StdRng { + StdRng::seed_from_u64(42) + } + + #[test] + fn compute_cumul_sum() { + let v = cumul_sum(&vec![1, 2, 3]); + assert_eq!(v, &[1, 3, 6]); + } + + #[test] + fn remove_mixnode_from_cumul() { + let mut cumul_stake = vec![1, 2, 3, 4, 5, 6]; + remove_mixnode_from_cumul_stake(3, &mut cumul_stake); + assert_eq!(cumul_stake, &[1, 2, 3, 3, 4, 5]); + } + + #[test] + fn max_norm() { + let v1 = vec![1.0, 2.0, 3.0]; + let v2 = vec![2.0, 4.0, -6.0]; + assert!((max_diff(&v1, &v2).unwrap() - 9.0).abs() < f64::EPSILON); + } + + #[test] + fn ls_norm() { + let v1 = vec![1.0, 2.0, 3.0]; + let v2 = vec![2.0, 3.0, -2.0]; + assert!((l2_diff(&v1, &v2).unwrap() - 5.196_152_422_706_632).abs() < 1e2 * f64::EPSILON); + } + + // Replicate the results from the Python simulation code in https://github.com/nymtech/team-core/issues/114 + #[test] + fn replicate_python_simulation() { + let active_set_size = 4; + let standby_set_size = 1; + + // this has to contain the total stake per node + let list_mix = vec![ + 100, 100, 3000, 500_000, 100, 10, 10, 10, 10, 10, 30000, 500, 200, 52345, + ]; + + let max_samples = 100_000; + let max_time = Duration::from_secs(10); + let mut rng = test_rng(); + + let SelectionProbability { + active_set_probability, + reserve_set_probability, + samples, + time, + delta_l2, + delta_max, + } = simulate_selection_probability_mixnodes( + &list_mix, + active_set_size, + standby_set_size, + max_samples, + max_time, + &mut rng, + ) + .unwrap(); + + // Check that any possible test failure wasn't because we ran it on 1970s hardware, and the + // sampling aborted prematurely due to hitting `max_time`. + assert!(time < max_time); + + // These values comes from running the python simulator for a very long time + let expected_active_set_probability = vec![ + 0.025_070_8, + 0.025_073_2, + 0.744_117, + 0.999_999, + 0.025_000_2, + 0.002_524_4, + 0.002_527_8, + 0.002_528_6, + 0.002_569_6, + 0.002_513_6, + 0.994, + 0.125_482_8, + 0.050_279_8, + 0.998_313_2, + ]; + // The same check is used in the convergence criterion, and hence should be reflected in + // `delta_max` too. + assert!( + max_diff(&active_set_probability, &expected_active_set_probability).unwrap() < 1e-2 + ); + + let expected_reserve_set_probability = vec![ + 0.076_392_4, + 0.076_499, + 0.204_893_6, + 1e-06, + 0.076_278_8, + 0.007_720_6, + 0.007_673, + 0.007_700_2, + 0.007_669_4, + 0.007_731_2, + 0.005_789_4, + 0.368_465_6, + 0.151_537_2, + 0.001_648_6, + ]; + assert!( + max_diff(&reserve_set_probability, &expected_reserve_set_probability).unwrap() < 1e-2 + ); + + // We converge around 20_000, add another 500 for some slack due to random values + assert_eq!(samples, 20_001); + assert!(delta_l2 < TOLERANCE_L2_NORM); + assert!(delta_max < TOLERANCE_MAX_NORM); + } + + #[test] + fn fewer_nodes_than_active_set_size() { + let active_set_size = 10; + let standby_set_size = 3; + let list_mix = vec![100, 100, 3000]; + let max_samples = 100_000; + let max_time = Duration::from_secs(10); + let mut rng = test_rng(); + + let SelectionProbability { + active_set_probability, + reserve_set_probability, + samples, + time: _, + delta_l2, + delta_max, + } = simulate_selection_probability_mixnodes( + &list_mix, + active_set_size, + standby_set_size, + max_samples, + max_time, + &mut rng, + ) + .unwrap(); + + // These values comes from running the python simulator for a very long time + let expected_active_set_probability = vec![1.0, 1.0, 1.0]; + let expected_reserve_set_probability = vec![0.0, 0.0, 0.0]; + assert!( + max_diff(&active_set_probability, &expected_active_set_probability).unwrap() + < 1e1 * f64::EPSILON + ); + assert!( + max_diff(&reserve_set_probability, &expected_reserve_set_probability).unwrap() + < 1e1 * f64::EPSILON + ); + + // We converge around 20_000, add another 500 for some slack due to random values + assert_eq!(samples, 0); + assert!(delta_l2 < f64::EPSILON); + assert!(delta_max < f64::EPSILON); + } + + #[test] + fn fewer_nodes_than_reward_set_size() { + let active_set_size = 4; + let standby_set_size = 3; + let list_mix = vec![100, 100, 3000, 342, 3_498_234]; + let max_samples = 100_000_000; + let max_time = Duration::from_secs(10); + let mut rng = test_rng(); + + let SelectionProbability { + active_set_probability, + reserve_set_probability, + samples, + time: _, + delta_l2, + delta_max, + } = simulate_selection_probability_mixnodes( + &list_mix, + active_set_size, + standby_set_size, + max_samples, + max_time, + &mut rng, + ) + .unwrap(); + + // These values comes from running the python simulator for a very long time + let expected_active_set_probability = vec![0.546, 0.538, 0.999, 0.915, 1.0]; + let expected_reserve_set_probability = vec![0.453, 0.461, 0.0005, 0.084, 0.0]; + assert!( + max_diff(&active_set_probability, &expected_active_set_probability).unwrap() < 1e-2, + ); + assert!( + max_diff(&reserve_set_probability, &expected_reserve_set_probability).unwrap() < 1e-2, + ); + + // We converge around 20_000, add another 500 for some slack due to random values + assert_eq!(samples, 20_001); + assert!(delta_l2 < TOLERANCE_L2_NORM); + assert!(delta_max < TOLERANCE_MAX_NORM); + } +} diff --git a/common/ledger/Cargo.toml b/common/ledger/Cargo.toml new file mode 100644 index 0000000000..9bea382a73 --- /dev/null +++ b/common/ledger/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "ledger" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bip32 = "0.3.0" +k256 = "0.10.4" +ledger-transport = "0.10.0" +ledger-transport-hid = "0.10.0" +thiserror = "1" \ No newline at end of file diff --git a/common/ledger/src/addr_secp265k1.rs b/common/ledger/src/addr_secp265k1.rs new file mode 100644 index 0000000000..93fe82b8c6 --- /dev/null +++ b/common/ledger/src/addr_secp265k1.rs @@ -0,0 +1,40 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::LedgerError; +use crate::helpers::answer_bytes; +use bip32::{PublicKey, PublicKeyBytes}; +use ledger_transport::APDUAnswer; + +/// SECP265K1 address of the device. +pub struct AddrSecp265k1Response { + /// SECP265K1 public key. + pub public_key: k256::PublicKey, + /// String representation of the Cosmos address. + pub address: String, +} + +impl TryFrom>> for AddrSecp265k1Response { + type Error = LedgerError; + + fn try_from(answer: APDUAnswer>) -> Result { + let bytes = answer_bytes(&answer)?; + if bytes.len() < 33 { + return Err(Self::Error::InvalidAnswerLength { + expected: 33, + received: bytes.len(), + }); + } + + let (pub_key, addr) = bytes.split_at(33); + let public_key = k256::PublicKey::from_bytes( + PublicKeyBytes::try_from(pub_key).expect("Public key should be 33 bytes"), + )?; + let address = String::from_utf8(addr.to_vec()).unwrap(); + + Ok(AddrSecp265k1Response { + public_key, + address, + }) + } +} diff --git a/common/ledger/src/error.rs b/common/ledger/src/error.rs new file mode 100644 index 0000000000..fc4a0a1b49 --- /dev/null +++ b/common/ledger/src/error.rs @@ -0,0 +1,37 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + +pub(crate) type Result = std::result::Result; + +/// Ledger specific errors. +#[derive(Debug, Error)] +pub enum LedgerError { + #[error("HID API - {0}")] + HidAPI(#[from] ledger_transport_hid::hidapi::HidError), + + #[error("HID transport - {0}")] + HidTransport(#[from] ledger_transport_hid::LedgerHIDError), + + #[error("Unknown error code - {err_code}")] + UnknownErrorCode { err_code: u16 }, + + #[error("APDU error - {reason}")] + APDU { reason: String }, + + #[error("Not enough bytes in answer. Expected at least {expected}, received {received}")] + InvalidAnswerLength { expected: usize, received: usize }, + + #[error("Not enough components in derivation path. Expected {expected}, received {received}")] + InvalidDerivationPath { expected: usize, received: usize }, + + #[error("Bip32 - {0}")] + Bip32(#[from] bip32::Error), + + #[error("Signature error - {0}")] + Signature(#[from] k256::ecdsa::Error), + + #[error("No message found for signing transaction")] + NoMessageFound, +} diff --git a/common/ledger/src/helpers.rs b/common/ledger/src/helpers.rs new file mode 100644 index 0000000000..df21167397 --- /dev/null +++ b/common/ledger/src/helpers.rs @@ -0,0 +1,37 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::{LedgerError, Result}; +use bip32::DerivationPath; +use ledger_transport::{APDUAnswer, APDUErrorCode}; + +pub(crate) fn answer_bytes(answer: &APDUAnswer>) -> Result<&[u8]> { + let error_code = answer + .error_code() + .map_err(|err_code| LedgerError::UnknownErrorCode { err_code })?; + match error_code { + APDUErrorCode::NoError => Ok(answer.data()), + e => Err(LedgerError::APDU { + reason: e.description(), + }), + } +} + +pub(crate) fn path_bytes(path: DerivationPath) -> Result<[[u8; 4]; 5]> { + let received = path.len(); + let components: Vec<[u8; 4]> = path.into_iter().map(|c| c.0.to_le_bytes()).collect(); + if components.len() != 5 { + Err(LedgerError::InvalidDerivationPath { + expected: 5, + received, + }) + } else { + Ok([ + components[0], + components[1], + components[2], + components[3], + components[4], + ]) + } +} diff --git a/common/ledger/src/lib.rs b/common/ledger/src/lib.rs new file mode 100644 index 0000000000..2ff4a18956 --- /dev/null +++ b/common/ledger/src/lib.rs @@ -0,0 +1,140 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod addr_secp265k1; +pub mod error; +pub(crate) mod helpers; +mod sign_secp265k1; +pub mod version; + +use crate::addr_secp265k1::AddrSecp265k1Response; +use crate::error::LedgerError; +use crate::helpers::path_bytes; +use crate::sign_secp265k1::SignSecp265k1Response; +use crate::version::VersionResponse; +use bip32::DerivationPath; +use error::Result; +use ledger_transport::APDUCommand; +use ledger_transport_hid::hidapi::HidApi; +use ledger_transport_hid::TransportNativeHID; +use std::fmt::Debug; +use std::fmt::Formatter; +use std::sync::Arc; + +const CLA: u8 = 0x55; +const INS_GET_VERSION: u8 = 0x00; +const INS_SIGN_SECP256K1: u8 = 0x02; +const INS_GET_ADDR_SECP256K1: u8 = 0x04; + +const PAYLOAD_TYPE_INIT: u8 = 0x00; +const PAYLOAD_TYPE_ADD: u8 = 0x01; +const PAYLOAD_TYPE_LAST: u8 = 0x02; +const CHUNK_SIZE: usize = 250; + +/// Manage hardware Ledger device with Cosmos specific operations, as described in the +/// specification: https://github.com/cosmos/ledger-cosmos/blob/main/docs/APDUSPEC.md +#[derive(Clone)] +pub struct CosmosLedger { + path: DerivationPath, + prefix: String, + transport: Arc, +} + +impl Debug for CosmosLedger { + fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + write!(f, "()") + } +} + +impl CosmosLedger { + /// Create the connection to the first Ledger device that we can find. + pub fn new(path: DerivationPath, prefix: String) -> Result { + let api = HidApi::new()?; + let transport = Arc::new(TransportNativeHID::new(&api)?); + + Ok(CosmosLedger { + path, + prefix, + transport, + }) + } + + /// Get the version of the device. + pub fn get_version(&self) -> Result { + let command = APDUCommand { + cla: CLA, + ins: INS_GET_VERSION, + p1: 0, + p2: 0, + data: vec![], + }; + let response = self.transport.exchange(&command)?; + VersionResponse::try_from(response) + } + + /// Get the SECP265K1 address of the device. + pub fn get_addr_secp265k1(&self, display: bool) -> Result { + let display = u8::from(display); + let components = path_bytes(self.path.clone())?; + let data: Vec = vec![ + [self.prefix.len() as u8].as_slice(), + self.prefix.as_bytes(), + components[0].as_slice(), + components[1].as_slice(), + components[2].as_slice(), + components[3].as_slice(), + components[4].as_slice(), + ] + .into_iter() + .flatten() + .copied() + .collect(); + + let command = APDUCommand { + cla: CLA, + ins: INS_GET_ADDR_SECP256K1, + p1: display, + p2: 0, + data, + }; + let response = self.transport.exchange(&command)?; + AddrSecp265k1Response::try_from(response) + } + + pub fn sign_secp265k1(&self, message: String) -> Result { + let serialized_path: Vec = path_bytes(self.path.clone())? + .into_iter() + .flatten() + .collect(); + let mut chunks = vec![serialized_path]; + if message.is_empty() { + return Err(LedgerError::NoMessageFound); + } + for chunk in message.into_bytes().chunks(CHUNK_SIZE) { + chunks.push(chunk.to_vec()); + } + let length = chunks.len(); + for (idx, chunk) in chunks.into_iter().enumerate() { + let payload_desc = if idx == 0 { + PAYLOAD_TYPE_INIT + } else if idx + 1 == length { + PAYLOAD_TYPE_LAST + } else { + PAYLOAD_TYPE_ADD + }; + let command = APDUCommand { + cla: CLA, + ins: INS_SIGN_SECP256K1, + p1: payload_desc, + p2: 0, + data: chunk, + }; + let sign_response = self.transport.exchange(&command)?; + if payload_desc == PAYLOAD_TYPE_LAST { + return SignSecp265k1Response::try_from(sign_response); + } + } + // It should never reach this, as the message is not empty + Err(LedgerError::NoMessageFound) + } +} diff --git a/common/ledger/src/sign_secp265k1.rs b/common/ledger/src/sign_secp265k1.rs new file mode 100644 index 0000000000..25596d6492 --- /dev/null +++ b/common/ledger/src/sign_secp265k1.rs @@ -0,0 +1,25 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::LedgerError; +use crate::helpers::answer_bytes; +use k256::ecdsa::Signature; +use ledger_transport::APDUAnswer; + +/// Version and status data of the device. +pub struct SignSecp265k1Response { + /// DER encoded signature data + pub signature: Signature, +} + +impl TryFrom>> for SignSecp265k1Response { + type Error = LedgerError; + + fn try_from(answer: APDUAnswer>) -> Result { + let bytes = answer_bytes(&answer)?; + + Ok(SignSecp265k1Response { + signature: Signature::from_der(bytes)?, + }) + } +} diff --git a/common/ledger/src/version.rs b/common/ledger/src/version.rs new file mode 100644 index 0000000000..c2ee169958 --- /dev/null +++ b/common/ledger/src/version.rs @@ -0,0 +1,42 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::LedgerError; +use crate::helpers::answer_bytes; +use ledger_transport::APDUAnswer; + +/// Version and status data of the device. +pub struct VersionResponse { + /// Activation status of test mode. + pub test_mode: bool, + /// Major part of Cosmos application version. + pub major: u8, + /// Minor part of Cosmos application version. + pub minor: u8, + /// Patch part of Cosmos application version. + pub patch: u8, + /// PIN locked status. + pub device_locked: bool, +} + +impl TryFrom>> for VersionResponse { + type Error = LedgerError; + + fn try_from(answer: APDUAnswer>) -> Result { + let bytes = answer_bytes(&answer)?; + if bytes.len() != 5 { + return Err(Self::Error::InvalidAnswerLength { + expected: 5, + received: bytes.len(), + }); + } + + Ok(VersionResponse { + test_mode: bytes[0] != 0, + major: bytes[1], + minor: bytes[2], + patch: bytes[3], + device_locked: bytes[4] != 0, + }) + } +} diff --git a/common/logging/Cargo.toml b/common/logging/Cargo.toml new file mode 100644 index 0000000000..14d2e3a737 --- /dev/null +++ b/common/logging/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "logging" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4.0" +pretty_env_logger = "0.4.0" \ No newline at end of file diff --git a/common/logging/src/lib.rs b/common/logging/src/lib.rs new file mode 100644 index 0000000000..6c026e9b90 --- /dev/null +++ b/common/logging/src/lib.rs @@ -0,0 +1,25 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// I'd argue we should start transitioning from `log` to `tracing` +pub fn setup_logging() { + let mut log_builder = pretty_env_logger::formatted_timed_builder(); + if let Ok(s) = ::std::env::var("RUST_LOG") { + log_builder.parse_filters(&s); + } else { + // default to 'Info' + log_builder.filter(None, log::LevelFilter::Info); + } + + log_builder + .filter_module("hyper", log::LevelFilter::Warn) + .filter_module("tokio_reactor", log::LevelFilter::Warn) + .filter_module("reqwest", log::LevelFilter::Warn) + .filter_module("mio", log::LevelFilter::Warn) + .filter_module("want", log::LevelFilter::Warn) + .filter_module("tungstenite", log::LevelFilter::Warn) + .filter_module("tokio_tungstenite", log::LevelFilter::Warn) + .filter_module("handlebars", log::LevelFilter::Warn) + .filter_module("sled", log::LevelFilter::Warn) + .init(); +} diff --git a/common/mixnode-common/Cargo.toml b/common/mixnode-common/Cargo.toml index 212a3b30d5..53676023af 100644 --- a/common/mixnode-common/Cargo.toml +++ b/common/mixnode-common/Cargo.toml @@ -8,18 +8,16 @@ edition = "2021" [dependencies] bytes = "1.0" -dashmap = "4.0" futures = "0.3" 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" crypto = { path = "../crypto" } -nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" } nymsphinx-acknowledgements = { path = "../nymsphinx/acknowledgements" } nymsphinx-addressing = { path = "../nymsphinx/addressing" } nymsphinx-forwarding = { path = "../nymsphinx/forwarding" } diff --git a/common/mixnode-common/src/packet_processor/processor.rs b/common/mixnode-common/src/packet_processor/processor.rs index 344172dca4..918e001fec 100644 --- a/common/mixnode-common/src/packet_processor/processor.rs +++ b/common/mixnode-common/src/packet_processor/processor.rs @@ -116,7 +116,10 @@ impl SphinxPacketProcessor { trace!("received an ack packet!"); Ok((None, data)) } - PacketSize::RegularPacket | PacketSize::ExtendedPacket => { + PacketSize::RegularPacket + | PacketSize::ExtendedPacket8 + | PacketSize::ExtendedPacket16 + | PacketSize::ExtendedPacket32 => { trace!("received a normal packet!"); let (ack_data, message) = self.split_hop_data_into_ack_and_message(data)?; let (ack_first_hop, ack_packet) = SurbAck::try_recover_first_hop_packet(&ack_data)?; diff --git a/common/mixnode-common/src/verloc/mod.rs b/common/mixnode-common/src/verloc/mod.rs index 610a7a1a66..48e9a7b483 100644 --- a/common/mixnode-common/src/verloc/mod.rs +++ b/common/mixnode-common/src/verloc/mod.rs @@ -321,17 +321,18 @@ impl VerlocMeasurer { let tested_nodes = all_mixes .into_iter() .filter_map(|node| { + let mix_node = node.bond_information.mix_node; // check if the node has sufficient version to be able to understand the packets - let node_version = parse_version(&node.mix_node.version).ok()?; + let node_version = parse_version(&mix_node.version).ok()?; if node_version < self.config.minimum_compatible_node_version { return None; } // try to parse the identity and host let node_identity = - identity::PublicKey::from_base58_string(node.mix_node.identity_key).ok()?; + identity::PublicKey::from_base58_string(mix_node.identity_key).ok()?; - let verloc_host = (&*node.mix_node.host, node.mix_node.verloc_port) + let verloc_host = (&*mix_node.host, mix_node.verloc_port) .to_socket_addrs() .ok()? .next()?; diff --git a/common/network-defaults/envs/mainnet.env b/common/network-defaults/envs/mainnet.env new file mode 100644 index 0000000000..dce1c3c9c0 --- /dev/null +++ b/common/network-defaults/envs/mainnet.env @@ -0,0 +1,20 @@ +CONFIGURED=true + +RUST_LOG=info +RUST_BACKTRACE=1 + +BECH32_PREFIX=n +MIX_DENOM=unym +MIX_DENOM_DISPLAY=nym +STAKE_DENOM=unyx +STAKE_DENOM_DISPLAY=nyx +DENOMS_EXPONENT=6 +MIXNET_CONTRACT_ADDRESS=n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g +VESTING_CONTRACT_ADDRESS=n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw +BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 +COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 +MULTISIG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 +REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy +STATISTICS_SERVICE_DOMAIN_ADDRESS="http://127.0.0.1:8090" +NYMD_VALIDATOR="https://rpc.nyx.nodes.guru/" +API_VALIDATOR="https://validator.nymtech.net/api/" diff --git a/common/network-defaults/envs/qa.env b/common/network-defaults/envs/qa.env new file mode 100644 index 0000000000..842fb54c4d --- /dev/null +++ b/common/network-defaults/envs/qa.env @@ -0,0 +1,20 @@ +CONFIGURED=true + +RUST_LOG=info +RUST_BACKTRACE=1 + +BECH32_PREFIX=n +MIX_DENOM=unym +MIX_DENOM_DISPLAY=nym +STAKE_DENOM=unyx +STAKE_DENOM_DISPLAY=nyx +DENOMS_EXPONENT=6 +MIXNET_CONTRACT_ADDRESS=n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep +VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav +BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 +COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw +MULTISIG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs +REWARDING_VALIDATOR_ADDRESS=n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq +STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" +NYMD_VALIDATOR="https://qa-validator.nymtech.net" +API_VALIDATOR="https://qa-validator-api.nymtech.net/api" 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 a156394e56..c600199140 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -1,16 +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 eth_contract; pub mod mainnet; -pub mod qa; -pub mod sandbox; pub mod var_names; pub const ETH_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_CONTRACT_ADDRESS; @@ -104,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, @@ -350,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 4a59806183..ed415e2622 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -25,48 +25,113 @@ pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy"; pub(crate) const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "https://mainnet-stats.nymte.ch:8090/"; -pub const NYMD_VALIDATOR: &str = "https://rpc.nyx.nodes.guru/"; +pub const NYMD_VALIDATOR: &str = "https://rpc.nymtech.net"; pub const API_VALIDATOR: &str = "https://validator.nymtech.net/api/"; 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/nymcoconut/src/error.rs b/common/nymcoconut/src/error.rs index 9bcab1d6e4..7faf11c86e 100644 --- a/common/nymcoconut/src/error.rs +++ b/common/nymcoconut/src/error.rs @@ -42,7 +42,7 @@ pub enum CoconutError { )] DeserializationMinLength { min: usize, actual: usize }, - #[error("Tried to deserialize {object} with bytes of invalid length. Expected {actual} < {} or {modulus_target} % {modulus} == 0")] + #[error("Tried to deserialize {object} with bytes of invalid length. Expected {actual} < {object} or {modulus_target} % {modulus} == 0")] DeserializationInvalidLength { actual: usize, target: usize, 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/forwarding/src/packet.rs b/common/nymsphinx/forwarding/src/packet.rs index 86cb7871fe..62df1028e3 100644 --- a/common/nymsphinx/forwarding/src/packet.rs +++ b/common/nymsphinx/forwarding/src/packet.rs @@ -25,8 +25,10 @@ impl Display for MixPacketFormattingError { InvalidPacketSize(actual) => write!( f, - "received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {} (EXTENDED))", - actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(), PacketSize::ExtendedPacket.size() + "received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))", + actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(), + PacketSize::ExtendedPacket8.size(), PacketSize::ExtendedPacket16.size(), + PacketSize::ExtendedPacket32.size() ), MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"), InvalidPacketMode => write!(f, "provided packet mode is invalid") diff --git a/common/nymsphinx/framing/src/codec.rs b/common/nymsphinx/framing/src/codec.rs index f03280091d..53bd77c3b7 100644 --- a/common/nymsphinx/framing/src/codec.rs +++ b/common/nymsphinx/framing/src/codec.rs @@ -6,7 +6,6 @@ use bytes::{Buf, BufMut, BytesMut}; use nymsphinx_params::packet_modes::InvalidPacketMode; use nymsphinx_params::packet_sizes::{InvalidPacketSize, PacketSize}; use nymsphinx_types::SphinxPacket; -use std::convert::TryFrom; use std::io; use tokio_util::codec::{Decoder, Encoder}; @@ -75,7 +74,7 @@ impl Decoder for SphinxCodec { if src.is_empty() { // can't do anything if we have no bytes, but let's reserve enough for the most // conservative case, i.e. receiving an ack packet - src.reserve(Header::SIZE + PacketSize::AckPacket.size()); + src.reserve(Header::LEGACY_SIZE + PacketSize::AckPacket.size()); return Ok(None); } @@ -87,7 +86,7 @@ impl Decoder for SphinxCodec { }; let sphinx_packet_size = header.packet_size.size(); - let frame_len = Header::SIZE + sphinx_packet_size; + let frame_len = header.size() + sphinx_packet_size; if src.len() < frame_len { // we don't have enough bytes to read the rest of frame @@ -96,7 +95,7 @@ impl Decoder for SphinxCodec { } // advance buffer past the header - at this point we have enough bytes - src.advance(Header::SIZE); + src.advance(header.size()); let sphinx_packet_bytes = src.split_to(sphinx_packet_size); let sphinx_packet = match SphinxPacket::from_bytes(&sphinx_packet_bytes) { Ok(sphinx_packet) => sphinx_packet, @@ -115,21 +114,27 @@ impl Decoder for SphinxCodec { // has appropriate capacity in anticipation of future calls to decode. // Failing to do so leads to inefficiency. - // if we have at least one more byte available, we can reserve enough bytes for + // if we have enough bytes to decode the header of the next packet, we can reserve enough bytes for // the entire next frame, if not, we assume the next frame is an ack packet and // reserve for that. + // we also assume the next packet coming from the same client will use exactly the same versioning + // as the current packet + let mut allocate_for_next_packet = header.size() + PacketSize::AckPacket.size(); if !src.is_empty() { - let next_packet_len = match PacketSize::try_from(src[0]) { - Ok(next_packet_len) => next_packet_len, + match Header::decode(src) { + Ok(Some(next_header)) => { + allocate_for_next_packet = next_header.size() + next_header.packet_size.size(); + } + Ok(None) => { + // we don't have enough information to know how much to reserve, fallback to the ack case + } + // the next frame will be malformed but let's leave handling the error to the next // call to 'decode', as presumably, the current sphinx packet is still valid Err(_) => return Ok(Some(nymsphinx_packet)), }; - let next_frame_len = next_packet_len.size() + Header::SIZE; - src.reserve(next_frame_len - 1); - } else { - src.reserve(Header::SIZE + PacketSize::AckPacket.size()); } + src.reserve(allocate_for_next_packet); Ok(Some(nymsphinx_packet)) } @@ -199,6 +204,8 @@ mod packet_encoding { #[cfg(test)] mod decode_will_allocate_enough_bytes_for_next_call { use super::*; + use nymsphinx_params::packet_version::PacketVersion; + use nymsphinx_params::PacketMode; #[test] fn for_empty_bytes() { @@ -207,20 +214,23 @@ mod packet_encoding { assert!(SphinxCodec.decode(&mut empty_bytes).unwrap().is_none()); assert_eq!( empty_bytes.capacity(), - Header::SIZE + PacketSize::AckPacket.size() + Header::LEGACY_SIZE + PacketSize::AckPacket.size() ); } #[test] - fn for_bytes_with_header() { + fn for_bytes_with_legacy_header() { // if header gets decoded there should be enough bytes for the entire frame let packet_sizes = vec![ PacketSize::AckPacket, PacketSize::RegularPacket, - PacketSize::ExtendedPacket, + PacketSize::ExtendedPacket8, + PacketSize::ExtendedPacket16, + PacketSize::ExtendedPacket32, ]; for packet_size in packet_sizes { let header = Header { + packet_version: PacketVersion::Legacy, packet_size, packet_mode: Default::default(), }; @@ -228,12 +238,60 @@ mod packet_encoding { header.encode(&mut bytes); assert!(SphinxCodec.decode(&mut bytes).unwrap().is_none()); - assert_eq!(bytes.capacity(), Header::SIZE + packet_size.size()) + assert_eq!(bytes.capacity(), Header::LEGACY_SIZE + packet_size.size()) } } #[test] - fn for_full_frame() { + fn for_bytes_with_versioned_header() { + // if header gets decoded there should be enough bytes for the entire frame + let packet_sizes = vec![ + PacketSize::AckPacket, + PacketSize::RegularPacket, + PacketSize::ExtendedPacket8, + PacketSize::ExtendedPacket16, + PacketSize::ExtendedPacket32, + ]; + for packet_size in packet_sizes { + let header = Header { + packet_version: PacketVersion::Versioned(123), + packet_size, + packet_mode: Default::default(), + }; + let mut bytes = BytesMut::new(); + header.encode(&mut bytes); + assert!(SphinxCodec.decode(&mut bytes).unwrap().is_none()); + + assert_eq!( + bytes.capacity(), + Header::VERSIONED_SIZE + packet_size.size() + ) + } + } + + #[test] + fn for_full_frame_with_legacy_header() { + // if full frame is used exactly, there should be enough space for header + ack packet + let packet = FramedSphinxPacket { + header: Header { + packet_version: PacketVersion::Legacy, + packet_size: Default::default(), + packet_mode: Default::default(), + }, + packet: make_valid_sphinx_packet(Default::default()), + }; + + let mut bytes = BytesMut::new(); + SphinxCodec.encode(packet, &mut bytes).unwrap(); + assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some()); + assert_eq!( + bytes.capacity(), + Header::LEGACY_SIZE + PacketSize::AckPacket.size() + ); + } + + #[test] + fn for_full_frame_with_versioned_header() { // if full frame is used exactly, there should be enough space for header + ack packet let packet = FramedSphinxPacket { header: Header::default(), @@ -245,17 +303,50 @@ mod packet_encoding { assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some()); assert_eq!( bytes.capacity(), - Header::SIZE + PacketSize::AckPacket.size() + Header::VERSIONED_SIZE + PacketSize::AckPacket.size() ); } #[test] - fn for_full_frame_with_extra_byte() { - // if there was at least 1 byte left, there should be enough space for entire next frame + fn for_full_frame_with_extra_bytes_with_legacy_header() { + // if there was at least 2 byte left, there should be enough space for entire next frame let packet_sizes = vec![ PacketSize::AckPacket, PacketSize::RegularPacket, - PacketSize::ExtendedPacket, + PacketSize::ExtendedPacket8, + PacketSize::ExtendedPacket16, + PacketSize::ExtendedPacket32, + ]; + + for packet_size in packet_sizes { + let first_packet = FramedSphinxPacket { + header: Header { + packet_version: PacketVersion::Legacy, + packet_size: Default::default(), + packet_mode: Default::default(), + }, + packet: make_valid_sphinx_packet(Default::default()), + }; + + let mut bytes = BytesMut::new(); + SphinxCodec.encode(first_packet, &mut bytes).unwrap(); + bytes.put_u8(packet_size as u8); + bytes.put_u8(PacketMode::default() as u8); + assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some()); + + assert!(bytes.capacity() >= Header::LEGACY_SIZE + packet_size.size()) + } + } + + #[test] + fn for_full_frame_with_extra_bytes_with_versioned_header() { + // if there was at least 3 byte left, there should be enough space for entire next frame + let packet_sizes = vec![ + PacketSize::AckPacket, + PacketSize::RegularPacket, + PacketSize::ExtendedPacket8, + PacketSize::ExtendedPacket16, + PacketSize::ExtendedPacket32, ]; for packet_size in packet_sizes { @@ -266,10 +357,12 @@ mod packet_encoding { let mut bytes = BytesMut::new(); SphinxCodec.encode(first_packet, &mut bytes).unwrap(); + bytes.put_u8(PacketVersion::new_versioned(123).as_u8().unwrap()); bytes.put_u8(packet_size as u8); + bytes.put_u8(PacketMode::default() as u8); assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some()); - assert!(bytes.capacity() >= Header::SIZE + packet_size.size()) + assert!(bytes.capacity() >= Header::VERSIONED_SIZE + packet_size.size()) } } } diff --git a/common/nymsphinx/framing/src/packet.rs b/common/nymsphinx/framing/src/packet.rs index 7c1881f318..997f2e6c35 100644 --- a/common/nymsphinx/framing/src/packet.rs +++ b/common/nymsphinx/framing/src/packet.rs @@ -4,6 +4,7 @@ use crate::codec::SphinxCodecError; use bytes::{BufMut, BytesMut}; use nymsphinx_params::packet_sizes::PacketSize; +use nymsphinx_params::packet_version::PacketVersion; use nymsphinx_params::PacketMode; use nymsphinx_types::SphinxPacket; use std::convert::TryFrom; @@ -17,12 +18,14 @@ pub struct FramedSphinxPacket { } impl FramedSphinxPacket { - pub fn new(packet: SphinxPacket, packet_mode: PacketMode) -> Self { + pub fn new(packet: SphinxPacket, packet_mode: PacketMode, use_legacy_version: bool) -> Self { // If this fails somebody is using the library in a super incorrect way, because they // already managed to somehow create a sphinx packet let packet_size = PacketSize::get_type(packet.len()).unwrap(); + FramedSphinxPacket { header: Header { + packet_version: PacketVersion::new(use_legacy_version), packet_size, packet_mode, }, @@ -48,6 +51,9 @@ impl FramedSphinxPacket { // but would that really be worth it? #[derive(Debug, Default, PartialEq, Eq, Copy, Clone)] pub struct Header { + /// Represents the wire format version used to construct this packet. + pub(crate) packet_version: PacketVersion, + /// Represents type and consequently size of the included SphinxPacket. pub(crate) packet_size: PacketSize, @@ -64,11 +70,25 @@ pub struct Header { } impl Header { - pub(crate) const SIZE: usize = 2; + pub(crate) const LEGACY_SIZE: usize = 2; + pub(crate) const VERSIONED_SIZE: usize = 3; + + pub(crate) fn size(&self) -> usize { + if self.packet_version.is_legacy() { + Self::LEGACY_SIZE + } else { + Self::VERSIONED_SIZE + } + } pub(crate) fn encode(&self, dst: &mut BytesMut) { // we reserve one byte for `packet_size` and the other for `mode` - dst.reserve(Self::SIZE); + dst.reserve(Self::LEGACY_SIZE); + if let Some(version) = self.packet_version.as_u8() { + dst.reserve(Self::VERSIONED_SIZE); + dst.put_u8(version) + } + dst.put_u8(self.packet_size as u8); dst.put_u8(self.packet_mode as u8); // reserve bytes for the actual packet @@ -76,16 +96,30 @@ impl Header { } pub(crate) fn decode(src: &mut BytesMut) -> Result, SphinxCodecError> { - if src.len() < Self::SIZE { + if src.len() < Self::LEGACY_SIZE { // can't do anything if we don't have enough bytes - but reserve enough for the next call - src.reserve(Self::SIZE); + src.reserve(Self::LEGACY_SIZE); return Ok(None); } - Ok(Some(Header { - packet_size: PacketSize::try_from(src[0])?, - packet_mode: PacketMode::try_from(src[1])?, - })) + let packet_version = PacketVersion::from(src[0]); + if packet_version.is_legacy() { + Ok(Some(Header { + packet_version, + packet_size: PacketSize::try_from(src[0])?, + packet_mode: PacketMode::try_from(src[1])?, + })) + } else if src.len() < Self::VERSIONED_SIZE { + // we're missing that 1 byte to read the full header... + src.reserve(Self::VERSIONED_SIZE); + Ok(None) + } else { + Ok(Some(Header { + packet_version, + packet_size: PacketSize::try_from(src[1])?, + packet_mode: PacketMode::try_from(src[2])?, + })) + } } } @@ -108,7 +142,16 @@ mod header_encoding { // make sure this is still 'unknown' for if we make changes in the future assert!(PacketSize::try_from(unknown_packet_size).is_err()); - let mut bytes = BytesMut::from([unknown_packet_size, PacketMode::default() as u8].as_ref()); + // unfortunately this will only work for the 'versioned' variant + // due to the hack used to get legacy mode compatibility + let mut bytes = BytesMut::from( + [ + PacketVersion::new_versioned(123).as_u8().unwrap(), + unknown_packet_size, + PacketMode::default() as u8, + ] + .as_ref(), + ); assert!(Header::decode(&mut bytes).is_err()) } @@ -127,23 +170,47 @@ mod header_encoding { let mut empty_bytes = BytesMut::new(); let decode_attempt_1 = Header::decode(&mut empty_bytes).unwrap(); assert!(decode_attempt_1.is_none()); - assert!(empty_bytes.capacity() > Header::SIZE); + assert!(empty_bytes.capacity() > Header::LEGACY_SIZE); let mut empty_bytes = BytesMut::with_capacity(1); let decode_attempt_2 = Header::decode(&mut empty_bytes).unwrap(); assert!(decode_attempt_2.is_none()); - assert!(empty_bytes.capacity() > Header::SIZE); + assert!(empty_bytes.capacity() > Header::LEGACY_SIZE); } #[test] - fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet() { + fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_legacy_mode() { let packet_sizes = vec![ PacketSize::AckPacket, PacketSize::RegularPacket, - PacketSize::ExtendedPacket, + PacketSize::ExtendedPacket8, + PacketSize::ExtendedPacket16, + PacketSize::ExtendedPacket32, ]; for packet_size in packet_sizes { let header = Header { + packet_version: PacketVersion::Legacy, + packet_size, + packet_mode: Default::default(), + }; + let mut bytes = BytesMut::new(); + header.encode(&mut bytes); + assert_eq!(bytes.capacity(), bytes.len() + packet_size.size()) + } + } + + #[test] + fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_versioned_mode() { + let packet_sizes = vec![ + PacketSize::AckPacket, + PacketSize::RegularPacket, + PacketSize::ExtendedPacket8, + PacketSize::ExtendedPacket16, + PacketSize::ExtendedPacket32, + ]; + for packet_size in packet_sizes { + let header = Header { + packet_version: PacketVersion::Versioned(123), packet_size, packet_mode: Default::default(), }; diff --git a/common/nymsphinx/params/src/lib.rs b/common/nymsphinx/params/src/lib.rs index fee4853e96..d954163861 100644 --- a/common/nymsphinx/params/src/lib.rs +++ b/common/nymsphinx/params/src/lib.rs @@ -13,6 +13,7 @@ pub use packet_sizes::PacketSize; pub mod packet_modes; pub mod packet_sizes; +pub mod packet_version; // If somebody can provide an argument why it might be reasonable to have more than 255 mix hops, // I will change this to [`usize`] @@ -24,6 +25,17 @@ pub const DEFAULT_NUM_MIX_HOPS: u8 = 3; pub const FRAG_ID_LEN: usize = 5; pub type SerializedFragmentIdentifier = [u8; FRAG_ID_LEN]; +// wait, wait, but why are we starting with version 7? +// when packet header gets serialized, the following bytes (in that order) are put onto the wire: +// - packet_version (starting with v1.1.0) +// - packet_size indicator +// - packet_mode +// it also just so happens that the only valid values for packet_size indicator include values 1-6 +// therefore if we receive byte `7` (or larger than that) we'll know we received a versioned packet, +// otherwise we should treat it as legacy +/// Increment it whenever we perform any breaking change in the wire format! +const CURRENT_PACKET_VERSION_NUMBER: u8 = 7; + // TODO: ask @AP about the choice of below algorithms /// Hashing algorithm used during hkdf for ephemeral shared key generation per sphinx packet payload. diff --git a/common/nymsphinx/params/src/packet_sizes.rs b/common/nymsphinx/params/src/packet_sizes.rs index c7fcd43e04..aa573ffe11 100644 --- a/common/nymsphinx/params/src/packet_sizes.rs +++ b/common/nymsphinx/params/src/packet_sizes.rs @@ -5,6 +5,7 @@ use crate::FRAG_ID_LEN; use nymsphinx_types::header::HEADER_SIZE; use nymsphinx_types::PAYLOAD_OVERHEAD_SIZE; use std::convert::TryFrom; +use std::str::FromStr; // it's up to the smart people to figure those values out : ) const REGULAR_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 2 * 1024; @@ -15,11 +16,16 @@ const REGULAR_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 2 * 102 const ACK_IV_SIZE: usize = 16; const ACK_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + ACK_IV_SIZE + FRAG_ID_LEN; -const EXTENDED_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 32 * 1024; +const EXTENDED_PACKET_SIZE_8: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 8 * 1024; +const EXTENDED_PACKET_SIZE_16: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 16 * 1024; +const EXTENDED_PACKET_SIZE_32: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 32 * 1024; #[derive(Debug)] pub struct InvalidPacketSize; +#[derive(Debug)] +pub struct InvalidExtendedPacketSize; + #[repr(u8)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum PacketSize { @@ -30,7 +36,28 @@ pub enum PacketSize { AckPacket = 2, // for example for streaming fast and furious in uncompressed 10bit 4K HDR quality - ExtendedPacket = 3, + ExtendedPacket32 = 3, + + // for example for streaming fast and furious in heavily compressed lossy RealPlayer quality + ExtendedPacket8 = 4, + + // for example for streaming fast and furious in compressed XviD quality + ExtendedPacket16 = 5, +} + +impl FromStr for PacketSize { + type Err = InvalidPacketSize; + + fn from_str(s: &str) -> Result { + match s { + "regular" => Ok(Self::RegularPacket), + "ack" => Ok(Self::AckPacket), + "extended8" => Ok(Self::ExtendedPacket8), + "extended16" => Ok(Self::ExtendedPacket16), + "extended32" => Ok(Self::ExtendedPacket32), + _ => Err(InvalidPacketSize), + } + } } impl TryFrom for PacketSize { @@ -40,7 +67,9 @@ impl TryFrom for PacketSize { match value { _ if value == (PacketSize::RegularPacket as u8) => Ok(Self::RegularPacket), _ if value == (PacketSize::AckPacket as u8) => Ok(Self::AckPacket), - _ if value == (PacketSize::ExtendedPacket as u8) => Ok(Self::ExtendedPacket), + _ if value == (PacketSize::ExtendedPacket8 as u8) => Ok(Self::ExtendedPacket8), + _ if value == (PacketSize::ExtendedPacket16 as u8) => Ok(Self::ExtendedPacket16), + _ if value == (PacketSize::ExtendedPacket32 as u8) => Ok(Self::ExtendedPacket32), _ => Err(InvalidPacketSize), } } @@ -51,7 +80,9 @@ impl PacketSize { match self { PacketSize::RegularPacket => REGULAR_PACKET_SIZE, PacketSize::AckPacket => ACK_PACKET_SIZE, - PacketSize::ExtendedPacket => EXTENDED_PACKET_SIZE, + PacketSize::ExtendedPacket8 => EXTENDED_PACKET_SIZE_8, + PacketSize::ExtendedPacket16 => EXTENDED_PACKET_SIZE_16, + PacketSize::ExtendedPacket32 => EXTENDED_PACKET_SIZE_32, } } @@ -68,12 +99,33 @@ impl PacketSize { Ok(PacketSize::RegularPacket) } else if PacketSize::AckPacket.size() == size { Ok(PacketSize::AckPacket) - } else if PacketSize::ExtendedPacket.size() == size { - Ok(PacketSize::ExtendedPacket) + } else if PacketSize::ExtendedPacket8.size() == size { + Ok(PacketSize::ExtendedPacket8) + } else if PacketSize::ExtendedPacket16.size() == size { + Ok(PacketSize::ExtendedPacket16) + } else if PacketSize::ExtendedPacket32.size() == size { + Ok(PacketSize::ExtendedPacket32) } else { Err(InvalidPacketSize) } } + + pub fn is_extended_size(&self) -> bool { + match self { + PacketSize::RegularPacket | PacketSize::AckPacket => false, + PacketSize::ExtendedPacket8 + | PacketSize::ExtendedPacket16 + | PacketSize::ExtendedPacket32 => true, + } + } + + pub fn as_extended_size(self) -> Option { + if self.is_extended_size() { + Some(self) + } else { + None + } + } } impl Default for PacketSize { diff --git a/common/nymsphinx/params/src/packet_version.rs b/common/nymsphinx/params/src/packet_version.rs new file mode 100644 index 0000000000..001f46c84d --- /dev/null +++ b/common/nymsphinx/params/src/packet_version.rs @@ -0,0 +1,59 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{PacketSize, CURRENT_PACKET_VERSION_NUMBER}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PacketVersion { + // this will allow updated mixnodes to still understand packets from before the update + Legacy, + Versioned(u8), +} + +impl PacketVersion { + pub fn new(use_legacy: bool) -> Self { + if use_legacy { + Self::new_legacy() + } else { + Self::new_versioned(CURRENT_PACKET_VERSION_NUMBER) + } + } + + pub fn new_legacy() -> Self { + PacketVersion::Legacy + } + + pub fn new_versioned(version: u8) -> Self { + PacketVersion::Versioned(version) + } + + pub fn is_legacy(&self) -> bool { + matches!(self, PacketVersion::Legacy) + } + + pub fn as_u8(&self) -> Option { + match self { + PacketVersion::Legacy => None, + PacketVersion::Versioned(version) => Some(*version), + } + } +} + +impl From for PacketVersion { + fn from(v: u8) -> Self { + match v { + n if n == PacketSize::RegularPacket as u8 => PacketVersion::Legacy, + n if n == PacketSize::AckPacket as u8 => PacketVersion::Legacy, + n if n == PacketSize::ExtendedPacket8 as u8 => PacketVersion::Legacy, + n if n == PacketSize::ExtendedPacket16 as u8 => PacketVersion::Legacy, + n if n == PacketSize::ExtendedPacket32 as u8 => PacketVersion::Legacy, + n => PacketVersion::Versioned(n), + } + } +} + +impl Default for PacketVersion { + fn default() -> Self { + PacketVersion::Versioned(CURRENT_PACKET_VERSION_NUMBER) + } +} diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index 3f56f9bada..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 } @@ -213,7 +214,7 @@ where /// - compute vk_b = g^x || v_b /// - compute sphinx_plaintext = SURB_ACK || g^x || v_b /// - compute sphinx_packet = Sphinx(recipient, sphinx_plaintext) - pub async fn prepare_chunk_for_sending( + pub fn prepare_chunk_for_sending( &mut self, fragment: Fragment, topology: &NymTopology, @@ -222,8 +223,7 @@ where ) -> Result { // create an ack let (ack_delay, surb_ack_bytes) = self - .generate_surb_ack(fragment.fragment_identifier(), topology, ack_key) - .await? + .generate_surb_ack(fragment.fragment_identifier(), topology, ack_key)? .prepare_for_sending(); // TODO: @@ -294,7 +294,7 @@ where } /// Construct an acknowledgement SURB for the given [`FragmentIdentifier`] - async fn generate_surb_ack( + fn generate_surb_ack( &mut self, fragment_id: FragmentIdentifier, topology: &NymTopology, @@ -357,8 +357,7 @@ where // gateways could not distinguish reply packets from normal messages due to lack of said acks // note: the ack delay is irrelevant since we do not know the delay of actual surb let (_, surb_ack_bytes) = self - .generate_surb_ack(reply_id, topology, ack_key) - .await? + .generate_surb_ack(reply_id, topology, ack_key)? .prepare_for_sending(); let zero_pad_len = self.packet_size.plaintext_size() diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index 1707983ca3..0b6a8e96dd 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -203,9 +203,8 @@ mod message_receiver { mixes.insert( 1, vec![mix::Node { + mix_id: 123, owner: "foomp1".to_string(), - stake: 123, - delegation: 456, host: "10.20.30.40".parse().unwrap(), mix_host: "10.20.30.40:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -224,9 +223,8 @@ mod message_receiver { mixes.insert( 2, vec![mix::Node { + mix_id: 234, owner: "foomp2".to_string(), - stake: 123, - delegation: 456, host: "11.21.31.41".parse().unwrap(), mix_host: "11.21.31.41:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -245,9 +243,8 @@ mod message_receiver { mixes.insert( 3, vec![mix::Node { + mix_id: 456, owner: "foomp3".to_string(), - stake: 123, - delegation: 456, host: "12.22.32.42".parse().unwrap(), mix_host: "12.22.32.42:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( diff --git a/common/socks5/ordered-buffer/README.md b/common/socks5/ordered-buffer/README.md index a173c537b0..4a2593f588 100644 --- a/common/socks5/ordered-buffer/README.md +++ b/common/socks5/ordered-buffer/README.md @@ -1,5 +1,7 @@ Ordered Buffer ============== +**CURRENTLY UNUSED** + This crate takes care of reliably and speedily turning bytes into a series of ordered message fragments on one side, and of reliably reassembling the fragments into the original message on the other. diff --git a/common/socks5/ordered-buffer/src/buffer.rs b/common/socks5/ordered-buffer/src/buffer.rs index 19d946f8d6..33c4d69142 100644 --- a/common/socks5/ordered-buffer/src/buffer.rs +++ b/common/socks5/ordered-buffer/src/buffer.rs @@ -13,6 +13,13 @@ pub struct OrderedMessageBuffer { messages: HashMap, } +/// Data returned from `OrderedMessageBuffer` on a successful read of gapless ordered data. +#[derive(Debug, PartialEq, Eq)] +pub struct ReadContiguousData { + pub data: Vec, + pub last_index: u64, +} + impl OrderedMessageBuffer { pub fn new() -> OrderedMessageBuffer { OrderedMessageBuffer { @@ -42,7 +49,7 @@ impl OrderedMessageBuffer { /// a read will return the bytes of messages 0, 1, 2. Subsequent reads will /// return `None` until message 3 comes in, at which point 3, 4, and any /// further contiguous messages which have arrived will be returned. - pub fn read(&mut self) -> Option> { + pub fn read(&mut self) -> Option { if !self.messages.contains_key(&self.next_index) { return None; } @@ -66,7 +73,10 @@ impl OrderedMessageBuffer { .collect(); trace!("Returning {} bytes from ordered message buffer", data.len()); - Some(data) + Some(ReadContiguousData { + data, + last_index: index, + }) } } @@ -102,11 +112,11 @@ mod test_chunking_and_reassembling { }; buffer.write(first_message); - let first_read = buffer.read().unwrap(); + let first_read = buffer.read().unwrap().data; assert_eq!(vec![1, 2, 3, 4], first_read); buffer.write(second_message); - let second_read = buffer.read().unwrap(); + let second_read = buffer.read().unwrap().data; assert_eq!(vec![5, 6, 7, 8], second_read); assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty @@ -128,7 +138,7 @@ mod test_chunking_and_reassembling { buffer.write(first_message); buffer.write(second_message); let second_read = buffer.read(); - assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], second_read.unwrap()); + assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], second_read.unwrap().data); assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty } @@ -147,8 +157,8 @@ mod test_chunking_and_reassembling { buffer.write(second_message); buffer.write(first_message); - let read = buffer.read(); - assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], read.unwrap()); + let read = buffer.read().unwrap().data; + assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], read); assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty } } @@ -182,7 +192,7 @@ mod test_chunking_and_reassembling { #[test] fn everything_up_to_the_indexing_gap_is_returned() { let mut buffer = setup(); - let ordered_bytes = buffer.read().unwrap(); + let ordered_bytes = buffer.read().unwrap().data; assert_eq!([0, 0, 0, 0, 1, 1, 1, 1].to_vec(), ordered_bytes); // we shouldn't get any more from a second attempt if nothing is added @@ -208,7 +218,7 @@ mod test_chunking_and_reassembling { }; buffer.write(two_message); - let more_ordered_bytes = buffer.read().unwrap(); + let more_ordered_bytes = buffer.read().unwrap().data; assert_eq!([2, 2, 2, 2, 3, 3, 3, 3].to_vec(), more_ordered_bytes); // let's add another message @@ -227,7 +237,10 @@ mod test_chunking_and_reassembling { }; buffer.write(four_message); - assert_eq!([4, 4, 4, 4, 5, 5, 5, 5].to_vec(), buffer.read().unwrap()); + assert_eq!( + [4, 4, 4, 4, 5, 5, 5, 5].to_vec(), + buffer.read().unwrap().data + ); // at this point we should again get back nothing if we try a read assert_eq!(None, buffer.read()); diff --git a/common/socks5/ordered-buffer/src/lib.rs b/common/socks5/ordered-buffer/src/lib.rs index 7a3c9e6501..9ed9d9c867 100644 --- a/common/socks5/ordered-buffer/src/lib.rs +++ b/common/socks5/ordered-buffer/src/lib.rs @@ -2,7 +2,7 @@ mod buffer; mod message; mod sender; -pub use buffer::OrderedMessageBuffer; +pub use buffer::{OrderedMessageBuffer, ReadContiguousData}; pub use message::MessageError; pub use message::OrderedMessage; pub use sender::OrderedMessageSender; diff --git a/common/socks5/proxy-helpers/Cargo.toml b/common/socks5/proxy-helpers/Cargo.toml index 92ac6a2bae..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. @@ -16,6 +16,7 @@ futures = "0.3" log = "0.4" socks5-requests = { path = "../requests" } ordered-buffer = { path = "../ordered-buffer" } +task = { path = "../../task" } [dev-dependencies] tokio-test = "0.4.2" diff --git a/common/socks5/proxy-helpers/src/connection_controller.rs b/common/socks5/proxy-helpers/src/connection_controller.rs index 03f3d4b5cc..1a9b13cc29 100644 --- a/common/socks5/proxy-helpers/src/connection_controller.rs +++ b/common/socks5/proxy-helpers/src/connection_controller.rs @@ -4,9 +4,10 @@ use futures::channel::mpsc; use futures::StreamExt; use log::*; -use ordered_buffer::{OrderedMessage, OrderedMessageBuffer}; +use ordered_buffer::{OrderedMessage, OrderedMessageBuffer, ReadContiguousData}; use socks5_requests::ConnectionId; use std::collections::{HashMap, HashSet}; +use task::ShutdownListener; /// A generic message produced after reading from a socket/connection. It includes data that was /// actually read alongside boolean indicating whether the connection got closed so that @@ -37,12 +38,13 @@ pub enum ControllerCommand { struct ActiveConnection { is_closed: bool, + closed_at_index: Option, connection_sender: Option, ordered_buffer: OrderedMessageBuffer, } impl ActiveConnection { - fn write_to_buf(&mut self, payload: Vec) { + fn write_to_buf(&mut self, payload: Vec, is_closed: bool) { let ordered_message = match OrderedMessage::try_from_bytes(payload) { Ok(msg) => msg, Err(err) => { @@ -50,10 +52,13 @@ impl ActiveConnection { return; } }; + if is_closed { + self.closed_at_index = Some(ordered_message.index); + } self.ordered_buffer.write(ordered_message); } - fn read_from_buf(&mut self) -> Option> { + fn read_from_buf(&mut self) -> Option { self.ordered_buffer.read() } } @@ -74,10 +79,12 @@ pub struct Controller { // buffer for messages received before connection was established due to mixnet being able to // un-order messages. Note we don't ever expect to have more than 1-2 messages per connection here pending_messages: HashMap, bool)>>, + + shutdown: ShutdownListener, } impl Controller { - pub fn new() -> (Self, ControllerSender) { + pub fn new(shutdown: ShutdownListener) -> (Self, ControllerSender) { let (sender, receiver) = mpsc::unbounded(); ( Controller { @@ -85,6 +92,7 @@ impl Controller { receiver, recently_closed: HashSet::new(), pending_messages: HashMap::new(), + shutdown, }, sender, ) @@ -95,6 +103,7 @@ impl Controller { is_closed: false, connection_sender: Some(connection_sender), ordered_buffer: OrderedMessageBuffer::new(), + closed_at_index: None, }; if let Some(_active_conn) = self.active_connections.insert(conn_id, active_connection) { error!("Received a duplicate 'Connect'!") @@ -123,21 +132,23 @@ impl Controller { fn send_to_connection(&mut self, conn_id: ConnectionId, payload: Vec, is_closed: bool) { if let Some(active_connection) = self.active_connections.get_mut(&conn_id) { if !payload.is_empty() { - active_connection.write_to_buf(payload); + active_connection.write_to_buf(payload, is_closed); } else if !is_closed { error!("Tried to write an empty message to a not-closing connection. Please let us know if you see this message"); } - // if messages get unordered, make sure we don't lose information about - // remote socket getting closed! - active_connection.is_closed |= is_closed; if let Some(payload) = active_connection.read_from_buf() { + if let Some(closed_at_index) = active_connection.closed_at_index { + if payload.last_index > closed_at_index { + active_connection.is_closed = true; + } + } if let Err(err) = active_connection .connection_sender .as_mut() .unwrap() .unbounded_send(ConnectionMessage { - payload, + payload: payload.data, socket_closed: active_connection.is_closed, }) { @@ -170,16 +181,24 @@ impl Controller { } pub async fn run(&mut self) { - while let Some(command) = self.receiver.next().await { - match command { - ControllerCommand::Send(conn_id, data, is_closed) => { - self.send_to_connection(conn_id, data, is_closed) + loop { + tokio::select! { + command = self.receiver.next() => match command { + Some(ControllerCommand::Send(conn_id, data, is_closed)) => { + self.send_to_connection(conn_id, data, is_closed) + } + Some(ControllerCommand::Insert(conn_id, sender)) => { + self.insert_connection(conn_id, sender) + } + Some(ControllerCommand::Remove(conn_id)) => self.remove_connection(conn_id), + None => { + log::trace!("SOCKS5 Controller: Stopping since channel closed"); + break; + } } - ControllerCommand::Insert(conn_id, sender) => { - self.insert_connection(conn_id, sender) - } - ControllerCommand::Remove(conn_id) => self.remove_connection(conn_id), } } + assert!(self.shutdown.is_shutdown_poll()); + log::debug!("SOCKS5 Controller: Exiting"); } } diff --git a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs index 979ae918c4..5efc297ce7 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs @@ -11,6 +11,7 @@ use log::*; use ordered_buffer::OrderedMessageSender; use socks5_requests::ConnectionId; use std::{io, sync::Arc}; +use task::ShutdownListener; use tokio::select; use tokio::{net::tcp::OwnedReadHalf, sync::Notify, time::sleep}; @@ -74,6 +75,7 @@ where is_finished } +#[allow(clippy::too_many_arguments)] pub(super) async fn run_inbound( mut reader: OwnedReadHalf, local_destination_address: String, // addresses are provided for better logging @@ -82,6 +84,7 @@ pub(super) async fn run_inbound( mix_sender: MixProxySender, adapter_fn: F, shutdown_notify: Arc, + mut shutdown_listener: ShutdownListener, ) -> OwnedReadHalf where F: Fn(ConnectionId, Vec, bool) -> S + Send + 'static, @@ -106,6 +109,10 @@ where send_empty_close(connection_id, &mut message_sender, &mix_sender, &adapter_fn); break; } + _ = shutdown_listener.recv() => { + log::trace!("ProxyRunner inbound: Received shutdown"); + break; + } } } trace!("{} - inbound closed", connection_id); diff --git a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs index d31ef8ced7..1d5b493ced 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs @@ -5,6 +5,7 @@ use crate::connection_controller::ConnectionReceiver; use futures::channel::mpsc; use socks5_requests::ConnectionId; use std::{sync::Arc, time::Duration}; +use task::ShutdownListener; use tokio::{net::TcpStream, sync::Notify}; mod inbound; @@ -44,6 +45,9 @@ pub struct ProxyRunner { local_destination_address: String, remote_source_address: String, connection_id: ConnectionId, + + // Listens to shutdown commands from higher up + shutdown_listener: ShutdownListener, } impl ProxyRunner @@ -57,6 +61,7 @@ where mix_receiver: ConnectionReceiver, mix_sender: MixProxySender, connection_id: ConnectionId, + shutdown_listener: ShutdownListener, ) -> Self { ProxyRunner { mix_receiver: Some(mix_receiver), @@ -65,6 +70,7 @@ where local_destination_address, remote_source_address, connection_id, + shutdown_listener, } } @@ -86,6 +92,7 @@ where self.mix_sender.clone(), adapter_fn, Arc::clone(&shutdown_notify), + self.shutdown_listener.clone(), ); let outbound_future = outbound::run_outbound( @@ -95,6 +102,7 @@ where self.mix_receiver.take().unwrap(), self.connection_id, shutdown_notify, + self.shutdown_listener.clone(), ); // TODO: this shouldn't really have to spawn tasks inside "library" code, but diff --git a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs index 111bc6bf9e..8fad665785 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs @@ -8,6 +8,7 @@ use futures::StreamExt; use log::*; use socks5_requests::ConnectionId; use std::{sync::Arc, time::Duration}; +use task::ShutdownListener; use tokio::io::AsyncWriteExt; use tokio::select; use tokio::{net::tcp::OwnedWriteHalf, sync::Notify, time::sleep, time::Instant}; @@ -50,6 +51,7 @@ pub(super) async fn run_outbound( mut mix_receiver: ConnectionReceiver, connection_id: ConnectionId, shutdown_notify: Arc, + mut shutdown_listener: ShutdownListener, ) -> (OwnedWriteHalf, ConnectionReceiver) { let shutdown_future = shutdown_notify.notified().then(|_| sleep(SHUTDOWN_TIMEOUT)); tokio::pin!(shutdown_future); @@ -78,6 +80,10 @@ pub(super) async fn run_outbound( debug!("closing outbound proxy after inbound was closed {:?} ago", SHUTDOWN_TIMEOUT); break; } + _ = shutdown_listener.recv() => { + log::trace!("ProxyRunner outbound: Received shutdown"); + break; + } } } diff --git a/common/socks5/requests/Cargo.toml b/common/socks5/requests/Cargo.toml index 39ccacb4b1..e7bd46d279 100644 --- a/common/socks5/requests/Cargo.toml +++ b/common/socks5/requests/Cargo.toml @@ -8,4 +8,4 @@ edition = "2021" [dependencies] nymsphinx-addressing = { path = "../../../common/nymsphinx/addressing" } -ordered-buffer = {path = "../ordered-buffer"} +thiserror = "1" diff --git a/common/socks5/requests/src/lib.rs b/common/socks5/requests/src/lib.rs index 434bf0df24..b7da4498fa 100644 --- a/common/socks5/requests/src/lib.rs +++ b/common/socks5/requests/src/lib.rs @@ -1,7 +1,12 @@ +// Copyright 2020-2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + pub mod msg; +pub mod network_requester_response; pub mod request; pub mod response; pub use msg::*; +pub use network_requester_response::*; pub use request::*; pub use response::*; diff --git a/common/socks5/requests/src/msg.rs b/common/socks5/requests/src/msg.rs index 68d1160b4e..b13f346f35 100644 --- a/common/socks5/requests/src/msg.rs +++ b/common/socks5/requests/src/msg.rs @@ -1,36 +1,40 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2020-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use thiserror::Error; + +use crate::network_requester_response::{Error as NrError, NetworkRequesterResponse}; use crate::request::{Request, RequestError}; use crate::response::{Response, ResponseError}; -#[derive(Debug)] +#[derive(Debug, Error)] pub enum MessageError { + #[error("{0}")] Request(RequestError), - Response(ResponseError), - NoData, - UnknownMessageType, -} -impl std::fmt::Display for MessageError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - MessageError::Request(r) => write!(f, "{}", r), - MessageError::Response(r) => write!(f, "{:?}", r), - MessageError::NoData => write!(f, "no data provided"), - MessageError::UnknownMessageType => write!(f, "unknown message type received"), - } - } + #[error("{0:?}")] + Response(ResponseError), + + #[error("{0}")] + NetworkRequesterResponseError(NrError), + + #[error("no data")] + NoData, + + #[error("unknown message type received")] + UnknownMessageType, } pub enum Message { Request(Request), Response(Response), + NetworkRequesterResponse(NetworkRequesterResponse), } impl Message { const REQUEST_FLAG: u8 = 0; const RESPONSE_FLAG: u8 = 1; + const NR_RESPONSE_FLAG: u8 = 2; pub fn conn_id(&self) -> u64 { match self { @@ -39,6 +43,7 @@ impl Message { Request::Send(conn_id, _, _) => *conn_id, }, Message::Response(resp) => resp.connection_id, + Message::NetworkRequesterResponse(resp) => resp.connection_id, } } @@ -49,6 +54,7 @@ impl Message { Request::Send(_, data, _) => data.len(), }, Message::Response(resp) => resp.data.len(), + Message::NetworkRequesterResponse(_) => 0, } } @@ -65,6 +71,10 @@ impl Message { Response::try_from_bytes(&b[1..]) .map(Message::Response) .map_err(MessageError::Response) + } else if b[0] == Self::NR_RESPONSE_FLAG { + NetworkRequesterResponse::try_from_bytes(&b[1..]) + .map(Message::NetworkRequesterResponse) + .map_err(MessageError::NetworkRequesterResponseError) } else { Err(MessageError::UnknownMessageType) } @@ -78,6 +88,9 @@ impl Message { Self::Response(r) => std::iter::once(Self::RESPONSE_FLAG) .chain(r.into_bytes().iter().cloned()) .collect(), + Self::NetworkRequesterResponse(r) => std::iter::once(Self::NR_RESPONSE_FLAG) + .chain(r.into_bytes().iter().cloned()) + .collect(), } } } diff --git a/common/socks5/requests/src/network_requester_response.rs b/common/socks5/requests/src/network_requester_response.rs new file mode 100644 index 0000000000..64e459dfb0 --- /dev/null +++ b/common/socks5/requests/src/network_requester_response.rs @@ -0,0 +1,112 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::ConnectionId; + +#[derive(Debug)] +pub struct NetworkRequesterResponse { + pub connection_id: ConnectionId, + pub network_requester_error: String, +} + +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum Error { + #[error("no data provided")] + NoData, + + #[error("not enough bytes to recover the connection id")] + ConnectionIdTooShort, + + #[error("message is not utf8 encoded")] + MalformedErrorMessage(#[from] std::string::FromUtf8Error), +} + +impl NetworkRequesterResponse { + pub fn new(connection_id: ConnectionId, network_requester_error: String) -> Self { + NetworkRequesterResponse { + connection_id, + network_requester_error, + } + } + + pub fn try_from_bytes(b: &[u8]) -> Result { + if b.is_empty() { + return Err(Error::NoData); + } + + if b.len() < 8 { + return Err(Error::ConnectionIdTooShort); + } + + let mut connection_id_bytes = b.to_vec(); + let network_requester_error_bytes = connection_id_bytes.split_off(8); + + let connection_id = u64::from_be_bytes([ + connection_id_bytes[0], + connection_id_bytes[1], + connection_id_bytes[2], + connection_id_bytes[3], + connection_id_bytes[4], + connection_id_bytes[5], + connection_id_bytes[6], + connection_id_bytes[7], + ]); + let network_requester_error = String::from_utf8(network_requester_error_bytes)?; + + Ok(NetworkRequesterResponse { + connection_id, + network_requester_error, + }) + } + + pub fn into_bytes(self) -> Vec { + self.connection_id + .to_be_bytes() + .iter() + .cloned() + .chain(self.network_requester_error.into_bytes().into_iter()) + .collect() + } +} + +#[cfg(test)] +mod network_requester_response_serde_tests { + use super::*; + + #[test] + fn simple_serde() { + let conn_id = 42; + let network_requester_error = String::from("This is a test msg"); + let response = NetworkRequesterResponse::new(conn_id, network_requester_error.clone()); + let bytes = response.into_bytes(); + let deserialized_response = NetworkRequesterResponse::try_from_bytes(&bytes).unwrap(); + + assert_eq!(conn_id, deserialized_response.connection_id); + assert_eq!( + network_requester_error, + deserialized_response.network_requester_error + ); + } + + #[test] + fn deserialization_errors() { + let err = NetworkRequesterResponse::try_from_bytes(&[]).err().unwrap(); + assert_eq!(err, Error::NoData); + + let bytes: [u8; 5] = [1, 2, 3, 4, 5]; + let err = NetworkRequesterResponse::try_from_bytes(&bytes) + .err() + .unwrap(); + assert_eq!(err, Error::ConnectionIdTooShort); + + let bytes: Vec = 42u64 + .to_be_bytes() + .into_iter() + .chain([0, 159, 146, 150].into_iter()) + .collect(); + let err = NetworkRequesterResponse::try_from_bytes(&bytes) + .err() + .unwrap(); + assert!(matches!(err, Error::MalformedErrorMessage(_))); + } +} diff --git a/common/socks5/requests/src/request.rs b/common/socks5/requests/src/request.rs index b6551da1d4..f6921e362a 100644 --- a/common/socks5/requests/src/request.rs +++ b/common/socks5/requests/src/request.rs @@ -1,6 +1,9 @@ +// Copyright 2020-2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use nymsphinx_addressing::clients::{Recipient, RecipientFormattingError}; use std::convert::TryFrom; -use std::fmt::{self}; +use thiserror::Error; pub type ConnectionId = u64; pub type RemoteAddress = String; @@ -12,39 +15,30 @@ pub enum RequestFlag { Send = 1, } -#[derive(Debug)] +#[derive(Debug, Error)] pub enum RequestError { + #[error("not enough bytes to recover the length of the address")] AddressLengthTooShort, + + #[error("not enough bytes to recover the address")] AddressTooShort, + + #[error("not enough bytes to recover the connection id")] ConnectionIdTooShort, + + #[error("no data provided")] NoData, + + #[error("request of unknown type")] UnknownRequestFlag, + + #[error("too short return address")] ReturnAddressTooShort, + + #[error("malformed return address - {0}")] MalformedReturnAddress(RecipientFormattingError), } -impl fmt::Display for RequestError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { - match self { - RequestError::AddressLengthTooShort => { - write!(f, "not enough bytes to recover the length of the address") - } - RequestError::AddressTooShort => write!(f, "not enough bytes to recover the address"), - RequestError::ConnectionIdTooShort => { - write!(f, "not enough bytes to recover the connection id") - } - RequestError::NoData => write!(f, "no data provided"), - RequestError::UnknownRequestFlag => write!(f, "request of unknown type"), - RequestError::ReturnAddressTooShort => write!(f, "too short return address"), - RequestError::MalformedReturnAddress(recipient_err) => { - write!(f, "malformed return address - {}", recipient_err) - } - } - } -} - -impl std::error::Error for RequestError {} - impl RequestError { pub fn is_malformed_return(&self) -> bool { matches!(self, RequestError::MalformedReturnAddress(_)) diff --git a/common/socks5/requests/src/response.rs b/common/socks5/requests/src/response.rs index ab409eb03e..07d78be964 100644 --- a/common/socks5/requests/src/response.rs +++ b/common/socks5/requests/src/response.rs @@ -1,8 +1,15 @@ +// Copyright 2020-2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use thiserror::Error; + use crate::ConnectionId; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Error, PartialEq, Eq)] pub enum ResponseError { + #[error("not enough bytes to recover the connection id")] ConnectionIdTooShort, + #[error("no data provided")] NoData, } /// A remote network response retrieved by the Socks5 service provider. This 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 0e8df18f3a..4c0c088d67 100644 --- a/common/task/Cargo.toml +++ b/common/task/Cargo.toml @@ -7,8 +7,7 @@ edition = "2021" [dependencies] log = "0.4" -tokio = { version = "1.19.1", features = ["rt-multi-thread", "net", "signal"] } -tokio-util = { version = "0.7.3", features = ["codec"] } +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/task/src/lib.rs b/common/task/src/lib.rs index b983af29db..b8ddb7076a 100644 --- a/common/task/src/lib.rs +++ b/common/task/src/lib.rs @@ -2,5 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod shutdown; +pub mod signal; pub use shutdown::{ShutdownListener, ShutdownNotifier}; +pub use signal::wait_for_signal; diff --git a/common/task/src/shutdown.rs b/common/task/src/shutdown.rs index 8c8b472459..21ea12569d 100644 --- a/common/task/src/shutdown.rs +++ b/common/task/src/shutdown.rs @@ -5,13 +5,14 @@ use std::time::Duration; use tokio::sync::watch::{self, error::SendError}; -const SHUTDOWN_TIMER_SECS: u64 = 5; +const DEFAULT_SHUTDOWN_TIMER_SECS: u64 = 5; /// Used to notify other tasks to gracefully shutdown #[derive(Debug)] pub struct ShutdownNotifier { notify_tx: watch::Sender<()>, notify_rx: Option>, + shutdown_timer_secs: u64, } impl Default for ShutdownNotifier { @@ -20,11 +21,19 @@ impl Default for ShutdownNotifier { Self { notify_tx, notify_rx: Some(notify_rx), + shutdown_timer_secs: DEFAULT_SHUTDOWN_TIMER_SECS, } } } impl ShutdownNotifier { + pub fn new(shutdown_timer_secs: u64) -> Self { + Self { + shutdown_timer_secs, + ..Default::default() + } + } + pub fn subscribe(&self) -> ShutdownListener { ShutdownListener::new( self.notify_rx @@ -50,7 +59,7 @@ impl ShutdownNotifier { _ = tokio::signal::ctrl_c() => { log::info!("Forcing shutdown"); } - _ = tokio::time::sleep(Duration::from_secs(SHUTDOWN_TIMER_SECS)) => { + _ = tokio::time::sleep(Duration::from_secs(self.shutdown_timer_secs)) => { log::info!("Timout reached, forcing shutdown"); }, } @@ -65,7 +74,7 @@ pub struct ShutdownListener { } impl ShutdownListener { - pub fn new(notify: watch::Receiver<()>) -> ShutdownListener { + fn new(notify: watch::Receiver<()>) -> ShutdownListener { ShutdownListener { shutdown: false, notify, @@ -83,6 +92,25 @@ impl ShutdownListener { let _ = self.notify.changed().await; self.shutdown = true; } + + pub fn is_shutdown_poll(&mut self) -> bool { + if self.shutdown { + return true; + } + match self.notify.has_changed() { + Ok(has_changed) => { + if has_changed { + self.shutdown = true; + } + has_changed + } + Err(err) => { + log::debug!("Polling shutdown failed: {err}"); + log::debug!("Assuming this means we should shutdown..."); + true + } + } + } } #[cfg(test)] diff --git a/common/task/src/signal.rs b/common/task/src/signal.rs new file mode 100644 index 0000000000..40ca122848 --- /dev/null +++ b/common/task/src/signal.rs @@ -0,0 +1,27 @@ +#[cfg(unix)] +pub async fn wait_for_signal() { + use tokio::signal::unix::{signal, SignalKind}; + let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM channel"); + let mut sigquit = signal(SignalKind::quit()).expect("Failed to setup SIGQUIT channel"); + + tokio::select! { + _ = tokio::signal::ctrl_c() => { + log::info!("Received SIGINT"); + }, + _ = sigterm.recv() => { + log::info!("Received SIGTERM"); + } + _ = sigquit.recv() => { + log::info!("Received SIGQUIT"); + } + } +} + +#[cfg(not(unix))] +pub async fn wait_for_signal() { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + log::info!("Received SIGINT"); + }, + } +} diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 7d5b9843d4..ed6b7c71fb 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -3,7 +3,8 @@ use crate::filter::VersionFilterable; use log::warn; -use mixnet_contract_common::{GatewayBond, MixNodeBond}; +use mixnet_contract_common::mixnode::MixNodeDetails; +use mixnet_contract_common::GatewayBond; use nymsphinx_addressing::nodes::NodeIdentity; use nymsphinx_types::Node as SphinxNode; use rand::Rng; @@ -224,12 +225,15 @@ impl NymTopology { } } -pub fn nym_topology_from_bonds( - mix_bonds: Vec, +pub fn nym_topology_from_detailed( + mix_details: Vec, gateway_bonds: Vec, ) -> NymTopology { let mut mixes = HashMap::new(); - for bond in mix_bonds.into_iter() { + for bond in mix_details + .into_iter() + .map(|details| details.bond_information) + { let layer = bond.layer as MixLayer; if layer == 0 || layer > 3 { warn!( @@ -238,13 +242,14 @@ pub fn nym_topology_from_bonds( ); continue; } - let mix_id = bond.mix_node.identity_key.clone(); + 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); match bond.try_into() { Ok(mix) => layer_entry.push(mix), Err(err) => { - warn!("Mix {} is malformed - {}", mix_id, err); + warn!("Mix {} / {} is malformed - {}", mix_id, mix_identity, err); continue; } } @@ -279,9 +284,8 @@ mod converting_mixes_to_vec { #[test] fn returns_a_vec_with_hashmap_values() { let node1 = mix::Node { + mix_id: 42, owner: "N/A".to_string(), - stake: 0, - delegation: 0, host: "3.3.3.3".parse().unwrap(), mix_host: "3.3.3.3:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index 9d3eaf3b65..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}; +use mixnet_contract_common::{Layer, MixId, MixNodeBond}; use nymsphinx_addressing::nodes::NymNodeRoutingAddress; use nymsphinx_types::Node as SphinxNode; use std::convert::{TryFrom, TryInto}; @@ -68,11 +68,8 @@ impl Display for MixnodeConversionError { #[derive(Debug, Clone)] pub struct Node { + pub mix_id: MixId, pub owner: String, - // somebody correct me if I'm wrong, but we should only ever have a single denom of currency - // on the network at a type, right? - pub stake: u128, - pub delegation: u128, pub host: NetworkAddress, // we're keeping this as separate resolved field since we do not want to be resolving the potential // hostname every time we want to construct a path via this node @@ -116,9 +113,8 @@ impl<'a> TryFrom<&'a MixNodeBond> for Node { })?[0]; Ok(Node { + mix_id: bond.mix_id, owner: bond.owner.as_str().to_owned(), - stake: bond.pledge_amount.amount.into(), - delegation: bond.total_delegation.amount.into(), host, mix_host, identity_key: identity::PublicKey::from_base58_string(&bond.mix_node.identity_key)?, diff --git a/common/types/Cargo.toml b/common/types/Cargo.toml index 8e9d7c3edf..54603c02d2 100644 --- a/common/types/Cargo.toml +++ b/common/types/Cargo.toml @@ -19,7 +19,7 @@ thiserror = "1.0" url = "2.2" ts-rs = "6.1.2" -cosmwasm-std = "1.0.0-beta8" +cosmwasm-std = "1.0.0" cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" } validator-client = { path = "../../common/client-libs/validator-client", features = [ @@ -37,4 +37,4 @@ tempfile = "3.3.0" [features] default = [] -generate-ts = [] +generate-ts = ["mixnet-contract-common/generate-ts"] diff --git a/common/types/src/currency.rs b/common/types/src/currency.rs index d2bafe589c..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) } @@ -71,6 +76,50 @@ impl RegisteredCoins { self.0.remove(denom) } + pub fn attempt_create_display_coin_from_base_dec_amount( + &self, + denom: &Denom, + base_amount: Decimal, + ) -> Result { + for registered_coin in self.0.values() { + if let Some(exponent) = registered_coin.get_exponent(denom) { + // if this fails it means we haven't registered our display denom which honestly should never be the case + // unless somebody is rocking their own custom network + let display_exponent = registered_coin + .get_exponent(®istered_coin.display) + .ok_or_else(|| TypesError::UnknownCoinDenom(denom.clone()))?; + + return match exponent.cmp(&display_exponent) { + Ordering::Greater => { + // we need to scale up, unlikely to ever be needed but included for completion sake, + // for example if we decided to created knym with exponent 9 and wanted to convert to nym with exponent 6 + Ok(DecCoin { + denom: denom.into(), + amount: try_scale_up_decimal(base_amount, exponent - display_exponent)?, + }) + } + // we're already in the display denom + Ordering::Equal => Ok(DecCoin { + denom: denom.into(), + amount: base_amount, + }), + Ordering::Less => { + // we need to scale down, the most common case, for example we're in base unym with exponent 0 and want to convert to nym with exponent 6 + Ok(DecCoin { + denom: denom.into(), + amount: try_scale_down_decimal( + base_amount, + display_exponent - exponent, + )?, + }) + } + }; + } + } + + Err(TypesError::UnknownCoinDenom(denom.clone())) + } + pub fn attempt_convert_to_base_coin(&self, coin: DecCoin) -> Result { // check if this is already in the base denom if self.0.contains_key(&coin.denom) { @@ -469,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"), @@ -483,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"), @@ -507,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 0f5c5c0c96..bee74dd646 100644 --- a/common/types/src/delegation.rs +++ b/common/types/src/delegation.rs @@ -1,8 +1,9 @@ use crate::currency::{DecCoin, RegisteredCoins}; +use crate::deprecated::DelegationEvent; use crate::error::TypesError; -use mixnet_contract_common::mixnode::DelegationEvent as ContractDelegationEvent; -use mixnet_contract_common::mixnode::PendingUndelegate as ContractPendingUndelegate; -use mixnet_contract_common::Delegation as MixnetContractDelegation; +use crate::mixnode::MixNodeCostParams; +use cosmwasm_std::Decimal; +use mixnet_contract_common::{Delegation as MixnetContractDelegation, MixId}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -14,9 +15,9 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct Delegation { pub owner: String, - pub node_identity: String, + pub mix_id: MixId, pub amount: DecCoin, - pub block_height: u64, + pub height: u64, pub proxy: Option, // proxy address used to delegate the funds on behalf of another address } @@ -27,48 +28,41 @@ impl Delegation { ) -> Result { Ok(Delegation { owner: delegation.owner.to_string(), - node_identity: delegation.node_identity, + mix_id: delegation.mix_id, amount: reg.attempt_convert_to_display_dec_coin(delegation.amount.into())?, - block_height: delegation.block_height, + height: delegation.height, proxy: delegation.proxy.map(|d| d.to_string()), }) } } -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationRecord.ts") -)] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] -pub struct DelegationRecord { - pub amount: DecCoin, - pub block_height: u64, - pub delegated_on_iso_datetime: String, - pub uses_vesting_contract_tokens: bool, -} - #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", ts(export_to = "ts-packages/types/src/types/rust/DelegationWithEverything.ts") )] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct DelegationWithEverything { pub owner: String, + pub mix_id: MixId, pub node_identity: String, pub amount: DecCoin, - pub total_delegation: Option, - pub pledge_amount: Option, + pub accumulated_by_delegates: Option, + pub accumulated_by_operator: Option, pub block_height: u64, pub delegated_on_iso_datetime: String, - pub profit_margin_percent: Option, + pub cost_params: Option, pub avg_uptime_percent: Option, - pub stake_saturation: Option, + + #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] + pub stake_saturation: Option, + pub uses_vesting_contract_tokens: bool, - pub accumulated_rewards: Option, + pub unclaimed_rewards: Option, + + // DEPRECATED, IF POSSIBLE TRY TO DISCONTINUE USE OF IT! pub pending_events: Vec, - pub history: Vec, + pub mixnode_is_unbonding: Option, } #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] @@ -83,82 +77,6 @@ pub struct DelegationResult { amount: Option, } -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationEventKind.ts") -)] -#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] -pub enum DelegationEventKind { - Delegate, - Undelegate, -} - -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationEvent.ts") -)] -#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] -pub struct DelegationEvent { - pub kind: DelegationEventKind, - pub node_identity: String, - pub address: String, - pub amount: Option, - pub block_height: u64, - pub proxy: Option, -} - -impl DelegationEvent { - pub fn from_mixnet_contract( - event: ContractDelegationEvent, - reg: &RegisteredCoins, - ) -> Result { - Ok(match event { - ContractDelegationEvent::Delegate(delegation) => DelegationEvent { - kind: DelegationEventKind::Delegate, - block_height: delegation.block_height, - address: delegation.owner.into_string(), - node_identity: delegation.node_identity, - amount: Some(reg.attempt_convert_to_display_dec_coin(delegation.amount.into())?), - proxy: delegation.proxy.map(|p| p.into_string()), - }, - ContractDelegationEvent::Undelegate(pending_undelegate) => DelegationEvent { - kind: DelegationEventKind::Undelegate, - block_height: pending_undelegate.block_height(), - address: pending_undelegate.delegate().into_string(), - node_identity: pending_undelegate.mix_identity(), - amount: None, - proxy: pending_undelegate.proxy().map(|p| p.into_string()), - }, - }) - } -} - -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PendingUndelegate.ts") -)] -#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -pub struct PendingUndelegate { - mix_identity: String, - delegate: String, - proxy: Option, - block_height: u64, -} - -impl From for PendingUndelegate { - fn from(pending_undelegate: ContractPendingUndelegate) -> Self { - PendingUndelegate { - mix_identity: pending_undelegate.mix_identity(), - delegate: pending_undelegate.delegate().to_string(), - proxy: pending_undelegate.proxy().map(|p| p.to_string()), - block_height: pending_undelegate.block_height(), - } - } -} - #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", diff --git a/common/types/src/deprecated.rs b/common/types/src/deprecated.rs new file mode 100644 index 0000000000..62c7193c9e --- /dev/null +++ b/common/types/src/deprecated.rs @@ -0,0 +1,109 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::currency::DecCoin; +use crate::error::TypesError; +use crate::pending_events::{PendingEpochEvent, PendingEpochEventData}; +use mixnet_contract_common::{IdentityKey, MixId}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/DelegationEventKind.ts") +)] +#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] +pub enum DelegationEventKind { + Delegate, + Undelegate, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/DelegationEvent.ts") +)] +#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] +pub struct DelegationEvent { + pub kind: DelegationEventKind, + pub mix_id: MixId, + pub address: String, + pub amount: Option, + pub proxy: Option, +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/WrappedDelegationEvent.ts") +)] +#[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] +pub struct WrappedDelegationEvent { + pub event: DelegationEvent, + pub node_identity: IdentityKey, +} + +impl WrappedDelegationEvent { + pub fn new(event: DelegationEvent, node_identity: IdentityKey) -> Self { + Self { + event, + node_identity, + } + } +} + +impl DelegationEvent { + pub fn address_matches(&self, address: &str) -> bool { + self.address == address + } +} + +pub fn convert_to_delegation_events(epoch_events: Vec) -> Vec { + epoch_events + .into_iter() + // filter out all events that are not delegation-related + .filter_map(|e| e.try_into().ok()) + .collect() +} + +impl TryFrom for DelegationEvent { + type Error = TypesError; + + fn try_from(value: PendingEpochEvent) -> Result { + value.event.try_into() + } +} + +impl TryFrom for DelegationEvent { + type Error = TypesError; + + fn try_from(value: PendingEpochEventData) -> Result { + match value { + PendingEpochEventData::Delegate { + owner, + mix_id, + amount, + proxy, + } => Ok(DelegationEvent { + kind: DelegationEventKind::Delegate, + address: owner, + mix_id, + proxy, + amount: Some(amount), + }), + PendingEpochEventData::Undelegate { + owner, + mix_id, + proxy, + } => Ok(DelegationEvent { + kind: DelegationEventKind::Undelegate, + address: owner, + mix_id, + proxy, + amount: None, + }), + _ => Err(TypesError::NotADelegationEvent), + } + } +} diff --git a/common/types/src/error.rs b/common/types/src/error.rs index 20c3736152..b0874ba7aa 100644 --- a/common/types/src/error.rs +++ b/common/types/src/error.rs @@ -70,6 +70,10 @@ pub enum TypesError { LossyCoinConversion, #[error("The provided coin has an unknown denomination - {0}")] 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/lib.rs b/common/types/src/lib.rs index 8a64ecac07..86aee3b75c 100644 --- a/common/types/src/lib.rs +++ b/common/types/src/lib.rs @@ -1,10 +1,15 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + pub mod account; pub mod currency; pub mod delegation; +pub mod deprecated; pub mod error; pub mod fees; pub mod gas; pub mod gateway; pub mod mixnode; +pub mod pending_events; pub mod transaction; pub mod vesting; diff --git a/common/types/src/mixnode.rs b/common/types/src/mixnode.rs index d1a302d489..e1c3bf4940 100644 --- a/common/types/src/mixnode.rs +++ b/common/types/src/mixnode.rs @@ -1,53 +1,44 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; +use cosmwasm_std::Decimal; use mixnet_contract_common::{ - MixNode as MixnetContractMixNode, MixNodeBond as MixnetContractMixNodeBond, + EpochId, MixId, MixNode, MixNodeBond as MixnetContractMixNodeBond, + MixNodeCostParams as MixnetContractMixNodeCostParams, + MixNodeDetails as MixnetContractMixNodeDetails, + MixNodeRewarding as MixnetContractMixNodeRewarding, Percent, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use validator_client::nymd::Coin; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Mixnode.ts") + ts(export_to = "ts-packages/types/src/types/rust/MixNodeDetails.ts") )] -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema)] -pub struct MixNode { - pub host: String, - pub mix_port: u16, - pub verloc_port: u16, - pub http_api_port: u16, - pub sphinx_key: String, - /// Base58 encoded ed25519 EdDSA public key. - pub identity_key: String, - pub version: String, - pub profit_margin_percent: u8, +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct MixNodeDetails { + pub bond_information: MixNodeBond, + pub rewarding_details: MixNodeRewarding, } -impl From for MixNode { - fn from(value: MixnetContractMixNode) -> Self { - let MixnetContractMixNode { - host, - mix_port, - verloc_port, - http_api_port, - sphinx_key, - identity_key, - version, - profit_margin_percent, - } = value; - - Self { - host, - mix_port, - verloc_port, - http_api_port, - sphinx_key, - identity_key, - version, - profit_margin_percent, - } +impl MixNodeDetails { + pub fn from_mixnet_contract_mixnode_details( + details: MixnetContractMixNodeDetails, + reg: &RegisteredCoins, + ) -> Result { + Ok(MixNodeDetails { + bond_information: MixNodeBond::from_mixnet_contract_mixnode_bond( + details.bond_information, + reg, + )?, + rewarding_details: MixNodeRewarding::from_mixnet_contract_mixnode_rewarding( + details.rewarding_details, + reg, + )?, + }) } } @@ -58,14 +49,14 @@ impl From for MixNode { )] #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] pub struct MixNodeBond { - pub pledge_amount: DecCoin, - pub total_delegation: DecCoin, + pub mix_id: MixId, pub owner: String, + pub original_pledge: DecCoin, pub layer: String, - pub block_height: u64, pub mix_node: MixNode, pub proxy: Option, - pub accumulated_rewards: Option, + pub bonding_height: u64, + pub is_unbonding: bool, } impl MixNodeBond { @@ -73,24 +64,100 @@ impl MixNodeBond { bond: MixnetContractMixNodeBond, reg: &RegisteredCoins, ) -> Result { - let denom = bond.pledge_amount.denom.clone(); Ok(MixNodeBond { - pledge_amount: reg.attempt_convert_to_display_dec_coin(bond.pledge_amount.into())?, - total_delegation: reg - .attempt_convert_to_display_dec_coin(bond.total_delegation.into())?, + mix_id: bond.mix_id, owner: bond.owner.into_string(), + original_pledge: reg + .attempt_convert_to_display_dec_coin(bond.original_pledge.into())?, layer: bond.layer.into(), - block_height: bond.block_height, - mix_node: bond.mix_node.into(), - proxy: bond.proxy.map(|p| p.to_string()), - accumulated_rewards: bond - .accumulated_rewards - .map(|reward| { - // here we're making an assumption that rewards always use the same denom as the pledge - // (which I think is a reasonable assumption) - reg.attempt_convert_to_display_dec_coin(Coin::new(reward.u128(), denom)) - }) - .transpose()?, + mix_node: bond.mix_node, + proxy: bond.proxy.map(|p| p.into_string()), + bonding_height: bond.bonding_height, + is_unbonding: bond.is_unbonding, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/MixNodeRewarding.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct MixNodeRewarding { + pub cost_params: MixNodeCostParams, + + #[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 total_unit_reward: Decimal, + + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub unit_delegation: Decimal, + + pub last_rewarded_epoch: EpochId, + + pub unique_delegations: u32, +} + +impl MixNodeRewarding { + pub fn from_mixnet_contract_mixnode_rewarding( + mix_rewarding: MixnetContractMixNodeRewarding, + reg: &RegisteredCoins, + ) -> Result { + Ok(MixNodeRewarding { + cost_params: MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + mix_rewarding.cost_params, + reg, + )?, + operator: mix_rewarding.operator, + delegates: mix_rewarding.delegates, + total_unit_reward: mix_rewarding.total_unit_reward, + unit_delegation: mix_rewarding.unit_delegation, + last_rewarded_epoch: mix_rewarding.last_rewarded_epoch, + unique_delegations: mix_rewarding.unique_delegations, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/MixNodeCostParams.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub struct MixNodeCostParams { + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub profit_margin_percent: Percent, + + pub interval_operating_cost: DecCoin, +} + +impl MixNodeCostParams { + pub fn from_mixnet_contract_mixnode_cost_params( + cost_params: MixnetContractMixNodeCostParams, + reg: &RegisteredCoins, + ) -> Result { + Ok(MixNodeCostParams { + profit_margin_percent: cost_params.profit_margin_percent, + interval_operating_cost: reg + .attempt_convert_to_display_dec_coin(cost_params.interval_operating_cost.into())?, + }) + } + + pub fn try_convert_to_mixnet_contract_cost_params( + self, + reg: &RegisteredCoins, + ) -> Result { + Ok(MixnetContractMixNodeCostParams { + profit_margin_percent: self.profit_margin_percent, + interval_operating_cost: reg + .attempt_convert_to_base_coin(self.interval_operating_cost)? + .into(), }) } } diff --git a/common/types/src/pending_events.rs b/common/types/src/pending_events.rs new file mode 100644 index 0000000000..afe5a54d9e --- /dev/null +++ b/common/types/src/pending_events.rs @@ -0,0 +1,179 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::currency::{DecCoin, RegisteredCoins}; +use crate::error::TypesError; +use crate::mixnode::MixNodeCostParams; +use mixnet_contract_common::{ + BlockHeight, EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, MixId, + PendingEpochEvent as MixnetContractPendingEpochEvent, + PendingEpochEventKind as MixnetContractPendingEpochEventKind, + PendingIntervalEvent as MixnetContractPendingIntervalEvent, + PendingIntervalEventKind as MixnetContractPendingIntervalEventKind, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PendingEpochEvent.ts") +)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct PendingEpochEvent { + pub id: EpochEventId, + pub created_at: BlockHeight, + pub event: PendingEpochEventData, +} + +impl PendingEpochEvent { + pub fn try_from_mixnet_contract( + pending_event: MixnetContractPendingEpochEvent, + reg: &RegisteredCoins, + ) -> Result { + Ok(PendingEpochEvent { + id: pending_event.id, + created_at: pending_event.event.created_at, + event: PendingEpochEventData::try_from_mixnet_contract(pending_event.event.kind, reg)?, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PendingEpochEventData.ts") +)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub enum PendingEpochEventData { + Delegate { + owner: String, + mix_id: MixId, + amount: DecCoin, + proxy: Option, + }, + Undelegate { + owner: String, + mix_id: MixId, + proxy: Option, + }, + UnbondMixnode { + mix_id: MixId, + }, + UpdateActiveSetSize { + new_size: u32, + }, +} + +impl PendingEpochEventData { + pub fn try_from_mixnet_contract( + pending_event: MixnetContractPendingEpochEventKind, + reg: &RegisteredCoins, + ) -> Result { + match pending_event { + MixnetContractPendingEpochEventKind::Delegate { + owner, + mix_id, + amount, + proxy, + } => Ok(PendingEpochEventData::Delegate { + owner: owner.into_string(), + mix_id, + amount: reg.attempt_convert_to_display_dec_coin(amount.into())?, + proxy: proxy.map(|p| p.into_string()), + }), + MixnetContractPendingEpochEventKind::Undelegate { + owner, + mix_id, + proxy, + } => Ok(PendingEpochEventData::Undelegate { + owner: owner.into_string(), + mix_id, + proxy: proxy.map(|p| p.into_string()), + }), + MixnetContractPendingEpochEventKind::UnbondMixnode { mix_id } => { + Ok(PendingEpochEventData::UnbondMixnode { mix_id }) + } + MixnetContractPendingEpochEventKind::UpdateActiveSetSize { new_size } => { + Ok(PendingEpochEventData::UpdateActiveSetSize { new_size }) + } + } + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PendingIntervalEvent.ts") +)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub struct PendingIntervalEvent { + pub id: IntervalEventId, + pub created_at: BlockHeight, + pub event: PendingIntervalEventData, +} + +impl PendingIntervalEvent { + pub fn try_from_mixnet_contract( + pending_event: MixnetContractPendingIntervalEvent, + reg: &RegisteredCoins, + ) -> Result { + Ok(PendingIntervalEvent { + id: pending_event.id, + created_at: pending_event.event.created_at, + event: PendingIntervalEventData::try_from_mixnet_contract( + pending_event.event.kind, + reg, + )?, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "ts-packages/types/src/types/rust/PendingIntervalEventData.ts") +)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +pub enum PendingIntervalEventData { + ChangeMixCostParams { + mix_id: MixId, + new_costs: MixNodeCostParams, + }, + + UpdateRewardingParams { + update: IntervalRewardingParamsUpdate, + }, + UpdateIntervalConfig { + epochs_in_interval: u32, + epoch_duration_secs: u64, + }, +} + +impl PendingIntervalEventData { + pub fn try_from_mixnet_contract( + pending_event: MixnetContractPendingIntervalEventKind, + reg: &RegisteredCoins, + ) -> Result { + match pending_event { + MixnetContractPendingIntervalEventKind::ChangeMixCostParams { mix_id, new_costs } => { + Ok(PendingIntervalEventData::ChangeMixCostParams { + mix_id, + new_costs: MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + new_costs, reg, + )?, + }) + } + MixnetContractPendingIntervalEventKind::UpdateRewardingParams { update } => { + Ok(PendingIntervalEventData::UpdateRewardingParams { update }) + } + MixnetContractPendingIntervalEventKind::UpdateIntervalConfig { + epochs_in_interval, + epoch_duration_secs, + } => Ok(PendingIntervalEventData::UpdateIntervalConfig { + epochs_in_interval, + epoch_duration_secs, + }), + } + } +} 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 new file mode 100644 index 0000000000..361ceec70e --- /dev/null +++ b/contracts/CHANGELOG.md @@ -0,0 +1,64 @@ +## Unreleased + +### Added +- vesting-contract: added query for obtaining contract build information ([#1726]) + +[#1726]: https://github.com/nymtech/nym/pull/1726 + + +## [nym-contracts-v1.0.2](https://github.com/nymtech/nym/tree/nym-contracts-v1.0.2) (2022-09-13) + +### 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 + +- vesting-contract: the contract now correctly stores delegations with their timestamp as opposed to using block height ([#1544]) +- mixnet-contract: compounding delegator rewards is now possible even if the associated mixnode had already unbonded ([#1571]) +- mixnet-contract: fixed reward accumulation after claiming rewards ([#1613]) + +[#1544]: https://github.com/nymtech/nym/pull/1544 +[#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) + +### Added + +- mixnet-contract: Added ClaimOperatorReward and ClaimDelegatorReward messages ([#1292]) +- mixnet-contract: Replace all naked `-` with `saturating_sub`. +- mixnet-contract: Added staking_supply field to ContractStateParams. +- mixnet-contract: Added a query to get MixnodeBond by identity key ([#1369]). +- mixnet-contract: Added a query to get GatewayBond by identity key ([#1369]). +- vesting-contract: Added ClaimOperatorReward and ClaimDelegatorReward messages ([#1292]) +- vesting-contract: Added limit to the amount of tokens one can pledge ([#1331]) + +### Fixed + +- mixnet-contract: `estimated_delegator_reward` calculation ([#1284]) +- mixnet-contract: delegator and operator rewards use lambda and sigma instead of lambda_ticked and sigma_ticked ([#1284]) +- mixnet-contract: removed `expect` in `query_delegator_reward` and queries containing invalid proxy address should now return a more human-readable error ([#1257]) +- mixnet-contract: replaced integer division with fixed for performance calculations ([#1284]) +- mixnet-contract: Under certain circumstances nodes could not be unbonded ([#1255](https://github.com/nymtech/nym/issues/1255)) ([#1258]) +- mixnet-contract: Using correct staking supply when distributing rewards. ([#1373]) +- vesting-contract: replaced `checked_sub` with `saturating_sub` to fix the underflow in `get_vesting_tokens` ([#1275]) + + +[#1255]: https://github.com/nymtech/nym/pull/1255 +[#1257]: https://github.com/nymtech/nym/pull/1257 +[#1258]: https://github.com/nymtech/nym/pull/1258 +[#1275]: https://github.com/nymtech/nym/pull/1275 +[#1284]: https://github.com/nymtech/nym/pull/1284 +[#1292]: https://github.com/nymtech/nym/pull/1292 +[#1331]: https://github.com/nymtech/nym/pull/1331 +[#1369]: https://github.com/nymtech/nym/pull/1369 +[#1373]: https://github.com/nymtech/nym/pull/1373 \ No newline at end of file diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index e0676f0832..706a7c5e36 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - [[package]] name = "aes" version = "0.7.5" @@ -39,24 +33,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" -[[package]] -name = "az" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f771a5d1f5503f7f4279a30f3643d3421ba149848b89ecaaec0ea2acf04a5ac4" - -[[package]] -name = "bandwidth-claim" -version = "1.0.0" -dependencies = [ - "bandwidth-claim-contract", - "cosmwasm-std", - "cosmwasm-storage", - "schemars", - "serde", - "thiserror", -] - [[package]] name = "bandwidth-claim-contract" version = "0.1.0" @@ -101,18 +77,6 @@ dependencies = [ "opaque-debug 0.2.3", ] -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - [[package]] name = "block-buffer" version = "0.9.0" @@ -122,15 +86,6 @@ dependencies = [ "generic-array 0.14.5", ] -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", -] - [[package]] name = "bs58" version = "0.4.0" @@ -149,12 +104,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" -[[package]] -name = "bytemuck" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439989e6b8c38d1b6570a384ef1e49c8848128f5a97f3914baef02920842712f" - [[package]] name = "byteorder" version = "1.4.3" @@ -261,20 +210,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "config" -version = "0.1.0" -dependencies = [ - "cfg-if", - "handlebars", - "humantime-serde", - "log", - "network-defaults", - "serde", - "toml", - "url", -] - [[package]] name = "const-oid" version = "0.7.1" @@ -286,6 +221,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", + "serde", + "thiserror", ] [[package]] @@ -367,7 +305,6 @@ name = "crypto" version = "0.1.0" dependencies = [ "bs58", - "config", "ed25519-dalek", "nymsphinx-types", "pemstore", @@ -612,12 +549,6 @@ dependencies = [ "generic-array 0.14.5", ] -[[package]] -name = "dotenv" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" - [[package]] name = "dyn-clone" version = "1.0.4" @@ -718,12 +649,6 @@ dependencies = [ "syn", ] -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "ff" version = "0.11.1" @@ -734,19 +659,6 @@ dependencies = [ "subtle 2.4.1", ] -[[package]] -name = "fixed" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80a9a8cb2e34880a498f09367089339bda5e12d6f871640f947850f7113058c0" -dependencies = [ - "az", - "bytemuck", - "half", - "serde", - "typenum", -] - [[package]] name = "form_urlencoded" version = "1.0.1" @@ -842,38 +754,12 @@ dependencies = [ "subtle 2.4.1", ] -[[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - -[[package]] -name = "handlebars" -version = "3.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4498fc115fa7d34de968184e473529abb40eeb6be8bc5f7faba3d08c316cb3e3" -dependencies = [ - "log", - "pest", - "pest_derive", - "quick-error", - "serde", - "serde_json", -] - [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "hex-literal" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0" - [[package]] name = "hkdf" version = "0.11.0" @@ -902,9 +788,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "humantime-serde" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac34a56cfd4acddb469cc7fff187ed5ac36f498ba085caf8bbc725e3ff474058" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" dependencies = [ "humantime", "serde", @@ -1036,12 +922,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - [[package]] name = "matches" version = "0.1.9" @@ -1050,20 +930,15 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "mixnet-contract" -version = "1.0.1" +version = "1.0.2" dependencies = [ - "az", "bs58", - "config", "cosmwasm-schema", "cosmwasm-std", "cosmwasm-storage", "crypto", "cw-storage-plus", - "fixed", - "hex", "mixnet-contract-common", - "rand", "rand_chacha", "schemars", "serde", @@ -1077,17 +952,17 @@ dependencies = [ name = "mixnet-contract-common" version = "0.1.0" dependencies = [ - "az", + "bs58", "contracts-common", "cosmwasm-std", - "fixed", + "humantime-serde", "log", "schemars", "serde", + "serde_json", "serde_repr", "thiserror", "time 0.3.6", - "ts-rs", ] [[package]] @@ -1102,19 +977,6 @@ dependencies = [ "serde", ] -[[package]] -name = "network-defaults" -version = "0.1.0" -dependencies = [ - "cfg-if", - "dotenv", - "hex-literal", - "once_cell", - "serde", - "thiserror", - "url", -] - [[package]] name = "num-integer" version = "0.1.44" @@ -1193,49 +1055,6 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pest_meta" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" -dependencies = [ - "maplit", - "pest", - "sha-1", -] - [[package]] name = "pkcs8" version = "0.8.0" @@ -1315,12 +1134,6 @@ dependencies = [ "syn", ] -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quote" version = "1.0.14" @@ -1542,25 +1355,13 @@ dependencies = [ "syn", ] -[[package]] -name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "sha2" version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ - "block-buffer 0.9.0", + "block-buffer", "cfg-if", "cpufeatures", "digest 0.9.0", @@ -1660,15 +1461,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" version = "1.0.30" @@ -1732,50 +1524,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" -[[package]] -name = "toml" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" -dependencies = [ - "serde", -] - -[[package]] -name = "ts-rs" -version = "6.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc59f479df54269b400dd95bc3b7e81623b3e4b9c70c8ca7125ab8341eafa64e" -dependencies = [ - "thiserror", - "ts-rs-macros", -] - -[[package]] -name = "ts-rs-macros" -version = "6.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f807fdb3151fee75df7485b901a89624358cd07a67a8fb1a5831bf5a07681ff" -dependencies = [ - "Inflector", - "proc-macro2", - "quote", - "syn", - "termcolor", -] - [[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" -[[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - [[package]] name = "uint" version = "0.9.3" @@ -1852,14 +1606,16 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" -version = "1.0.1" +version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", "schemars", "serde", "thiserror", + "vergen", "vesting-contract-common", ] @@ -1867,13 +1623,12 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ - "config", + "contracts-common", "cosmwasm-std", - "cw-storage-plus", + "log", "mixnet-contract-common", "schemars", "serde", - "ts-rs", ] [[package]] @@ -1958,15 +1713,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index ccf7857d21..1787d0ca52 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["bandwidth-claim", "coconut-bandwidth", "mixnet", "vesting", "multisig/cw3-flex-multisig", "multisig/cw4-group", "coconut-test"] +members = ["coconut-bandwidth", "mixnet", "vesting", "multisig/cw3-flex-multisig", "multisig/cw4-group", "coconut-test"] [profile.release] opt-level = 3 diff --git a/contracts/bandwidth-claim/Cargo.toml b/contracts/bandwidth-claim/Cargo.toml deleted file mode 100644 index b6491dda88..0000000000 --- a/contracts/bandwidth-claim/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "bandwidth-claim" -version = "1.0.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[lib] -crate-type = ["cdylib", "rlib"] - -[dev-dependencies] -[dependencies] -bandwidth-claim-contract = { path = "../../common/bandwidth-claim-contract" } - -cosmwasm-std = "1.0.0" -cosmwasm-storage = "1.0.0" - -schemars = "0.8" -serde = { version = "1.0.103", default-features = false, features = ["derive"] } -thiserror = "1.0.23" diff --git a/contracts/bandwidth-claim/src/error.rs b/contracts/bandwidth-claim/src/error.rs deleted file mode 100644 index 02f3523eeb..0000000000 --- a/contracts/bandwidth-claim/src/error.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use cosmwasm_std::{StdError, VerificationError}; -use thiserror::Error; - -/// Custom errors for contract failure conditions. -/// -/// Add any other custom errors you like here. -/// Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. -#[derive(Error, Debug, PartialEq)] -pub enum ContractError { - #[error("{0}")] - Std(#[from] StdError), - - #[error("Invalid size for signature items")] - InvalidSignatureSize, - - #[error("This payment has already been claimed by someone")] - PaymentAlreadyClaimed, - - #[error("Error while verifying ed25519 signature - {0}")] - VerificationError(#[from] VerificationError), - - #[error("The payment is not properly signed")] - BadSignature, -} diff --git a/contracts/bandwidth-claim/src/lib.rs b/contracts/bandwidth-claim/src/lib.rs deleted file mode 100644 index 91deceaefa..0000000000 --- a/contracts/bandwidth-claim/src/lib.rs +++ /dev/null @@ -1,103 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -mod error; -mod queries; -mod storage; -mod support; -mod transactions; - -use cosmwasm_std::{ - entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, -}; - -use crate::error::ContractError; -use bandwidth_claim_contract::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; - -/// Instantiate the contract. -/// -/// `deps` contains Storage, API and Querier -/// `env` contains block, message and contract info -/// `msg` is the contract initialization message, sort of like a constructor call. -#[entry_point] -pub fn instantiate( - _deps: DepsMut<'_>, - _env: Env, - _info: MessageInfo, - _msg: InstantiateMsg, -) -> Result { - Ok(Response::default()) -} - -/// Handle an incoming message -#[entry_point] -pub fn execute( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - msg: ExecuteMsg, -) -> Result { - match msg { - ExecuteMsg::LinkPayment { data } => transactions::link_payment(deps, env, info, data), - } -} - -#[entry_point] -pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result { - let query_res = match msg { - QueryMsg::GetPayments { start_after, limit } => { - to_binary(&queries::query_payments_paged(deps, start_after, limit)?) - } - }; - - Ok(query_res?) -} - -#[entry_point] -pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result { - Ok(Default::default()) -} - -#[cfg(test)] -pub mod tests { - use super::*; - use bandwidth_claim_contract::payment::PagedPaymentResponse; - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{coins, from_binary}; - - const TEST_MIX_DENOM: &str = "unym"; - - #[test] - fn initialize_contract() { - let mut deps = mock_dependencies(); - let env = mock_env(); - let msg = InstantiateMsg {}; - let info = mock_info("creator", &[]); - - let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); - assert_eq!(0, res.messages.len()); - - // payments should be empty after initialization - let res = query( - deps.as_ref(), - env.clone(), - QueryMsg::GetPayments { - start_after: None, - limit: Option::from(2), - }, - ) - .unwrap(); - let page: PagedPaymentResponse = from_binary(&res).unwrap(); - assert_eq!(0, page.payments.len()); // there are no payments in the list when it's just been initialized - - // Contract balance should match what we initialized it as - assert_eq!( - coins(0, TEST_MIX_DENOM), - vec![deps - .as_ref() - .querier - .query_balance(env.contract.address, TEST_MIX_DENOM) - .unwrap()] - ); - } -} diff --git a/contracts/bandwidth-claim/src/queries.rs b/contracts/bandwidth-claim/src/queries.rs deleted file mode 100644 index 1a4e61ca1e..0000000000 --- a/contracts/bandwidth-claim/src/queries.rs +++ /dev/null @@ -1,191 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use cosmwasm_std::{Deps, Order, StdResult}; - -use crate::storage::payments_read; -use bandwidth_claim_contract::keys::PublicKey; -use bandwidth_claim_contract::payment::{PagedPaymentResponse, Payment}; - -const PAYMENT_PAGE_MAX_LIMIT: u32 = 100; -const PAYMENT_PAGE_DEFAULT_LIMIT: u32 = 50; - -/// Adds a 0 byte to terminate the `start_after` value given. This allows CosmWasm -/// to get the succeeding key as the start of the next page. -fn calculate_start_value>(start_after: Option) -> Option> { - start_after.as_ref().map(|identity| { - identity - .as_ref() - .iter() - .cloned() - .chain(std::iter::once(0)) - .collect() - }) -} - -pub fn query_payments_paged( - deps: Deps<'_>, - start_after: Option, - limit: Option, -) -> StdResult { - let limit = limit - .unwrap_or(PAYMENT_PAGE_DEFAULT_LIMIT) - .min(PAYMENT_PAGE_MAX_LIMIT) as usize; - let start = calculate_start_value(start_after); - - let payments = payments_read(deps.storage) - .range(start.as_deref(), None, Order::Ascending) - .take(limit) - .map(|res| res.map(|item| item.1)) - .collect::>>()?; - - let start_next_after = payments.last().map(|payment| payment.verification_key()); - - Ok(PagedPaymentResponse::new(payments, limit, start_next_after)) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::storage::payments; - use crate::support::tests::helpers; - use std::convert::TryInto; - - #[test] - fn payments_empty_on_init() { - let deps = helpers::init_contract(); - let response = query_payments_paged(deps.as_ref(), None, Option::from(2)).unwrap(); - assert_eq!(0, response.payments.len()); - } - - #[test] - fn payments_paged_retrieval_obeys_limits() { - let mut deps = helpers::init_contract(); - let storage = deps.as_mut().storage; - let limit = 2; - for n in 0u32..10000 { - let bytes: Vec = std::iter::repeat(n.to_be_bytes()) - .take(8) - .flatten() - .collect(); - let verification_key = PublicKey::new(bytes.try_into().unwrap()); - let payment = helpers::payment_fixture(); - payments(storage) - .save(&verification_key.to_bytes(), &payment) - .unwrap(); - } - - let page1 = query_payments_paged(deps.as_ref(), None, Option::from(limit)).unwrap(); - assert_eq!(limit, page1.payments.len() as u32); - } - - #[test] - fn payments_paged_retrieval_has_default_limit() { - let mut deps = helpers::init_contract(); - let storage = deps.as_mut().storage; - for n in 0u32..100 { - let bytes: Vec = std::iter::repeat(n.to_be_bytes()) - .take(8) - .flatten() - .collect(); - let verification_key = PublicKey::new(bytes.try_into().unwrap()); - let payment = helpers::payment_fixture(); - payments(storage) - .save(&verification_key.to_bytes(), &payment) - .unwrap(); - } - - // query without explicitly setting a limit - let page1 = query_payments_paged(deps.as_ref(), None, None).unwrap(); - - assert_eq!(PAYMENT_PAGE_DEFAULT_LIMIT, page1.payments.len() as u32); - } - - #[test] - fn payments_paged_retrieval_has_max_limit() { - let mut deps = helpers::init_contract(); - let storage = deps.as_mut().storage; - for n in 0u32..10000 { - let bytes: Vec = std::iter::repeat(n.to_be_bytes()) - .take(8) - .flatten() - .collect(); - let verification_key = PublicKey::new(bytes.try_into().unwrap()); - let payment = helpers::payment_fixture(); - payments(storage) - .save(&verification_key.to_bytes(), &payment) - .unwrap(); - } - - // query with a crazily high limit in an attempt to use too many resources - let crazy_limit = 1000; - let page1 = query_payments_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap(); - - // we default to a decent sized upper bound instead - assert_eq!(PAYMENT_PAGE_MAX_LIMIT, page1.payments.len() as u32); - } - - #[test] - fn payments_pagination_works() { - let key1 = PublicKey::new([1; 32]); - let key2 = PublicKey::new([2; 32]); - let key3 = PublicKey::new([3; 32]); - let key4 = PublicKey::new([4; 32]); - - let mut deps = helpers::init_contract(); - let payment = helpers::payment_fixture(); - payments(&mut deps.storage) - .save(&key1.to_bytes(), &payment) - .unwrap(); - - let per_page = 2; - let page1 = query_payments_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); - - // page should have 1 result on it - assert_eq!(1, page1.payments.len()); - - // save another - payments(&mut deps.storage) - .save(&key2.to_bytes(), &payment) - .unwrap(); - - // page1 should have 2 results on it - let page1 = query_payments_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); - assert_eq!(2, page1.payments.len()); - - payments(&mut deps.storage) - .save(&key3.to_bytes(), &payment) - .unwrap(); - - // page1 still has 2 results - let page1 = query_payments_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); - assert_eq!(2, page1.payments.len()); - - // retrieving the next page should start after the last key on this page - let start_after = key2; - let page2 = query_payments_paged( - deps.as_ref(), - Option::from(start_after), - Option::from(per_page), - ) - .unwrap(); - - assert_eq!(1, page2.payments.len()); - - // save another one - payments(&mut deps.storage) - .save(&key4.to_bytes(), &payment) - .unwrap(); - - let start_after = key2; - let page2 = query_payments_paged( - deps.as_ref(), - Option::from(start_after), - Option::from(per_page), - ) - .unwrap(); - - // now we have 2 pages, with 2 results on the second page - assert_eq!(2, page2.payments.len()); - } -} diff --git a/contracts/bandwidth-claim/src/storage.rs b/contracts/bandwidth-claim/src/storage.rs deleted file mode 100644 index 35227270c9..0000000000 --- a/contracts/bandwidth-claim/src/storage.rs +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use cosmwasm_std::Storage; -use cosmwasm_storage::{bucket, bucket_read, Bucket, ReadonlyBucket}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use bandwidth_claim_contract::payment::Payment; - -// buckets -const PREFIX_PAYMENTS: &[u8] = b"payments"; -const PREFIX_STATUS: &[u8] = b"status"; - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] -pub enum Status { - Unchecked, - Checked, - Spent, -} - -pub fn payments(storage: &mut dyn Storage) -> Bucket<'_, Payment> { - bucket(storage, PREFIX_PAYMENTS) -} - -pub fn payments_read(storage: &dyn Storage) -> ReadonlyBucket<'_, Payment> { - bucket_read(storage, PREFIX_PAYMENTS) -} - -pub fn status(storage: &mut dyn Storage) -> Bucket<'_, Status> { - bucket(storage, PREFIX_STATUS) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::support::tests::helpers; - use bandwidth_claim_contract::keys::PublicKey; - use cosmwasm_std::testing::MockStorage; - - #[test] - fn payments_single_read_retrieval() { - let mut storage = MockStorage::new(); - let key1 = PublicKey::new([1; 32]); - let key2 = PublicKey::new([2; 32]); - let payment1 = helpers::payment_fixture(); - let payment2 = helpers::payment_fixture(); - payments(&mut storage) - .save(key1.as_ref(), &payment1) - .unwrap(); - payments(&mut storage) - .save(key2.as_ref(), &payment2) - .unwrap(); - - let res1 = payments_read(&storage).load(key1.as_ref()).unwrap(); - let res2 = payments_read(&storage).load(key2.as_ref()).unwrap(); - assert_eq!(payment1, res1); - assert_eq!(payment2, res2); - } - - #[test] - fn status_single_read_retrieval() { - let mut storage = MockStorage::new(); - let key1 = PublicKey::new([1; 32]); - let key2 = PublicKey::new([2; 32]); - let status_value = Status::Unchecked; - status(&mut storage) - .save(key1.as_ref(), &status_value) - .unwrap(); - status(&mut storage) - .save(key2.as_ref(), &status_value) - .unwrap(); - - let res1 = status(&mut storage).load(key1.as_ref()).unwrap(); - assert_eq!(status_value, res1); - let res2 = status(&mut storage).load(key2.as_ref()).unwrap(); - assert_eq!(status_value, res2); - } -} diff --git a/contracts/bandwidth-claim/src/support/tests.rs b/contracts/bandwidth-claim/src/support/tests.rs deleted file mode 100644 index cdd4b87f3a..0000000000 --- a/contracts/bandwidth-claim/src/support/tests.rs +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -#[cfg(test)] -pub mod helpers { - use crate::instantiate; - use bandwidth_claim_contract::keys::PublicKey; - use bandwidth_claim_contract::msg::InstantiateMsg; - use bandwidth_claim_contract::payment::Payment; - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}; - use cosmwasm_std::{Empty, MemoryStorage, OwnedDeps}; - - pub fn init_contract() -> OwnedDeps> { - let mut deps = mock_dependencies(); - let msg = InstantiateMsg {}; - let env = mock_env(); - let info = mock_info("creator", &[]); - instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); - return deps; - } - - pub fn payment_fixture() -> Payment { - let public_key = PublicKey::new([1; 32]); - let gateway_identity = PublicKey::new([2; 32]); - let bandwidth = 42; - Payment::new(public_key, gateway_identity, bandwidth) - } -} diff --git a/contracts/bandwidth-claim/src/transactions.rs b/contracts/bandwidth-claim/src/transactions.rs deleted file mode 100644 index 9677204942..0000000000 --- a/contracts/bandwidth-claim/src/transactions.rs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; - -use crate::error::ContractError; -use crate::storage::{payments, status, Status}; -use bandwidth_claim_contract::payment::{LinkPaymentData, Payment}; - -pub(crate) fn link_payment( - deps: DepsMut<'_>, - _env: Env, - _info: MessageInfo, - data: LinkPaymentData, -) -> Result { - let mut status_bucket = status(deps.storage); - - let verification_key = data.verification_key.to_bytes(); - let gateway_identity = data.gateway_identity.to_bytes(); - let message: Vec = verification_key - .iter() - .chain(gateway_identity.iter()) - .copied() - .collect(); - let signature = data.signature.to_bytes(); - - if let Ok(Some(_)) = status_bucket.may_load(&verification_key) { - return Err(ContractError::PaymentAlreadyClaimed); - } - - if !deps - .api - .ed25519_verify(&message, &signature, &verification_key)? - { - return Err(ContractError::BadSignature); - } - - status_bucket.save(&verification_key, &Status::Unchecked)?; - payments(deps.storage).save( - &verification_key, - &Payment::new(data.verification_key, data.gateway_identity, data.bandwidth), - )?; - - Ok(Response::default()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::storage::payments_read; - use crate::support::tests::helpers; - use bandwidth_claim_contract::keys::PublicKey; - use cosmwasm_std::testing::{mock_env, mock_info}; - - #[test] - fn bad_signature_payment() { - let mut deps = helpers::init_contract(); - let env = mock_env(); - let info = mock_info("owner", &[]); - - let payment_data = LinkPaymentData::new([1; 32], [2; 32], 42, [3; 64]); - - assert_eq!( - link_payment(deps.as_mut(), env, info, payment_data), - Err(ContractError::BadSignature) - ); - } - - #[test] - fn good_payment() { - let mut deps = helpers::init_contract(); - let env = mock_env(); - let info = mock_info("owner", &[]); - - let verification_key = [ - 78, 142, 213, 13, 39, 169, 76, 205, 242, 206, 129, 208, 190, 51, 139, 206, 245, 199, - 120, 151, 181, 250, 192, 153, 123, 104, 129, 139, 60, 254, 243, 98, - ]; - let gateway_identity = [ - 106, 76, 76, 238, 214, 177, 233, 112, 56, 33, 21, 201, 89, 42, 69, 196, 175, 56, 6, - 110, 184, 167, 203, 63, 1, 167, 134, 102, 165, 215, 3, 212, - ]; - let bandwidth = 42; - let signature = [ - 200, 134, 156, 198, 113, 180, 129, 90, 70, 28, 176, 201, 35, 208, 145, 28, 15, 16, 9, - 110, 148, 188, 193, 75, 157, 201, 206, 211, 128, 215, 66, 207, 175, 155, 48, 24, 171, - 254, 9, 37, 108, 205, 143, 37, 77, 189, 162, 52, 44, 130, 173, 60, 220, 22, 193, 3, - 111, 90, 123, 147, 206, 8, 137, 1, - ]; - - let payment_data = - LinkPaymentData::new(verification_key, gateway_identity, bandwidth, signature); - - assert!(link_payment(deps.as_mut(), env, info, payment_data).is_ok()); - - assert_eq!( - payments_read(&deps.storage) - .load(&verification_key) - .unwrap(), - Payment::new( - PublicKey::new(verification_key), - PublicKey::new(gateway_identity), - bandwidth - ) - ); - assert_eq!( - status(&mut deps.storage).load(&verification_key).unwrap(), - Status::Unchecked - ) - } - - #[test] - fn double_spend_protection() { - let mut deps = helpers::init_contract(); - let env = mock_env(); - let info = mock_info("owner", &[]); - - let verification_key = [ - 78, 142, 213, 13, 39, 169, 76, 205, 242, 206, 129, 208, 190, 51, 139, 206, 245, 199, - 120, 151, 181, 250, 192, 153, 123, 104, 129, 139, 60, 254, 243, 98, - ]; - let gateway_identity = [ - 106, 76, 76, 238, 214, 177, 233, 112, 56, 33, 21, 201, 89, 42, 69, 196, 175, 56, 6, - 110, 184, 167, 203, 63, 1, 167, 134, 102, 165, 215, 3, 212, - ]; - let bandwidth = 42; - let signature = [ - 200, 134, 156, 198, 113, 180, 129, 90, 70, 28, 176, 201, 35, 208, 145, 28, 15, 16, 9, - 110, 148, 188, 193, 75, 157, 201, 206, 211, 128, 215, 66, 207, 175, 155, 48, 24, 171, - 254, 9, 37, 108, 205, 143, 37, 77, 189, 162, 52, 44, 130, 173, 60, 220, 22, 193, 3, - 111, 90, 123, 147, 206, 8, 137, 1, - ]; - - let payment_data = - LinkPaymentData::new(verification_key, gateway_identity, bandwidth, signature); - - link_payment(deps.as_mut(), env.clone(), info.clone(), payment_data).unwrap(); - - // Only the verification key is used for double spending protection, the other data is irrelevant - let second_payment_data = LinkPaymentData::new(verification_key, [1; 32], 10, [2; 64]); - assert_eq!( - link_payment(deps.as_mut(), env, info, second_payment_data), - Err(ContractError::PaymentAlreadyClaimed) - ) - } -} diff --git a/contracts/basic-bandwidth-generation/.gitignore b/contracts/basic-bandwidth-generation/.gitignore deleted file mode 100644 index b76c36af73..0000000000 --- a/contracts/basic-bandwidth-generation/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules -../.env -.to_do.md - -#Hardhat files -cache -artifacts diff --git a/contracts/basic-bandwidth-generation/.solcover.js b/contracts/basic-bandwidth-generation/.solcover.js deleted file mode 100644 index ec00128333..0000000000 --- a/contracts/basic-bandwidth-generation/.solcover.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - skipFiles: [ - 'CosmosToken.sol', - 'Gravity.sol' - ] -}; diff --git a/contracts/basic-bandwidth-generation/README.md b/contracts/basic-bandwidth-generation/README.md deleted file mode 100644 index 288a36dd54..0000000000 --- a/contracts/basic-bandwidth-generation/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Basic Bandwidth Credential Generator - -This directory contains the contract and unit tests for the `BandwidthGenerator` smart contract. - -This contract allows users to generate Basic Bandwidth Credentials (BBCs) on the Nym cosmos blockchain using ERC20 representations of NYM as payment, utilising the Cosmos Gravity Bridge for cross-chain payment. - -BBCs are credentials that will be presented to Gateways by a Nym Client, and represent a certain amount of bandwidth which can be sent through the Nym Mixnet. - -By default 1 NYM = 1 GB of bandwidth. The ratio of NYM - bandwidth is denominated in bytes, and represented in the smart contract by the `BytesPerToken` variable. This variable can be adjusted by the contract owner. - -The amount of bandwidth bought is calculated according to the following formula: -`(Token amount in 'wei' * BytesPerToken) / 10**18` - -## Usage -* `npm install` -* `npx hardhat compile` -* `npx hardhat test` diff --git a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/ERC20.sol/ERC20.dbg.json b/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/ERC20.sol/ERC20.dbg.json deleted file mode 100644 index 86d5f44dff..0000000000 --- a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/ERC20.sol/ERC20.dbg.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "_format": "hh-sol-dbg-1", - "buildInfo": "../../../../../build-info/1f95016aa87f8376998977fedfcae017.json" -} diff --git a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/ERC20.sol/ERC20.json b/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/ERC20.sol/ERC20.json deleted file mode 100644 index 8fd7d531d1..0000000000 --- a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/ERC20.sol/ERC20.json +++ /dev/null @@ -1,297 +0,0 @@ -{ - "_format": "hh-sol-artifact-1", - "contractName": "ERC20", - "sourceName": "@openzeppelin/contracts/token/ERC20/ERC20.sol", - "abi": [ - { - "inputs": [ - { - "internalType": "string", - "name": "name_", - "type": "string" - }, - { - "internalType": "string", - "name": "symbol_", - "type": "string" - } - ], - "stateMutability": "nonpayable", - "type": "constructor" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Approval", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Transfer", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "name": "allowance", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "approve", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "decimals", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "subtractedValue", - "type": "uint256" - } - ], - "name": "decreaseAllowance", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "addedValue", - "type": "uint256" - } - ], - "name": "increaseAllowance", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [], - "name": "name", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "symbol", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - } - ], - "bytecode": "0x60806040523480156200001157600080fd5b5060405162000b5638038062000b568339810160408190526200003491620001db565b81516200004990600390602085019062000068565b5080516200005f90600490602084019062000068565b50505062000282565b828054620000769062000245565b90600052602060002090601f0160209004810192826200009a5760008555620000e5565b82601f10620000b557805160ff1916838001178555620000e5565b82800160010185558215620000e5579182015b82811115620000e5578251825591602001919060010190620000c8565b50620000f3929150620000f7565b5090565b5b80821115620000f35760008155600101620000f8565b634e487b7160e01b600052604160045260246000fd5b600082601f8301126200013657600080fd5b81516001600160401b03808211156200015357620001536200010e565b604051601f8301601f19908116603f011681019082821181831017156200017e576200017e6200010e565b816040528381526020925086838588010111156200019b57600080fd5b600091505b83821015620001bf5785820183015181830184015290820190620001a0565b83821115620001d15760008385830101525b9695505050505050565b60008060408385031215620001ef57600080fd5b82516001600160401b03808211156200020757600080fd5b620002158683870162000124565b935060208501519150808211156200022c57600080fd5b506200023b8582860162000124565b9150509250929050565b600181811c908216806200025a57607f821691505b602082108114156200027c57634e487b7160e01b600052602260045260246000fd5b50919050565b6108c480620002926000396000f3fe608060405234801561001057600080fd5b50600436106100a95760003560e01c80633950935111610071578063395093511461012357806370a082311461013657806395d89b411461015f578063a457c2d714610167578063a9059cbb1461017a578063dd62ed3e1461018d57600080fd5b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100ef57806323b872dd14610101578063313ce56714610114575b600080fd5b6100b66101c6565b6040516100c39190610701565b60405180910390f35b6100df6100da366004610772565b610258565b60405190151581526020016100c3565b6002545b6040519081526020016100c3565b6100df61010f36600461079c565b61026e565b604051601281526020016100c3565b6100df610131366004610772565b61031d565b6100f36101443660046107d8565b6001600160a01b031660009081526020819052604090205490565b6100b6610359565b6100df610175366004610772565b610368565b6100df610188366004610772565b610401565b6100f361019b3660046107fa565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6060600380546101d59061082d565b80601f01602080910402602001604051908101604052809291908181526020018280546102019061082d565b801561024e5780601f106102235761010080835404028352916020019161024e565b820191906000526020600020905b81548152906001019060200180831161023157829003601f168201915b5050505050905090565b600061026533848461040e565b50600192915050565b600061027b848484610532565b6001600160a01b0384166000908152600160209081526040808320338452909152902054828110156103055760405162461bcd60e51b815260206004820152602860248201527f45524332303a207472616e7366657220616d6f756e74206578636565647320616044820152676c6c6f77616e636560c01b60648201526084015b60405180910390fd5b610312853385840361040e565b506001949350505050565b3360008181526001602090815260408083206001600160a01b03871684529091528120549091610265918590610354908690610868565b61040e565b6060600480546101d59061082d565b3360009081526001602090815260408083206001600160a01b0386168452909152812054828110156103ea5760405162461bcd60e51b815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b60648201526084016102fc565b6103f7338585840361040e565b5060019392505050565b6000610265338484610532565b6001600160a01b0383166104705760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b60648201526084016102fc565b6001600160a01b0382166104d15760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b60648201526084016102fc565b6001600160a01b0383811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925910160405180910390a3505050565b6001600160a01b0383166105965760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b60648201526084016102fc565b6001600160a01b0382166105f85760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b60648201526084016102fc565b6001600160a01b038316600090815260208190526040902054818110156106705760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b60648201526084016102fc565b6001600160a01b038085166000908152602081905260408082208585039055918516815290812080548492906106a7908490610868565b92505081905550826001600160a01b0316846001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516106f391815260200190565b60405180910390a350505050565b600060208083528351808285015260005b8181101561072e57858101830151858201604001528201610712565b81811115610740576000604083870101525b50601f01601f1916929092016040019392505050565b80356001600160a01b038116811461076d57600080fd5b919050565b6000806040838503121561078557600080fd5b61078e83610756565b946020939093013593505050565b6000806000606084860312156107b157600080fd5b6107ba84610756565b92506107c860208501610756565b9150604084013590509250925092565b6000602082840312156107ea57600080fd5b6107f382610756565b9392505050565b6000806040838503121561080d57600080fd5b61081683610756565b915061082460208401610756565b90509250929050565b600181811c9082168061084157607f821691505b6020821081141561086257634e487b7160e01b600052602260045260246000fd5b50919050565b6000821982111561088957634e487b7160e01b600052601160045260246000fd5b50019056fea2646970667358221220200d63d7227620e0df6481e76a67dffb28ef35c0c3c55e1ee631b46bcb5e66c264736f6c634300080a0033", - "deployedBytecode": "0x608060405234801561001057600080fd5b50600436106100a95760003560e01c80633950935111610071578063395093511461012357806370a082311461013657806395d89b411461015f578063a457c2d714610167578063a9059cbb1461017a578063dd62ed3e1461018d57600080fd5b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100ef57806323b872dd14610101578063313ce56714610114575b600080fd5b6100b66101c6565b6040516100c39190610701565b60405180910390f35b6100df6100da366004610772565b610258565b60405190151581526020016100c3565b6002545b6040519081526020016100c3565b6100df61010f36600461079c565b61026e565b604051601281526020016100c3565b6100df610131366004610772565b61031d565b6100f36101443660046107d8565b6001600160a01b031660009081526020819052604090205490565b6100b6610359565b6100df610175366004610772565b610368565b6100df610188366004610772565b610401565b6100f361019b3660046107fa565b6001600160a01b03918216600090815260016020908152604080832093909416825291909152205490565b6060600380546101d59061082d565b80601f01602080910402602001604051908101604052809291908181526020018280546102019061082d565b801561024e5780601f106102235761010080835404028352916020019161024e565b820191906000526020600020905b81548152906001019060200180831161023157829003601f168201915b5050505050905090565b600061026533848461040e565b50600192915050565b600061027b848484610532565b6001600160a01b0384166000908152600160209081526040808320338452909152902054828110156103055760405162461bcd60e51b815260206004820152602860248201527f45524332303a207472616e7366657220616d6f756e74206578636565647320616044820152676c6c6f77616e636560c01b60648201526084015b60405180910390fd5b610312853385840361040e565b506001949350505050565b3360008181526001602090815260408083206001600160a01b03871684529091528120549091610265918590610354908690610868565b61040e565b6060600480546101d59061082d565b3360009081526001602090815260408083206001600160a01b0386168452909152812054828110156103ea5760405162461bcd60e51b815260206004820152602560248201527f45524332303a2064656372656173656420616c6c6f77616e63652062656c6f77604482015264207a65726f60d81b60648201526084016102fc565b6103f7338585840361040e565b5060019392505050565b6000610265338484610532565b6001600160a01b0383166104705760405162461bcd60e51b8152602060048201526024808201527f45524332303a20617070726f76652066726f6d20746865207a65726f206164646044820152637265737360e01b60648201526084016102fc565b6001600160a01b0382166104d15760405162461bcd60e51b815260206004820152602260248201527f45524332303a20617070726f766520746f20746865207a65726f206164647265604482015261737360f01b60648201526084016102fc565b6001600160a01b0383811660008181526001602090815260408083209487168084529482529182902085905590518481527f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925910160405180910390a3505050565b6001600160a01b0383166105965760405162461bcd60e51b815260206004820152602560248201527f45524332303a207472616e736665722066726f6d20746865207a65726f206164604482015264647265737360d81b60648201526084016102fc565b6001600160a01b0382166105f85760405162461bcd60e51b815260206004820152602360248201527f45524332303a207472616e7366657220746f20746865207a65726f206164647260448201526265737360e81b60648201526084016102fc565b6001600160a01b038316600090815260208190526040902054818110156106705760405162461bcd60e51b815260206004820152602660248201527f45524332303a207472616e7366657220616d6f756e7420657863656564732062604482015265616c616e636560d01b60648201526084016102fc565b6001600160a01b038085166000908152602081905260408082208585039055918516815290812080548492906106a7908490610868565b92505081905550826001600160a01b0316846001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516106f391815260200190565b60405180910390a350505050565b600060208083528351808285015260005b8181101561072e57858101830151858201604001528201610712565b81811115610740576000604083870101525b50601f01601f1916929092016040019392505050565b80356001600160a01b038116811461076d57600080fd5b919050565b6000806040838503121561078557600080fd5b61078e83610756565b946020939093013593505050565b6000806000606084860312156107b157600080fd5b6107ba84610756565b92506107c860208501610756565b9150604084013590509250925092565b6000602082840312156107ea57600080fd5b6107f382610756565b9392505050565b6000806040838503121561080d57600080fd5b61081683610756565b915061082460208401610756565b90509250929050565b600181811c9082168061084157607f821691505b6020821081141561086257634e487b7160e01b600052602260045260246000fd5b50919050565b6000821982111561088957634e487b7160e01b600052601160045260246000fd5b50019056fea2646970667358221220200d63d7227620e0df6481e76a67dffb28ef35c0c3c55e1ee631b46bcb5e66c264736f6c634300080a0033", - "linkReferences": {}, - "deployedLinkReferences": {} -} diff --git a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/IERC20.sol/IERC20.dbg.json b/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/IERC20.sol/IERC20.dbg.json deleted file mode 100644 index 86d5f44dff..0000000000 --- a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/IERC20.sol/IERC20.dbg.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "_format": "hh-sol-dbg-1", - "buildInfo": "../../../../../build-info/1f95016aa87f8376998977fedfcae017.json" -} diff --git a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/IERC20.sol/IERC20.json b/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/IERC20.sol/IERC20.json deleted file mode 100644 index 663a02d9d8..0000000000 --- a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/IERC20.sol/IERC20.json +++ /dev/null @@ -1,194 +0,0 @@ -{ - "_format": "hh-sol-artifact-1", - "contractName": "IERC20", - "sourceName": "@openzeppelin/contracts/token/ERC20/IERC20.sol", - "abi": [ - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Approval", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Transfer", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "name": "allowance", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "approve", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - } - ], - "bytecode": "0x", - "deployedBytecode": "0x", - "linkReferences": {}, - "deployedLinkReferences": {} -} diff --git a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol/IERC20Metadata.dbg.json b/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol/IERC20Metadata.dbg.json deleted file mode 100644 index 5118accb6b..0000000000 --- a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol/IERC20Metadata.dbg.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "_format": "hh-sol-dbg-1", - "buildInfo": "../../../../../../build-info/1f95016aa87f8376998977fedfcae017.json" -} diff --git a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol/IERC20Metadata.json b/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol/IERC20Metadata.json deleted file mode 100644 index 31d6425e0f..0000000000 --- a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol/IERC20Metadata.json +++ /dev/null @@ -1,233 +0,0 @@ -{ - "_format": "hh-sol-artifact-1", - "contractName": "IERC20Metadata", - "sourceName": "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol", - "abi": [ - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Approval", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "internalType": "address", - "name": "from", - "type": "address" - }, - { - "indexed": true, - "internalType": "address", - "name": "to", - "type": "address" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "value", - "type": "uint256" - } - ], - "name": "Transfer", - "type": "event" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "owner", - "type": "address" - }, - { - "internalType": "address", - "name": "spender", - "type": "address" - } - ], - "name": "allowance", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "spender", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "approve", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "account", - "type": "address" - } - ], - "name": "balanceOf", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "decimals", - "outputs": [ - { - "internalType": "uint8", - "name": "", - "type": "uint8" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "name", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "symbol", - "outputs": [ - { - "internalType": "string", - "name": "", - "type": "string" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [], - "name": "totalSupply", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "transfer", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "address", - "name": "sender", - "type": "address" - }, - { - "internalType": "address", - "name": "recipient", - "type": "address" - }, - { - "internalType": "uint256", - "name": "amount", - "type": "uint256" - } - ], - "name": "transferFrom", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "nonpayable", - "type": "function" - } - ], - "bytecode": "0x", - "deployedBytecode": "0x", - "linkReferences": {}, - "deployedLinkReferences": {} -} diff --git a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/utils/Context.sol/Context.dbg.json b/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/utils/Context.sol/Context.dbg.json deleted file mode 100644 index 16b4690e2a..0000000000 --- a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/utils/Context.sol/Context.dbg.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "_format": "hh-sol-dbg-1", - "buildInfo": "../../../../build-info/1f95016aa87f8376998977fedfcae017.json" -} diff --git a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/utils/Context.sol/Context.json b/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/utils/Context.sol/Context.json deleted file mode 100644 index 8fe86fc78f..0000000000 --- a/contracts/basic-bandwidth-generation/artifacts/@openzeppelin/contracts/utils/Context.sol/Context.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "_format": "hh-sol-artifact-1", - "contractName": "Context", - "sourceName": "@openzeppelin/contracts/utils/Context.sol", - "abi": [], - "bytecode": "0x", - "deployedBytecode": "0x", - "linkReferences": {}, - "deployedLinkReferences": {} -} diff --git a/contracts/basic-bandwidth-generation/cache/solidity-files-cache.json b/contracts/basic-bandwidth-generation/cache/solidity-files-cache.json deleted file mode 100644 index e842d693af..0000000000 --- a/contracts/basic-bandwidth-generation/cache/solidity-files-cache.json +++ /dev/null @@ -1,549 +0,0 @@ -{ - "_format": "hh-sol-cache-2", - "files": { - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/contracts/BandwidthGenerator.sol": { - "lastModificationDate": 1643129770695, - "contentHash": "642b72e2d50d565db7bc994cace3f5a0", - "sourceName": "contracts/BandwidthGenerator.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [ - "./CosmosToken.sol", - "./Gravity.sol", - "@openzeppelin/contracts/access/Ownable.sol", - "@openzeppelin/contracts/utils/math/SafeMath.sol" - ], - "versionPragmas": [ - "0.8.10" - ], - "artifacts": [ - "BandwidthGenerator" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/contracts/CosmosToken.sol": { - "lastModificationDate": 1642773899716, - "contentHash": "fc5dd09fe73bc6cfece970f702a3aba1", - "sourceName": "contracts/CosmosToken.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [ - "@openzeppelin/contracts/token/ERC20/ERC20.sol" - ], - "versionPragmas": [ - "0.8.10" - ], - "artifacts": [ - "CosmosERC20" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/contracts/Gravity.sol": { - "lastModificationDate": 1642773899716, - "contentHash": "0d6dae561f7b541bafb892b8593a08ac", - "sourceName": "contracts/Gravity.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [ - "@openzeppelin/contracts/token/ERC20/IERC20.sol", - "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol", - "@openzeppelin/contracts/security/ReentrancyGuard.sol", - "@openzeppelin/contracts/utils/Address.sol", - "@openzeppelin/contracts/utils/cryptography/ECDSA.sol", - "./CosmosToken.sol" - ], - "versionPragmas": [ - "0.8.10" - ], - "artifacts": [ - "Gravity" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/node_modules/@openzeppelin/contracts/access/Ownable.sol": { - "lastModificationDate": 1641812554274, - "contentHash": "4fe56b59ced59d87df6b796758f62895", - "sourceName": "@openzeppelin/contracts/access/Ownable.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [ - "../utils/Context.sol" - ], - "versionPragmas": [ - "^0.8.0" - ], - "artifacts": [ - "Ownable" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/node_modules/@openzeppelin/contracts/utils/math/SafeMath.sol": { - "lastModificationDate": 1641812554378, - "contentHash": "5365090efc586b728719e562ebfed0d6", - "sourceName": "@openzeppelin/contracts/utils/math/SafeMath.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [], - "versionPragmas": [ - "^0.8.0" - ], - "artifacts": [ - "SafeMath" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/node_modules/@openzeppelin/contracts/token/ERC20/ERC20.sol": { - "lastModificationDate": 1641812553614, - "contentHash": "2cd550cedf51b8d294607dad5023d717", - "sourceName": "@openzeppelin/contracts/token/ERC20/ERC20.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [ - "./IERC20.sol", - "./extensions/IERC20Metadata.sol", - "../../utils/Context.sol" - ], - "versionPragmas": [ - "^0.8.0" - ], - "artifacts": [ - "ERC20" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol": { - "lastModificationDate": 1641812554110, - "contentHash": "0eac3e1a83ee62326ca007811285b274", - "sourceName": "@openzeppelin/contracts/token/ERC20/IERC20.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [], - "versionPragmas": [ - "^0.8.0" - ], - "artifacts": [ - "IERC20" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/node_modules/@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol": { - "lastModificationDate": 1641812554118, - "contentHash": "aa1be06992a99bb7393b26a6af3c4dfb", - "sourceName": "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [ - "../IERC20.sol" - ], - "versionPragmas": [ - "^0.8.0" - ], - "artifacts": [ - "IERC20Metadata" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/node_modules/@openzeppelin/contracts/utils/Context.sol": { - "lastModificationDate": 1641812553346, - "contentHash": "851485d5b925529b1a2f34a0be077891", - "sourceName": "@openzeppelin/contracts/utils/Context.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [], - "versionPragmas": [ - "^0.8.0" - ], - "artifacts": [ - "Context" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/node_modules/@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol": { - "lastModificationDate": 1641812554370, - "contentHash": "d37406082a74a9b6b114de522fcb6349", - "sourceName": "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [ - "../IERC20.sol", - "../../../utils/Address.sol" - ], - "versionPragmas": [ - "^0.8.0" - ], - "artifacts": [ - "SafeERC20" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/node_modules/@openzeppelin/contracts/security/ReentrancyGuard.sol": { - "lastModificationDate": 1641812554322, - "contentHash": "53fbff678f378956efcb207fa748eaa6", - "sourceName": "@openzeppelin/contracts/security/ReentrancyGuard.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [], - "versionPragmas": [ - "^0.8.0" - ], - "artifacts": [ - "ReentrancyGuard" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/node_modules/@openzeppelin/contracts/utils/Address.sol": { - "lastModificationDate": 1641812553270, - "contentHash": "c5f6c4e4df069c789e7d84b4c3011913", - "sourceName": "@openzeppelin/contracts/utils/Address.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [], - "versionPragmas": [ - "^0.8.0" - ], - "artifacts": [ - "Address" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/node_modules/@openzeppelin/contracts/utils/cryptography/ECDSA.sol": { - "lastModificationDate": 1641812553414, - "contentHash": "197dbfaf7146845fa76331a4c9980e9f", - "sourceName": "@openzeppelin/contracts/utils/cryptography/ECDSA.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [], - "versionPragmas": [ - "^0.8.0" - ], - "artifacts": [ - "ECDSA" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/contracts/test-contracts/TestCosmosToken.sol": { - "lastModificationDate": 1642773899716, - "contentHash": "957b6079ee0d5a6e048fce8555c14dee", - "sourceName": "contracts/test-contracts/TestCosmosToken.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [ - "@openzeppelin/contracts/token/ERC20/ERC20.sol" - ], - "versionPragmas": [ - "0.8.10" - ], - "artifacts": [ - "TestCosmosERC20" - ] - }, - "/home/max/dev/nymtech/nym/contracts/basic-bandwidth-generation/contracts/test-contracts/TestGravity.sol": { - "lastModificationDate": 1642773899716, - "contentHash": "f24299c3acb20aff23914e16b7ba95e1", - "sourceName": "contracts/test-contracts/TestGravity.sol", - "solcConfig": { - "version": "0.8.10", - "settings": { - "optimizer": { - "enabled": true, - "runs": 200 - }, - "outputSelection": { - "*": { - "*": [ - "abi", - "evm.bytecode", - "evm.deployedBytecode", - "evm.methodIdentifiers" - ], - "": [ - "ast" - ] - } - } - } - }, - "imports": [ - "@openzeppelin/contracts/token/ERC20/IERC20.sol", - "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol", - "@openzeppelin/contracts/security/ReentrancyGuard.sol", - "@openzeppelin/contracts/utils/Address.sol", - "@openzeppelin/contracts/utils/cryptography/ECDSA.sol", - "./TestCosmosToken.sol" - ], - "versionPragmas": [ - "0.8.10" - ], - "artifacts": [ - "TestGravity" - ] - } - } -} diff --git a/contracts/basic-bandwidth-generation/contractAddresses.json b/contracts/basic-bandwidth-generation/contractAddresses.json deleted file mode 100644 index 3d94f9b469..0000000000 --- a/contracts/basic-bandwidth-generation/contractAddresses.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "rinkeby": - {"NYM_ERC20":"0xe58c12f8bA4a28e87841b10BdEdf80A47F86BA9B", - "BANDWIDTH_GENERATOR":"0xfa2714Bf14EB5Bb887e4A54984C6F7A7e3E6c84b", - "GRAVITY":"0xe58c12f8bA4a28e87841b10BdEdf80A47F86BA9B"}, - "mainnet": - {"NYM_ERC20":"0x525A8F6F3Ba4752868cde25164382BfbaE3990e1", - "NYMT":"0xE8883BAeF3869e14E4823F46662e81D4F7d2A81F", - "BANDWIDTH_GENERATOR":"0x3FfEb99acca159A182f35F9944dAf3BF41Ae8165", - "BANDWIDTH_GENERATOR_NYMT":"0xB3BF30DD53044c9050B7309031Bbf26b2cecF3be", - "GRAVITY":"0xa4108aA1Ec4967F8b52220a4f7e94A8201F2D906"} -} \ No newline at end of file diff --git a/contracts/basic-bandwidth-generation/contracts/BandwidthGenerator.sol b/contracts/basic-bandwidth-generation/contracts/BandwidthGenerator.sol deleted file mode 100644 index 933a47974d..0000000000 --- a/contracts/basic-bandwidth-generation/contracts/BandwidthGenerator.sol +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.10; - -import "./CosmosToken.sol"; -import "./Gravity.sol"; -import "@openzeppelin/contracts/access/Ownable.sol"; -import "@openzeppelin/contracts/utils/math/SafeMath.sol"; - -/** - * @title BandwidthGenerator - * @dev Contract for generating Basic Bandwidth Credentials (BBCs) on the Nym cosmos blockchain, - * using ERC20 representations of NYM as payment. Utilises the Gravity Bridge for cross-chain payment. - * - * Credential generation can be switched on/off by the contract owner. - * - * Credentials represent a certain amount of bandwidth which can be sent through the Nym Mixnet. - * By default 1 NYM = 1 GB of bandwidth. The `BytesPerToken` amount can be adjusted by the contract owner. - * The amount of bandwidth bought is calculated according to the following formula: - * `(Token amount in 'wei' / 10**18) * BytesPerToken` - */ -contract BandwidthGenerator is Ownable { - - using SafeMath for uint256; - - CosmosERC20 public erc20; - Gravity public gravityBridge; - uint256 public BytesPerToken; - bool public credentialGenerationEnabled; - - event BBCredentialPurchased( - uint256 Bandwidth, - uint256 indexed VerificationKey, - bytes SignedVerificationKey, - string CosmosRecipient - ); - - event RatioChanged( - uint256 indexed NewBytesPerToken - ); - - event CredentialGenerationSwitch( - bool Enabled - ); - - modifier checkEnabled() { - require(credentialGenerationEnabled, "BandwidthGenerator: credential generation isn't currently enabled"); - _; - } - - /** - * @param _erc20 Address of the erc20NYM deployed through the Gravity Bridge. - * @param _gravityBridge Address of the deployed Gravity Bridge. - */ - constructor(CosmosERC20 _erc20, Gravity _gravityBridge) { - require(address(_erc20) != address(0), "BandwidthGenerator: erc20 address cannot be null"); - require(address(_gravityBridge) != address(0), "BandwidthGenerator: gravity bridge address cannot be null"); - erc20 = _erc20; - gravityBridge = _gravityBridge; - BytesPerToken = 1073741824; // default amount set at deployment: 1 erc20NYM = 1073741824 Bytes = 1GB - credentialGenerationEnabled = true; - } - - /** - * @dev Changes amount of Bytes each erc20NYM is tradable for. Can only be called by Owner. - * @param _newBytesPerTokenAmount Amount of Bytes BBC is worth per 1 erc20NYM token. - */ - function changeRatio(uint256 _newBytesPerTokenAmount) public onlyOwner { - require(_newBytesPerTokenAmount != 0, "BandwidthGenerator: price cannot be 0"); - BytesPerToken = _newBytesPerTokenAmount; - emit RatioChanged(_newBytesPerTokenAmount); - } - - /** - * @dev Switches credential generation on/off. Can only be called by Owner. - * @param _generation Whether credential generation is turned on/off. - */ - function credentialGenerationSwitch(bool _generation) public onlyOwner { - credentialGenerationEnabled = _generation; - emit CredentialGenerationSwitch(_generation); - } - - /** - * @dev Function to create a BBC for account owning the verification key on the Nym Cosmos Blockchain - * by transfering erc20NYM via the Gravity Bridge. - * @param _amount Amount of erc20NYM tokens to spend on BBC - denominated in wei. - * @param _verificationKey Verification key of account on Nym blockchain who is purchasing BBC. - * @param _signedVerificationKey Number of erc20NYMs to spend signed by _verificationKey for auth on Cosmos Blockchain. - * @param _cosmosRecipient Address of the recipient of payment on Nym Cosmos Blockchain. - */ - function generateBasicBandwidthCredential(uint256 _amount, uint256 _verificationKey, bytes memory _signedVerificationKey, string calldata _cosmosRecipient) public checkEnabled { - require(_signedVerificationKey.length == 64, "BandwidthGenerator: Signature doesn't have 64 bytes"); - erc20.transferFrom(msg.sender, address(this), _amount); - erc20.approve(address(gravityBridge), _amount); - gravityBridge.sendToCosmos( - address(erc20), - _cosmosRecipient, - _amount - ); - uint256 bandwidth = bandwidthFromToken(_amount); - emit BBCredentialPurchased( - bandwidth, - _verificationKey, - _signedVerificationKey, - _cosmosRecipient - ); - } - - function bandwidthFromToken(uint256 _amount) public view returns (uint256) { - uint256 amountMulBytes = _amount.mul(BytesPerToken); - return amountMulBytes.div(10**18); - } - -} - - diff --git a/contracts/basic-bandwidth-generation/contracts/CosmosToken.sol b/contracts/basic-bandwidth-generation/contracts/CosmosToken.sol deleted file mode 100644 index 9c5556cd79..0000000000 --- a/contracts/basic-bandwidth-generation/contracts/CosmosToken.sol +++ /dev/null @@ -1,35 +0,0 @@ -//SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.10; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -contract CosmosERC20 is ERC20 { - uint256 MAX_UINT = 2**256 - 1; - uint8 private cosmosDecimals; - address private gravityAddress; - - // This override ensures we return the proper number of decimals - // for the cosmos token - function decimals() public view virtual override returns (uint8) { - return cosmosDecimals; - } - - // This is not an accurate total supply. Instead this is the total supply - // of the given cosmos asset on Ethereum at this moment in time. Keeping - // a totally accurate supply would require constant updates from the Cosmos - // side, while in theory this could be piggy-backed on some existing bridge - // operation it's a lot of complextiy to add so we chose to forgoe it. - function totalSupply() public view virtual override returns (uint256) { - return MAX_UINT - balanceOf(gravityAddress); - } - - constructor( - address _gravityAddress, - string memory _name, - string memory _symbol, - uint8 _decimals - ) ERC20(_name, _symbol) { - cosmosDecimals = _decimals; - gravityAddress = _gravityAddress; - _mint(_gravityAddress, MAX_UINT); - } -} \ No newline at end of file diff --git a/contracts/basic-bandwidth-generation/contracts/Gravity.sol b/contracts/basic-bandwidth-generation/contracts/Gravity.sol deleted file mode 100644 index 0b541280e1..0000000000 --- a/contracts/basic-bandwidth-generation/contracts/Gravity.sol +++ /dev/null @@ -1,663 +0,0 @@ -//SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.10; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -import "@openzeppelin/contracts/utils/Address.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "./CosmosToken.sol"; - -error InvalidSignature(); -error InvalidValsetNonce(uint256 newNonce, uint256 currentNonce); -error InvalidBatchNonce(uint256 newNonce, uint256 currentNonce); -error InvalidLogicCallNonce(uint256 newNonce, uint256 currentNonce); -error InvalidLogicCallTransfers(); -error InvalidLogicCallFees(); -error InvalidSendToCosmos(); -error IncorrectCheckpoint(); -error MalformedNewValidatorSet(); -error MalformedCurrentValidatorSet(); -error MalformedBatch(); -error InsufficientPower(uint256 cumulativePower, uint256 powerThreshold); -error BatchTimedOut(); -error LogicCallTimedOut(); - -// This is being used purely to avoid stack too deep errors -struct LogicCallArgs { - // Transfers out to the logic contract - uint256[] transferAmounts; - address[] transferTokenContracts; - // The fees (transferred to msg.sender) - uint256[] feeAmounts; - address[] feeTokenContracts; - // The arbitrary logic call - address logicContractAddress; - bytes payload; - // Invalidation metadata - uint256 timeOut; - bytes32 invalidationId; - uint256 invalidationNonce; -} - -// This is used purely to avoid stack too deep errors -// represents everything about a given validator set -struct ValsetArgs { - // the validators in this set, represented by an Ethereum address - address[] validators; - // the powers of the given validators in the same order as above - uint256[] powers; - // the nonce of this validator set - uint256 valsetNonce; - // the reward amount denominated in the below reward token, can be - // set to zero - uint256 rewardAmount; - // the reward token, should be set to the zero address if not being used - address rewardToken; -} - -// This represents a validator signature -struct Signature { - uint8 v; - bytes32 r; - bytes32 s; -} - -contract Gravity is ReentrancyGuard { - using SafeERC20 for IERC20; - - // The number of 'votes' required to execute a valset - // update or batch execution, set to 2/3 of 2^32 - uint256 constant constant_powerThreshold = 2863311530; - - // These are updated often - bytes32 public state_lastValsetCheckpoint; - mapping(address => uint256) public state_lastBatchNonces; - mapping(bytes32 => uint256) public state_invalidationMapping; - uint256 public state_lastValsetNonce = 0; - // event nonce zero is reserved by the Cosmos module as a special - // value indicating that no events have yet been submitted - uint256 public state_lastEventNonce = 1; - - // This is set once at initialization - bytes32 public immutable state_gravityId; - - // TransactionBatchExecutedEvent and SendToCosmosEvent both include the field _eventNonce. - // This is incremented every time one of these events is emitted. It is checked by the - // Cosmos module to ensure that all events are received in order, and that none are lost. - // - // ValsetUpdatedEvent does not include the field _eventNonce because it is never submitted to the Cosmos - // module. It is purely for the use of relayers to allow them to successfully submit batches. - event TransactionBatchExecutedEvent( - uint256 indexed _batchNonce, - address indexed _token, - uint256 _eventNonce - ); - event SendToCosmosEvent( - address indexed _tokenContract, - address indexed _sender, - string _destination, - uint256 _amount, - uint256 _eventNonce - ); - event ERC20DeployedEvent( - // FYI: Can't index on a string without doing a bunch of weird stuff - string _cosmosDenom, - address indexed _tokenContract, - string _name, - string _symbol, - uint8 _decimals, - uint256 _eventNonce - ); - event ValsetUpdatedEvent( - uint256 indexed _newValsetNonce, - uint256 _eventNonce, - uint256 _rewardAmount, - address _rewardToken, - address[] _validators, - uint256[] _powers - ); - event LogicCallEvent( - bytes32 _invalidationId, - uint256 _invalidationNonce, - bytes _returnData, - uint256 _eventNonce - ); - - // TEST FIXTURES - // These are here to make it easier to measure gas usage. They should be removed before production - function testMakeCheckpoint(ValsetArgs calldata _valsetArgs, bytes32 _gravityId) external pure { - makeCheckpoint(_valsetArgs, _gravityId); - } - - function testCheckValidatorSignatures( - ValsetArgs calldata _currentValset, - Signature[] calldata _sigs, - bytes32 _theHash, - uint256 _powerThreshold - ) external pure { - checkValidatorSignatures(_currentValset, _sigs, _theHash, _powerThreshold); - } - - // END TEST FIXTURES - - function lastBatchNonce(address _erc20Address) external view returns (uint256) { - return state_lastBatchNonces[_erc20Address]; - } - - function lastLogicCallNonce(bytes32 _invalidation_id) external view returns (uint256) { - return state_invalidationMapping[_invalidation_id]; - } - - // Utility function to verify geth style signatures - function verifySig( - address _signer, - bytes32 _theHash, - Signature calldata _sig - ) private pure returns (bool) { - bytes32 messageDigest = keccak256( - abi.encodePacked("\x19Ethereum Signed Message:\n32", _theHash) - ); - return _signer == ECDSA.recover(messageDigest, _sig.v, _sig.r, _sig.s); - } - - // Utility function to determine that a validator set and signatures are well formed - function validateValset(ValsetArgs calldata _valset, Signature[] calldata _sigs) private pure { - // Check that current validators, powers, and signatures (v,r,s) set is well-formed - if ( - _valset.validators.length != _valset.powers.length || - _valset.validators.length != _sigs.length - ) { - revert MalformedCurrentValidatorSet(); - } - } - - // Make a new checkpoint from the supplied validator set - // A checkpoint is a hash of all relevant information about the valset. This is stored by the contract, - // instead of storing the information directly. This saves on storage and gas. - // The format of the checkpoint is: - // h(gravityId, "checkpoint", valsetNonce, validators[], powers[]) - // Where h is the keccak256 hash function. - // The validator powers must be decreasing or equal. This is important for checking the signatures on the - // next valset, since it allows the caller to stop verifying signatures once a quorum of signatures have been verified. - function makeCheckpoint(ValsetArgs memory _valsetArgs, bytes32 _gravityId) - private - pure - returns (bytes32) - { - // bytes32 encoding of the string "checkpoint" - bytes32 methodName = 0x636865636b706f696e7400000000000000000000000000000000000000000000; - - bytes32 checkpoint = keccak256( - abi.encode( - _gravityId, - methodName, - _valsetArgs.valsetNonce, - _valsetArgs.validators, - _valsetArgs.powers, - _valsetArgs.rewardAmount, - _valsetArgs.rewardToken - ) - ); - - return checkpoint; - } - - function checkValidatorSignatures( - // The current validator set and their powers - ValsetArgs calldata _currentValset, - // The current validator's signatures - Signature[] calldata _sigs, - // This is what we are checking they have signed - bytes32 _theHash, - uint256 _powerThreshold - ) private pure { - uint256 cumulativePower = 0; - - for (uint256 i = 0; i < _currentValset.validators.length; i++) { - // If v is set to 0, this signifies that it was not possible to get a signature from this validator and we skip evaluation - // (In a valid signature, it is either 27 or 28) - if (_sigs[i].v != 0) { - // Check that the current validator has signed off on the hash - if (!verifySig(_currentValset.validators[i], _theHash, _sigs[i])) { - revert InvalidSignature(); - } - - // Sum up cumulative power - cumulativePower = cumulativePower + _currentValset.powers[i]; - - // Break early to avoid wasting gas - if (cumulativePower > _powerThreshold) { - break; - } - } - } - - // Check that there was enough power - if (cumulativePower <= _powerThreshold) { - revert InsufficientPower(cumulativePower, _powerThreshold); - } - // Success - } - - // This updates the valset by checking that the validators in the current valset have signed off on the - // new valset. The signatures supplied are the signatures of the current valset over the checkpoint hash - // generated from the new valset. - // Anyone can call this function, but they must supply valid signatures of constant_powerThreshold of the current valset over - // the new valset. - function updateValset( - // The new version of the validator set - ValsetArgs calldata _newValset, - // The current validators that approve the change - ValsetArgs calldata _currentValset, - // These are arrays of the parts of the current validator's signatures - Signature[] calldata _sigs - ) external { - // CHECKS - - // Check that the valset nonce is greater than the old one - if (_newValset.valsetNonce <= _currentValset.valsetNonce) { - revert InvalidValsetNonce({ - newNonce: _newValset.valsetNonce, - currentNonce: _currentValset.valsetNonce - }); - } - - // Check that the valset nonce is less than a million nonces forward from the old one - // this makes it difficult for an attacker to lock out the contract by getting a single - // bad validator set through with uint256 max nonce - if (_newValset.valsetNonce > _currentValset.valsetNonce + 1000000) { - revert InvalidValsetNonce({ - newNonce: _newValset.valsetNonce, - currentNonce: _currentValset.valsetNonce - }); - } - - // Check that new validators and powers set is well-formed - if ( - _newValset.validators.length != _newValset.powers.length || - _newValset.validators.length == 0 - ) { - revert MalformedNewValidatorSet(); - } - - // Check that current validators, powers, and signatures (v,r,s) set is well-formed - validateValset(_currentValset, _sigs); - - // Check cumulative power to ensure the contract has sufficient power to actually - // pass a vote - uint256 cumulativePower = 0; - for (uint256 i = 0; i < _newValset.powers.length; i++) { - cumulativePower = cumulativePower + _newValset.powers[i]; - if (cumulativePower > constant_powerThreshold) { - break; - } - } - if (cumulativePower <= constant_powerThreshold) { - revert InsufficientPower({ - cumulativePower: cumulativePower, - powerThreshold: constant_powerThreshold - }); - } - - // Check that the supplied current validator set matches the saved checkpoint - if (makeCheckpoint(_currentValset, state_gravityId) != state_lastValsetCheckpoint) { - revert IncorrectCheckpoint(); - } - - // Check that enough current validators have signed off on the new validator set - bytes32 newCheckpoint = makeCheckpoint(_newValset, state_gravityId); - - checkValidatorSignatures(_currentValset, _sigs, newCheckpoint, constant_powerThreshold); - - // ACTIONS - - // Stored to be used next time to validate that the valset - // supplied by the caller is correct. - state_lastValsetCheckpoint = newCheckpoint; - - // Store new nonce - state_lastValsetNonce = _newValset.valsetNonce; - - // Send submission reward to msg.sender if reward token is a valid value - if (_newValset.rewardToken != address(0) && _newValset.rewardAmount != 0) { - IERC20(_newValset.rewardToken).safeTransfer(msg.sender, _newValset.rewardAmount); - } - - // LOGS - - state_lastEventNonce = state_lastEventNonce + 1; - emit ValsetUpdatedEvent( - _newValset.valsetNonce, - state_lastEventNonce, - _newValset.rewardAmount, - _newValset.rewardToken, - _newValset.validators, - _newValset.powers - ); - } - - // submitBatch processes a batch of Cosmos -> Ethereum transactions by sending the tokens in the transactions - // to the destination addresses. It is approved by the current Cosmos validator set. - // Anyone can call this function, but they must supply valid signatures of constant_powerThreshold of the current valset over - // the batch. - function submitBatch( - // The validators that approve the batch - ValsetArgs calldata _currentValset, - // These are arrays of the parts of the validators signatures - Signature[] calldata _sigs, - // The batch of transactions - uint256[] calldata _amounts, - address[] calldata _destinations, - uint256[] calldata _fees, - uint256 _batchNonce, - address _tokenContract, - // a block height beyond which this batch is not valid - // used to provide a fee-free timeout - uint256 _batchTimeout - ) external nonReentrant { - // CHECKS scoped to reduce stack depth - { - // Check that the batch nonce is higher than the last nonce for this token - if (_batchNonce <= state_lastBatchNonces[_tokenContract]) { - revert InvalidBatchNonce({ - newNonce: _batchNonce, - currentNonce: state_lastBatchNonces[_tokenContract] - }); - } - - // Check that the batch nonce is less than one million nonces forward from the old one - // this makes it difficult for an attacker to lock out the contract by getting a single - // bad batch through with uint256 max nonce - if (_batchNonce > state_lastBatchNonces[_tokenContract] + 1000000) { - revert InvalidBatchNonce({ - newNonce: _batchNonce, - currentNonce: state_lastBatchNonces[_tokenContract] - }); - } - - // Check that the block height is less than the timeout height - if (block.number >= _batchTimeout) { - revert BatchTimedOut(); - } - - // Check that current validators, powers, and signatures (v,r,s) set is well-formed - validateValset(_currentValset, _sigs); - - // Check that the supplied current validator set matches the saved checkpoint - if (makeCheckpoint(_currentValset, state_gravityId) != state_lastValsetCheckpoint) { - revert IncorrectCheckpoint(); - } - - // Check that the transaction batch is well-formed - if (_amounts.length != _destinations.length || _amounts.length != _fees.length) { - revert MalformedBatch(); - } - - // Check that enough current validators have signed off on the transaction batch and valset - checkValidatorSignatures( - _currentValset, - _sigs, - // Get hash of the transaction batch and checkpoint - keccak256( - abi.encode( - state_gravityId, - // bytes32 encoding of "transactionBatch" - 0x7472616e73616374696f6e426174636800000000000000000000000000000000, - _amounts, - _destinations, - _fees, - _batchNonce, - _tokenContract, - _batchTimeout - ) - ), - constant_powerThreshold - ); - - // ACTIONS - - // Store batch nonce - state_lastBatchNonces[_tokenContract] = _batchNonce; - - { - // Send transaction amounts to destinations - uint256 totalFee; - for (uint256 i = 0; i < _amounts.length; i++) { - IERC20(_tokenContract).safeTransfer(_destinations[i], _amounts[i]); - totalFee = totalFee + _fees[i]; - } - - // Send transaction fees to msg.sender - IERC20(_tokenContract).safeTransfer(msg.sender, totalFee); - } - } - - // LOGS scoped to reduce stack depth - { - state_lastEventNonce = state_lastEventNonce + 1; - emit TransactionBatchExecutedEvent(_batchNonce, _tokenContract, state_lastEventNonce); - } - } - - // This makes calls to contracts that execute arbitrary logic - // First, it gives the logic contract some tokens - // Then, it gives msg.senders tokens for fees - // Then, it calls an arbitrary function on the logic contract - // invalidationId and invalidationNonce are used for replay prevention. - // They can be used to implement a per-token nonce by setting the token - // address as the invalidationId and incrementing the nonce each call. - // They can be used for nonce-free replay prevention by using a different invalidationId - // for each call. - function submitLogicCall( - // The validators that approve the call - ValsetArgs calldata _currentValset, - // These are arrays of the parts of the validators signatures - Signature[] calldata _sigs, - LogicCallArgs memory _args - ) external nonReentrant { - // CHECKS scoped to reduce stack depth - { - // Check that the call has not timed out - if (block.number >= _args.timeOut) { - revert LogicCallTimedOut(); - } - - // Check that the invalidation nonce is higher than the last nonce for this invalidation Id - if (state_invalidationMapping[_args.invalidationId] >= _args.invalidationNonce) { - revert InvalidLogicCallNonce({ - newNonce: _args.invalidationNonce, - currentNonce: state_invalidationMapping[_args.invalidationId] - }); - } - - // note the lack of nonce skipping check, it's not needed here since an attacker - // will never be able to fill the invalidationId space, therefore a nonce lockout - // is simply not possible - - // Check that current validators, powers, and signatures (v,r,s) set is well-formed - validateValset(_currentValset, _sigs); - - // Check that the supplied current validator set matches the saved checkpoint - if (makeCheckpoint(_currentValset, state_gravityId) != state_lastValsetCheckpoint) { - revert IncorrectCheckpoint(); - } - - if (_args.transferAmounts.length != _args.transferTokenContracts.length) { - revert InvalidLogicCallTransfers(); - } - - if (_args.feeAmounts.length != _args.feeTokenContracts.length) { - revert InvalidLogicCallFees(); - } - } - { - bytes32 argsHash = keccak256( - abi.encode( - state_gravityId, - // bytes32 encoding of "logicCall" - 0x6c6f67696343616c6c0000000000000000000000000000000000000000000000, - _args.transferAmounts, - _args.transferTokenContracts, - _args.feeAmounts, - _args.feeTokenContracts, - _args.logicContractAddress, - _args.payload, - _args.timeOut, - _args.invalidationId, - _args.invalidationNonce - ) - ); - - // Check that enough current validators have signed off on the transaction batch and valset - checkValidatorSignatures( - _currentValset, - _sigs, - // Get hash of the transaction batch and checkpoint - argsHash, - constant_powerThreshold - ); - } - - // ACTIONS - - // Update invaldiation nonce - state_invalidationMapping[_args.invalidationId] = _args.invalidationNonce; - - // Send tokens to the logic contract - for (uint256 i = 0; i < _args.transferAmounts.length; i++) { - IERC20(_args.transferTokenContracts[i]).safeTransfer( - _args.logicContractAddress, - _args.transferAmounts[i] - ); - } - - // Make call to logic contract - bytes memory returnData = Address.functionCall(_args.logicContractAddress, _args.payload); - - // Send fees to msg.sender - for (uint256 i = 0; i < _args.feeAmounts.length; i++) { - IERC20(_args.feeTokenContracts[i]).safeTransfer(msg.sender, _args.feeAmounts[i]); - } - - // LOGS scoped to reduce stack depth - { - state_lastEventNonce = state_lastEventNonce + 1; - emit LogicCallEvent( - _args.invalidationId, - _args.invalidationNonce, - returnData, - state_lastEventNonce - ); - } - } - - function sendToCosmos( - address _tokenContract, - string calldata _destination, - uint256 _amount - ) external nonReentrant { - // we snapshot our current balance of this token - uint256 ourStartingBalance = IERC20(_tokenContract).balanceOf(address(this)); - - // attempt to transfer the user specified amount - IERC20(_tokenContract).safeTransferFrom(msg.sender, address(this), _amount); - - // check what this particular ERC20 implementation actually gave us, since it doesn't - // have to be at all related to the _amount - uint256 ourEndingBalance = IERC20(_tokenContract).balanceOf(address(this)); - - // a very strange ERC20 may trigger this condition, if we didn't have this we would - // underflow, so it's mostly just an error message printer - if (ourEndingBalance <= ourStartingBalance) { - revert InvalidSendToCosmos(); - } - - state_lastEventNonce = state_lastEventNonce + 1; - - // emit to Cosmos the actual amount our balance has changed, rather than the user - // provided amount. This protects against a small set of wonky ERC20 behavior, like - // burning on send but not tokens that for example change every users balance every day. - emit SendToCosmosEvent( - _tokenContract, - msg.sender, - _destination, - ourEndingBalance - ourStartingBalance, - state_lastEventNonce - ); - } - - function deployERC20( - string calldata _cosmosDenom, - string calldata _name, - string calldata _symbol, - uint8 _decimals - ) external { - // Deploy an ERC20 with entire supply granted to Gravity.sol - CosmosERC20 erc20 = new CosmosERC20(address(this), _name, _symbol, _decimals); - - // Fire an event to let the Cosmos module know - state_lastEventNonce = state_lastEventNonce + 1; - emit ERC20DeployedEvent( - _cosmosDenom, - address(erc20), - _name, - _symbol, - _decimals, - state_lastEventNonce - ); - } - - constructor( - // A unique identifier for this gravity instance to use in signatures - bytes32 _gravityId, - // The validator set, not in valset args format since many of it's - // arguments would never be used in this case - address[] memory _validators, - uint256[] memory _powers - ) { - // CHECKS - - // Check that validators, powers, and signatures (v,r,s) set is well-formed - if (_validators.length != _powers.length || _validators.length == 0) { - revert MalformedCurrentValidatorSet(); - } - - // Check cumulative power to ensure the contract has sufficient power to actually - // pass a vote - uint256 cumulativePower = 0; - for (uint256 i = 0; i < _powers.length; i++) { - cumulativePower = cumulativePower + _powers[i]; - if (cumulativePower > constant_powerThreshold) { - break; - } - } - if (cumulativePower <= constant_powerThreshold) { - revert InsufficientPower({ - cumulativePower: cumulativePower, - powerThreshold: constant_powerThreshold - }); - } - - ValsetArgs memory _valset; - _valset = ValsetArgs(_validators, _powers, 0, 0, address(0)); - - bytes32 newCheckpoint = makeCheckpoint(_valset, _gravityId); - - // ACTIONS - - state_gravityId = _gravityId; - state_lastValsetCheckpoint = newCheckpoint; - - // LOGS - - emit ValsetUpdatedEvent( - state_lastValsetNonce, - state_lastEventNonce, - 0, - address(0), - _validators, - _powers - ); - } -} \ No newline at end of file diff --git a/contracts/basic-bandwidth-generation/contracts/test-contracts/TestCosmosToken.sol b/contracts/basic-bandwidth-generation/contracts/test-contracts/TestCosmosToken.sol deleted file mode 100644 index 9c88d8defd..0000000000 --- a/contracts/basic-bandwidth-generation/contracts/test-contracts/TestCosmosToken.sol +++ /dev/null @@ -1,55 +0,0 @@ -//SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.10; -import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; - -/** -* This is a slightly modified version of the cosmos erc20 contract -* which I have done for unit testing. -* -* All that has been changed is the MAX_UINT variable to allow -* me to mint some tokens more easily in unit tests, and the -* addition of the public mint() function. -*/ - - -contract TestCosmosERC20 is ERC20 { - /* canonical amount */ - // uint256 MAX_UINT = 2**256 - 1; - - /* unit testing amount */ - uint256 HALF_MAX_UINT = 2**256 / 2; - - uint8 private cosmosDecimals; - address private gravityAddress; - - // This override ensures we return the proper number of decimals - // for the cosmos token - function decimals() public view virtual override returns (uint8) { - return cosmosDecimals; - } - - // This is not an accurate total supply. Instead this is the total supply - // of the given cosmos asset on Ethereum at this moment in time. Keeping - // a totally accurate supply would require constant updates from the Cosmos - // side, while in theory this could be piggy-backed on some existing bridge - // operation it's a lot of complextiy to add so we chose to forgoe it. - function totalSupply() public view virtual override returns (uint256) { - return HALF_MAX_UINT - balanceOf(gravityAddress); - } - - constructor( - address _gravityAddress, - string memory _name, - string memory _symbol, - uint8 _decimals - ) ERC20(_name, _symbol) { - cosmosDecimals = _decimals; - gravityAddress = _gravityAddress; - _mint(_gravityAddress, HALF_MAX_UINT); - } - - // Additional function for our (nym repo) unit tests with bridge - function mintForUnitTesting(address _to, uint _amount) public { - _mint(_to, _amount); - } -} \ No newline at end of file diff --git a/contracts/basic-bandwidth-generation/contracts/test-contracts/TestGravity.sol b/contracts/basic-bandwidth-generation/contracts/test-contracts/TestGravity.sol deleted file mode 100644 index 3ac41eac39..0000000000 --- a/contracts/basic-bandwidth-generation/contracts/test-contracts/TestGravity.sol +++ /dev/null @@ -1,671 +0,0 @@ -//SPDX-License-Identifier: Apache-2.0 -pragma solidity 0.8.10; - -import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; -import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; -import "@openzeppelin/contracts/utils/Address.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "./TestCosmosToken.sol"; - -/** -* This is a slightly modified version of the gravity bridge contract -* which I have done for unit testing. -* -* All that has been changed is ___ -*/ - -error InvalidSignature(); -error InvalidValsetNonce(uint256 newNonce, uint256 currentNonce); -error InvalidBatchNonce(uint256 newNonce, uint256 currentNonce); -error InvalidLogicCallNonce(uint256 newNonce, uint256 currentNonce); -error InvalidLogicCallTransfers(); -error InvalidLogicCallFees(); -error InvalidSendToCosmos(); -error IncorrectCheckpoint(); -error MalformedNewValidatorSet(); -error MalformedCurrentValidatorSet(); -error MalformedBatch(); -error InsufficientPower(uint256 cumulativePower, uint256 powerThreshold); -error BatchTimedOut(); -error LogicCallTimedOut(); - -// This is being used purely to avoid stack too deep errors -struct LogicCallArgs { - // Transfers out to the logic contract - uint256[] transferAmounts; - address[] transferTokenContracts; - // The fees (transferred to msg.sender) - uint256[] feeAmounts; - address[] feeTokenContracts; - // The arbitrary logic call - address logicContractAddress; - bytes payload; - // Invalidation metadata - uint256 timeOut; - bytes32 invalidationId; - uint256 invalidationNonce; -} - -// This is used purely to avoid stack too deep errors -// represents everything about a given validator set -struct ValsetArgs { - // the validators in this set, represented by an Ethereum address - address[] validators; - // the powers of the given validators in the same order as above - uint256[] powers; - // the nonce of this validator set - uint256 valsetNonce; - // the reward amount denominated in the below reward token, can be - // set to zero - uint256 rewardAmount; - // the reward token, should be set to the zero address if not being used - address rewardToken; -} - -// This represents a validator signature -struct Signature { - uint8 v; - bytes32 r; - bytes32 s; -} - -contract TestGravity is ReentrancyGuard { - using SafeERC20 for IERC20; - - // The number of 'votes' required to execute a valset - // update or batch execution, set to 2/3 of 2^32 - uint256 constant constant_powerThreshold = 2863311530; - - // These are updated often - bytes32 public state_lastValsetCheckpoint; - mapping(address => uint256) public state_lastBatchNonces; - mapping(bytes32 => uint256) public state_invalidationMapping; - uint256 public state_lastValsetNonce = 0; - // event nonce zero is reserved by the Cosmos module as a special - // value indicating that no events have yet been submitted - uint256 public state_lastEventNonce = 1; - - // This is set once at initialization - bytes32 public immutable state_gravityId; - - // TransactionBatchExecutedEvent and SendToCosmosEvent both include the field _eventNonce. - // This is incremented every time one of these events is emitted. It is checked by the - // Cosmos module to ensure that all events are received in order, and that none are lost. - // - // ValsetUpdatedEvent does not include the field _eventNonce because it is never submitted to the Cosmos - // module. It is purely for the use of relayers to allow them to successfully submit batches. - event TransactionBatchExecutedEvent( - uint256 indexed _batchNonce, - address indexed _token, - uint256 _eventNonce - ); - event SendToCosmosEvent( - address indexed _tokenContract, - address indexed _sender, - string _destination, - uint256 _amount, - uint256 _eventNonce - ); - event ERC20DeployedEvent( - // FYI: Can't index on a string without doing a bunch of weird stuff - string _cosmosDenom, - address indexed _tokenContract, - string _name, - string _symbol, - uint8 _decimals, - uint256 _eventNonce - ); - event ValsetUpdatedEvent( - uint256 indexed _newValsetNonce, - uint256 _eventNonce, - uint256 _rewardAmount, - address _rewardToken, - address[] _validators, - uint256[] _powers - ); - event LogicCallEvent( - bytes32 _invalidationId, - uint256 _invalidationNonce, - bytes _returnData, - uint256 _eventNonce - ); - - // TEST FIXTURES - // These are here to make it easier to measure gas usage. They should be removed before production - function testMakeCheckpoint(ValsetArgs calldata _valsetArgs, bytes32 _gravityId) external pure { - makeCheckpoint(_valsetArgs, _gravityId); - } - - function testCheckValidatorSignatures( - ValsetArgs calldata _currentValset, - Signature[] calldata _sigs, - bytes32 _theHash, - uint256 _powerThreshold - ) external pure { - checkValidatorSignatures(_currentValset, _sigs, _theHash, _powerThreshold); - } - - // END TEST FIXTURES - - function lastBatchNonce(address _erc20Address) external view returns (uint256) { - return state_lastBatchNonces[_erc20Address]; - } - - function lastLogicCallNonce(bytes32 _invalidation_id) external view returns (uint256) { - return state_invalidationMapping[_invalidation_id]; - } - - // Utility function to verify geth style signatures - function verifySig( - address _signer, - bytes32 _theHash, - Signature calldata _sig - ) private pure returns (bool) { - bytes32 messageDigest = keccak256( - abi.encodePacked("\x19Ethereum Signed Message:\n32", _theHash) - ); - return _signer == ECDSA.recover(messageDigest, _sig.v, _sig.r, _sig.s); - } - - // Utility function to determine that a validator set and signatures are well formed - function validateValset(ValsetArgs calldata _valset, Signature[] calldata _sigs) private pure { - // Check that current validators, powers, and signatures (v,r,s) set is well-formed - if ( - _valset.validators.length != _valset.powers.length || - _valset.validators.length != _sigs.length - ) { - revert MalformedCurrentValidatorSet(); - } - } - - // Make a new checkpoint from the supplied validator set - // A checkpoint is a hash of all relevant information about the valset. This is stored by the contract, - // instead of storing the information directly. This saves on storage and gas. - // The format of the checkpoint is: - // h(gravityId, "checkpoint", valsetNonce, validators[], powers[]) - // Where h is the keccak256 hash function. - // The validator powers must be decreasing or equal. This is important for checking the signatures on the - // next valset, since it allows the caller to stop verifying signatures once a quorum of signatures have been verified. - function makeCheckpoint(ValsetArgs memory _valsetArgs, bytes32 _gravityId) - private - pure - returns (bytes32) - { - // bytes32 encoding of the string "checkpoint" - bytes32 methodName = 0x636865636b706f696e7400000000000000000000000000000000000000000000; - - bytes32 checkpoint = keccak256( - abi.encode( - _gravityId, - methodName, - _valsetArgs.valsetNonce, - _valsetArgs.validators, - _valsetArgs.powers, - _valsetArgs.rewardAmount, - _valsetArgs.rewardToken - ) - ); - - return checkpoint; - } - - function checkValidatorSignatures( - // The current validator set and their powers - ValsetArgs calldata _currentValset, - // The current validator's signatures - Signature[] calldata _sigs, - // This is what we are checking they have signed - bytes32 _theHash, - uint256 _powerThreshold - ) private pure { - uint256 cumulativePower = 0; - - for (uint256 i = 0; i < _currentValset.validators.length; i++) { - // If v is set to 0, this signifies that it was not possible to get a signature from this validator and we skip evaluation - // (In a valid signature, it is either 27 or 28) - if (_sigs[i].v != 0) { - // Check that the current validator has signed off on the hash - if (!verifySig(_currentValset.validators[i], _theHash, _sigs[i])) { - revert InvalidSignature(); - } - - // Sum up cumulative power - cumulativePower = cumulativePower + _currentValset.powers[i]; - - // Break early to avoid wasting gas - if (cumulativePower > _powerThreshold) { - break; - } - } - } - - // Check that there was enough power - if (cumulativePower <= _powerThreshold) { - revert InsufficientPower(cumulativePower, _powerThreshold); - } - // Success - } - - // This updates the valset by checking that the validators in the current valset have signed off on the - // new valset. The signatures supplied are the signatures of the current valset over the checkpoint hash - // generated from the new valset. - // Anyone can call this function, but they must supply valid signatures of constant_powerThreshold of the current valset over - // the new valset. - function updateValset( - // The new version of the validator set - ValsetArgs calldata _newValset, - // The current validators that approve the change - ValsetArgs calldata _currentValset, - // These are arrays of the parts of the current validator's signatures - Signature[] calldata _sigs - ) external { - // CHECKS - - // Check that the valset nonce is greater than the old one - if (_newValset.valsetNonce <= _currentValset.valsetNonce) { - revert InvalidValsetNonce({ - newNonce: _newValset.valsetNonce, - currentNonce: _currentValset.valsetNonce - }); - } - - // Check that the valset nonce is less than a million nonces forward from the old one - // this makes it difficult for an attacker to lock out the contract by getting a single - // bad validator set through with uint256 max nonce - if (_newValset.valsetNonce > _currentValset.valsetNonce + 1000000) { - revert InvalidValsetNonce({ - newNonce: _newValset.valsetNonce, - currentNonce: _currentValset.valsetNonce - }); - } - - // Check that new validators and powers set is well-formed - if ( - _newValset.validators.length != _newValset.powers.length || - _newValset.validators.length == 0 - ) { - revert MalformedNewValidatorSet(); - } - - // Check that current validators, powers, and signatures (v,r,s) set is well-formed - validateValset(_currentValset, _sigs); - - // Check cumulative power to ensure the contract has sufficient power to actually - // pass a vote - uint256 cumulativePower = 0; - for (uint256 i = 0; i < _newValset.powers.length; i++) { - cumulativePower = cumulativePower + _newValset.powers[i]; - if (cumulativePower > constant_powerThreshold) { - break; - } - } - if (cumulativePower <= constant_powerThreshold) { - revert InsufficientPower({ - cumulativePower: cumulativePower, - powerThreshold: constant_powerThreshold - }); - } - - // Check that the supplied current validator set matches the saved checkpoint - if (makeCheckpoint(_currentValset, state_gravityId) != state_lastValsetCheckpoint) { - revert IncorrectCheckpoint(); - } - - // Check that enough current validators have signed off on the new validator set - bytes32 newCheckpoint = makeCheckpoint(_newValset, state_gravityId); - - checkValidatorSignatures(_currentValset, _sigs, newCheckpoint, constant_powerThreshold); - - // ACTIONS - - // Stored to be used next time to validate that the valset - // supplied by the caller is correct. - state_lastValsetCheckpoint = newCheckpoint; - - // Store new nonce - state_lastValsetNonce = _newValset.valsetNonce; - - // Send submission reward to msg.sender if reward token is a valid value - if (_newValset.rewardToken != address(0) && _newValset.rewardAmount != 0) { - IERC20(_newValset.rewardToken).safeTransfer(msg.sender, _newValset.rewardAmount); - } - - // LOGS - - state_lastEventNonce = state_lastEventNonce + 1; - emit ValsetUpdatedEvent( - _newValset.valsetNonce, - state_lastEventNonce, - _newValset.rewardAmount, - _newValset.rewardToken, - _newValset.validators, - _newValset.powers - ); - } - - // submitBatch processes a batch of Cosmos -> Ethereum transactions by sending the tokens in the transactions - // to the destination addresses. It is approved by the current Cosmos validator set. - // Anyone can call this function, but they must supply valid signatures of constant_powerThreshold of the current valset over - // the batch. - function submitBatch( - // The validators that approve the batch - ValsetArgs calldata _currentValset, - // These are arrays of the parts of the validators signatures - Signature[] calldata _sigs, - // The batch of transactions - uint256[] calldata _amounts, - address[] calldata _destinations, - uint256[] calldata _fees, - uint256 _batchNonce, - address _tokenContract, - // a block height beyond which this batch is not valid - // used to provide a fee-free timeout - uint256 _batchTimeout - ) external nonReentrant { - // CHECKS scoped to reduce stack depth - { - // Check that the batch nonce is higher than the last nonce for this token - if (_batchNonce <= state_lastBatchNonces[_tokenContract]) { - revert InvalidBatchNonce({ - newNonce: _batchNonce, - currentNonce: state_lastBatchNonces[_tokenContract] - }); - } - - // Check that the batch nonce is less than one million nonces forward from the old one - // this makes it difficult for an attacker to lock out the contract by getting a single - // bad batch through with uint256 max nonce - if (_batchNonce > state_lastBatchNonces[_tokenContract] + 1000000) { - revert InvalidBatchNonce({ - newNonce: _batchNonce, - currentNonce: state_lastBatchNonces[_tokenContract] - }); - } - - // Check that the block height is less than the timeout height - if (block.number >= _batchTimeout) { - revert BatchTimedOut(); - } - - // Check that current validators, powers, and signatures (v,r,s) set is well-formed - validateValset(_currentValset, _sigs); - - // Check that the supplied current validator set matches the saved checkpoint - if (makeCheckpoint(_currentValset, state_gravityId) != state_lastValsetCheckpoint) { - revert IncorrectCheckpoint(); - } - - // Check that the transaction batch is well-formed - if (_amounts.length != _destinations.length || _amounts.length != _fees.length) { - revert MalformedBatch(); - } - - // Check that enough current validators have signed off on the transaction batch and valset - checkValidatorSignatures( - _currentValset, - _sigs, - // Get hash of the transaction batch and checkpoint - keccak256( - abi.encode( - state_gravityId, - // bytes32 encoding of "transactionBatch" - 0x7472616e73616374696f6e426174636800000000000000000000000000000000, - _amounts, - _destinations, - _fees, - _batchNonce, - _tokenContract, - _batchTimeout - ) - ), - constant_powerThreshold - ); - - // ACTIONS - - // Store batch nonce - state_lastBatchNonces[_tokenContract] = _batchNonce; - - { - // Send transaction amounts to destinations - uint256 totalFee; - for (uint256 i = 0; i < _amounts.length; i++) { - IERC20(_tokenContract).safeTransfer(_destinations[i], _amounts[i]); - totalFee = totalFee + _fees[i]; - } - - // Send transaction fees to msg.sender - IERC20(_tokenContract).safeTransfer(msg.sender, totalFee); - } - } - - // LOGS scoped to reduce stack depth - { - state_lastEventNonce = state_lastEventNonce + 1; - emit TransactionBatchExecutedEvent(_batchNonce, _tokenContract, state_lastEventNonce); - } - } - - // This makes calls to contracts that execute arbitrary logic - // First, it gives the logic contract some tokens - // Then, it gives msg.senders tokens for fees - // Then, it calls an arbitrary function on the logic contract - // invalidationId and invalidationNonce are used for replay prevention. - // They can be used to implement a per-token nonce by setting the token - // address as the invalidationId and incrementing the nonce each call. - // They can be used for nonce-free replay prevention by using a different invalidationId - // for each call. - function submitLogicCall( - // The validators that approve the call - ValsetArgs calldata _currentValset, - // These are arrays of the parts of the validators signatures - Signature[] calldata _sigs, - LogicCallArgs memory _args - ) external nonReentrant { - // CHECKS scoped to reduce stack depth - { - // Check that the call has not timed out - if (block.number >= _args.timeOut) { - revert LogicCallTimedOut(); - } - - // Check that the invalidation nonce is higher than the last nonce for this invalidation Id - if (state_invalidationMapping[_args.invalidationId] >= _args.invalidationNonce) { - revert InvalidLogicCallNonce({ - newNonce: _args.invalidationNonce, - currentNonce: state_invalidationMapping[_args.invalidationId] - }); - } - - // note the lack of nonce skipping check, it's not needed here since an attacker - // will never be able to fill the invalidationId space, therefore a nonce lockout - // is simply not possible - - // Check that current validators, powers, and signatures (v,r,s) set is well-formed - validateValset(_currentValset, _sigs); - - // Check that the supplied current validator set matches the saved checkpoint - if (makeCheckpoint(_currentValset, state_gravityId) != state_lastValsetCheckpoint) { - revert IncorrectCheckpoint(); - } - - if (_args.transferAmounts.length != _args.transferTokenContracts.length) { - revert InvalidLogicCallTransfers(); - } - - if (_args.feeAmounts.length != _args.feeTokenContracts.length) { - revert InvalidLogicCallFees(); - } - } - { - bytes32 argsHash = keccak256( - abi.encode( - state_gravityId, - // bytes32 encoding of "logicCall" - 0x6c6f67696343616c6c0000000000000000000000000000000000000000000000, - _args.transferAmounts, - _args.transferTokenContracts, - _args.feeAmounts, - _args.feeTokenContracts, - _args.logicContractAddress, - _args.payload, - _args.timeOut, - _args.invalidationId, - _args.invalidationNonce - ) - ); - - // Check that enough current validators have signed off on the transaction batch and valset - checkValidatorSignatures( - _currentValset, - _sigs, - // Get hash of the transaction batch and checkpoint - argsHash, - constant_powerThreshold - ); - } - - // ACTIONS - - // Update invaldiation nonce - state_invalidationMapping[_args.invalidationId] = _args.invalidationNonce; - - // Send tokens to the logic contract - for (uint256 i = 0; i < _args.transferAmounts.length; i++) { - IERC20(_args.transferTokenContracts[i]).safeTransfer( - _args.logicContractAddress, - _args.transferAmounts[i] - ); - } - - // Make call to logic contract - bytes memory returnData = Address.functionCall(_args.logicContractAddress, _args.payload); - - // Send fees to msg.sender - for (uint256 i = 0; i < _args.feeAmounts.length; i++) { - IERC20(_args.feeTokenContracts[i]).safeTransfer(msg.sender, _args.feeAmounts[i]); - } - - // LOGS scoped to reduce stack depth - { - state_lastEventNonce = state_lastEventNonce + 1; - emit LogicCallEvent( - _args.invalidationId, - _args.invalidationNonce, - returnData, - state_lastEventNonce - ); - } - } - - function sendToCosmos( - address _tokenContract, - string calldata _destination, - uint256 _amount - ) external nonReentrant { - // we snapshot our current balance of this token - uint256 ourStartingBalance = IERC20(_tokenContract).balanceOf(address(this)); - - // attempt to transfer the user specified amount - IERC20(_tokenContract).safeTransferFrom(msg.sender, address(this), _amount); - - // check what this particular ERC20 implementation actually gave us, since it doesn't - // have to be at all related to the _amount - uint256 ourEndingBalance = IERC20(_tokenContract).balanceOf(address(this)); - - // a very strange ERC20 may trigger this condition, if we didn't have this we would - // underflow, so it's mostly just an error message printer - if (ourEndingBalance <= ourStartingBalance) { - revert InvalidSendToCosmos(); - } - - state_lastEventNonce = state_lastEventNonce + 1; - - // emit to Cosmos the actual amount our balance has changed, rather than the user - // provided amount. This protects against a small set of wonky ERC20 behavior, like - // burning on send but not tokens that for example change every users balance every day. - emit SendToCosmosEvent( - _tokenContract, - msg.sender, - _destination, - ourEndingBalance - ourStartingBalance, - state_lastEventNonce - ); - } - - function deployERC20( - string calldata _cosmosDenom, - string calldata _name, - string calldata _symbol, - uint8 _decimals - ) external { - // NOTE this is an edit made for the nym codebase unit tests - in the canonical bridge it is the entire token supply - // Deploy an ERC20 with half of entire supply granted to Gravity.sol - TestCosmosERC20 erc20 = new TestCosmosERC20(address(this), _name, _symbol, _decimals); - - // Fire an event to let the Cosmos module know - state_lastEventNonce = state_lastEventNonce + 1; - emit ERC20DeployedEvent( - _cosmosDenom, - address(erc20), - _name, - _symbol, - _decimals, - state_lastEventNonce - ); - } - - constructor( - // A unique identifier for this gravity instance to use in signatures - bytes32 _gravityId, - // The validator set, not in valset args format since many of it's - // arguments would never be used in this case - address[] memory _validators, - uint256[] memory _powers - ) { - // CHECKS - - // Check that validators, powers, and signatures (v,r,s) set is well-formed - if (_validators.length != _powers.length || _validators.length == 0) { - revert MalformedCurrentValidatorSet(); - } - - // Check cumulative power to ensure the contract has sufficient power to actually - // pass a vote - uint256 cumulativePower = 0; - for (uint256 i = 0; i < _powers.length; i++) { - cumulativePower = cumulativePower + _powers[i]; - if (cumulativePower > constant_powerThreshold) { - break; - } - } - if (cumulativePower <= constant_powerThreshold) { - revert InsufficientPower({ - cumulativePower: cumulativePower, - powerThreshold: constant_powerThreshold - }); - } - - ValsetArgs memory _valset; - _valset = ValsetArgs(_validators, _powers, 0, 0, address(0)); - - bytes32 newCheckpoint = makeCheckpoint(_valset, _gravityId); - - // ACTIONS - - state_gravityId = _gravityId; - state_lastValsetCheckpoint = newCheckpoint; - - // LOGS - - emit ValsetUpdatedEvent( - state_lastValsetNonce, - state_lastEventNonce, - 0, - address(0), - _validators, - _powers - ); - } -} \ No newline at end of file diff --git a/contracts/basic-bandwidth-generation/hardhat.config.js b/contracts/basic-bandwidth-generation/hardhat.config.js deleted file mode 100644 index a11fc74668..0000000000 --- a/contracts/basic-bandwidth-generation/hardhat.config.js +++ /dev/null @@ -1,43 +0,0 @@ -require("@nomiclabs/hardhat-etherscan"); -require("@nomiclabs/hardhat-truffle5"); -require("@nomiclabs/hardhat-web3"); -require("@nomiclabs/hardhat-ethers"); -require('dotenv').config({ path: require('find-config')('.env') }); - -/** - * @type import('hardhat/config').HardhatUserConfig - */ -module.exports = { - solidity: { - version: "0.8.10", - settings: { - optimizer: { - enabled: true - } - } }, - // paths: { - // artifacts: "./artifacts/contracts" - // }, - networks: { - localhost: { - url: "http://127.0.0.1:8545" - }, - rinkeby: { - url: process.env.RINKEBY_URL, //Infura url with projectId - accounts: [process.env.PRIV_KEY], // private key of account used for contract interaction - gas: "auto", - gasPrice: "auto" - }, - mainnet: { - url: process.env.MAINNET_URL, //Infura url with projectId - accounts: [process.env.PRIV_KEY], // private key of account used for contract interaction - gas: "auto", - gasPrice: "auto" - } - }, - etherscan: { - // Your API key for Etherscan - // Obtain one at https://etherscan.io/ - apiKey: process.env.ETHERSCAN_API_KEY - } -}; diff --git a/contracts/basic-bandwidth-generation/package-lock.json b/contracts/basic-bandwidth-generation/package-lock.json index 934096cf6e..c954a7d488 100644 --- a/contracts/basic-bandwidth-generation/package-lock.json +++ b/contracts/basic-bandwidth-generation/package-lock.json @@ -7,7 +7,7 @@ "dependencies": { "@nomiclabs/hardhat-truffle5": "^2.0.5", "@nomiclabs/hardhat-web3": "^2.0.0", - "@openzeppelin/contracts": "4.5.0", + "@openzeppelin/contracts": "4.7.3", "@openzeppelin/test-helpers": "^0.5.15", "dotenv": "^16.0.0", "file-system": "^2.2.2", @@ -1920,9 +1920,9 @@ } }, "node_modules/@openzeppelin/contracts": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.5.0.tgz", - "integrity": "sha512-fdkzKPYMjrRiPK6K4y64e6GzULR7R7RwxSigHS8DDp7aWDeoReqsQI+cxHV1UuhAqX69L1lAaWDxenfP+xiqzA==" + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.7.3.tgz", + "integrity": "sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw==" }, "node_modules/@openzeppelin/test-helpers": { "version": "0.5.15", @@ -21190,9 +21190,9 @@ } }, "@openzeppelin/contracts": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.5.0.tgz", - "integrity": "sha512-fdkzKPYMjrRiPK6K4y64e6GzULR7R7RwxSigHS8DDp7aWDeoReqsQI+cxHV1UuhAqX69L1lAaWDxenfP+xiqzA==" + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-4.7.3.tgz", + "integrity": "sha512-dGRS0agJzu8ybo44pCIf3xBaPQN/65AIXNgK8+4gzKd5kbvlqyxryUYVLJv7fK98Seyd2hDZzVEHSWAh0Bt1Yw==" }, "@openzeppelin/test-helpers": { "version": "0.5.15", diff --git a/contracts/basic-bandwidth-generation/package.json b/contracts/basic-bandwidth-generation/package.json index 020625ad2d..0fee523d6c 100644 --- a/contracts/basic-bandwidth-generation/package.json +++ b/contracts/basic-bandwidth-generation/package.json @@ -12,7 +12,7 @@ "dependencies": { "@nomiclabs/hardhat-truffle5": "^2.0.5", "@nomiclabs/hardhat-web3": "^2.0.0", - "@openzeppelin/contracts": "4.5.0", + "@openzeppelin/contracts": "4.7.3", "@openzeppelin/test-helpers": "^0.5.15", "dotenv": "^16.0.0", "file-system": "^2.2.2", diff --git a/contracts/basic-bandwidth-generation/scripts/mainnet/deploy-bandwidth-generator.js b/contracts/basic-bandwidth-generation/scripts/mainnet/deploy-bandwidth-generator.js deleted file mode 100644 index 5346c8cfee..0000000000 --- a/contracts/basic-bandwidth-generation/scripts/mainnet/deploy-bandwidth-generator.js +++ /dev/null @@ -1,34 +0,0 @@ -const { ethers } = require('hardhat'); -const { constants } = require('@openzeppelin/test-helpers'); -const contracts = require("../../contractAddresses.json"); -const fs = require('file-system'); - -async function main() { - - const BandwidthGenerator = await ethers.getContractFactory("BandwidthGenerator"); - - console.log('preparing to deploy contract...') - - // if this is failing, check whether the ERC20 address has been manually added to the contract addresses json file - const bandwidthGenerator = await BandwidthGenerator.deploy( - contracts.mainnet.NYM_ERC20, - contracts.mainnet.GRAVITY - ); - - console.log('...contract successfully deployed...'); - - contracts.mainnet.BANDWIDTH_GENERATOR = bandwidthGenerator.address; - // the location of the json file is relative to where you are running the script from - run from root of directory - fs.writeFileSync('./contractAddresses.json', JSON.stringify(contracts), (err) => { - if (err) throw err; - }); - - console.log(`...bandwidthGenerator.sol deployed at ${bandwidthGenerator.address}`); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); diff --git a/contracts/basic-bandwidth-generation/scripts/mainnet/mainnet-nymt-bandwidth-args.js b/contracts/basic-bandwidth-generation/scripts/mainnet/mainnet-nymt-bandwidth-args.js deleted file mode 100644 index 5ba86557f7..0000000000 --- a/contracts/basic-bandwidth-generation/scripts/mainnet/mainnet-nymt-bandwidth-args.js +++ /dev/null @@ -1,6 +0,0 @@ -// arguments for verification of bandwidth generator constructed with NYMT via hardhat-etherscan plugin -// npx hardhat verify --constructor-args ./scripts/mainnet/mainnet-nymt-bandwidth-args.js --network mainnet 0xB3BF30DD53044c9050B7309031Bbf26b2cecF3be -module.exports = [ - "0xE8883BAeF3869e14E4823F46662e81D4F7d2A81F", - "0xa4108aA1Ec4967F8b52220a4f7e94A8201F2D906" -]; \ No newline at end of file diff --git a/contracts/basic-bandwidth-generation/scripts/mainnet/mainnet-token-args.js b/contracts/basic-bandwidth-generation/scripts/mainnet/mainnet-token-args.js deleted file mode 100644 index 25b602ce77..0000000000 --- a/contracts/basic-bandwidth-generation/scripts/mainnet/mainnet-token-args.js +++ /dev/null @@ -1,8 +0,0 @@ -// arguments for verification of gravity contract via hardhat-etherscan plugin -// npx hardhat verify --constructor-args ./scripts/mainnet/mainnet-token-args.js --network mainnet 0xCf6DeE9947fdDc958985E5283e63d41CBC83Ff61 -module.exports = [ - "0xa4108aA1Ec4967F8b52220a4f7e94A8201F2D906", - "nym", - "nym", - 6 -]; \ No newline at end of file diff --git a/contracts/basic-bandwidth-generation/scripts/rinkeby/deploy-bandwidth-generator.js b/contracts/basic-bandwidth-generation/scripts/rinkeby/deploy-bandwidth-generator.js deleted file mode 100644 index c9afa21a5d..0000000000 --- a/contracts/basic-bandwidth-generation/scripts/rinkeby/deploy-bandwidth-generator.js +++ /dev/null @@ -1,30 +0,0 @@ -const { ethers } = require('hardhat'); -const { constants } = require('@openzeppelin/test-helpers'); -const contracts = require("../../contractAddresses.json"); -const fs = require('file-system'); - -async function main() { - const BandwidthGenerator = await ethers.getContractFactory("BandwidthGenerator"); - // if this is failing, check whether the ERC20 address has been manually added to the contract addresses json file - const bandwidthGenerator = await BandwidthGenerator.deploy( - contracts.rinkeby.NYM_ERC20, - contracts.rinkeby.GRAVITY - ); - - console.log("deploying..."); - - contracts.rinkeby.BANDWIDTH_GENERATOR = bandwidthGenerator.address; - // the location of the json file is relative to where you are running the script from - run from root of directory - fs.writeFileSync('./contractAddresses.json', JSON.stringify(contracts), (err) => { - if (err) throw err; - }); - - console.log(`...bandwidthGenerator.sol deployed at ${bandwidthGenerator.address}`); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); diff --git a/contracts/basic-bandwidth-generation/scripts/rinkeby/deploy-gravity.js b/contracts/basic-bandwidth-generation/scripts/rinkeby/deploy-gravity.js deleted file mode 100644 index 44b269bbeb..0000000000 --- a/contracts/basic-bandwidth-generation/scripts/rinkeby/deploy-gravity.js +++ /dev/null @@ -1,32 +0,0 @@ -const { ethers } = require('hardhat'); -const { constants } = require('@openzeppelin/test-helpers'); -const contracts = require("../../contractAddresses.json"); -const fs = require('file-system'); - -async function main() { - const [deployer] = await ethers.getSigners(); - console.log(deployer.address); - console.log(constants.ZERO_BYTES32); - const Gravity = await ethers.getContractFactory("Gravity"); - // deploy with args from unit tests - const gravity = await Gravity.deploy( - constants.ZERO_BYTES32, - [deployer.address], - [2863311531] - ); - - contracts.rinkeby.GRAVITY = gravity.address; - // the location of the json file is relative to where you are running the script from - run from root of directory - fs.writeFileSync('./contractAddresses.json', JSON.stringify(contracts), (err) => { - if (err) throw err; - }); - - console.log(`gravity.sol deployed at ${gravity.address}`); -} - -main() - .then(() => process.exit(0)) - .catch((error) => { - console.error(error); - process.exit(1); - }); diff --git a/contracts/basic-bandwidth-generation/scripts/rinkeby/gravity-args.js b/contracts/basic-bandwidth-generation/scripts/rinkeby/gravity-args.js deleted file mode 100644 index 4fcb240804..0000000000 --- a/contracts/basic-bandwidth-generation/scripts/rinkeby/gravity-args.js +++ /dev/null @@ -1,6 +0,0 @@ -// arguments for verification of gravity contract via hardhat-etherscan plugin -module.exports = [ - "0x0000000000000000000000000000000000000000000000000000000000000000", - ["0xf5b1B5C9b89906219Ba29a6cb12F0528c4C25D18"], - [2863311531] -]; \ No newline at end of file diff --git a/contracts/basic-bandwidth-generation/scripts/rinkeby/test-token-args.js b/contracts/basic-bandwidth-generation/scripts/rinkeby/test-token-args.js deleted file mode 100644 index 31b58bd7e3..0000000000 --- a/contracts/basic-bandwidth-generation/scripts/rinkeby/test-token-args.js +++ /dev/null @@ -1,7 +0,0 @@ -// arguments for verification of gravity contract via hardhat-etherscan plugin -module.exports = [ - "0xe58c12f8bA4a28e87841b10BdEdf80A47F86BA9B", - "test", - "tst", - 6 -]; \ No newline at end of file diff --git a/contracts/basic-bandwidth-generation/test/BandwidthGenerator.js b/contracts/basic-bandwidth-generation/test/BandwidthGenerator.js deleted file mode 100644 index 86863b3345..0000000000 --- a/contracts/basic-bandwidth-generation/test/BandwidthGenerator.js +++ /dev/null @@ -1,266 +0,0 @@ -const { expect } = require("chai"); -const { constants, expectRevert, expectEvent } = require('@openzeppelin/test-helpers'); -const { artifacts, web3 } = require("hardhat"); -const BN = require('bn.js'); -const BandwidthGenerator = artifacts.require('BandwidthGenerator'); -const Gravity = artifacts.require('test-contracts/TestGravity'); -const CosmosToken = artifacts.require('TestCosmosERC20'); - - -contract('BandwidthGenerator', (accounts) => { - let bandwidthGenerator; - let gravity; - let erc20token; - let owner = accounts[0]; - let user = accounts[1]; - let cosmosRecipient = "nymt1f06hzmwf9chqewkpv93ajk6tayzp4784m2da9x"; // random sandbox testnet address - let initialRatio = 1073741824; // 1073741824 bytes = 1GB - let newRatio; - let tokenAmount = web3.utils.toWei('100'); // this is converting 100 tokens to their representation in wei: 100000000000000000000 - let halfTokenAmount = web3.utils.toWei('50'); - let unevenTokenAmount = web3.utils.toWei('11.5'); // 11500000000000000000 - let oneToken = web3.utils.toWei('1'); - - before('deploy contracts', async () => { - - // deploy gravity bridge with test data - gravity = await Gravity.new( - constants.ZERO_BYTES32, - [owner], - [2863311531] - ); - - // deploy erc20 NYM from bridge - await gravity.deployERC20( - 'cosmosNYMDenomination', - 'NYMERC20', - 'NYM', - 6 - ); - - // grab event args for getting token address - const logs = await gravity.getPastEvents({ - fromBlock: 0, - toBlock: "latest", - }); - - // create contract abstraction of deployed erc20NYM with address from event args - erc20token = await CosmosToken.at(logs[0].args._tokenContract); - - // deploy bandwidthGenerator contract with contract address of erc20NYM & address of gravity bridge - bandwidthGenerator = await BandwidthGenerator.new(erc20token.address, gravity.address); - - }); - - context(">> deployment parameters are valid", () => { - it("returns the correct erc20 address", async () => { - expect((await bandwidthGenerator.erc20()).toString()).to.equal((erc20token.address).toString()); - }); - it("returns the correct gravity address", async () => { - expect((await bandwidthGenerator.gravityBridge()).toString()).to.equal((gravity.address).toString()); - }); - it("returns the correct initial BytesPerToken ratio", async () => { - expect((await bandwidthGenerator.BytesPerToken()).toString()).to.equal((initialRatio).toString()); - }); - it("returns the correct contract admin", async () => { - expect((await bandwidthGenerator.owner()).toString()).to.equal((owner).toString()); - }); - it("returns the correct default generation state: true", async () => { - expect((await bandwidthGenerator.credentialGenerationEnabled())).to.equal(true); - }); - }); - - context(">> deployment parameters are invalid", () => { - it("cannot be deployed with invalid erc20 address (zero address)", async () => { - expectRevert( - BandwidthGenerator.new(constants.ZERO_ADDRESS, gravity.address), - "BandwidthGenerator: erc20 address cannot be null" - ) - }); - it("cannot be deployed with invalid gravity bridge address (zero address)", async () => { - expectRevert( - BandwidthGenerator.new(erc20token.address, constants.ZERO_ADDRESS), - "BandwidthGenerator: gravity bridge address cannot be null" - ) - }); - }); - - context(">> generateBasicBandwidthCredential()", () => { - before("mint tokens & approve", async () => { - // transfer tokens to account which will create a BBCredential - await erc20token.mintForUnitTesting(user, tokenAmount); - // approve transfer to contract - await erc20token.approve(bandwidthGenerator.address,(tokenAmount),{ from: user }); - }); - - it("transfers tokens to bridge and emits an event with the correct values: 50 erc20NYM = 50GB of bandwidth", async () => { - let tx = await bandwidthGenerator.generateBasicBandwidthCredential( - halfTokenAmount, - 15, - [0x39, 0x53, 0x0a, 0x00, 0xea, 0xe2, 0xa5, 0xaa, 0xc8, 0x14, 0x42, 0x09, 0xcc, 0xac, 0x91, 0x7a, 0xe5, 0x6b, 0xf4, 0xa9, 0x58, 0x95, 0x44, 0xcb, 0x00, 0x20, 0xf9, 0x2f, 0xee, 0x35, 0xa3, 0xba, - 0x39, 0x53, 0x0a, 0x00, 0xea, 0xe2, 0xa5, 0xaa, 0xc8, 0x14, 0x42, 0x09, 0xcc, 0xac, 0x91, 0x7a, 0xe5, 0x6b, 0xf4, 0xa9, 0x58, 0x95, 0x44, 0xcb, 0x00, 0x20, 0xf9, 0x2f, 0xee, 0x35, 0xa3, 0xba], - cosmosRecipient, - { from: user } - ); - - let expectedBandwidthInMB = ((halfTokenAmount/10**18)*initialRatio); // 50 * (1024*1024*1024) bytes = 51200MB = 50GB of bandwidth - - await expectEvent.inTransaction(tx.tx, bandwidthGenerator, 'BBCredentialPurchased', { - Bandwidth: expectedBandwidthInMB.toString(), - VerificationKey: '15', - SignedVerificationKey: '0x39530a00eae2a5aac8144209ccac917ae56bf4a9589544cb0020f92fee35a3ba39530a00eae2a5aac8144209ccac917ae56bf4a9589544cb0020f92fee35a3ba', - CosmosRecipient: cosmosRecipient - }); - - await expectEvent.inTransaction(tx.tx, erc20token, 'Transfer', { - from: user, - to: bandwidthGenerator.address, - }); - - await expectEvent.inTransaction(tx.tx, gravity, 'SendToCosmosEvent', { - _tokenContract: erc20token.address, - _sender: bandwidthGenerator.address, - _destination: cosmosRecipient, - _amount: halfTokenAmount - }); - - expect((await erc20token.balanceOf(bandwidthGenerator.address)).toString()).to.equal('0'); - expect((await erc20token.balanceOf(user)).toString()).to.equal(halfTokenAmount.toString()); - }); - - /** - * This can be out by a float still with amounts such as '.1' - */ - it("it transfers for uneven token amounts", async () => { - let tx = await bandwidthGenerator.generateBasicBandwidthCredential( - unevenTokenAmount, - 15, - [0x39, 0x53, 0x0a, 0x00, 0xea, 0xe2, 0xa5, 0xaa, 0xc8, 0x14, 0x42, 0x09, 0xcc, 0xac, 0x91, 0x7a, 0xe5, 0x6b, 0xf4, 0xa9, 0x58, 0x95, 0x44, 0xcb, 0x00, 0x20, 0xf9, 0x2f, 0xee, 0x35, 0xa3, 0xba, - 0x39, 0x53, 0x0a, 0x00, 0xea, 0xe2, 0xa5, 0xaa, 0xc8, 0x14, 0x42, 0x09, 0xcc, 0xac, 0x91, 0x7a, 0xe5, 0x6b, 0xf4, 0xa9, 0x58, 0x95, 0x44, 0xcb, 0x00, 0x20, 0xf9, 0x2f, 0xee, 0x35, 0xa3, 0xba], - cosmosRecipient, - { from: user } - ); - - let newexpectedBandwidthInMB = ((11500000000000000000*initialRatio)/10**18); - - await expectEvent.inTransaction(tx.tx, bandwidthGenerator, 'BBCredentialPurchased', { - Bandwidth: newexpectedBandwidthInMB.toString(), - VerificationKey: '15', - SignedVerificationKey: '0x39530a00eae2a5aac8144209ccac917ae56bf4a9589544cb0020f92fee35a3ba39530a00eae2a5aac8144209ccac917ae56bf4a9589544cb0020f92fee35a3ba', - CosmosRecipient: cosmosRecipient - }); - - await expectEvent.inTransaction(tx.tx, erc20token, 'Transfer', { - from: user, - to: bandwidthGenerator.address, - }); - - await expectEvent.inTransaction(tx.tx, gravity, 'SendToCosmosEvent', { - _tokenContract: erc20token.address, - _sender: bandwidthGenerator.address, - _destination: cosmosRecipient, - _amount: unevenTokenAmount - }); - }); - - it("reverts when signed verification key !=64 bytes", async () => { - await erc20token.approve(bandwidthGenerator.address,(halfTokenAmount),{ from: user }); - - await expectRevert( - bandwidthGenerator.generateBasicBandwidthCredential( - 1, - 16, - [0x0a, 0x00, 0xea, 0xe2, 0xa5, 0xaa, 0xc8, 0x14, 0x42, 0x09, 0xcc, 0xac, 0x91, 0x7a, 0xe5, 0x6b, 0xf4, 0xa9, 0x58, 0x95, 0x44, 0xcb, 0x00, 0x20, 0xf9, 0x2f, 0xee, 0x35, 0xa3, 0xba, - 0x39, 0x53, 0x0a, 0x00, 0xea, 0xe2, 0xa5, 0xaa, 0xc8, 0x14, 0x42, 0x09, 0xcc, 0xac, 0x91, 0x7a, 0xe5, 0x6b, 0xf4, 0xa9, 0x58, 0x95, 0x44, 0xcb, 0x00, 0x20, 0xf9, 0x2f, 0xee, 0x35, 0xa3, 0xba], - cosmosRecipient, - { from: user } - ), "BandwidthGenerator: Signature doesn't have 64 bytes" - ); - }); - }); - - context(">> changeRatio()", () => { - it("only admin can change token to MB ratio", async () => { - newRatio = 10 * initialRatio; // 10GB of bandwidth per 1 erc20NYM - await expectRevert( - bandwidthGenerator.changeRatio(newRatio, {from: user}), - "Ownable: caller is not the owner" - ); - }); - it("admin can change ratio, emits 'RatioChanged' event", async () => { - let tx = await bandwidthGenerator.changeRatio(newRatio, {from: owner}); - await expectEvent.inTransaction(tx.tx, bandwidthGenerator, 'RatioChanged', { - NewBytesPerToken: newRatio.toString() - }); - expect((await bandwidthGenerator.BytesPerToken()).toString()).to.equal((newRatio).toString()); - }); - it("BBCredential represents new ratio after change: 1 erc20NYM = 10GB of bandwidth", async () => { - let tx = await bandwidthGenerator.generateBasicBandwidthCredential( - oneToken, - 15, - [0x39, 0x53, 0x0a, 0x00, 0xea, 0xe2, 0xa5, 0xaa, 0xc8, 0x14, 0x42, 0x09, 0xcc, 0xac, 0x91, 0x7a, 0xe5, 0x6b, 0xf4, 0xa9, 0x58, 0x95, 0x44, 0xcb, 0x00, 0x20, 0xf9, 0x2f, 0xee, 0x35, 0xa3, 0xba, - 0x39, 0x53, 0x0a, 0x00, 0xea, 0xe2, 0xa5, 0xaa, 0xc8, 0x14, 0x42, 0x09, 0xcc, 0xac, 0x91, 0x7a, 0xe5, 0x6b, 0xf4, 0xa9, 0x58, 0x95, 0x44, 0xcb, 0x00, 0x20, 0xf9, 0x2f, 0xee, 0x35, 0xa3, 0xba], - cosmosRecipient, - { from: user } - ); - - let expectedBandwidthInMB = ((oneToken/10**18)*newRatio); - - await expectEvent.inTransaction(tx.tx, bandwidthGenerator, 'BBCredentialPurchased', { - Bandwidth: expectedBandwidthInMB.toString(), - VerificationKey: '15', - SignedVerificationKey: '0x39530a00eae2a5aac8144209ccac917ae56bf4a9589544cb0020f92fee35a3ba39530a00eae2a5aac8144209ccac917ae56bf4a9589544cb0020f92fee35a3ba', - CosmosRecipient: cosmosRecipient - }); - - await expectEvent.inTransaction(tx.tx, erc20token, 'Transfer', { - from: user, - to: bandwidthGenerator.address, - }); - - await expectEvent.inTransaction(tx.tx, gravity, 'SendToCosmosEvent', { - _tokenContract: erc20token.address, - _sender: bandwidthGenerator.address, - _destination: cosmosRecipient, - _amount: oneToken - }); - }); - }); - context(">>credential generation admin switch", () => { - it("only admin can switch credential generation off", async () => { - await expectRevert( - bandwidthGenerator.credentialGenerationSwitch(false, { from: user }), - "Ownable: caller is not the owner" - ) - }); - it("admin can switch credential generation on/off & switch generates an event", async () => { - let tx = await bandwidthGenerator.credentialGenerationSwitch(false); - expect((await bandwidthGenerator.credentialGenerationEnabled())).to.equal(false); - - await expectEvent.inTransaction(tx.tx, bandwidthGenerator, 'CredentialGenerationSwitch', { - Enabled: false, - }); - - tx = await bandwidthGenerator.credentialGenerationSwitch(true); - expect((await bandwidthGenerator.credentialGenerationEnabled())).to.equal(true); - - await expectEvent.inTransaction(tx.tx, bandwidthGenerator, 'CredentialGenerationSwitch', { - Enabled: true, - }); - }); - it("cannot generate credentials if switch = false", async () => { - await bandwidthGenerator.credentialGenerationSwitch(false); - await expectRevert( - bandwidthGenerator.generateBasicBandwidthCredential( - oneToken, - 15, - [0x39, 0x53, 0x0a, 0x00, 0xea, 0xe2, 0xa5, 0xaa, 0xc8, 0x14, 0x42, 0x09, 0xcc, 0xac, 0x91, 0x7a, 0xe5, 0x6b, 0xf4, 0xa9, 0x58, 0x95, 0x44, 0xcb, 0x00, 0x20, 0xf9, 0x2f, 0xee, 0x35, 0xa3, 0xba, - 0x39, 0x53, 0x0a, 0x00, 0xea, 0xe2, 0xa5, 0xaa, 0xc8, 0x14, 0x42, 0x09, 0xcc, 0xac, 0x91, 0x7a, 0xe5, 0x6b, 0xf4, 0xa9, 0x58, 0x95, 0x44, 0xcb, 0x00, 0x20, 0xf9, 0x2f, 0xee, 0x35, 0xa3, 0xba], - cosmosRecipient, - { from: user } - ), "BandwidthGenerator: credential generation isn't currently enabled" - ); - }); - }); -}); - diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 24df80eb65..f8e0780d87 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mixnet-contract" -version = "1.0.1" +version = "1.0.2" authors = ["Dave Hrycyszyn "] edition = "2021" @@ -18,26 +18,27 @@ crate-type = ["cdylib", "rlib"] [dependencies] mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" } -config = { path = "../../common/config"} +#config = { path = "../../common/config"} cosmwasm-std = "1.0.0" cosmwasm-storage = "1.0.0" cw-storage-plus = "0.13.4" -az = "1.2.0" bs58 = "0.4.0" schemars = "0.8" serde = { version = "1.0.103", default-features = false, features = ["derive"] } thiserror = { version = "1.0.23" } time = { version = "0.3", features = ["macros"] } -hex = "0.4.3" [dev-dependencies] cosmwasm-schema = "1.0.0" -fixed = "1.1" rand_chacha = "0.2" -rand = "0.7" +#rand = "0.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/constants.rs b/contracts/mixnet/src/constants.rs index a19aa4fef3..3d3ed82caf 100644 --- a/contracts/mixnet/src/constants.rs +++ b/contracts/mixnet/src/constants.rs @@ -1,17 +1,71 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -// approximately 1 epoch (assuming 5s per block) -pub const MINIMUM_BLOCK_AGE_FOR_REWARDING: u64 = 720; +use cosmwasm_std::Uint128; -pub const INTERVAL_REWARD_PERCENT: u8 = 2; // Used to calculate interval reward pool -pub const SYBIL_RESISTANCE_PERCENT: u8 = 30; -pub const ACTIVE_SET_WORK_FACTOR: u8 = 10; +/// Constant specifying minimum of coin amount required to bond a gateway +pub const INITIAL_GATEWAY_PLEDGE_AMOUNT: Uint128 = Uint128::new(100_000_000); -// TODO: this, in theory, represents "epoch" length. -// However, since the blocktime is not EXACTLY 5s, we can't really guarantee 720 epochs in interval -// and we can't change this easily to `Duration`, because then the entire rewarded set storage -// would be messed up... (as we look up stuff "by blocks") -pub const REWARDED_SET_REFRESH_BLOCKS: u64 = 720; // with blocktime being approximately 5s, it should be roughly 1h -pub const INTERVAL_SECONDS: u64 = 86400 * 30; // 30 days -pub const DEFAULT_OPERATOR_INTERVAL_COST: u64 = 40_000_000; +/// Constant specifying minimum of coin amount required to bond a mixnode +pub const INITIAL_MIXNODE_PLEDGE_AMOUNT: Uint128 = Uint128::new(100_000_000); + +// retrieval limits +// TODO: those would need to be empirically verified whether they're not way too small or way too high +pub const GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const GATEWAY_BOND_MAX_RETRIEVAL_LIMIT: u32 = 150; + +pub const MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const MIXNODE_BOND_MAX_RETRIEVAL_LIMIT: u32 = 150; + +pub const MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT: u32 = 75; +pub const MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT: u32 = 100; + +pub const UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT: u32 = 250; +pub const UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT: u32 = 300; + +pub const DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT: u32 = 250; +pub const DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT: u32 = 300; + +pub const EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 200; +pub const EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 250; + +pub const INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 200; +pub const INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 250; + +pub const REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT: u32 = 500; +pub const REWARDED_SET_MAX_RETRIEVAL_LIMIT: u32 = 1000; + +// storage keys +pub(crate) const DELEGATION_PK_NAMESPACE: &str = "dl"; +pub(crate) const DELEGATION_OWNER_IDX_NAMESPACE: &str = "dlo"; +pub(crate) const DELEGATION_MIXNODE_IDX_NAMESPACE: &str = "dlm"; + +pub(crate) const GATEWAYS_PK_NAMESPACE: &str = "gt"; +pub(crate) const GATEWAYS_OWNER_IDX_NAMESPACE: &str = "gto"; + +pub(crate) const REWARDED_SET_KEY: &str = "rs"; +pub(crate) const CURRENT_INTERVAL_KEY: &str = "ci"; +pub(crate) const EPOCH_EVENT_ID_COUNTER_KEY: &str = "eic"; +pub(crate) const INTERVAL_EVENT_ID_COUNTER_KEY: &str = "iic"; +pub(crate) const PENDING_EPOCH_EVENTS_NAMESPACE: &str = "pee"; +pub(crate) const PENDING_INTERVAL_EVENTS_NAMESPACE: &str = "pie"; + +pub(crate) const LAST_EPOCH_EVENT_ID_KEY: &str = "lee"; +pub(crate) const LAST_INTERVAL_EVENT_ID_KEY: &str = "lie"; + +pub(crate) const CONTRACT_STATE_KEY: &str = "state"; + +pub(crate) const LAYER_DISTRIBUTION_KEY: &str = "layers"; +pub(crate) const NODE_ID_COUNTER_KEY: &str = "nic"; +pub(crate) const MIXNODES_PK_NAMESPACE: &str = "mnn"; +pub(crate) const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno"; +pub(crate) const MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "mni"; +pub(crate) const MIXNODES_SPHINX_IDX_NAMESPACE: &str = "mns"; + +pub(crate) const UNBONDED_MIXNODES_PK_NAMESPACE: &str = "ubm"; +pub(crate) const UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE: &str = "umo"; +pub(crate) const UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "umi"; + +pub(crate) const REWARDING_PARAMS_KEY: &str = "rparams"; +pub(crate) const PENDING_REWARD_POOL_KEY: &str = "prp"; +pub(crate) const MIXNODES_REWARDING_PK_NAMESPACE: &str = "mnr"; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 3eaa1f6035..af41f50b83 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -1,83 +1,40 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::{ACTIVE_SET_WORK_FACTOR, INTERVAL_REWARD_PERCENT, SYBIL_RESISTANCE_PERCENT}; -use crate::delegations::queries::query_delegator_delegations_paged; -use crate::delegations::queries::query_mixnode_delegation; -use crate::delegations::queries::{ - query_mixnode_delegations_paged, query_pending_delegation_events, -}; -use crate::error::ContractError; -use crate::gateways::queries::query_owns_gateway; -use crate::gateways::queries::{query_gateway_bond, query_gateways_paged}; -use crate::interval::queries::query_current_epoch; -use crate::interval::queries::{ - query_current_rewarded_set_height, query_rewarded_set, - query_rewarded_set_refresh_minimum_blocks, query_rewarded_set_update_details, -}; -use crate::interval::transactions::{init_epoch, try_init_epoch}; -use crate::mixnet_contract_settings::models::ContractState; -use crate::mixnet_contract_settings::queries::{ - query_contract_settings_params, query_contract_version, query_rewarding_validator_address, -}; +use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; +use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnet_contract_settings::transactions::try_update_rewarding_validator_address; -use crate::mixnodes::bonding_queries as mixnode_queries; -use crate::mixnodes::bonding_queries::{ - query_checkpoints_for_mixnode, query_mixnode_at_height, query_mixnodes_paged, -}; -use crate::mixnodes::layer_queries::query_layer_distribution; -use crate::mixnodes::transactions::_try_remove_mixnode; -use crate::queued_migrations::migrate_config_from_env; -use crate::rewards::queries::{ - query_circulating_supply, query_reward_pool, query_rewarding_status, query_staking_supply, -}; +use crate::mixnodes::storage as mixnode_storage; use crate::rewards::storage as rewards_storage; use cosmwasm_std::{ - entry_point, to_binary, Addr, Api, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, - Storage, Uint128, + entry_point, to_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, }; +use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::{ - ContractStateParams, ExecuteMsg, InstantiateMsg, MigrateMsg, NodeToRemove, QueryMsg, + ContractState, ContractStateParams, ExecuteMsg, InstantiateMsg, Interval, MigrateMsg, QueryMsg, }; -use time::OffsetDateTime; - -/// Constant specifying minimum of coin required to bond a gateway -pub const INITIAL_GATEWAY_PLEDGE: Uint128 = Uint128::new(100_000_000); - -/// Constant specifying minimum of coin required to bond a mixnode -pub const INITIAL_MIXNODE_PLEDGE: Uint128 = Uint128::new(100_000_000); - -pub const INITIAL_MIXNODE_REWARDED_SET_SIZE: u32 = 200; -pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100; - -pub const INITIAL_REWARD_POOL: u128 = 250_000_000_000_000; -pub const INITIAL_ACTIVE_SET_WORK_FACTOR: u8 = 10; - -pub const DEFAULT_FIRST_INTERVAL_START: OffsetDateTime = - time::macros::datetime!(2022-01-01 12:00 UTC); - -pub const INITIAL_STAKING_SUPPLY: Uint128 = Uint128::new(100_000_000_000_000); - -pub fn debug_with_visibility>(api: &dyn Api, msg: S) { - api.debug(&*format!("\n\n\n=========================================\n{}\n=========================================\n\n\n", msg.into())); -} fn default_initial_state( owner: Addr, - mix_denom: String, rewarding_validator_address: Addr, + rewarding_denom: String, + vesting_contract_address: Addr, ) -> ContractState { ContractState { owner, - mix_denom, rewarding_validator_address, + vesting_contract_address, + rewarding_denom: rewarding_denom.clone(), params: ContractStateParams { - minimum_mixnode_pledge: INITIAL_MIXNODE_PLEDGE, - minimum_gateway_pledge: INITIAL_GATEWAY_PLEDGE, - mixnode_rewarded_set_size: INITIAL_MIXNODE_REWARDED_SET_SIZE, - mixnode_active_set_size: INITIAL_MIXNODE_ACTIVE_SET_SIZE, - staking_supply: INITIAL_STAKING_SUPPLY, + minimum_mixnode_delegation: None, + minimum_mixnode_pledge: Coin { + denom: rewarding_denom.clone(), + amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, + }, + minimum_gateway_pledge: Coin { + denom: rewarding_denom, + amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, + }, }, } } @@ -93,14 +50,25 @@ pub fn instantiate( env: Env, info: MessageInfo, msg: InstantiateMsg, -) -> Result { +) -> Result { let rewarding_validator_address = deps.api.addr_validate(&msg.rewarding_validator_address)?; - let state = default_initial_state(info.sender, msg.mixnet_denom, rewarding_validator_address); - init_epoch(deps.storage, env)?; + let vesting_contract_address = deps.api.addr_validate(&msg.vesting_contract_address)?; + let state = default_initial_state( + info.sender, + rewarding_validator_address, + msg.rewarding_denom, + vesting_contract_address, + ); + let starting_interval = + Interval::init_interval(msg.epochs_in_interval, msg.epoch_duration, &env); + let reward_params = msg + .initial_rewarding_params + .into_rewarding_params(msg.epochs_in_interval); - mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &state)?; - mixnet_params_storage::LAYERS.save(deps.storage, &Default::default())?; - rewards_storage::REWARD_POOL.save(deps.storage, &Uint128::new(INITIAL_REWARD_POOL))?; + interval_storage::initialise_storage(deps.storage, starting_interval)?; + mixnet_params_storage::initialise_storage(deps.storage, state)?; + mixnode_storage::initialise_storage(deps.storage)?; + rewards_storage::initialise_storage(deps.storage, reward_params)?; Ok(Response::default()) } @@ -112,56 +80,120 @@ pub fn execute( env: Env, info: MessageInfo, msg: ExecuteMsg, -) -> Result { +) -> Result { match msg { - ExecuteMsg::CompoundReward { - operator, - delegator, - mix_identity, - proxy, - } => crate::rewards::transactions::try_compound_reward( + // state/sys-params-related + ExecuteMsg::UpdateRewardingValidatorAddress { address } => { + crate::mixnet_contract_settings::transactions::try_update_rewarding_validator_address( + deps, info, address, + ) + } + ExecuteMsg::UpdateContractStateParams { updated_parameters } => { + crate::mixnet_contract_settings::transactions::try_update_contract_settings( + deps, + info, + updated_parameters, + ) + } + ExecuteMsg::UpdateActiveSetSize { + active_set_size, + force_immediately, + } => crate::rewards::transactions::try_update_active_set_size( deps, env, - operator, - delegator, - mix_identity, - proxy, + info, + active_set_size, + force_immediately, ), - ExecuteMsg::UpdateRewardingValidatorAddress { address } => { - try_update_rewarding_validator_address(deps, info, address) + ExecuteMsg::UpdateRewardingParams { + updated_params, + force_immediately, + } => crate::rewards::transactions::try_update_rewarding_params( + deps, + env, + info, + updated_params, + force_immediately, + ), + ExecuteMsg::UpdateIntervalConfig { + epochs_in_interval, + epoch_duration_secs, + force_immediately, + } => crate::interval::transactions::try_update_interval_config( + deps, + env, + info, + epochs_in_interval, + epoch_duration_secs, + force_immediately, + ), + ExecuteMsg::AdvanceCurrentEpoch { + new_rewarded_set, + expected_active_set_size, + } => crate::interval::transactions::try_advance_epoch( + deps, + env, + info, + new_rewarded_set, + expected_active_set_size, + ), + ExecuteMsg::ReconcileEpochEvents { limit } => { + crate::interval::transactions::try_reconcile_epoch_events(deps, env, info, limit) } - ExecuteMsg::InitEpoch {} => try_init_epoch(info, deps.storage, env), + + // mixnode-related: ExecuteMsg::BondMixnode { mix_node, + cost_params, owner_signature, } => crate::mixnodes::transactions::try_add_mixnode( deps, env, info, mix_node, + cost_params, + owner_signature, + ), + ExecuteMsg::BondMixnodeOnBehalf { + mix_node, + cost_params, + owner, + owner_signature, + } => crate::mixnodes::transactions::try_add_mixnode_on_behalf( + deps, + env, + info, + mix_node, + cost_params, + owner, owner_signature, ), ExecuteMsg::UnbondMixnode {} => { - crate::mixnodes::transactions::try_remove_mixnode(&env, deps.storage, deps.api, info) + crate::mixnodes::transactions::try_remove_mixnode(deps, env, info) } - ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - } => crate::mixnodes::transactions::try_update_mixnode_config( - deps, - env, - info, - profit_margin_percent, - ), - ExecuteMsg::UpdateMixnodeConfigOnBehalf { - profit_margin_percent, - owner, - } => crate::mixnodes::transactions::try_update_mixnode_config_on_behalf( - deps, - env, - info, - profit_margin_percent, - owner, - ), + ExecuteMsg::UnbondMixnodeOnBehalf { 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, env, info, new_costs, + ) + } + ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { new_costs, owner } => { + crate::mixnodes::transactions::try_update_mixnode_cost_params_on_behalf( + deps, env, info, new_costs, owner, + ) + } + ExecuteMsg::UpdateMixnodeConfig { new_config } => { + crate::mixnodes::transactions::try_update_mixnode_config(deps, info, new_config) + } + ExecuteMsg::UpdateMixnodeConfigOnBehalf { new_config, owner } => { + crate::mixnodes::transactions::try_update_mixnode_config_on_behalf( + deps, info, new_config, owner, + ) + } + + // gateway-related: ExecuteMsg::BondGateway { gateway, owner_signature, @@ -172,78 +204,6 @@ pub fn execute( gateway, owner_signature, ), - ExecuteMsg::UnbondGateway {} => { - crate::gateways::transactions::try_remove_gateway(deps, info) - } - ExecuteMsg::UpdateContractStateParams(params) => { - crate::mixnet_contract_settings::transactions::try_update_contract_settings( - deps, info, params, - ) - } - ExecuteMsg::RewardMixnode { identity, params } => { - crate::rewards::transactions::try_reward_mixnode(deps, env, info, identity, params) - } - ExecuteMsg::DelegateToMixnode { mix_identity } => { - crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_identity) - } - ExecuteMsg::UndelegateFromMixnode { mix_identity } => { - crate::delegations::transactions::try_remove_delegation_from_mixnode( - deps, - env, - info, - mix_identity, - ) - } - // ExecuteMsg::RewardNextMixDelegators { - // mix_identity, - // interval_id, - // } => crate::rewards::transactions::try_reward_next_mixnode_delegators( - // deps, - // info, - // mix_identity, - // interval_id, - // ), - ExecuteMsg::DelegateToMixnodeOnBehalf { - mix_identity, - delegate, - } => crate::delegations::transactions::try_delegate_to_mixnode_on_behalf( - deps, - env, - info, - mix_identity, - delegate, - ), - ExecuteMsg::UndelegateFromMixnodeOnBehalf { - mix_identity, - delegate, - } => crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf( - deps, - env, - info, - mix_identity, - delegate, - ), - ExecuteMsg::BondMixnodeOnBehalf { - mix_node, - owner, - owner_signature, - } => crate::mixnodes::transactions::try_add_mixnode_on_behalf( - deps, - env, - info, - mix_node, - owner, - owner_signature, - ), - ExecuteMsg::UnbondMixnodeOnBehalf { owner } => { - crate::mixnodes::transactions::try_remove_mixnode_on_behalf( - &env, - deps.storage, - deps.api, - info, - owner, - ) - } ExecuteMsg::BondGatewayOnBehalf { gateway, owner, @@ -256,311 +216,351 @@ pub fn execute( owner, owner_signature, ), + ExecuteMsg::UnbondGateway {} => { + crate::gateways::transactions::try_remove_gateway(deps, info) + } ExecuteMsg::UnbondGatewayOnBehalf { owner } => { crate::gateways::transactions::try_remove_gateway_on_behalf(deps, info, owner) } - ExecuteMsg::WriteRewardedSet { - rewarded_set, - expected_active_set_size, - } => crate::interval::transactions::try_write_rewarded_set( - deps, - env, - info, - rewarded_set, - expected_active_set_size, - ), - ExecuteMsg::AdvanceCurrentEpoch {} => crate::interval::transactions::try_advance_epoch( - env, - deps.storage, - info.sender.to_string(), - ), - ExecuteMsg::CompoundDelegatorReward { mix_identity } => { - crate::rewards::transactions::try_compound_delegator_reward( - deps, - env, - info, - mix_identity, + + // delegation-related: + ExecuteMsg::DelegateToMixnode { 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, env, info, mix_id, delegate, ) } - ExecuteMsg::CompoundOperatorReward {} => { - crate::rewards::transactions::try_compound_operator_reward(deps, env, info) - } - ExecuteMsg::CompoundDelegatorRewardOnBehalf { - owner, - mix_identity, - } => crate::rewards::transactions::try_compound_delegator_reward_on_behalf( - deps, - env, - info, - owner, - mix_identity, - ), - ExecuteMsg::CompoundOperatorRewardOnBehalf { owner } => { - crate::rewards::transactions::try_compound_operator_reward_on_behalf( - deps, env, info, owner, + ExecuteMsg::UndelegateFromMixnode { mix_id } => { + crate::delegations::transactions::try_remove_delegation_from_mixnode( + deps, env, info, mix_id, ) } - ExecuteMsg::ReconcileDelegations {} => { - crate::delegations::transactions::try_reconcile_all_delegation_events(deps) - } - ExecuteMsg::CheckpointMixnodes {} => { - crate::mixnodes::transactions::try_checkpoint_mixnodes( - deps.storage, - env.block.height, - info, + ExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, delegate } => { + crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf( + deps, env, info, mix_id, delegate, ) } - ExecuteMsg::ClaimOperatorReward {} => { - crate::rewards::transactions::try_claim_operator_reward(deps, &env, &info) + + // reward-related + ExecuteMsg::RewardMixnode { + mix_id, + performance, + } => crate::rewards::transactions::try_reward_mixnode(deps, env, info, mix_id, performance), + + ExecuteMsg::WithdrawOperatorReward {} => { + crate::rewards::transactions::try_withdraw_operator_reward(deps, info) } - ExecuteMsg::ClaimOperatorRewardOnBehalf { owner } => { - crate::rewards::transactions::try_claim_operator_reward_on_behalf( - deps, &env, &info, owner, + ExecuteMsg::WithdrawOperatorRewardOnBehalf { owner } => { + crate::rewards::transactions::try_withdraw_operator_reward_on_behalf(deps, info, owner) + } + ExecuteMsg::WithdrawDelegatorReward { mix_id } => { + crate::rewards::transactions::try_withdraw_delegator_reward(deps, info, mix_id) + } + ExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, owner } => { + crate::rewards::transactions::try_withdraw_delegator_reward_on_behalf( + deps, info, mix_id, owner, ) } - ExecuteMsg::ClaimDelegatorReward { mix_identity } => { - crate::rewards::transactions::try_claim_delegator_reward( - deps, - &env, - &info, - &mix_identity, - ) + + // testing-only + #[cfg(feature = "contract-testing")] + ExecuteMsg::TestingResolveAllPendingEvents { limit } => { + crate::testing::transactions::try_resolve_all_pending_events(deps, env, limit) } - ExecuteMsg::ClaimDelegatorRewardOnBehalf { - mix_identity, - owner, - } => crate::rewards::transactions::try_claim_delegator_reward_on_behalf( - deps, - &env, - &info, - owner, - &mix_identity, - ), } } #[entry_point] -pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { +pub fn query( + deps: Deps<'_>, + env: Env, + msg: QueryMsg, +) -> Result { let query_res = match msg { - QueryMsg::GetBlacklistedNodes {} => to_binary( - &crate::mixnodes::bonding_queries::get_blacklisted_nodes(deps), + QueryMsg::GetContractVersion {} => { + to_binary(&crate::mixnet_contract_settings::queries::query_contract_version()) + } + QueryMsg::GetStateParams {} => to_binary( + &crate::mixnet_contract_settings::queries::query_contract_settings_params(deps)?, ), - QueryMsg::GetRewardingValidatorAddress {} => { - to_binary(&query_rewarding_validator_address(deps)?) + QueryMsg::GetRewardingValidatorAddress {} => to_binary( + &crate::mixnet_contract_settings::queries::query_rewarding_validator_address(deps)?, + ), + QueryMsg::GetState {} => { + to_binary(&crate::mixnet_contract_settings::queries::query_contract_state(deps)?) } - QueryMsg::GetContractVersion {} => to_binary(&query_contract_version()), - QueryMsg::GetMixNodes { start_after, limit } => { - to_binary(&query_mixnodes_paged(deps, start_after, limit)?) + QueryMsg::GetRewardingParams {} => { + to_binary(&crate::rewards::queries::query_rewarding_params(deps)?) } - QueryMsg::GetGateways { limit, start_after } => { - to_binary(&query_gateways_paged(deps, start_after, limit)?) + QueryMsg::GetCurrentIntervalDetails {} => to_binary( + &crate::interval::queries::query_current_interval_details(deps, env)?, + ), + QueryMsg::GetRewardedSet { limit, start_after } => to_binary( + &crate::interval::queries::query_rewarded_set_paged(deps, start_after, limit)?, + ), + + // mixnode-related: + QueryMsg::GetMixNodeBonds { start_after, limit } => to_binary( + &crate::mixnodes::queries::query_mixnode_bonds_paged(deps, start_after, limit)?, + ), + QueryMsg::GetMixNodesDetailed { start_after, limit } => to_binary( + &crate::mixnodes::queries::query_mixnodes_details_paged(deps, start_after, limit)?, + ), + QueryMsg::GetUnbondedMixNodes { limit, start_after } => to_binary( + &crate::mixnodes::queries::query_unbonded_mixnodes_paged(deps, start_after, limit)?, + ), + QueryMsg::GetUnbondedMixNodesByOwner { + owner, + limit, + start_after, + } => to_binary( + &crate::mixnodes::queries::query_unbonded_mixnodes_by_owner_paged( + deps, + owner, + start_after, + limit, + )?, + ), + QueryMsg::GetUnbondedMixNodesByIdentityKey { + identity_key, + limit, + start_after, + } => to_binary( + &crate::mixnodes::queries::query_unbonded_mixnodes_by_identity_paged( + deps, + identity_key, + start_after, + limit, + )?, + ), + QueryMsg::GetOwnedMixnode { address } => to_binary( + &crate::mixnodes::queries::query_owned_mixnode(deps, address)?, + ), + QueryMsg::GetMixnodeDetails { mix_id } => to_binary( + &crate::mixnodes::queries::query_mixnode_details(deps, mix_id)?, + ), + QueryMsg::GetMixnodeRewardingDetails { mix_id } => to_binary( + &crate::mixnodes::queries::query_mixnode_rewarding_details(deps, mix_id)?, + ), + QueryMsg::GetStakeSaturation { mix_id } => to_binary( + &crate::mixnodes::queries::query_stake_saturation(deps, mix_id)?, + ), + QueryMsg::GetUnbondedMixNodeInformation { mix_id } => to_binary( + &crate::mixnodes::queries::query_unbonded_mixnode(deps, mix_id)?, + ), + QueryMsg::GetBondedMixnodeDetailsByIdentity { mix_identity } => to_binary( + &crate::mixnodes::queries::query_mixnode_details_by_identity(deps, mix_identity)?, + ), + QueryMsg::GetLayerDistribution {} => { + to_binary(&crate::mixnodes::queries::query_layer_distribution(deps)?) } - QueryMsg::OwnsMixnode { address } => { - to_binary(&mixnode_queries::query_owns_mixnode(deps, address)?) - } - QueryMsg::GetMixnodeBond { identity } => { - to_binary(&mixnode_queries::query_mixnode_bond(deps, identity)?) - } - QueryMsg::GetGatewayBond { identity } => to_binary(&query_gateway_bond(deps, identity)?), - QueryMsg::OwnsGateway { address } => to_binary(&query_owns_gateway(deps, address)?), - QueryMsg::StateParams {} => to_binary(&query_contract_settings_params(deps)?), - QueryMsg::LayerDistribution {} => to_binary(&query_layer_distribution(deps)?), + + // gateway-related: + QueryMsg::GetGateways { limit, start_after } => to_binary( + &crate::gateways::queries::query_gateways_paged(deps, start_after, limit)?, + ), + QueryMsg::GetGatewayBond { identity } => to_binary( + &crate::gateways::queries::query_gateway_bond(deps, identity)?, + ), + QueryMsg::GetOwnedGateway { address } => to_binary( + &crate::gateways::queries::query_owned_gateway(deps, address)?, + ), + + // delegation-related: QueryMsg::GetMixnodeDelegations { - mix_identity, + mix_id, start_after, limit, - } => to_binary(&query_mixnode_delegations_paged( - deps, - mix_identity, - start_after, - limit, - )?), + } => to_binary( + &crate::delegations::queries::query_mixnode_delegations_paged( + deps, + mix_id, + start_after, + limit, + )?, + ), QueryMsg::GetDelegatorDelegations { - delegator: delegation_owner, + delegator, start_after, limit, - } => to_binary(&query_delegator_delegations_paged( - deps, - delegation_owner, - start_after, - limit, - )?), + } => to_binary( + &crate::delegations::queries::query_delegator_delegations_paged( + deps, + delegator, + start_after, + limit, + )?, + ), QueryMsg::GetDelegationDetails { - mix_identity, - delegator, - proxy, - } => to_binary(&query_mixnode_delegation( - deps.storage, - deps.api, - mix_identity, + mix_id, delegator, proxy, + } => to_binary(&crate::delegations::queries::query_mixnode_delegation( + deps, mix_id, delegator, proxy, )?), - QueryMsg::GetRewardPool {} => to_binary(&query_reward_pool(deps)?), - QueryMsg::GetCirculatingSupply {} => to_binary(&query_circulating_supply(deps)?), - QueryMsg::GetStakingSupply {} => to_binary(&query_staking_supply(deps)?), - QueryMsg::GetIntervalRewardPercent {} => to_binary(&INTERVAL_REWARD_PERCENT), - QueryMsg::GetSybilResistancePercent {} => to_binary(&SYBIL_RESISTANCE_PERCENT), - QueryMsg::GetActiveSetWorkFactor {} => to_binary(&ACTIVE_SET_WORK_FACTOR), - QueryMsg::GetRewardingStatus { - mix_identity, - interval_id, - } => to_binary(&query_rewarding_status(deps, mix_identity, interval_id)?), - QueryMsg::GetRewardedSet { - height, - start_after, - limit, - } => to_binary(&query_rewarded_set( - deps.storage, - height, - start_after, - limit, - )?), - QueryMsg::GetRewardedSetUpdateDetails {} => { - to_binary(&query_rewarded_set_update_details(env, deps.storage)?) - } - QueryMsg::GetCurrentRewardedSetHeight {} => { - to_binary(&query_current_rewarded_set_height(deps.storage)?) - } - // QueryMsg::GetCurrentInterval {} => to_binary(&query_current_interval(deps.storage)?), - QueryMsg::GetRewardedSetRefreshBlocks {} => { - to_binary(&query_rewarded_set_refresh_minimum_blocks()) - } - QueryMsg::GetEpochsInInterval {} => { - to_binary(&crate::support::helpers::epochs_in_interval(deps.storage)?) - } - QueryMsg::GetCurrentOperatorCost {} => to_binary( - &crate::support::helpers::current_operator_epoch_cost(deps.storage)?, + QueryMsg::GetAllDelegations { start_after, limit } => to_binary( + &crate::delegations::queries::query_all_delegations_paged(deps, start_after, limit)?, ), - QueryMsg::GetCurrentEpoch {} => to_binary(&query_current_epoch(deps.storage)?), - QueryMsg::QueryOperatorReward { address } => to_binary( - &crate::rewards::queries::query_operator_reward(deps, address)?, + + // rewards related + QueryMsg::GetPendingOperatorReward { address } => to_binary( + &crate::rewards::queries::query_pending_operator_reward(deps, address)?, ), - QueryMsg::QueryDelegatorReward { + QueryMsg::GetPendingMixNodeOperatorReward { mix_id } => to_binary( + &crate::rewards::queries::query_pending_mixnode_operator_reward(deps, mix_id)?, + ), + QueryMsg::GetPendingDelegatorReward { address, - mix_identity, + mix_id, proxy, - } => to_binary(&crate::rewards::queries::query_delegator_reward( - deps, + } => to_binary(&crate::rewards::queries::query_pending_delegator_reward( + deps, address, mix_id, proxy, + )?), + QueryMsg::GetEstimatedCurrentEpochOperatorReward { + mix_id, + estimated_performance, + } => to_binary( + &crate::rewards::queries::query_estimated_current_epoch_operator_reward( + deps, + mix_id, + estimated_performance, + )?, + ), + QueryMsg::GetEstimatedCurrentEpochDelegatorReward { address, - mix_identity, + mix_id, proxy, - )?), - QueryMsg::GetPendingDelegationEvents { - owner_address, - proxy_address, - } => to_binary(&query_pending_delegation_events( - deps, - owner_address, - proxy_address, - )?), - QueryMsg::GetAllDelegationKeys {} => to_binary( - &crate::delegations::queries::query_all_delegation_keys(deps.storage)?, + estimated_performance, + } => to_binary( + &crate::rewards::queries::query_estimated_current_epoch_delegator_reward( + deps, + address, + mix_id, + proxy, + estimated_performance, + )?, ), - QueryMsg::DebugGetAllDelegationValues {} => to_binary( - &crate::delegations::queries::debug_query_all_delegation_values(deps.storage)?, - ), - QueryMsg::GetCheckpointsForMixnode { mix_identity } => { - to_binary(&query_checkpoints_for_mixnode(deps, mix_identity)?) + + // interval-related + QueryMsg::GetPendingEpochEvents { limit, start_after } => { + to_binary(&crate::interval::queries::query_pending_epoch_events_paged( + deps, + env, + start_after, + limit, + )?) } - QueryMsg::GetMixnodeAtHeight { - mix_identity, - height, - } => to_binary(&query_mixnode_at_height(deps, mix_identity, height)?), + QueryMsg::GetPendingIntervalEvents { limit, start_after } => to_binary( + &crate::interval::queries::query_pending_interval_events_paged( + deps, + env, + start_after, + limit, + )?, + ), }; Ok(query_res?) } -fn blacklist_malicious_node(storage: &mut dyn Storage, owner: &Addr) -> Result<(), ContractError> { - let mixnode_bond = match crate::mixnodes::storage::mixnodes() - .idx - .owner - .item(storage, owner.clone())? - { - Some(record) => record.1, - None => { - return Err(ContractError::NoAssociatedMixNodeBond { - owner: owner.to_owned(), - }) - } - }; - - crate::mixnodes::storage::MIXNODES_BOND_BLACKLIST.save(storage, mixnode_bond.identity(), &0)?; - - Ok(()) -} - -// Removes nodes we've deemed malicious, returns the pledge to the owners, but does not send any rewards -fn remove_malicious_node( - storage: &mut dyn Storage, - api: &dyn Api, - env: &Env, - node: &NodeToRemove, -) -> Result { - let proxy = node.proxy().map(|p| { - api.addr_validate(p) - .unwrap_or_else(|_| panic!("Invalid address: {}", p)) - }); - let owner_addr = api.addr_validate(node.owner())?; - blacklist_malicious_node(storage, &owner_addr)?; - _try_remove_mixnode(env, storage, api, node.owner(), proxy, false) -} - #[entry_point] -pub fn migrate(deps: DepsMut<'_>, env: Env, msg: MigrateMsg) -> Result { - migrate_config_from_env(deps.storage, &msg)?; - let mut response = Response::new(); - for node in msg.nodes_to_remove().iter() { - let mut sub_response = remove_malicious_node(deps.storage, deps.api, &env, node) - .unwrap_or_else(|_| panic!("Could not remove node: {:?}", node)); - response.messages.append(&mut sub_response.messages); - response.attributes.append(&mut sub_response.attributes); - response.events.append(&mut sub_response.events); - } - - Ok(response) +pub fn migrate( + _deps: DepsMut<'_>, + _env: Env, + _msg: MigrateMsg, +) -> Result { + Ok(Default::default()) } #[cfg(test)] -pub mod tests { +mod tests { use super::*; - use crate::support::tests; - use crate::support::tests::fixtures::{TEST_COIN_DENOM, TEST_REWARDING_VALIDATOR_ADDRESS}; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; - use cosmwasm_std::{coins, from_binary}; - use mixnet_contract_common::PagedMixnodeResponse; + use cosmwasm_std::Decimal; + use mixnet_contract_common::reward_params::{IntervalRewardParams, RewardingParams}; + use mixnet_contract_common::{InitialRewardingParams, Percent}; + use std::time::Duration; #[test] fn initialize_contract() { let mut deps = mock_dependencies(); let env = mock_env(); - let msg = InstantiateMsg { - rewarding_validator_address: TEST_REWARDING_VALIDATOR_ADDRESS.to_string(), - mixnet_denom: TEST_COIN_DENOM.to_string(), - }; - let info = mock_info("creator", &[]); - let res = instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); - assert_eq!(0, res.messages.len()); - - // mix_node_bonds should be empty after initialization - let res = query( - deps.as_ref(), - env.clone(), - QueryMsg::GetMixNodes { - start_after: None, - limit: Option::from(2), + let init_msg = InstantiateMsg { + rewarding_validator_address: "foomp123".to_string(), + vesting_contract_address: "bar456".to_string(), + rewarding_denom: "uatom".to_string(), + epochs_in_interval: 1234, + epoch_duration: Duration::from_secs(4321), + initial_rewarding_params: InitialRewardingParams { + initial_reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(), + initial_staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(), + staking_supply_scale_factor: Percent::hundred(), + sybil_resistance: Percent::from_percentage_value(23).unwrap(), + active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), + interval_pool_emission: Percent::from_percentage_value(1).unwrap(), + rewarded_set_size: 543, + active_set_size: 123, }, - ) - .unwrap(); - let page: PagedMixnodeResponse = from_binary(&res).unwrap(); - assert_eq!(0, page.nodes.len()); // there are no mixnodes in the list when it's just been initialized + }; - // Contract balance should match what we initialized it as - assert_eq!( - coins(0, TEST_COIN_DENOM), - tests::queries::query_contract_balance(env.contract.address, deps) - ); + let sender = mock_info("sender", &[]); + let res = instantiate(deps.as_mut(), env, sender, init_msg); + assert!(res.is_ok()); + + let expected_state = ContractState { + owner: Addr::unchecked("sender"), + rewarding_validator_address: Addr::unchecked("foomp123"), + vesting_contract_address: Addr::unchecked("bar456"), + rewarding_denom: "uatom".into(), + params: ContractStateParams { + minimum_mixnode_delegation: None, + minimum_mixnode_pledge: Coin { + denom: "uatom".into(), + amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, + }, + minimum_gateway_pledge: Coin { + denom: "uatom".into(), + amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, + }, + }, + }; + + let expected_epoch_reward_budget = + Decimal::from_ratio(100_000_000_000_000u128, 1234u32) * Decimal::percent(1); + let expected_stake_saturation_point = Decimal::from_ratio(123_456_000_000_000u128, 543u32); + + let expected_rewarding_params = RewardingParams { + interval: IntervalRewardParams { + reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(), + staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(), + staking_supply_scale_factor: Percent::hundred(), + epoch_reward_budget: expected_epoch_reward_budget, + stake_saturation_point: expected_stake_saturation_point, + sybil_resistance: Percent::from_percentage_value(23).unwrap(), + active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), + interval_pool_emission: Percent::from_percentage_value(1).unwrap(), + }, + rewarded_set_size: 543, + active_set_size: 123, + }; + + let state = mixnet_params_storage::CONTRACT_STATE + .load(deps.as_ref().storage) + .unwrap(); + assert_eq!(state, expected_state); + + let rewarding_params = rewards_storage::REWARDING_PARAMS + .load(deps.as_ref().storage) + .unwrap(); + assert_eq!(rewarding_params, expected_rewarding_params); + + let interval = interval_storage::current_interval(deps.as_ref().storage).unwrap(); + assert_eq!(interval.epochs_in_interval(), 1234); + assert_eq!(interval.epoch_length(), Duration::from_secs(4321)); + assert_eq!(interval.current_interval_id(), 0); + assert_eq!(interval.current_epoch_id(), 0); } } diff --git a/contracts/mixnet/src/delegations/helpers.rs b/contracts/mixnet/src/delegations/helpers.rs new file mode 100644 index 0000000000..b21e0d9ba4 --- /dev/null +++ b/contracts/mixnet/src/delegations/helpers.rs @@ -0,0 +1,65 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::delegations::storage; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Coin, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::mixnode::MixNodeRewarding; +use mixnet_contract_common::Delegation; + +pub(crate) fn undelegate( + store: &mut dyn Storage, + delegation: Delegation, + mut mix_rewarding: MixNodeRewarding, +) -> Result { + let tokens = mix_rewarding.undelegate(&delegation)?; + + rewards_storage::MIXNODE_REWARDING.save(store, delegation.mix_id, &mix_rewarding)?; + storage::delegations().replace(store, delegation.storage_key(), None, Some(&delegation))?; + + Ok(tokens) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_helpers::{performance, TestSetup}; + use cosmwasm_std::{Addr, Decimal, Uint128}; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; + + #[test] + fn undelegation_updates_mix_rewarding_storage_and_deletes_delegation() { + let mut test = TestSetup::new(); + + let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(100_000_000_000))); + let delegator = "delegator"; + let og_amount = Uint128::new(200_000_000); + test.add_immediate_delegation(delegator, og_amount, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + let dist1 = test.reward_with_distribution(mix_id, performance(100.0)); + test.skip_to_next_epoch_end(); + let dist2 = test.reward_with_distribution(mix_id, performance(100.0)); + + let mix_rewarding = test.mix_rewarding(mix_id); + let delegation = test.delegation(mix_id, delegator, &None); + + let expected_amount = og_amount + truncate_reward_amount(dist1.delegates + dist2.delegates); + + let res = undelegate(test.deps_mut().storage, delegation, mix_rewarding).unwrap(); + assert_eq!(res.amount, expected_amount); + + let mix_rewarding = test.mix_rewarding(mix_id); + assert_eq!(mix_rewarding.delegates, Decimal::zero()); + assert_eq!(mix_rewarding.unique_delegations, 0); + + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(delegator), None); + assert!(storage::delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + } +} diff --git a/contracts/mixnet/src/delegations/mod.rs b/contracts/mixnet/src/delegations/mod.rs index 32fd774de7..026c0d13ec 100644 --- a/contracts/mixnet/src/delegations/mod.rs +++ b/contracts/mixnet/src/delegations/mod.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub(crate) mod helpers; pub(crate) mod queries; pub(crate) mod storage; pub(crate) mod transactions; diff --git a/contracts/mixnet/src/delegations/queries.rs b/contracts/mixnet/src/delegations/queries.rs index 8b60091e4f..ff913c030b 100644 --- a/contracts/mixnet/src/delegations/queries.rs +++ b/contracts/mixnet/src/delegations/queries.rs @@ -1,69 +1,80 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::error::ContractError; +use crate::constants::{ + DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, +}; +use crate::mixnodes::storage as mixnodes_storage; +use cosmwasm_std::Deps; use cosmwasm_std::Order; use cosmwasm_std::StdResult; -use cosmwasm_std::{Api, Deps, Storage}; -use cw_storage_plus::{Bound, PrimaryKey}; -use mixnet_contract_common::mixnode::DelegationEvent; +use cw_storage_plus::Bound; +use mixnet_contract_common::delegation::{MixNodeDelegationResponse, OwnerProxySubKey}; use mixnet_contract_common::{ - delegation, Delegation, IdentityKey, PagedDelegatorDelegationsResponse, - PagedMixDelegationsResponse, + delegation, Delegation, MixId, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, + PagedMixNodeDelegationsResponse, }; -pub(crate) fn query_pending_delegation_events( +pub(crate) fn query_mixnode_delegations_paged( deps: Deps<'_>, - owner_address: String, - proxy_address: Option, -) -> Result, ContractError> { - let validated_owner = deps.api.addr_validate(&owner_address)?; - let validated_proxy = proxy_address - .map(|proxy| deps.api.addr_validate(&proxy)) - .transpose()?; + mix_id: MixId, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT) + .min(DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT) as usize; - let key_prefix = delegation::generate_storage_key(&validated_owner, validated_proxy.as_ref()); + let start = start_after.map(|subkey| { + Bound::exclusive(Delegation::generate_storage_key_with_subkey(mix_id, subkey)) + }); - Ok(storage::PENDING_DELEGATION_EVENTS - .sub_prefix(key_prefix) - .range(deps.storage, None, None, Order::Ascending) - .filter_map(|r| r.ok()) - .map(|(_key, delegation_event)| delegation_event) - .collect::>()) + let delegations = storage::delegations() + .idx + .mixnode + .prefix(mix_id) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|record| record.map(|r| r.1)) + .collect::>>()?; + + let start_next_after = delegations.last().map(|del| del.proxy_storage_key()); + + Ok(PagedMixNodeDelegationsResponse::new( + delegations, + start_next_after, + )) } pub(crate) fn query_delegator_delegations_paged( deps: Deps<'_>, delegation_owner: String, - start_after: Option, + start_after: Option<(MixId, OwnerProxySubKey)>, limit: Option, ) -> StdResult { let validated_owner = deps.api.addr_validate(&delegation_owner)?; let limit = limit - .unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT) - .min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize; - let start = start_after.map(|mix_identity| { - Bound::ExclusiveRaw((mix_identity, validated_owner.clone()).joined_key()) + .unwrap_or(DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT) + .min(DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(|(mix_id, subkey)| { + Bound::exclusive(Delegation::generate_storage_key_with_subkey(mix_id, subkey)) }); let delegations = storage::delegations() .idx .owner .prefix(validated_owner) - .range_raw(deps.storage, start, None, Order::Ascending) + .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|record| record.map(|r| r.1)) .collect::>>()?; - let start_next_after = if delegations.len() < limit { - None - } else { - delegations - .last() - .map(|delegation| delegation.node_identity()) - }; + let start_next_after = delegations + .last() + .map(|del| (del.mix_id, del.proxy_storage_key())); Ok(PagedDelegatorDelegationsResponse::new( delegations, @@ -71,540 +82,657 @@ pub(crate) fn query_delegator_delegations_paged( )) } -pub fn query_all_delegation_keys(storage: &dyn Storage) -> Result, ContractError> { - Ok(storage::delegations() - .keys_raw(storage, None, None, Order::Ascending) - .map(hex::encode) - .collect()) -} - -use std::collections::HashSet; - -// This should only be exposed directly on the contract via nymd binary, not through the nymd clients -pub fn debug_query_all_delegation_values( - storage: &dyn Storage, -) -> Result, ContractError> { - use crate::delegations::storage::{ - DelegationIndex, DELEGATION_MIXNODE_IDX_NAMESPACE, DELEGATION_OWNER_IDX_NAMESPACE, - DELEGATION_PK_NAMESPACE, - }; - - use cw_storage_plus::{IndexedMap, MultiIndex}; - - type PrimaryKey = Vec; - - fn all_delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, DelegationIndex<'a>> { - let indexes = DelegationIndex { - owner: MultiIndex::new( - |d| d.owner.clone(), - DELEGATION_PK_NAMESPACE, - DELEGATION_OWNER_IDX_NAMESPACE, - ), - mixnode: MultiIndex::new( - |d| d.node_identity.clone(), - DELEGATION_PK_NAMESPACE, - DELEGATION_MIXNODE_IDX_NAMESPACE, - ), - }; - - IndexedMap::new(DELEGATION_PK_NAMESPACE, indexes) - } - - let all_delegations = all_delegations() - .range(storage, None, None, Order::Ascending) - .filter_map(|r| r.ok()) - .map(|(_key, delegation)| delegation) - .collect::>(); - - Ok(all_delegations) -} - // queries for delegation value of given address for particular node pub(crate) fn query_mixnode_delegation( - storage: &dyn Storage, - api: &dyn Api, - mix_identity: IdentityKey, - delegator: String, + deps: Deps<'_>, + mix_id: MixId, + delegation_owner: String, proxy: Option, -) -> Result, ContractError> { - let validated_delegator = api.addr_validate(&delegator)?; - let proxy = proxy.map(|p| api.addr_validate(&p)).transpose()?; - let storage_key = ( - mix_identity.clone(), - mixnet_contract_common::delegation::generate_storage_key( - &validated_delegator, - proxy.as_ref(), - ), - ); +) -> StdResult { + let validated_owner = deps.api.addr_validate(&delegation_owner)?; + let validated_proxy = proxy + .map(|proxy| deps.api.addr_validate(&proxy)) + .transpose()?; + let storage_key = + Delegation::generate_storage_key(mix_id, &validated_owner, validated_proxy.as_ref()); - let delegations = storage::delegations() - .prefix(storage_key) - .range(storage, None, None, Order::Ascending) - .filter_map(|d| d.ok()) - .map(|r| r.1) - .collect::>(); + let delegation = storage::delegations().may_load(deps.storage, storage_key)?; - if delegations.is_empty() { - Err(ContractError::NoMixnodeDelegationFound { - identity: mix_identity, - address: delegator, - }) - } else { - Ok(delegations) - } + let mixnode_still_bonded = mixnodes_storage::mixnode_bonds() + .may_load(deps.storage, mix_id)? + .map(|bond| !bond.is_unbonding) + .unwrap_or_default(); + + Ok(MixNodeDelegationResponse::new( + delegation, + mixnode_still_bonded, + )) } -pub(crate) fn query_mixnode_delegations_paged( +pub(crate) fn query_all_delegations_paged( deps: Deps<'_>, - mix_identity: IdentityKey, - start_after: Option<(String, u64)>, + start_after: Option, limit: Option, -) -> StdResult { +) -> StdResult { let limit = limit - .unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT) - .min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize; + .unwrap_or(DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT) + .min(DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT) as usize; - let start = start_after.map(|(addr, height)| { - Bound::exclusive(( - hex::decode(addr).expect("Could not hex decode proxy_storage_key"), - height, - )) - }); + let start = start_after.map(Bound::exclusive); let delegations = storage::delegations() - .sub_prefix(mix_identity) .range(deps.storage, start, None, Order::Ascending) .take(limit) - .filter_map(|r| r.ok()) - .map(|record| record.1) - .collect::>(); + .map(|res| res.map(|item| item.1)) + .collect::>>()?; - let start_next_after = if delegations.len() < limit { - None - } else { - delegations.last().map(|delegation| { - ( - hex::encode(delegation.proxy_storage_key()), - delegation.block_height(), - ) - }) - }; + let start_next_after = delegations.last().map(|del| del.storage_key()); - Ok(PagedMixDelegationsResponse::new( + Ok(PagedAllDelegationsResponse::new( delegations, start_next_after, )) } #[cfg(test)] -pub(crate) mod tests { +mod tests { use super::*; - use crate::support::tests::{fixtures::TEST_COIN_DENOM, test_helpers}; - use cosmwasm_std::{coin, Addr, Storage}; - use rand::Rng; + use crate::support::tests::test_helpers::TestSetup; - pub fn store_n_mix_delegations(n: u32, storage: &mut dyn Storage, node_identity: &str) { - for i in 0..n { - let address = format!("address{}", i); - test_helpers::save_dummy_delegation(storage, node_identity, address, 1); + fn add_dummy_mixes_with_delegations(test: &mut TestSetup, delegators: usize, mixes: usize) { + for i in 0..mixes { + let mix_id = test.add_dummy_mixnode(&format!("mix-owner{}", i), None); + for delegator in 0..delegators { + let name = &format!("delegator{}", delegator); + test.add_immediate_delegation(name, 100_000_000u32, mix_id) + } } } #[cfg(test)] - mod querying_for_mixnode_delegations_paged { - use std::collections::HashSet; - + mod mixnode_delegations { use super::*; - use mixnet_contract_common::IdentityKey; - use rand::{distributions::Alphanumeric, SeedableRng}; + use crate::support::tests::test_helpers; #[test] - fn retrieval_obeys_limits() { - let mut deps = test_helpers::init_contract(); - let limit = 2; - let node_identity: IdentityKey = "foo".into(); - store_n_mix_delegations(100, &mut deps.storage, &node_identity); + fn obeys_limits() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); - let page1 = query_mixnode_delegations_paged( - deps.as_ref(), - node_identity, - None, - Option::from(limit), - ) - .unwrap(); + let env = test.env(); + test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 200); + + let limit = 2; + + let page1 = + query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(limit)).unwrap(); assert_eq!(limit, page1.delegations.len() as u32); } #[test] - fn retrieval_has_default_limit() { - let mut deps = test_helpers::init_contract(); - let node_identity: IdentityKey = "foo".into(); - store_n_mix_delegations( - storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10, - &mut deps.storage, - &node_identity, - ); + fn has_default_limit() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let env = test.env(); + test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 500); // query without explicitly setting a limit - let page1 = - query_mixnode_delegations_paged(deps.as_ref(), node_identity, None, None).unwrap(); + let page1 = query_mixnode_delegations_paged(test.deps(), mix_id, None, None).unwrap(); + assert_eq!( - storage::DELEGATION_PAGE_DEFAULT_LIMIT, + DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, page1.delegations.len() as u32 ); } #[test] - fn retrieval_has_max_limit() { - let mut deps = test_helpers::init_contract(); - let node_identity: IdentityKey = "foo".into(); - store_n_mix_delegations( - storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10, - &mut deps.storage, - &node_identity, - ); + fn has_max_limit() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let env = test.env(); + test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 5000); // query with a crazily high limit in an attempt to use too many resources - let crazy_limit = 1000 * storage::DELEGATION_PAGE_DEFAULT_LIMIT; - let page1 = query_mixnode_delegations_paged( - deps.as_ref(), - node_identity, - None, - Option::from(crazy_limit), - ) - .unwrap(); + let crazy_limit = 10000; + let page1 = + query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(crazy_limit)) + .unwrap(); - // we default to a decent sized upper bound instead - let expected_limit = storage::DELEGATION_PAGE_MAX_LIMIT; - assert_eq!(expected_limit, page1.delegations.len() as u32); + assert_eq!( + DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, + page1.delegations.len() as u32 + ); } #[test] fn pagination_works() { - let dummy_seed = [42u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let mut test = TestSetup::new(); - let mut deps = test_helpers::init_contract(); - let node_identity: IdentityKey = "foo".into(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + test.add_immediate_delegation("addr1", 1000u32, mix_id); - let mut delegation_test_data = vec![]; - let mut returned_delegation_data = HashSet::new(); + let per_page = 2; + let page1 = + query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); - // Crete a bunch of randomly ordered (in storage) delegations - for _ in 0..200 { - delegation_test_data.push(( - rng.clone() - .sample_iter(&Alphanumeric) - .take(30) - .map(char::from) - .collect::(), - rng.gen::() as u64, - )) - } + // page should have 1 result on it + assert_eq!(1, page1.delegations.len()); - for (address, block_height) in delegation_test_data.iter() { - test_helpers::save_dummy_delegation( - &mut deps.storage, - &node_identity, - address, - *block_height, - ); - } + // save another + test.add_immediate_delegation("addr2", 1000u32, mix_id); - let per_page = 100; + // page1 should have 2 results on it + let page1 = + query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); + assert_eq!(2, page1.delegations.len()); - // page1 still has 2 results - let page1 = query_mixnode_delegations_paged( - deps.as_ref(), - node_identity.clone(), - None, - Option::from(per_page), - ) - .unwrap(); + test.add_immediate_delegation("addr3", 1000u32, mix_id); - let start_after = page1.start_next_after.unwrap(); - assert_eq!(100, page1.delegations.len()); - assert_eq!( - (( - "5874735a724c52587679656777795a446a754a746c59694735423165694a".to_string(), - 1594717548 - )), - start_after - ); - - for delegation in page1.delegations { - returned_delegation_data.insert(delegation.owner().to_string()); - } + // page1 still has the same 2 results + let another_page1 = + query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); + assert_eq!(2, another_page1.delegations.len()); + assert_eq!(page1, another_page1); // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_mixnode_delegations_paged( + test.deps(), + mix_id, + Some(start_after.clone()), + Some(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.delegations.len()); + + // save another one + test.add_immediate_delegation("addr4", 1000u32, mix_id); let page2 = query_mixnode_delegations_paged( - deps.as_ref(), - node_identity.clone(), - Option::from(start_after.clone()), - Option::from(per_page), + test.deps(), + mix_id, + Some(start_after), + Some(per_page), ) .unwrap(); - let start_after = page2.start_next_after.unwrap(); - assert_eq!( - ( - "7a6b48546c63674f57417948384e6f494a326c6b5a63767668597346696b".to_string(), - 3448133410 - ), - start_after - ); - - for delegation in page2.delegations { - returned_delegation_data.insert(delegation.owner().to_string()); - } - - let page3 = query_mixnode_delegations_paged( - deps.as_ref(), - node_identity.clone(), - Option::from(start_after), - Option::from(per_page), - ) - .unwrap(); - - assert!(page3.start_next_after.is_none()); - - for delegation in delegation_test_data { - assert!(returned_delegation_data.contains(&*delegation.0)); - } - } - } - - #[test] - fn mix_deletion_query_returns_current_delegation_value() { - let mut deps = test_helpers::init_contract(); - let node_identity: IdentityKey = "foo".into(); - let delegation_owner = Addr::unchecked("bar"); - - let delegation = Delegation::new( - delegation_owner.clone(), - node_identity.clone(), - coin(1234, TEST_COIN_DENOM), - 1234, - None, - ); - - storage::delegations() - .save(deps.as_mut().storage, delegation.storage_key(), &delegation) - .unwrap(); - - assert_eq!( - Ok(vec![delegation]), - query_mixnode_delegation( - &deps.storage, - &deps.api, - node_identity, - delegation_owner.to_string(), - None - ) - ) - } - - #[test] - fn mix_deletion_query_returns_error_if_delegation_doesnt_exist() { - let mut deps = test_helpers::init_contract(); - - let node_identity1: IdentityKey = "foo1".into(); - let node_identity2: IdentityKey = "foo2".into(); - let delegation_owner1 = Addr::unchecked("bar"); - let delegation_owner2 = Addr::unchecked("bar2"); - - assert_eq!( - Err(ContractError::NoMixnodeDelegationFound { - identity: node_identity1.clone(), - address: delegation_owner1.to_string(), - }), - query_mixnode_delegation( - &deps.storage, - &deps.api, - node_identity1.clone(), - delegation_owner1.to_string(), - None - ) - ); - - // add delegation from a different address - let delegation = Delegation::new( - delegation_owner2, - node_identity1.clone(), - coin(1234, TEST_COIN_DENOM), - 1234, - None, - ); - - storage::delegations() - .save(deps.as_mut().storage, delegation.storage_key(), &delegation) - .unwrap(); - - assert_eq!( - Err(ContractError::NoMixnodeDelegationFound { - identity: node_identity1.clone(), - address: delegation_owner1.to_string(), - }), - query_mixnode_delegation( - &deps.storage, - &deps.api, - node_identity1.clone(), - delegation_owner1.to_string(), - None - ) - ); - - // add delegation for a different node - let delegation = Delegation::new( - delegation_owner1.clone(), - node_identity2, - coin(1234, TEST_COIN_DENOM), - 1234, - None, - ); - - storage::delegations() - .save(deps.as_mut().storage, delegation.storage_key(), &delegation) - .unwrap(); - - assert_eq!( - Err(ContractError::NoMixnodeDelegationFound { - identity: node_identity1.clone(), - address: delegation_owner1.to_string() - }), - query_mixnode_delegation( - &deps.storage, - &deps.api, - node_identity1, - delegation_owner1.to_string(), - None - ) - ) - } - - #[cfg(test)] - mod querying_for_reverse_mixnode_delegations_paged { - use super::*; - - fn store_n_reverse_delegations(n: u32, storage: &mut dyn Storage, delegation_owner: &str) { - for i in 0..n { - let node_identity = format!("node{}", i); - test_helpers::save_dummy_delegation(storage, node_identity, delegation_owner, 1); - } + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.delegations.len()); } #[test] - fn retrieval_obeys_limits() { - let mut deps = test_helpers::init_contract(); + fn all_retrieved_delegations_are_towards_specified_mixnode() { + let mut test = TestSetup::new(); + let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); + let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); + let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); + let mix_id4 = test.add_dummy_mixnode("mix-owner4", None); + + let env = test.env(); + // add other "out of order" delegations manually + test.add_immediate_delegation("random-delegator1", 1000u32, mix_id2); + test.add_immediate_delegation("random-delegator2", 1000u32, mix_id2); + test_helpers::add_dummy_delegations(test.deps_mut(), env.clone(), mix_id1, 10); + test_helpers::add_dummy_delegations(test.deps_mut(), env.clone(), mix_id2, 10); + test_helpers::add_dummy_delegations(test.deps_mut(), env.clone(), mix_id3, 10); + test.add_immediate_delegation("random-delegator3", 1000u32, mix_id2); + test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id4, 10); + test.add_immediate_delegation("random-delegator4", 1000u32, mix_id2); + + 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.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.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.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.mix_id == mix_id4)); + } + } + + mod delegator_delegations { + use super::*; + use crate::delegations::transactions::try_delegate_to_mixnode_on_behalf; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{coin, Addr}; + + #[test] + fn obeys_limits() { + let mut test = TestSetup::new(); + + // 50 mixnodes with 500 delegations each; + add_dummy_mixes_with_delegations(&mut test, 500, 50); + let limit = 2; - let delegation_owner = "foo".to_string(); - store_n_reverse_delegations(100, &mut deps.storage, &delegation_owner); let page1 = query_delegator_delegations_paged( - deps.as_ref(), - delegation_owner, + test.deps(), + "delegator1".into(), None, - Option::from(limit), + Some(limit), ) .unwrap(); assert_eq!(limit, page1.delegations.len() as u32); } #[test] - fn retrieval_has_default_limit() { - let mut deps = test_helpers::init_contract(); - let delegation_owner = "foo".to_string(); - store_n_reverse_delegations( - storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10, - &mut deps.storage, - &delegation_owner, - ); + fn has_default_limit() { + let mut test = TestSetup::new(); + add_dummy_mixes_with_delegations(&mut test, 10, 500); // query without explicitly setting a limit let page1 = - query_delegator_delegations_paged(deps.as_ref(), delegation_owner, None, None) + query_delegator_delegations_paged(test.deps(), "delegator1".into(), None, None) .unwrap(); + assert_eq!( - storage::DELEGATION_PAGE_DEFAULT_LIMIT, + DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, page1.delegations.len() as u32 ); } #[test] - fn retrieval_has_max_limit() { - let mut deps = test_helpers::init_contract(); - let delegation_owner = "foo".to_string(); - store_n_reverse_delegations( - storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10, - &mut deps.storage, - &delegation_owner, - ); + fn has_max_limit() { + let mut test = TestSetup::new(); + add_dummy_mixes_with_delegations(&mut test, 10, 500); - // query with a crazy high limit in an attempt to use too many resources - let crazy_limit = 1000 * storage::DELEGATION_PAGE_DEFAULT_LIMIT; + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 10000; let page1 = query_delegator_delegations_paged( - deps.as_ref(), - delegation_owner, + test.deps(), + "delegator1".into(), None, - Option::from(crazy_limit), + Some(crazy_limit), ) .unwrap(); - // we default to a decent sized upper bound instead - let expected_limit = storage::DELEGATION_PAGE_MAX_LIMIT; - assert_eq!(expected_limit, page1.delegations.len() as u32); + assert_eq!( + DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, + page1.delegations.len() as u32 + ); } #[test] fn pagination_works() { - let mut deps = test_helpers::init_contract(); - let delegation_owner = "bar".to_string(); + let mut test = TestSetup::new(); - for j in 0..20 { - for i in 0..10 { - test_helpers::save_dummy_delegation( - &mut deps.storage, - format!("{}-{}", j, i), - delegation_owner.clone(), - i, - ); - } + // note that mix_ids are monotonically increasing + let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); + let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); + let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); + let mix_id4 = test.add_dummy_mixnode("mix-owner4", None); + let mix_id5 = test.add_dummy_mixnode("mix-owner5", None); + + // add few delegations from unrelated delegators + for mix_id in [mix_id1, mix_id2, mix_id3, mix_id4, mix_id5] { + test.add_immediate_delegation("random1", 1000u32, mix_id); + test.add_immediate_delegation("random2", 1000u32, mix_id); + test.add_immediate_delegation("random1", 1000u32, mix_id); } - let per_page = 100; + let delegator = "delegator"; + + test.add_immediate_delegation(delegator, 1000u32, mix_id1); + + let per_page = 2; let page1 = query_delegator_delegations_paged( - deps.as_ref(), - delegation_owner.clone(), + test.deps(), + delegator.into(), None, - Option::from(per_page), + Some(per_page), ) .unwrap(); + // page should have 1 result on it + assert_eq!(1, page1.delegations.len()); + + // save another + test.add_immediate_delegation(delegator, 1000u32, mix_id2); + + // page1 should have 2 results on it + let page1 = query_delegator_delegations_paged( + test.deps(), + delegator.into(), + None, + Some(per_page), + ) + .unwrap(); + assert_eq!(2, page1.delegations.len()); + + test.add_immediate_delegation(delegator, 1000u32, mix_id3); + + // page1 still has the same 2 results + let another_page1 = query_delegator_delegations_paged( + test.deps(), + delegator.into(), + None, + Some(per_page), + ) + .unwrap(); + assert_eq!(2, another_page1.delegations.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page let start_after = page1.start_next_after.unwrap(); - assert_eq!(per_page as usize, page1.delegations.len()); - assert_eq!(start_after, "9-9".to_string()); + let page2 = query_delegator_delegations_paged( + test.deps(), + delegator.into(), + Some(start_after.clone()), + Some(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.delegations.len()); + + // save another one + test.add_immediate_delegation(delegator, 1000u32, mix_id4); let page2 = query_delegator_delegations_paged( - deps.as_ref(), - delegation_owner.clone(), + test.deps(), + delegator.into(), Some(start_after), - Option::from(per_page), + Some(per_page), ) .unwrap(); - let start_after = page2.start_next_after.unwrap(); - assert_eq!(start_after, "19-9".to_string()); + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.delegations.len()); + } - let page3 = query_delegator_delegations_paged( - deps.as_ref(), - delegation_owner.clone(), - Some(start_after), - Option::from(per_page), - ) - .unwrap(); + #[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); - assert!(page3.start_next_after.is_none()); + // add some proxies while we're at it to make sure they're queried for separately + let with_proxy = "delegator42"; + let vesting_contract = test.vesting_contract(); + 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(), + ) + .unwrap(); + } + test.execute_all_pending_events(); + + // make few queries + let res1 = + query_delegator_delegations_paged(test.deps(), "delegator2".into(), None, None) + .unwrap(); + assert_eq!(res1.delegations.len(), 100); + assert!(res1 + .delegations + .into_iter() + .all(|d| d.owner == Addr::unchecked("delegator2"))); + + let res2 = + query_delegator_delegations_paged(test.deps(), "delegator35".into(), None, None) + .unwrap(); + assert_eq!(res2.delegations.len(), 100); + assert!(res2 + .delegations + .into_iter() + .all(|d| d.owner == Addr::unchecked("delegator35"))); + + let with_proxy_full = + query_delegator_delegations_paged(test.deps(), with_proxy.into(), None, None) + .unwrap(); + assert_eq!(with_proxy_full.delegations.len(), 125); + + // all delegations have correct owner + assert!(with_proxy_full + .delegations + .iter() + .all(|d| d.owner == Addr::unchecked(with_proxy))); + + // and we have 100 delegations without proxy and 25 with + let no_proxy = with_proxy_full + .delegations + .iter() + .filter(|d| d.proxy.is_none()) + .count(); + assert_eq!(no_proxy, 100); + let proxy = with_proxy_full + .delegations + .iter() + .filter(|d| d.proxy.is_some()) + .count(); + assert_eq!(proxy, 25); + + assert!(with_proxy_full + .delegations + .iter() + .filter(|d| d.proxy.is_some()) + .all(|d| d.proxy.as_ref().unwrap() == &vesting_contract)); + + // now make sure that if we do it in paged manner, we'll get exactly the same result + let per_page = Some(15); + let mut delegations = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = query_delegator_delegations_paged( + test.deps(), + with_proxy.into(), + start_after, + per_page, + ) + .unwrap(); + delegations.append(&mut paged_response.delegations); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + assert_eq!(with_proxy_full.delegations, delegations) + } + } + + mod all_delegations { + use super::*; + + #[test] + fn obeys_limits() { + let mut test = TestSetup::new(); + + // 50 mixnodes with 500 delegations each; + add_dummy_mixes_with_delegations(&mut test, 500, 50); + + let limit = 2; + + let page1 = query_all_delegations_paged(test.deps(), None, Some(limit)).unwrap(); + assert_eq!(limit, page1.delegations.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut test = TestSetup::new(); + add_dummy_mixes_with_delegations(&mut test, 10, 500); + + // query without explicitly setting a limit + let page1 = query_all_delegations_paged(test.deps(), None, None).unwrap(); + + assert_eq!( + DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, + page1.delegations.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut test = TestSetup::new(); + add_dummy_mixes_with_delegations(&mut test, 10, 500); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 10000; + let page1 = query_all_delegations_paged(test.deps(), None, Some(crazy_limit)).unwrap(); + + assert_eq!( + DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, + page1.delegations.len() as u32 + ); + } + + #[test] + fn pagination_works() { + let mut test = TestSetup::new(); + + // note that mix_ids are monotonically increasing and are the first chunk of all + // delegation storage keys, + let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); + let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); + + let delegator1 = "delegator1"; + let delegator2 = "delegator2"; + + test.add_immediate_delegation(delegator1, 1000u32, mix_id1); + + let per_page = 2; + let page1 = query_all_delegations_paged(test.deps(), None, Some(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.delegations.len()); + assert!( + page1.delegations[0].owner.as_str() == delegator1 + && page1.delegations[0].mix_id == mix_id1 + ); + + test.add_immediate_delegation(delegator1, 1000u32, mix_id2); + + let page1 = query_all_delegations_paged(test.deps(), None, Some(per_page)).unwrap(); + + // page1 should have 2 results on it + assert_eq!(2, page1.delegations.len()); + assert!( + page1.delegations[0].owner.as_str() == delegator1 + && page1.delegations[0].mix_id == mix_id1 + ); + assert!( + page1.delegations[1].owner.as_str() == delegator1 + && page1.delegations[1].mix_id == mix_id2 + ); + + test.add_immediate_delegation(delegator2, 1000u32, mix_id1); + + // note that the order of results changed on page1 + let another_page1 = + query_all_delegations_paged(test.deps(), None, Some(per_page)).unwrap(); + assert_eq!(2, another_page1.delegations.len()); + assert!( + another_page1.delegations[0].owner.as_str() == delegator1 + && another_page1.delegations[0].mix_id == mix_id1 + ); + assert!( + another_page1.delegations[1].owner.as_str() == delegator2 + && another_page1.delegations[1].mix_id == mix_id1 + ); + + // retrieving the next page should start after the last key on this page + let start_after = another_page1.start_next_after.unwrap(); + let page2 = + query_all_delegations_paged(test.deps(), Some(start_after.clone()), Some(per_page)) + .unwrap(); + + assert_eq!(1, page2.delegations.len()); + assert!( + page2.delegations[0].owner.as_str() == delegator1 + && page2.delegations[0].mix_id == mix_id2 + ); + + // save another one + test.add_immediate_delegation(delegator2, 1000u32, mix_id2); + + let page2 = query_all_delegations_paged(test.deps(), Some(start_after), Some(per_page)) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.delegations.len()); + assert!( + page2.delegations[0].owner.as_str() == delegator1 + && page2.delegations[0].mix_id == mix_id2 + ); + assert!( + page2.delegations[1].owner.as_str() == delegator2 + && page2.delegations[1].mix_id == mix_id2 + ); + } + } + + #[cfg(test)] + mod querying_for_specific_mixnode_delegation { + use super::*; + + #[test] + fn when_delegation_doesnt_exist() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let owner = "owner"; + + let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert!(res.delegation.is_none()); + assert!(res.mixnode_still_bonded); + } + + #[test] + fn when_delegation_exists_but_mixnode_has_unbonded() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + test.immediately_unbond_mixnode(mix_id); + + let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.mixnode_still_bonded); + } + + #[test] + fn when_delegation_exists_but_mixnode_is_unbonding() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + test.start_unbonding_mixnode(mix_id); + + let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.mixnode_still_bonded); + } + + #[test] + fn when_delegation_exists_with_fully_bonded_node() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + + let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(res.mixnode_still_bonded); } } } diff --git a/contracts/mixnet/src/delegations/storage.rs b/contracts/mixnet/src/delegations/storage.rs index 5715bc2c27..04d07f57f8 100644 --- a/contracts/mixnet/src/delegations/storage.rs +++ b/contracts/mixnet/src/delegations/storage.rs @@ -1,32 +1,20 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cw_storage_plus::{Index, IndexList, IndexedMap, Map, MultiIndex}; -use mixnet_contract_common::{mixnode::DelegationEvent, Addr, Delegation, IdentityKey}; +use crate::constants::{ + DELEGATION_MIXNODE_IDX_NAMESPACE, DELEGATION_OWNER_IDX_NAMESPACE, DELEGATION_PK_NAMESPACE, +}; +use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex}; +use mixnet_contract_common::delegation::OwnerProxySubKey; +use mixnet_contract_common::{Addr, Delegation, MixId}; -// storage prefixes -pub const DELEGATION_PK_NAMESPACE: &str = "dl"; -pub const DELEGATION_OWNER_IDX_NAMESPACE: &str = "dlo"; -pub const DELEGATION_MIXNODE_IDX_NAMESPACE: &str = "dlm"; - -pub const PENDING_DELEGATION_EVENTS: Map< - (OwnerAddress, BlockHeight, IdentityKey), - DelegationEvent, -> = Map::new("pend2"); - -// paged retrieval limits for all queries and transactions -pub(crate) const DELEGATION_PAGE_MAX_LIMIT: u32 = 500; -pub(crate) const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 250; - -type BlockHeight = u64; -type OwnerAddress = Vec; -// It's a composite key on node's identity, delegator address, and block height -type PrimaryKey = (IdentityKey, OwnerAddress, BlockHeight); +// It's a composite key on node's id and delegator address +type PrimaryKey = (MixId, OwnerProxySubKey); pub(crate) struct DelegationIndex<'a> { pub(crate) owner: MultiIndex<'a, Addr, Delegation, PrimaryKey>, - pub(crate) mixnode: MultiIndex<'a, IdentityKey, Delegation, PrimaryKey>, + pub(crate) mixnode: MultiIndex<'a, MixId, Delegation, PrimaryKey>, } impl<'a> IndexList for DelegationIndex<'a> { @@ -36,20 +24,6 @@ impl<'a> IndexList for DelegationIndex<'a> { } } -// I was really going back and forth about the data stored on the disk vs primary key duplication. -// It was basically between convenience and bloat, but in the end I decided the convenience wins. -// -// Basically I had 2 approaches. a) store delegator address and mixnode identity only as primary key of delegation or -// b) store it both as primary key AND inside delegation data. -// For the longest time I was in favour of a), since that removed any data duplication. However..., -// that also required that during index creation I recovered delegator address and mixnode identity -// from the Vec. That doesn't sound that terrible. However, even though I'm 99.99% certain that -// conversion would be impossible to fail, I'd still have to call an `unwrap` here due to required -// type signature and I didn't feel super comfortable doing that in our smart contract... -// So to get rid of this uncertainty I went with the b) approach. Even though each stored delegation -// takes over ~250B (since the key has to be duplicated), in the grand blockchain scheme of things -// it's not that terrible. Say we had 100_000_000 delegations -> that's still only 25GB of data -// and as a nice by-product it cleans up code a little bit by only having a single Delegation type. pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, DelegationIndex<'a>> { let indexes = DelegationIndex { owner: MultiIndex::new( @@ -58,7 +32,7 @@ pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, Delega DELEGATION_OWNER_IDX_NAMESPACE, ), mixnode: MultiIndex::new( - |d| d.node_identity.clone(), + |d| d.mix_id, DELEGATION_PK_NAMESPACE, DELEGATION_MIXNODE_IDX_NAMESPACE, ), @@ -66,130 +40,3 @@ pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, Delega IndexedMap::new(DELEGATION_PK_NAMESPACE, indexes) } - -#[cfg(test)] -mod tests { - use crate::delegations::storage; - use cosmwasm_std::Addr; - use mixnet_contract_common::IdentityKey; - - #[cfg(test)] - mod reverse_mix_delegations { - use super::*; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers; - use cosmwasm_std::testing::mock_env; - use cosmwasm_std::{coin, Order}; - use mixnet_contract_common::Delegation; - - #[test] - fn reverse_mix_delegation_exists() { - let mut deps = test_helpers::init_contract(); - let node_identity: IdentityKey = "foo".into(); - let delegation_owner = Addr::unchecked("bar"); - let delegation = coin(12345, TEST_COIN_DENOM); - - let dummy_data = Delegation::new( - delegation_owner.clone(), - node_identity.clone(), - delegation, - mock_env().block.height, - None, - ); - - storage::delegations() - .save( - &mut deps.storage, - (node_identity, delegation_owner.as_bytes().to_vec(), 0), - &dummy_data, - ) - .unwrap(); - - let read = storage::delegations() - .idx - .owner - .prefix(delegation_owner) - .range(&deps.storage, None, None, Order::Ascending) - .map(|record| record.unwrap().1) - .collect::>(); - - assert_eq!(1, read.len()); - assert_eq!(dummy_data, read[0]); - } - - #[test] - fn reverse_mix_delegation_returns_none_if_delegation_doesnt_exist() { - let mut deps = test_helpers::init_contract(); - - let node_identity1: IdentityKey = "foo1".into(); - let node_identity2: IdentityKey = "foo2".into(); - let delegation_owner1 = Addr::unchecked("bar"); - let delegation_owner2 = Addr::unchecked("bar2"); - let delegation = coin(12345, TEST_COIN_DENOM); - - assert!(test_helpers::read_delegation( - deps.as_ref().storage, - &node_identity1, - delegation_owner1.as_bytes(), - mock_env().block.height - ) - .is_none()); - - // add delegation for a different node - let dummy_data = Delegation::new( - delegation_owner1.clone(), - node_identity2, - delegation.clone(), - mock_env().block.height, - None, - ); - storage::delegations() - .save( - &mut deps.storage, - ( - node_identity1.clone(), - delegation_owner1.as_bytes().to_vec(), - 0, - ), - &dummy_data, - ) - .unwrap(); - - storage::delegations() - .idx - .owner - .prefix(delegation_owner1.clone()) - .range(&deps.storage, None, None, Order::Ascending) - .map(|record| record.unwrap().1) - .for_each(|delegation| assert_ne!(delegation.node_identity, node_identity1)); - - // add delegation from a different owner - let dummy_data = Delegation::new( - delegation_owner2.clone(), - node_identity1.clone(), - delegation, - mock_env().block.height, - None, - ); - storage::delegations() - .save( - &mut deps.storage, - ( - node_identity1.clone(), - delegation_owner2.as_bytes().to_vec(), - 0, - ), - &dummy_data, - ) - .unwrap(); - - storage::delegations() - .idx - .owner - .prefix(delegation_owner1.clone()) - .range(&deps.storage, None, None, Order::Ascending) - .map(|record| record.unwrap().1) - .for_each(|delegation| assert_ne!(delegation.node_identity, node_identity1)); - } - } -} diff --git a/contracts/mixnet/src/delegations/transactions.rs b/contracts/mixnet/src/delegations/transactions.rs index c5e462929e..271a2c190d 100644 --- a/contracts/mixnet/src/delegations/transactions.rs +++ b/contracts/mixnet/src/delegations/transactions.rs @@ -1,1193 +1,466 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::storage::{self, PENDING_DELEGATION_EVENTS}; -// use crate::contract::debug_with_visibility; -// use crate::contract::debug_with_visibility; -use crate::error::ContractError; -use crate::mixnet_contract_settings::storage::{self as mixnet_params_storage}; + +use super::storage; +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 cosmwasm_std::{ - coins, wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, Event, MessageInfo, Order, - Response, Storage, Uint128, WasmMsg, -}; +use crate::support::helpers::validate_delegation_stake; +use cosmwasm_std::{Addr, Coin, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_error_event, new_pending_delegation_event, new_pending_undelegation_event, - new_undelegation_event, + new_pending_delegation_event, new_pending_undelegation_event, }; -use mixnet_contract_common::mixnode::{DelegationEvent, PendingUndelegate}; -use mixnet_contract_common::{Delegation, IdentityKey}; -use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; -use vesting_contract_common::one_ucoin; - -pub fn try_reconcile_all_delegation_events(deps: DepsMut<'_>) -> Result { - _try_reconcile_all_delegation_events(deps.storage, deps.api) -} - -// TODO: Error handling? -pub(crate) fn _try_reconcile_all_delegation_events( - storage: &mut dyn Storage, - api: &dyn Api, -) -> Result { - let pending_delegation_events = PENDING_DELEGATION_EVENTS - .range(storage, None, None, Order::Ascending) - .filter_map(|r| r.ok()) - .collect::, u64, String), DelegationEvent)>>(); - - let mut response = Response::new(); - - // debug_with_visibility(api, "Reconciling delegation events"); - - for (key, delegation_event) in pending_delegation_events { - match delegation_event { - DelegationEvent::Delegate(delegation) => { - // if for some reason the delegation is zero, don't do anything since it should be a no-op anyway - if delegation.amount.amount == Uint128::zero() { - continue; - } - let event = try_reconcile_delegation(storage, delegation)?; - response = response.add_event(event); - } - DelegationEvent::Undelegate(pending_undelegate) => { - let undelegate_response = - try_reconcile_undelegation(storage, api, &pending_undelegate)?; - response = response.add_event(undelegate_response.event); - if let Some(msg) = undelegate_response.bank_msg { - response = response.add_message(msg); - } - if let Some(msg) = undelegate_response.wasm_msg { - response = response.add_message(msg); - } - } - } - PENDING_DELEGATION_EVENTS.remove(storage, key); - } - Ok(response) -} - -fn validate_delegation_stake( - mut delegation: Vec, - mix_denom: String, -) -> Result { - // check if anything was put as delegation - if delegation.is_empty() { - return Err(ContractError::EmptyDelegation); - } - - if delegation.len() > 1 { - return Err(ContractError::MultipleDenoms); - } - - // check that the denomination is correct - if delegation[0].denom != mix_denom { - return Err(ContractError::WrongDenom { mix_denom }); - } - - // check that we have provided a non-zero amount in the delegation - if delegation[0].amount.is_zero() { - return Err(ContractError::EmptyDelegation); - } - - Ok(delegation.pop().unwrap()) -} +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_identity: IdentityKey, -) -> Result { - // check if the delegation contains any funds of the appropriate denomination - let amount = - validate_delegation_stake(info.funds, mixnet_params_storage::mix_denom(deps.storage)?)?; - - _try_delegate_to_mixnode( - deps, - env.block.height, - &mix_identity, - info.sender.as_str(), - amount, - None, - ) + mix_id: MixId, +) -> Result { + _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_identity: IdentityKey, + mix_id: MixId, delegate: String, -) -> Result { - // check if the delegation contains any funds of the appropriate denomination - let amount = - validate_delegation_stake(info.funds, mixnet_params_storage::mix_denom(deps.storage)?)?; - - _try_delegate_to_mixnode( - deps, - env.block.height, - &mix_identity, - &delegate, - amount, - Some(info.sender), - ) -} - -pub(crate) fn try_reconcile_delegation( - storage: &mut dyn Storage, - delegation: Delegation, -) -> Result { - // update total_delegation of this node - mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>( - storage, - &delegation.node_identity, - |total_delegation| { - // since we know that the target node exists and because the total_delegation bucket - // entry is created whenever the node itself is added, the unwrap here is fine - // as the entry MUST exist - Ok(total_delegation.unwrap() + delegation.amount.amount) - }, - )?; - - // update [or create new] pending delegation of this delegator - storage::delegations().update::<_, ContractError>( - storage, - delegation.storage_key(), - |existing_delegation| { - Ok(match existing_delegation { - Some(mut existing_delegation) => { - existing_delegation - .increment_amount(delegation.amount.amount, Some(delegation.block_height)); - existing_delegation - } - None => delegation.clone(), - }) - }, - )?; - - Ok(new_pending_delegation_event( - &delegation.owner, - &delegation.proxy, - &delegation.amount, - &delegation.node_identity, - )) +) -> Result { + let delegate = deps.api.addr_validate(&delegate)?; + _try_delegate_to_mixnode(deps, env, mix_id, delegate, info.funds, Some(info.sender)) } pub(crate) fn _try_delegate_to_mixnode( deps: DepsMut<'_>, - block_height: u64, - mix_identity: &str, - delegate: &str, - amount: Coin, + env: Env, + mix_id: MixId, + delegate: Addr, + amount: Vec, proxy: Option, -) -> Result { - let delegate = deps.api.addr_validate(delegate)?; - - // check if the target node actually exists - if mixnodes_storage::mixnodes() - .may_load(deps.storage, mix_identity)? - .is_none() - { - return Err(ContractError::MixNodeBondNotFound { - identity: mix_identity.to_string(), - }); - } - - let delegation = Delegation::new( - delegate.to_owned(), - mix_identity.to_string(), - amount.clone(), - block_height, - proxy.clone(), - ); - - if storage::PENDING_DELEGATION_EVENTS - .may_load(deps.storage, delegation.event_storage_key())? - .is_some() - { - return Err(ContractError::DelegationEventAlreadyPending { - block_height, - identity: mix_identity.to_string(), - kind: "delgation".to_string(), - }); - } - - storage::PENDING_DELEGATION_EVENTS.save( - deps.storage, - delegation.event_storage_key(), - &DelegationEvent::Delegate(delegation), +) -> Result { + // check if the delegation contains any funds of the appropriate denomination + let contract_state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; + let delegation = validate_delegation_stake( + amount, + contract_state.params.minimum_mixnode_delegation, + contract_state.rewarding_denom, )?; - Ok(Response::new().add_event(new_pending_delegation_event( - &delegate, - &proxy, - &amount, - mix_identity, - ))) + // 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 { mix_id }), + Some(bond) if bond.is_unbonding => { + return Err(MixnetContractError::MixnodeIsUnbonding { mix_id }) + } + _ => (), + } + + // 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 = PendingEpochEventKind::Delegate { + owner: delegate, + mix_id, + amount: delegation, + proxy, + }; + 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_identity: IdentityKey, -) -> Result { - _try_remove_delegation_from_mixnode(deps, env, mix_identity, info.sender.as_str(), None) + mix_id: MixId, +) -> Result { + _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_identity: IdentityKey, + mix_id: MixId, delegate: String, -) -> Result { - _try_remove_delegation_from_mixnode(deps, env, mix_identity, &delegate, Some(info.sender)) -} - -pub struct ReconcileUndelegateResponse { - bank_msg: Option, - wasm_msg: Option, - event: Event, -} - -pub(crate) fn try_reconcile_undelegation( - storage: &mut dyn Storage, - api: &dyn Api, - pending_undelegate: &PendingUndelegate, -) -> Result { - let delegation_map = storage::delegations(); - let mix_denom = mixnet_params_storage::mix_denom(storage)?; - - // debug_with_visibility(api, "Reconciling undelegations"); - - let any_delegations = delegation_map - .prefix(pending_undelegate.storage_key()) - .keys(storage, None, None, cosmwasm_std::Order::Ascending) - .filter_map(|v| v.ok()) - .next() - .is_some(); - - if !any_delegations { - return Ok(ReconcileUndelegateResponse { - bank_msg: None, - wasm_msg: None, - event: new_error_event( - ContractError::NoMixnodeDelegationFound { - identity: pending_undelegate.mix_identity(), - address: pending_undelegate.delegate().to_string(), - } - .to_string(), - ), - }); - } - - let reward = crate::rewards::transactions::calculate_delegator_reward( - storage, - api, - pending_undelegate.proxy_storage_key(), - &pending_undelegate.mix_identity(), - )?; - - // debug_with_visibility(api, format!("Delegator reward: {}", reward)); - - // Might want to introduce paging here - let delegation_heights = delegation_map - .prefix(pending_undelegate.storage_key()) - .keys(storage, None, None, cosmwasm_std::Order::Ascending) - .filter_map(|v| v.ok()) - .collect::>(); - - if delegation_heights.is_empty() { - return Ok(ReconcileUndelegateResponse { - bank_msg: None, - wasm_msg: None, - event: new_error_event( - ContractError::NoMixnodeDelegationFound { - identity: pending_undelegate.mix_identity(), - address: pending_undelegate.delegate().to_string(), - } - .to_string(), - ), - }); - } - - let mut total_delegation = Uint128::zero(); - - // debug_with_visibility(api, "Reducing accumulated rewards"); - - { - if let Some(mut bond) = crate::mixnodes::storage::mixnodes() - .may_load(storage, &pending_undelegate.mix_identity())? - { - let remaining = bond.accumulated_rewards().saturating_sub(reward); - // debug_with_visibility(api, format!("Remaining accumulated rewards: {}", remaining)); - bond.accumulated_rewards = Some(remaining); - - crate::mixnodes::storage::mixnodes().save( - storage, - &pending_undelegate.mix_identity(), - &bond, - pending_undelegate.block_height(), - )?; - } - } - - for h in delegation_heights { - let delegation = - delegation_map.load(storage, pending_undelegate.delegation_key(h).clone())?; - total_delegation += delegation.amount.amount; - delegation_map.replace( - storage, - pending_undelegate.delegation_key(h), - None, - Some(&delegation), - )?; - } - - mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>( - storage, - &pending_undelegate.mix_identity(), - |total_node_delegation| { - // debug_with_visibility(api, "Setting total delegation"); - let remaining = match total_node_delegation.unwrap().checked_sub(total_delegation) { - Ok(remaining) => remaining, - Err(_) => { - // debug_with_visibility( - // api, - // format!( - // "Overflowed delegation subsctraction, {} - {}", - // total_node_delegation.unwrap(), - // total_delegation - // ), - // ); - return Err(ContractError::TotalDelegationSubOverflow { - mix_identity: pending_undelegate.mix_identity(), - total_node_delegation: total_node_delegation.unwrap().u128(), - to_subtract: total_delegation.u128(), - }); - } - }; - // debug_with_visibility(api, format!("Remaining total delegation: {}", remaining)); - // the first unwrap is fine because the delegation information MUST exist, otherwise we would - // have never gotten here in the first place - // the second unwrap is also fine because we should NEVER underflow here, - // if we do, it means we have some serious error in our logic - Ok(remaining) - }, - )?; - - let total_funds = total_delegation + reward; - - // don't add a bank message if it would have resulted in attempting to send 0 tokens - let bank_msg = if total_delegation != Uint128::zero() { - Some(BankMsg::Send { - to_address: pending_undelegate - .proxy() - .as_ref() - .unwrap_or(&pending_undelegate.delegate()) - .to_string(), - amount: coins(total_funds.u128(), mix_denom.clone()), - }) - } else { - None - }; - - let mut wasm_msg = None; - - if let Some(proxy) = &pending_undelegate.proxy() { - let msg = Some(VestingContractExecuteMsg::TrackUndelegation { - owner: pending_undelegate.delegate().as_str().to_string(), - mix_identity: pending_undelegate.mix_identity(), - amount: Coin::new(total_funds.u128(), mix_denom.clone()), - }); - - wasm_msg = Some(wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom)])?); - } - - let event = new_undelegation_event( - &pending_undelegate.delegate(), - &pending_undelegate.proxy(), - &pending_undelegate.mix_identity(), - total_funds, - ); - - // debug_with_visibility(api, "Done"); - - Ok(ReconcileUndelegateResponse { - bank_msg, - wasm_msg, - event, - }) +) -> Result { + let delegate = deps.api.addr_validate(&delegate)?; + _try_remove_delegation_from_mixnode(deps, env, mix_id, delegate, Some(info.sender)) } pub(crate) fn _try_remove_delegation_from_mixnode( deps: DepsMut<'_>, env: Env, - mix_identity: IdentityKey, - delegate: &str, + mix_id: MixId, + delegate: Addr, proxy: Option, -) -> Result { - let delegate = deps.api.addr_validate(delegate)?; +) -> Result { + // see if the delegation even exists + let storage_key = Delegation::generate_storage_key(mix_id, &delegate, proxy.as_ref()); - let event = PendingUndelegate::new( - mix_identity.to_string(), - delegate.clone(), - proxy.clone(), - env.block.height, - ); - - if storage::PENDING_DELEGATION_EVENTS - .may_load(deps.storage, event.event_storage_key())? - .is_some() + if storage::delegations() + .may_load(deps.storage, storage_key)? + .is_none() { - return Err(ContractError::DelegationEventAlreadyPending { - block_height: event.block_height(), - identity: mix_identity, - kind: "undelgation".to_string(), + return Err(MixnetContractError::NoMixnodeDelegationFound { + mix_id, + address: delegate.into_string(), + proxy: proxy.map(Addr::into_string), }); } - PENDING_DELEGATION_EVENTS.save( - deps.storage, - event.event_storage_key(), - &DelegationEvent::Undelegate(event), - )?; + // 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); - Ok(Response::new().add_event(new_pending_undelegation_event( - &delegate, - &proxy, - &mix_identity, - ))) + let epoch_event = PendingEpochEventKind::Undelegate { + owner: delegate, + mix_id, + proxy, + }; + interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; + + Ok(Response::new().add_event(cosmos_event)) } #[cfg(test)] mod tests { - use cosmwasm_std::coins; - - use crate::support::tests; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers; - use super::*; #[cfg(test)] - mod delegation_stake_validation { - use cosmwasm_std::coin; - - use super::*; - - #[test] - fn stake_cant_be_empty() { - assert_eq!( - Err(ContractError::EmptyDelegation), - validate_delegation_stake(vec![], TEST_COIN_DENOM.to_string()) - ) - } - - #[test] - fn stake_must_have_single_coin_type() { - assert_eq!( - Err(ContractError::MultipleDenoms), - validate_delegation_stake( - vec![ - coin(123, TEST_COIN_DENOM), - coin(123, "BTC"), - coin(123, "DOGE") - ], - TEST_COIN_DENOM.to_string() - ) - ) - } - - #[test] - fn stake_coin_must_be_of_correct_type() { - assert_eq!( - Err(ContractError::WrongDenom { - mix_denom: TEST_COIN_DENOM.to_string() - }), - validate_delegation_stake(coins(123, "DOGE"), TEST_COIN_DENOM.to_string()) - ) - } - - #[test] - fn stake_coin_must_have_value_greater_than_zero() { - assert_eq!( - Err(ContractError::EmptyDelegation), - validate_delegation_stake(coins(0, TEST_COIN_DENOM), TEST_COIN_DENOM.to_string()) - ) - } - - #[test] - fn stake_can_have_any_positive_value() { - // this might change in the future, but right now an arbitrary (positive) value can be delegated - assert!(validate_delegation_stake( - coins(1, TEST_COIN_DENOM), - TEST_COIN_DENOM.to_string() - ) - .is_ok()); - assert!(validate_delegation_stake( - coins(123, TEST_COIN_DENOM), - TEST_COIN_DENOM.to_string() - ) - .is_ok()); - assert!(validate_delegation_stake( - coins(10000000000, TEST_COIN_DENOM), - TEST_COIN_DENOM.to_string() - ) - .is_ok()); - } - } - - #[cfg(test)] - mod mix_stake_delegation { + mod delegating_to_mixnode { use super::*; use crate::mixnodes::transactions::try_remove_mixnode; - use cosmwasm_std::coin; - use cosmwasm_std::testing::mock_env; + use crate::rewards::storage as rewards_storage; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::Addr; + use cosmwasm_std::{coin, Decimal}; #[test] - fn fails_if_node_doesnt_exist() { - let mut deps = test_helpers::init_contract(); + 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(), env, sender, 42); assert_eq!( - Err(ContractError::MixNodeBondNotFound { - identity: "non-existent-mix-identity".into() - }), - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("sender", &coins(123, TEST_COIN_DENOM)), - "non-existent-mix-identity".into(), - ) - ); - } - - #[test] - fn succeeds_for_existing_node() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner = Addr::unchecked("sender"); - let delegation = coin(123, TEST_COIN_DENOM); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &[delegation.clone()]), - identity.clone(), - ) - .is_ok()); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - let expected = Delegation::new( - delegation_owner.clone(), - identity.clone(), - delegation.clone(), - mock_env().block.height, - None, - ); - - assert_eq!( - expected, - test_helpers::read_delegation( - &deps.storage, - &identity, - delegation_owner.as_bytes(), - mock_env().block.height - ) - .unwrap() - ); - - // node's "total_delegation" is increased - assert_eq!( - delegation.amount, - mixnodes_storage::TOTAL_DELEGATION - .load(&deps.storage, &identity) - .unwrap() + res, + Err(MixnetContractError::MixNodeBondNotFound { mix_id: 42 }) ) } #[test] - fn fails_if_node_unbonded() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), + 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(), env.clone(), sender1, mix_id); + assert_eq!(res, Err(MixnetContractError::EmptyDelegation)); + 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(), env, sender3, mix_id); + assert_eq!( + res, + Err(MixnetContractError::WrongDenom { + received: "some-weird-coin".to_string(), + expected: TEST_COIN_DENOM.to_string() + }) ); - let delegation_owner = Addr::unchecked("sender"); - let api = deps.api.clone(); + } + + #[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)]); + let sender2 = mock_info(owner, &[coin(150_000_000, TEST_COIN_DENOM)]); + + let min_delegation = coin(150_000_000, TEST_COIN_DENOM); + let mut contract_state = mixnet_params_storage::CONTRACT_STATE + .load(test.deps().storage) + .unwrap(); + contract_state.params.minimum_mixnode_delegation = Some(min_delegation.clone()); + mixnet_params_storage::CONTRACT_STATE + .save(test.deps_mut().storage, &contract_state) + .unwrap(); + + let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id); + assert_eq!( + res, + Err(MixnetContractError::InsufficientDelegation { + received: coin(100_000_000, TEST_COIN_DENOM), + minimum: min_delegation + }) + ); + + 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)]); + + let mix_id_unbonding = test.add_dummy_mixnode("mix-owner-unbonding", None); + let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None); + let mix_id_unbonded_leftover = + test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + + // manually adjust delegation info as to indicate the rewarding information shouldnt get removed + let mut rewarding_details = rewards_storage::MIXNODE_REWARDING + .load(test.deps().storage, mix_id_unbonded_leftover) + .unwrap(); + rewarding_details.delegates = Decimal::raw(12345); + rewarding_details.unique_delegations = 1; + rewards_storage::MIXNODE_REWARDING + .save( + test.deps_mut().storage, + mix_id_unbonded_leftover, + &rewarding_details, + ) + .unwrap(); + try_remove_mixnode( - &mock_env(), - deps.as_mut().storage, - &api, - mock_info(mixnode_owner, &[]), - ) - .unwrap(); - assert_eq!( - Err(ContractError::MixNodeBondNotFound { - identity: identity.clone() - }), - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &coins(123, TEST_COIN_DENOM)), - identity, - ) - ); - } - - #[test] - fn succeeds_if_node_rebonded() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let api = deps.api.clone(); - try_remove_mixnode( - &mock_env(), - deps.as_mut().storage, - &api, - mock_info(mixnode_owner, &[]), - ) - .unwrap(); - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation = coin(123, TEST_COIN_DENOM); - let delegation_owner = Addr::unchecked("sender"); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &[delegation.clone()]), - identity.clone(), - ) - .is_ok()); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - let expected = Delegation::new( - delegation_owner.clone(), - identity.clone(), - delegation.clone(), - mock_env().block.height, - None, - ); - - assert_eq!( - expected, - test_helpers::read_delegation( - &deps.storage, - &identity, - delegation_owner.as_bytes(), - mock_env().block.height - ) - .unwrap() - ); - - // node's "total_delegation" is increased - assert_eq!( - delegation.amount, - mixnodes_storage::TOTAL_DELEGATION - .load(&deps.storage, &identity) - .unwrap() - ) - } - - #[test] - fn is_possible_for_an_already_delegated_node() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner = Addr::unchecked("sender"); - let delegation1 = coin(100, TEST_COIN_DENOM); - let delegation2 = coin(50, TEST_COIN_DENOM); - - let mut env = mock_env(); - - try_delegate_to_mixnode( - deps.as_mut(), + test.deps_mut(), env.clone(), - mock_info(delegation_owner.as_str(), &[delegation1.clone()]), - identity.clone(), + mock_info("mix-owner-unbonded", &[]), ) .unwrap(); - - env.block.height += 1; - - try_delegate_to_mixnode( - deps.as_mut(), - env, - mock_info(delegation_owner.as_str(), &[delegation2.clone()]), - identity.clone(), - ) - .unwrap(); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - // let expected = Delegation::new( - // delegation_owner.clone(), - // identity.clone(), - // coin(delegation1.amount.u128() + delegation2.amount.u128(), TEST_COIN_DENOM), - // mock_env().block.height, - // None, - // ); - - // assert_eq!( - // expected, - // test_helpers::read_delegation( - // &deps.storage, - // &identity, - // delegation_owner.as_bytes(), - // mock_env().block.height - // ) - // .unwrap() - // ); - - // node's "total_delegation" is sum of both - assert_eq!( - delegation1.amount + delegation2.amount, - mixnodes_storage::TOTAL_DELEGATION - .load(&deps.storage, &identity) - .unwrap() - ) - } - - #[test] - fn block_height_is_updated_on_new_delegation() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner = Addr::unchecked("sender"); - let delegation = coin(100, TEST_COIN_DENOM); - let env1 = mock_env(); - let mut env2 = mock_env(); - let initial_height = env1.block.height; - let updated_height = initial_height + 42; - // second env has grown in block height - env2.block.height = updated_height; - try_delegate_to_mixnode( - deps.as_mut(), - env1.clone(), - mock_info(delegation_owner.as_str(), &[delegation.clone()]), - identity.clone(), - ) - .unwrap(); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - assert_eq!( - initial_height, - test_helpers::read_delegation( - &deps.storage, - &identity, - delegation_owner.as_bytes(), - env1.block.height - ) - .unwrap() - .block_height - ); - try_delegate_to_mixnode( - deps.as_mut(), - env2, - mock_info(delegation_owner.as_str(), &[delegation.clone()]), - identity.clone(), - ) - .unwrap(); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - let delegations = crate::delegations::queries::query_mixnode_delegation( - &deps.storage, - &deps.api, - identity, - delegation_owner.to_string(), - None, - ) - .unwrap(); - - let total_delegation = delegations - .iter() - .fold(Uint128::zero(), |acc, d| acc + d.amount.amount); - - assert_eq!(delegation.amount + delegation.amount, total_delegation); - } - - #[test] - fn block_height_is_not_updated_on_different_delegator() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner1 = Addr::unchecked("sender1"); - let delegation_owner2 = Addr::unchecked("sender2"); - let delegation1 = coin(100, TEST_COIN_DENOM); - let delegation2 = coin(120, TEST_COIN_DENOM); - let env1 = mock_env(); - let mut env2 = mock_env(); - let initial_height = env1.block.height; - let second_height = initial_height + 42; - // second env has grown in block height - env2.block.height = second_height; - try_delegate_to_mixnode( - deps.as_mut(), - env1.clone(), - mock_info(delegation_owner1.as_str(), &[delegation1]), - identity.clone(), - ) - .unwrap(); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - assert_eq!( - initial_height, - test_helpers::read_delegation( - &deps.storage, - &identity, - delegation_owner1.as_bytes(), - env1.block.height - ) - .unwrap() - .block_height - ); - try_delegate_to_mixnode( - deps.as_mut(), - env2.clone(), - mock_info(delegation_owner2.as_str(), &[delegation2]), - identity.clone(), - ) - .unwrap(); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - assert_eq!( - initial_height, - test_helpers::read_delegation( - &deps.storage, - &identity, - delegation_owner1.as_bytes(), - env1.block.height - ) - .unwrap() - .block_height - ); - assert_eq!( - second_height, - test_helpers::read_delegation( - &deps.storage, - identity, - delegation_owner2.as_bytes(), - env2.block.height - ) - .unwrap() - .block_height - ); - } - - #[test] - fn is_disallowed_for_already_delegated_node_if_it_unbonded() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner = Addr::unchecked("sender"); - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &coins(100, TEST_COIN_DENOM)), - identity.clone(), - ) - .unwrap(); - let api = deps.api.clone(); try_remove_mixnode( - &mock_env(), - deps.as_mut().storage, - &api, - mock_info(mixnode_owner, &[]), - ) - .unwrap(); - assert_eq!( - Err(ContractError::MixNodeBondNotFound { - identity: identity.clone() - }), - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &coins(50, TEST_COIN_DENOM)), - identity, - ) - ); - } - - #[test] - fn is_allowed_for_multiple_nodes() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner1 = "bob"; - let mixnode_owner2 = "fred"; - let identity1 = test_helpers::add_mixnode( - mixnode_owner1, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let identity2 = test_helpers::add_mixnode( - mixnode_owner2, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner = Addr::unchecked("sender"); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &coins(123, TEST_COIN_DENOM)), - identity1.clone(), - ) - .is_ok()); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &coins(42, TEST_COIN_DENOM)), - identity2.clone(), - ) - .is_ok()); - - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - let expected1 = Delegation::new( - delegation_owner.clone(), - identity1.clone(), - coin(123, TEST_COIN_DENOM), - mock_env().block.height, - None, - ); - - let expected2 = Delegation::new( - delegation_owner.clone(), - identity2.clone(), - coin(42, TEST_COIN_DENOM), - mock_env().block.height, - None, - ); - - assert_eq!( - expected1, - test_helpers::read_delegation( - &deps.storage, - identity1, - delegation_owner.as_bytes(), - mock_env().block.height - ) - .unwrap() - ); - assert_eq!( - expected2, - test_helpers::read_delegation( - &deps.storage, - identity2, - delegation_owner.as_bytes(), - mock_env().block.height - ) - .unwrap() - ); - } - - #[test] - fn is_allowed_by_multiple_users() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation1 = coin(123, TEST_COIN_DENOM); - let delegation2 = coin(234, TEST_COIN_DENOM); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("sender1", &[delegation1.clone()]), - identity.clone(), - ) - .is_ok()); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("sender2", &[delegation2.clone()]), - identity.clone(), - ) - .is_ok()); - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - // node's "total_delegation" is sum of both - assert_eq!( - delegation1.amount + delegation2.amount, - mixnodes_storage::TOTAL_DELEGATION - .load(&deps.storage, &identity) - .unwrap() - ) - } - - #[test] - fn delegation_is_not_removed_if_node_unbonded() { - let mut deps = test_helpers::init_contract(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner = Addr::unchecked("sender"); - let delegation_amount = coin(100, TEST_COIN_DENOM); - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner.as_str(), &[delegation_amount.clone()]), - identity.clone(), + test.deps_mut(), + env.clone(), + mock_info("mix-owner-unbonded-leftover", &[]), ) .unwrap(); - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - let api = deps.api.clone(); + test.execute_all_pending_events(); try_remove_mixnode( - &mock_env(), - deps.as_mut().storage, - &api, - mock_info(mixnode_owner, &[]), + test.deps_mut(), + env.clone(), + mock_info("mix-owner-unbonding", &[]), ) .unwrap(); - let expected = Delegation::new( - delegation_owner.clone(), - identity.clone(), - delegation_amount, - mock_env().block.height, - None, + let res = try_delegate_to_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id_unbonding, + ); + assert_eq!( + res, + Err(MixnetContractError::MixnodeIsUnbonding { + mix_id: mix_id_unbonding + }) + ); + + let res = try_delegate_to_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id_unbonded, + ); + assert_eq!( + res, + Err(MixnetContractError::MixNodeBondNotFound { + mix_id: mix_id_unbonded + }) + ); + + let res = + try_delegate_to_mixnode(test.deps_mut(), env, sender, mix_id_unbonded_leftover); + assert_eq!( + res, + Err(MixnetContractError::MixNodeBondNotFound { + mix_id: mix_id_unbonded_leftover + }) + ); + } + + #[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(), env.clone(), sender1, mix_id); + assert!(res.is_ok()); + + // still OK + 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); + + let amount1 = coin(100_000_000, TEST_COIN_DENOM); + let amount2 = coin(50_000_000, TEST_COIN_DENOM); + + 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(), 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].kind, + PendingEpochEventKind::Delegate { + owner: Addr::unchecked(owner), + mix_id, + amount: amount1, + proxy: None + } ); assert_eq!( - expected, - test_helpers::read_delegation( - &deps.storage, - identity, - delegation_owner.as_bytes(), - mock_env().block.height - ) - .unwrap() - ) + events[1].kind, + PendingEpochEventKind::Delegate { + owner: Addr::unchecked(owner), + mix_id, + amount: amount2, + proxy: Some(test.vesting_contract()) + } + ); } } #[cfg(test)] - mod removing_mix_stake_delegation { + mod removing_mixnode_delegation { use super::*; - use crate::support::tests; + use crate::mixnodes::transactions::try_remove_mixnode; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::coin; - use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::Addr; #[test] - fn total_delegation_is_preserved_if_only_some_undelegate() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let mixnode_owner = "bob"; - let identity = test_helpers::add_mixnode( - mixnode_owner, - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - let delegation_owner1 = Addr::unchecked("sender1"); - let delegation_owner2 = Addr::unchecked("sender2"); - let delegation1 = coin(123, TEST_COIN_DENOM); - let delegation2 = coin(234, TEST_COIN_DENOM); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner1.as_str(), &[delegation1]), - identity.clone(), + 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(), env, sender, mix_id); + assert_eq!( + res, + Err(MixnetContractError::NoMixnodeDelegationFound { + mix_id, + address: owner.to_string(), + proxy: None + }) ) - .is_ok()); + } - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); + #[test] + fn cannot_be_performed_if_the_delegation_is_still_pending() { + let mut test = TestSetup::new(); + let env = test.env(); - assert!(try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info(delegation_owner2.as_str(), &[delegation2.clone()]), - identity.clone(), + 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(), env.clone(), sender1, mix_id).unwrap(); + + let res = try_remove_delegation_from_mixnode(test.deps_mut(), env, sender2, mix_id); + assert_eq!( + res, + Err(MixnetContractError::NoMixnodeDelegationFound { + mix_id, + address: owner.to_string(), + proxy: None + }) ) - .is_ok()); + } - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); + #[test] + fn as_long_as_delegation_exists_can_always_be_performed() { + let mut test = TestSetup::new(); + let env = test.env(); - // sender1 undelegates - try_remove_delegation_from_mixnode( - deps.as_mut(), - env, - mock_info(delegation_owner1.as_str(), &[]), - identity.clone(), + let owner = "delegator"; + let sender = mock_info(owner, &[]); + + let normal_mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id_unbonding = test.add_dummy_mixnode("mix-owner-unbonding", None); + let mix_id_unbonded_leftover = + test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + + test.add_immediate_delegation(owner, 10000u32, normal_mix_id); + test.add_immediate_delegation(owner, 10000u32, mix_id_unbonding); + test.add_immediate_delegation(owner, 10000u32, mix_id_unbonded_leftover); + + try_remove_mixnode( + test.deps_mut(), + env.clone(), + mock_info("mix-owner-unbonded-leftover", &[]), ) .unwrap(); - _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - // but total delegation should still equal to what sender2 sent - // node's "total_delegation" is sum of both - assert_eq!( - delegation2.amount, - mixnodes_storage::TOTAL_DELEGATION - .load(&deps.storage, &identity) - .unwrap() + test.execute_all_pending_events(); + try_remove_mixnode( + test.deps_mut(), + env.clone(), + mock_info("mix-owner-unbonding", &[]), ) + .unwrap(); + + 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, + ); + assert!(res.is_ok()); + + let res = try_remove_delegation_from_mixnode( + test.deps_mut(), + env, + sender, + mix_id_unbonded_leftover, + ); + assert!(res.is_ok()); } } - - // #[cfg(test)] - // mod multi_delegations { - // use super::*; - // use crate::delegations::helpers; - // use crate::delegations::queries::tests::store_n_mix_delegations; - // use crate::support::tests::test_helpers; - // use mixnet_contract::IdentityKey; - // use mixnet_contract::RawDelegationData; - // - // #[test] - // fn multiple_page_delegations() { - // let mut deps = test_helpers::init_contract(); - // let node_identity: IdentityKey = "foo".into(); - // store_n_mix_delegations( - // storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10, - // &mut deps.storage, - // &node_identity, - // ); - // let mix_bucket = storage::all_mix_delegations_read::(&deps.storage); - // let mix_delegations = helpers::Delegations::new(mix_bucket); - // assert_eq!( - // storage::DELEGATION_PAGE_DEFAULT_LIMIT * 10, - // mix_delegations.count() as u32 - // ); - // } - // } } diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs deleted file mode 100644 index e1d3a22335..0000000000 --- a/contracts/mixnet/src/error.rs +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use cosmwasm_std::{Addr, StdError}; -use mixnet_contract_common::{error::MixnetContractError, IdentityKey}; -use thiserror::Error; - -/// Custom errors for contract failure conditions. -/// -/// Add any other custom errors you like here. -/// Look at https://docs.rs/thiserror/1.0.21/thiserror/ for details. -#[derive(Error, Debug, PartialEq)] -pub enum ContractError { - #[error("MIXNET ({}): {0}", line!())] - Std(#[from] StdError), - - #[error("MIXNET ({}): Not enough funds sent for mixnode bond. (received {received}, minimum {minimum})", line!())] - InsufficientMixNodeBond { received: u128, minimum: u128 }, - - #[error("MIXNET ({}): Mixnode ({identity}) does not exist", line!())] - MixNodeBondNotFound { identity: IdentityKey }, - - #[error("MIXNET ({}): Not enough funds sent for gateway bond. (received {received}, minimum {minimum})", line!())] - InsufficientGatewayBond { received: u128, minimum: u128 }, - - #[error("MIXNET ({}): {owner} does not seem to own any mixnodes", line!())] - NoAssociatedMixNodeBond { owner: Addr }, - - #[error("MIXNET ({}): {owner} does not seem to own any gateways", line!())] - NoAssociatedGatewayBond { owner: Addr }, - - #[error("MIXNET ({}): Unauthorized", line!())] - Unauthorized, - - #[error("MIXNET ({}): Wrong coin denomination, you must send {mix_denom}", line!())] - WrongDenom { mix_denom: String }, - - #[error("MIXNET ({}): Received multiple coin types during staking", line!())] - MultipleDenoms, - - #[error("MIXNET ({}): No coin was sent for the bonding, you must send {mix_denom}", line!())] - NoBondFound { mix_denom: String }, - - #[error("MIXNET ({}): Provided active set size is bigger than the rewarded set", line!())] - InvalidActiveSetSize, - - #[error("MIXNET ({}): Provided active set size is zero", line!())] - ZeroActiveSet, - - #[error("MIXNET ({}): Provided rewarded set size is zero", line!())] - ZeroRewardedSet, - - #[error("MIXNET ({}): This address has already bonded a mixnode", line!())] - AlreadyOwnsMixnode, - - #[error("MIXNET ({}): This address has already bonded a gateway", line!())] - AlreadyOwnsGateway, - - #[error("MIXNET ({}): Mixnode with this identity already exists. Its owner is {owner}", line!())] - DuplicateMixnode { owner: Addr }, - - #[error("MIXNET ({}): Gateway with this identity already exists. Its owner is {owner}", line!())] - DuplicateGateway { owner: Addr }, - - #[error("MIXNET ({}): No funds were provided for the delegation", line!())] - EmptyDelegation, - - #[error("MIXNET ({}): Could not find any delegation information associated with mixnode {identity} for {address}", line!())] - NoMixnodeDelegationFound { - identity: IdentityKey, - address: String, - }, - - #[error("MIXNET ({}): We tried to remove more funds then are available in the Reward pool. Wanted to remove {to_remove}, but have only {reward_pool}", line!())] - OutOfFunds { to_remove: u128, reward_pool: u128 }, - - #[error("MIXNET ({}): Received invalid interval id. Expected {expected}, received {received}", line!())] - InvalidIntervalId { received: u32, expected: u32 }, - - #[error("MIXNET ({}): Mixnode {identity} has already been rewarded during the current rewarding interval", line!())] - MixnodeAlreadyRewarded { identity: IdentityKey }, - - #[error("MIXNET ({}): Some of mixnodes {identity} delegators are still pending reward", line!())] - DelegatorsPendingReward { identity: IdentityKey }, - - #[error("MIXNET ({}): Mixnode's {identity} operator has not been rewarded yet - cannot perform delegator rewarding until that happens", line!())] - MixnodeOperatorNotRewarded { identity: IdentityKey }, - - #[error("MIXNET ({}): Proxy address mismatch, expected {existing}, got {incoming}", line!())] - ProxyMismatch { existing: String, incoming: String }, - - #[error("MIXNET ({}): Failed to recover ed25519 public key from its base58 representation - {0}", line!())] - MalformedEd25519IdentityKey(String), - - #[error("MIXNET ({}): Failed to recover ed25519 signature from its base58 representation - {0}", line!())] - MalformedEd25519Signature(String), - - #[error("MIXNET ({}): Provided ed25519 signature did not verify correctly", line!())] - InvalidEd25519Signature, - - #[error("MIXNET ({}): Profit margin percent needs to be an integer in range [0, 100], received {0}", line!())] - InvalidProfitMarginPercent(u8), - - #[error("MIXNET ({}): Rewarded set height not set, was rewarding set determined?", line!())] - RewardSetHeightMapEmpty, - - #[error("MIXNET ({}): Received unexpected value for the active set. Got: {received}, expected: {expected}", line!())] - UnexpectedActiveSetSize { received: u32, expected: u32 }, - - #[error("MIXNET ({}): Received unexpected value for the rewarded set. Got: {received}, expected at most: {expected}", line!())] - UnexpectedRewardedSetSize { received: u32, expected: u32 }, - - #[error("MIXNET ({}): There hasn't been sufficient delay since last rewarded set update. It was last updated at height {last_update}. The delay is {minimum_delay}. The current block height is {current_height}", line!())] - TooFrequentRewardedSetUpdate { - last_update: u64, - minimum_delay: u64, - current_height: u64, - }, - - #[error("MIXNET ({}): Can't change to the desired interval as it's not in progress yet. It starts at {interval_start} and finishes at {interval_end}, while the current block time is {current_block_time}", line!())] - IntervalNotInProgress { - current_block_time: u64, - interval_start: i64, - interval_end: i64, - }, - - #[error("MIXNET ({}): Can't change to the desired interval as it's not in progress yet. It starts at {epoch_start} and finishes at {epoch_end}, while the current block time is {current_block_time}", line!())] - EpochNotInProgress { - current_block_time: u64, - epoch_start: i64, - epoch_end: i64, - }, - - #[error("MIXNET ({}): Can't change to the desired interval as it hasn't started yet. It starts at {epoch_start} and finishes at {epoch_end}, while the current block time is {current_block_time}", line!())] - EpochNotStarted { - current_block_time: u64, - epoch_start: i64, - epoch_end: i64, - }, - - #[error("MIXNET ({}): Can't change to the desired interval as it's in progress. It starts at {epoch_start} and finishes at {epoch_end}, while the current block time is {current_block_time}", line!())] - EpochInProgress { - current_block_time: u64, - epoch_start: i64, - epoch_end: i64, - }, - - #[error("Could not cast reward to a u128, this should be impossible, at {}", line!())] - CastError, - #[error("{source}")] - MixnetCommonError { - #[from] - source: MixnetContractError, - }, - #[error("No rewards to claim for mixnode {identity} for {address}")] - NoRewardsToClaim { identity: String, address: String }, - - #[error("Epoch not initialized yet!")] - EpochNotInitialized, - - #[error("Invalid address: {0}")] - InvalidAddress(String), - - #[error("Pending {kind} event already exists at block {block_height} for mixnode {identity}")] - DelegationEventAlreadyPending { - block_height: u64, - identity: String, - kind: String, // delegation | undelegation - }, - #[error("Attempted to subsctract more then the total delegation, this MUST never happen! mix: {mix_identity}, total_node_delegation {total_node_delegation}, to_subtract {to_subtract}")] - TotalDelegationSubOverflow { - mix_identity: String, - total_node_delegation: u128, - to_subtract: u128, - }, - #[error("Profit margin can be updated only once during a rolling 30 day interval, last update was at {last_update_time} and current block time is {current_block_time}")] - UpdatePMTooSoon { - last_update_time: u64, - current_block_time: u64, - }, - #[error("`mix_identity` is required when `delegator` is set")] - MissingMixIdentity, - #[error("Compounding has been disabled temporarily")] - CompoundDisabled, - - #[error("Mixnode {identity} has been blacklisted on the network")] - MixnodeBlacklisted { identity: String }, -} diff --git a/contracts/mixnet/src/gateways/queries.rs b/contracts/mixnet/src/gateways/queries.rs index 87237f39ec..cc58b58944 100644 --- a/contracts/mixnet/src/gateways/queries.rs +++ b/contracts/mixnet/src/gateways/queries.rs @@ -1,8 +1,8 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::mixnodes::storage::{BOND_PAGE_DEFAULT_LIMIT, BOND_PAGE_MAX_LIMIT}; // Keeps gateway and mixnode retrieval in sync by re-using the constant. Could be split into its own constant. +use crate::constants::{GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT, GATEWAY_BOND_MAX_RETRIEVAL_LIMIT}; // Keeps gateway and mixnode retrieval in sync by re-using the constant. Could be split into its own constant. use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::Bound; use mixnet_contract_common::{ @@ -15,8 +15,8 @@ pub(crate) fn query_gateways_paged( limit: Option, ) -> StdResult { let limit = limit - .unwrap_or(BOND_PAGE_DEFAULT_LIMIT) - .min(BOND_PAGE_MAX_LIMIT) as usize; + .unwrap_or(GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT) + .min(GATEWAY_BOND_MAX_RETRIEVAL_LIMIT) as usize; let start = start_after.as_deref().map(Bound::exclusive); @@ -31,7 +31,7 @@ pub(crate) fn query_gateways_paged( Ok(PagedGatewayResponse::new(nodes, limit, start_next_after)) } -pub(crate) fn query_owns_gateway( +pub(crate) fn query_owned_gateway( deps: Deps<'_>, address: String, ) -> StdResult { @@ -74,11 +74,11 @@ pub(crate) mod tests { #[test] fn gateways_paged_retrieval_obeys_limits() { let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + let limit = 2; - for n in 0..1000 { - let key = format!("bond{}", n); - test_helpers::add_gateway(&key, tests::fixtures::good_gateway_pledge(), deps.as_mut()); - } + test_helpers::add_dummy_gateways(&mut rng, deps.as_mut(), env, 1000); let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(limit)).unwrap(); assert_eq!(limit, page1.nodes.len() as u32); @@ -87,50 +87,55 @@ pub(crate) mod tests { #[test] fn gateways_paged_retrieval_has_default_limit() { let mut deps = test_helpers::init_contract(); - for n in 0..1000 { - let key = format!("bond{}", n); - test_helpers::add_gateway(&key, tests::fixtures::good_gateway_pledge(), deps.as_mut()); - } + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + test_helpers::add_dummy_gateways(&mut rng, deps.as_mut(), env, 1000); // query without explicitly setting a limit let page1 = query_gateways_paged(deps.as_ref(), None, None).unwrap(); - assert_eq!(BOND_PAGE_DEFAULT_LIMIT, page1.nodes.len() as u32); + assert_eq!( + GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); } #[test] fn gateways_paged_retrieval_has_max_limit() { let mut deps = test_helpers::init_contract(); - for n in 0..1000 { - let key = format!("bond{}", n); - test_helpers::add_gateway(&key, tests::fixtures::good_gateway_pledge(), deps.as_mut()); - } + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + test_helpers::add_dummy_gateways(&mut rng, deps.as_mut(), env, 1000); // query with a crazily high limit in an attempt to use too many resources - let crazy_limit = 1000 * BOND_PAGE_DEFAULT_LIMIT; + let crazy_limit = 1000 * GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT; let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap(); // we default to a decent sized upper bound instead - let expected_limit = BOND_PAGE_MAX_LIMIT; + let expected_limit = GATEWAY_BOND_MAX_RETRIEVAL_LIMIT; assert_eq!(expected_limit, page1.nodes.len() as u32); } #[test] fn gateway_pagination_works() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + // prepare 4 messages and identities that are sorted by the generated identities // (because we query them in an ascended manner) let mut exec_data = (0..4) .map(|i| { let sender = format!("nym-addr{}", i); - let (msg, identity) = tests::messages::valid_bond_gateway_msg(&sender); + let (msg, identity) = tests::messages::valid_bond_gateway_msg(&mut rng, &sender); (msg, (sender, identity)) }) .collect::>(); exec_data.sort_by(|(_, (_, id1)), (_, (_, id2))| id1.cmp(id2)); let (messages, sender_identities): (Vec<_>, Vec<_>) = exec_data.into_iter().unzip(); - let mut deps = test_helpers::init_contract(); - let info = mock_info( &sender_identities[0].0.clone(), &tests::fixtures::good_gateway_pledge(), @@ -196,32 +201,42 @@ pub(crate) mod tests { #[test] fn query_for_gateway_owner_works() { let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); // "fred" does not own a mixnode if there are no mixnodes - let res = query_owns_gateway(deps.as_ref(), "fred".to_string()).unwrap(); + let res = query_owned_gateway(deps.as_ref(), "fred".to_string()).unwrap(); assert!(res.gateway.is_none()); // mixnode was added to "bob", "fred" still does not own one - test_helpers::add_gateway("bob", tests::fixtures::good_gateway_pledge(), deps.as_mut()); + test_helpers::add_gateway( + &mut rng, + deps.as_mut(), + env.clone(), + "bob", + tests::fixtures::good_gateway_pledge(), + ); - let res = query_owns_gateway(deps.as_ref(), "fred".to_string()).unwrap(); + let res = query_owned_gateway(deps.as_ref(), "fred".to_string()).unwrap(); assert!(res.gateway.is_none()); // "fred" now owns a gateway! test_helpers::add_gateway( + &mut rng, + deps.as_mut(), + env, "fred", tests::fixtures::good_gateway_pledge(), - deps.as_mut(), ); - let res = query_owns_gateway(deps.as_ref(), "fred".to_string()).unwrap(); + let res = query_owned_gateway(deps.as_ref(), "fred".to_string()).unwrap(); assert!(res.gateway.is_some()); // but after unbonding it, he doesn't own one anymore crate::gateways::transactions::try_remove_gateway(deps.as_mut(), mock_info("fred", &[])) .unwrap(); - let res = query_owns_gateway(deps.as_ref(), "fred".to_string()).unwrap(); + let res = query_owned_gateway(deps.as_ref(), "fred".to_string()).unwrap(); assert!(res.gateway.is_none()); } } diff --git a/contracts/mixnet/src/gateways/storage.rs b/contracts/mixnet/src/gateways/storage.rs index 89786a3019..9b5892844a 100644 --- a/contracts/mixnet/src/gateways/storage.rs +++ b/contracts/mixnet/src/gateways/storage.rs @@ -1,14 +1,11 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::constants::{GATEWAYS_OWNER_IDX_NAMESPACE, GATEWAYS_PK_NAMESPACE}; use cosmwasm_std::Addr; use cw_storage_plus::{Index, IndexList, IndexedMap, UniqueIndex}; use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; -// storage prefixes -const GATEWAYS_PK_NAMESPACE: &str = "gt"; -const GATEWAYS_OWNER_IDX_NAMESPACE: &str = "gto"; - pub(crate) struct GatewayBondIndex<'a> { pub(crate) owner: UniqueIndex<'a, Addr, GatewayBond>, } @@ -30,77 +27,3 @@ pub(crate) fn gateways<'a>() -> IndexedMap<'a, IdentityKeyRef<'a>, GatewayBond, }; IndexedMap::new(GATEWAYS_PK_NAMESPACE, indexes) } - -// currently not used outside tests -#[cfg(test)] -mod tests { - use super::super::storage; - use crate::support::tests; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use cosmwasm_std::testing::MockStorage; - use cosmwasm_std::StdResult; - use cosmwasm_std::Storage; - use cosmwasm_std::{coin, Addr, Uint128}; - use mixnet_contract_common::{Gateway, GatewayBond, IdentityKey, IdentityKeyRef}; - - // currently this is only used in tests but may become useful later on - pub(crate) fn read_gateway_pledge_amount( - storage: &dyn Storage, - identity: IdentityKeyRef<'_>, - ) -> StdResult { - let node = storage::gateways().load(storage, identity)?; - Ok(node.pledge_amount.amount) - } - - #[test] - fn gateway_single_read_retrieval() { - let mut storage = MockStorage::new(); - let bond1 = tests::fixtures::gateway_bond_fixture("owner1"); - let bond2 = tests::fixtures::gateway_bond_fixture("owner2"); - storage::gateways() - .save(&mut storage, "bond1", &bond1) - .unwrap(); - storage::gateways() - .save(&mut storage, "bond2", &bond2) - .unwrap(); - - let res1 = storage::gateways().load(&storage, "bond1").unwrap(); - let res2 = storage::gateways().load(&storage, "bond2").unwrap(); - assert_eq!(bond1, res1); - assert_eq!(bond2, res2); - } - - #[test] - fn reading_gateway_bond() { - let mut mock_storage = MockStorage::new(); - let node_owner: Addr = Addr::unchecked("node-owner"); - let node_identity: IdentityKey = "nodeidentity".into(); - - // produces an error if target gateway doesn't exist - let res = read_gateway_pledge_amount(&mock_storage, &node_identity); - assert!(res.is_err()); - - // returns appropriate value otherwise - let pledge_amount = 1000; - - let gateway_bond = GatewayBond { - pledge_amount: coin(pledge_amount, TEST_COIN_DENOM), - owner: node_owner, - block_height: 12_345, - gateway: Gateway { - identity_key: node_identity.clone(), - ..tests::fixtures::gateway_fixture() - }, - proxy: None, - }; - - storage::gateways() - .save(&mut mock_storage, &node_identity, &gateway_bond) - .unwrap(); - - assert_eq!( - Uint128::new(pledge_amount), - read_gateway_pledge_amount(&mock_storage, &node_identity).unwrap() - ); - } -} diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index 6315a2b2e2..55973f7589 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -1,17 +1,16 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::error::ContractError; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::support::helpers::{ensure_no_existing_bond, validate_node_identity_signature}; -use cosmwasm_std::{ - wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Uint128, +use crate::support::helpers::{ + ensure_no_existing_bond, validate_node_identity_signature, validate_pledge, }; +use cosmwasm_std::{wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{new_gateway_bonding_event, new_gateway_unbonding_event}; -use mixnet_contract_common::{Gateway, GatewayBond, Layer}; +use mixnet_contract_common::{Gateway, GatewayBond}; use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; -use vesting_contract_common::one_ucoin; pub fn try_add_gateway( deps: DepsMut<'_>, @@ -19,21 +18,13 @@ pub fn try_add_gateway( info: MessageInfo, gateway: Gateway, owner_signature: String, -) -> Result { - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::CONTRACT_STATE - .load(deps.storage)? - .params - .minimum_mixnode_pledge; - let mix_denom = mixnet_params_storage::mix_denom(deps.storage)?; - let pledge = validate_gateway_pledge(info.funds, minimum_pledge, mix_denom)?; - +) -> Result { _try_add_gateway( deps, env, gateway, - pledge, - info.sender.as_str(), + info.funds, + info.sender, owner_signature, None, ) @@ -46,22 +37,15 @@ pub fn try_add_gateway_on_behalf( gateway: Gateway, owner: String, owner_signature: String, -) -> Result { - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::CONTRACT_STATE - .load(deps.storage)? - .params - .minimum_mixnode_pledge; - let mix_denom = mixnet_params_storage::mix_denom(deps.storage)?; - let pledge = validate_gateway_pledge(info.funds, minimum_pledge, mix_denom)?; - +) -> Result { let proxy = info.sender; + let owner = deps.api.addr_validate(&owner)?; _try_add_gateway( deps, env, gateway, - pledge, - &owner, + info.funds, + owner, owner_signature, Some(proxy), ) @@ -71,12 +55,14 @@ pub(crate) fn _try_add_gateway( deps: DepsMut<'_>, env: Env, gateway: Gateway, - pledge: Coin, - owner: &str, + pledge: Vec, + owner: Addr, owner_signature: String, proxy: Option, -) -> Result { - let owner = deps.api.addr_validate(owner)?; +) -> Result { + // check if the pledge contains any funds of the appropriate denomination + let minimum_pledge = mixnet_params_storage::minimum_gateway_pledge(deps.storage)?; + let pledge = validate_pledge(pledge, minimum_pledge)?; // if the client has an active bonded mixnode or gateway, don't allow bonding ensure_no_existing_bond(deps.storage, &owner)?; @@ -86,7 +72,7 @@ pub(crate) fn _try_add_gateway( storage::gateways().may_load(deps.storage, &gateway.identity_key)? { if existing_bond.owner != owner { - return Err(ContractError::DuplicateGateway { + return Err(MixnetContractError::DuplicateGateway { owner: existing_bond.owner, }); } @@ -110,7 +96,6 @@ pub(crate) fn _try_add_gateway( ); storage::gateways().save(deps.storage, bond.identity(), &bond)?; - mixnet_params_storage::increment_layer_count(deps.storage, Layer::Gateway)?; Ok(Response::new().add_event(new_gateway_bonding_event( &owner, @@ -124,22 +109,24 @@ pub fn try_remove_gateway_on_behalf( deps: DepsMut<'_>, info: MessageInfo, owner: String, -) -> Result { +) -> Result { let proxy = info.sender; - _try_remove_gateway(deps, &owner, Some(proxy)) + let owner = deps.api.addr_validate(&owner)?; + _try_remove_gateway(deps, owner, Some(proxy)) } -pub fn try_remove_gateway(deps: DepsMut<'_>, info: MessageInfo) -> Result { - _try_remove_gateway(deps, info.sender.as_ref(), None) +pub fn try_remove_gateway( + deps: DepsMut<'_>, + info: MessageInfo, +) -> Result { + _try_remove_gateway(deps, info.sender, None) } pub(crate) fn _try_remove_gateway( deps: DepsMut<'_>, - owner: &str, + owner: Addr, proxy: Option, -) -> Result { - let owner = deps.api.addr_validate(owner)?; - let mix_denom = mixnet_params_storage::mix_denom(deps.storage)?; +) -> Result { // try to find the node of the sender let gateway_bond = match storage::gateways() .idx @@ -147,11 +134,11 @@ pub(crate) fn _try_remove_gateway( .item(deps.storage, owner.clone())? { Some(record) => record.1, - None => return Err(ContractError::NoAssociatedGatewayBond { owner }), + None => return Err(MixnetContractError::NoAssociatedGatewayBond { owner }), }; if proxy != gateway_bond.proxy { - return Err(ContractError::ProxyMismatch { + return Err(MixnetContractError::ProxyMismatch { existing: gateway_bond .proxy .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), @@ -168,9 +155,6 @@ pub(crate) fn _try_remove_gateway( // remove the bond storage::gateways().remove(deps.storage, gateway_bond.identity())?; - // decrement layer count - mixnet_params_storage::decrement_layer_count(deps.storage, Layer::Gateway)?; - let mut response = Response::new().add_message(return_tokens); if let Some(proxy) = &proxy { @@ -179,7 +163,7 @@ pub(crate) fn _try_remove_gateway( amount: gateway_bond.pledge_amount(), }; - let track_unbond_message = wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom)])?; + let track_unbond_message = wasm_execute(proxy, &msg, vec![])?; response = response.add_message(track_unbond_message); } @@ -191,235 +175,116 @@ pub(crate) fn _try_remove_gateway( ))) } -fn validate_gateway_pledge( - mut pledge: Vec, - minimum_pledge: Uint128, - mix_denom: String, -) -> Result { - // check if anything was put as bond - if pledge.is_empty() { - return Err(ContractError::NoBondFound { mix_denom }); - } - - if pledge.len() > 1 { - return Err(ContractError::MultipleDenoms); - } - - // check that the denomination is correct - if pledge[0].denom != mix_denom { - return Err(ContractError::WrongDenom { mix_denom }); - } - - // check that we have at least 100 coins in our pledge - if pledge[0].amount < minimum_pledge { - return Err(ContractError::InsufficientGatewayBond { - received: pledge[0].amount.into(), - minimum: minimum_pledge.into(), - }); - } - - Ok(pledge.pop().unwrap()) -} - #[cfg(test)] pub mod tests { - use super::*; - use crate::contract::{execute, query, INITIAL_GATEWAY_PLEDGE}; - use crate::error::ContractError; - use crate::gateways::transactions::validate_gateway_pledge; + use crate::contract::execute; + use crate::gateways::transactions::try_add_gateway; + use crate::interval::pending_events; + use crate::mixnet_contract_settings::storage::minimum_gateway_pledge; use crate::support::tests; use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers; + use crate::support::tests::{fixtures, test_helpers}; use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::{coins, BankMsg, Response}; - use cosmwasm_std::{from_binary, Addr, Uint128}; - use mixnet_contract_common::{ExecuteMsg, Gateway, PagedGatewayResponse, QueryMsg}; + use cosmwasm_std::{coin, Addr, BankMsg, Response, Uint128}; + use mixnet_contract_common::error::MixnetContractError; + use mixnet_contract_common::events::new_gateway_unbonding_event; + use mixnet_contract_common::ExecuteMsg; #[test] fn gateway_add() { let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); // if we fail validation (by say not sending enough funds - let insufficient_bond = Into::::into(INITIAL_GATEWAY_PLEDGE) - 1; - let info = mock_info("anyone", &coins(insufficient_bond, TEST_COIN_DENOM)); - let (msg, _) = tests::messages::valid_bond_gateway_msg("anyone"); + let sender = "alice"; + let minimum_pledge = minimum_gateway_pledge(deps.as_ref().storage).unwrap(); + let mut insufficient_pledge = minimum_pledge.clone(); + insufficient_pledge.amount -= Uint128::new(1000); + + let info = mock_info(sender, &[insufficient_pledge.clone()]); + let (gateway, sig) = test_helpers::gateway_with_signature(&mut rng, sender); + + let result = try_add_gateway( + deps.as_mut(), + env.clone(), + info, + gateway.clone(), + sig.clone(), + ); // we are informed that we didn't send enough funds - let result = execute(deps.as_mut(), mock_env(), info, msg); assert_eq!( result, - Err(ContractError::InsufficientGatewayBond { - received: insufficient_bond, - minimum: INITIAL_GATEWAY_PLEDGE.into(), + Err(MixnetContractError::InsufficientPledge { + received: insufficient_pledge, + minimum: minimum_pledge.clone(), }) ); - // make sure no gateway was inserted into the topology - let res = query( - deps.as_ref(), - mock_env(), - QueryMsg::GetGateways { - start_after: None, - limit: Option::from(2), - }, - ) - .unwrap(); - let page: PagedGatewayResponse = from_binary(&res).unwrap(); - assert_eq!(0, page.nodes.len()); + // if the signature provided is invalid, the bonding also fails + let info = mock_info(sender, &[minimum_pledge]); - // if we send enough funds - let info = mock_info("anyone", &tests::fixtures::good_gateway_pledge()); - let (msg, identity) = tests::messages::valid_bond_gateway_msg("anyone"); - - // we get back a message telling us everything was OK - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert!(execute_response.is_ok()); - - // we can query topology and the new node is there - let query_response = query( - deps.as_ref(), - mock_env(), - QueryMsg::GetGateways { - start_after: None, - limit: Option::from(2), - }, - ) - .unwrap(); - let page: PagedGatewayResponse = from_binary(&query_response).unwrap(); - assert_eq!(1, page.nodes.len()); - assert_eq!( - &Gateway { - identity_key: identity, - ..tests::fixtures::gateway_fixture() - }, - page.nodes[0].gateway() + let result = try_add_gateway( + deps.as_mut(), + env.clone(), + info.clone(), + gateway.clone(), + "bad-signature".into(), ); + assert!(matches!( + result, + Err(MixnetContractError::MalformedEd25519Signature(..)) + )); // if there was already a gateway bonded by particular user - let info = mock_info("foomper", &tests::fixtures::good_gateway_pledge()); - let (msg, _) = tests::messages::valid_bond_gateway_msg("foomper"); - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - let info = mock_info("foomper", &tests::fixtures::good_gateway_pledge()); - let (msg, _) = tests::messages::valid_bond_gateway_msg("foomper"); + test_helpers::add_gateway( + &mut rng, + deps.as_mut(), + env.clone(), + sender, + fixtures::good_gateway_pledge(), + ); // it fails - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(Err(ContractError::AlreadyOwnsGateway), execute_response); + let result = try_add_gateway(deps.as_mut(), env.clone(), info, gateway, sig); + assert_eq!(Err(MixnetContractError::AlreadyOwnsGateway), result); - // bonding fails if the user already owns a mixnode - test_helpers::add_mixnode( - "mixnode-owner", - tests::fixtures::good_mixnode_pledge(), + // the same holds if the user already owns a mixnode + let sender2 = "mixnode-owner"; + + let mix_id = test_helpers::add_mixnode( + &mut rng, deps.as_mut(), + env.clone(), + sender2, + vec![coin(100_000_000, TEST_COIN_DENOM)], ); - let info = mock_info("mixnode-owner", &tests::fixtures::good_gateway_pledge()); - let (msg, _) = tests::messages::valid_bond_gateway_msg("mixnode-owner"); + let info = mock_info(sender2, &fixtures::good_gateway_pledge()); + let (gateway, sig) = test_helpers::gateway_with_signature(&mut rng, sender2); - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(execute_response, Err(ContractError::AlreadyOwnsMixnode)); + let result = try_add_gateway( + deps.as_mut(), + env.clone(), + info.clone(), + gateway.clone(), + sig.clone(), + ); + assert_eq!(Err(MixnetContractError::AlreadyOwnsMixnode), result); // but after he unbonds it, it's all fine again - let info = mock_info("mixnode-owner", &[]); - let msg = ExecuteMsg::UnbondMixnode {}; - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + pending_events::unbond_mixnode(deps.as_mut(), &env, 123, mix_id).unwrap(); - let info = mock_info("mixnode-owner", &tests::fixtures::good_gateway_pledge()); - let (msg, _) = tests::messages::valid_bond_gateway_msg("mixnode-owner"); - - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert!(execute_response.is_ok()); - - // adding another node from another account, but with the same IP, should fail (or we would have a weird state). - // Is that right? Think about this, not sure yet. - } - - #[test] - fn adding_gateway_without_existing_owner() { - let mut deps = test_helpers::init_contract(); - - let info = mock_info("gateway-owner", &tests::fixtures::good_gateway_pledge()); - - // before the execution the node had no associated owner - assert!(storage::gateways() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("gateway-owner")) - .unwrap() - .is_none()); - - let (msg, identity) = tests::messages::valid_bond_gateway_msg("gateway-owner"); - - // it's all fine, owner is saved - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert!(execute_response.is_ok()); - - assert_eq!( - &identity, - storage::gateways() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("gateway-owner")) - .unwrap() - .unwrap() - .1 - .identity() - ); - } - - #[test] - fn adding_gateway_with_existing_owner() { - let mut deps = test_helpers::init_contract(); - - let identity = test_helpers::add_gateway( - "gateway-owner", - tests::fixtures::good_gateway_pledge(), - deps.as_mut(), - ); - - // request fails giving the existing owner address in the message - let info = mock_info( - "gateway-owner-pretender", - &tests::fixtures::good_gateway_pledge(), - ); - let msg = ExecuteMsg::BondGateway { - gateway: Gateway { - identity_key: identity, - ..tests::fixtures::gateway_fixture() - }, - owner_signature: "foomp".to_string(), - }; - - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!( - Err(ContractError::DuplicateGateway { - owner: Addr::unchecked("gateway-owner") - }), - execute_response - ); - } - - #[test] - fn adding_gateway_with_existing_unchanged_owner() { - let mut deps = test_helpers::init_contract(); - - test_helpers::add_gateway( - "gateway-owner", - tests::fixtures::good_gateway_pledge(), - deps.as_mut(), - ); - - let info = mock_info("gateway-owner", &tests::fixtures::good_gateway_pledge()); - let (msg, _) = tests::messages::valid_bond_gateway_msg("gateway-owner"); - - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(Err(ContractError::AlreadyOwnsGateway), res); + let result = try_add_gateway(deps.as_mut(), env, info, gateway, sig); + assert!(result.is_ok()); } #[test] fn gateway_remove() { let mut deps = test_helpers::init_contract(); + let mut rng = test_helpers::test_rng(); + let env = mock_env(); // try unbond when no nodes exist yet let info = mock_info("anyone", &[]); @@ -429,13 +294,19 @@ pub mod tests { // we're told that there is no node for our address assert_eq!( result, - Err(ContractError::NoAssociatedGatewayBond { + Err(MixnetContractError::NoAssociatedGatewayBond { owner: Addr::unchecked("anyone") }) ); // let's add a node owned by bob - test_helpers::add_gateway("bob", tests::fixtures::good_gateway_pledge(), deps.as_mut()); + test_helpers::add_gateway( + &mut rng, + deps.as_mut(), + env.clone(), + "bob", + fixtures::good_gateway_pledge(), + ); // attempt to unbond fred's node, which doesn't exist let info = mock_info("fred", &[]); @@ -443,7 +314,7 @@ pub mod tests { let result = execute(deps.as_mut(), mock_env(), info, msg); assert_eq!( result, - Err(ContractError::NoAssociatedGatewayBond { + Err(MixnetContractError::NoAssociatedGatewayBond { owner: Addr::unchecked("fred") }) ); @@ -457,9 +328,11 @@ pub mod tests { // add a node owned by fred let fred_identity = test_helpers::add_gateway( + &mut rng, + deps.as_mut(), + env, "fred", tests::fixtures::good_gateway_pledge(), - deps.as_mut(), ); // let's make sure we now have 2 nodes: @@ -494,124 +367,4 @@ pub mod tests { assert_eq!(1, gateway_bonds.len()); assert_eq!(&Addr::unchecked("bob"), gateway_bonds[0].owner()); } - - #[test] - fn removing_gateway_clears_ownership() { - let mut deps = test_helpers::init_contract(); - - let info = mock_info("gateway-owner", &tests::fixtures::good_gateway_pledge()); - let (bond_msg, identity) = tests::messages::valid_bond_gateway_msg("gateway-owner"); - execute(deps.as_mut(), mock_env(), info, bond_msg.clone()).unwrap(); - - assert_eq!( - &identity, - storage::gateways() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("gateway-owner")) - .unwrap() - .unwrap() - .1 - .identity() - ); - - let info = mock_info("gateway-owner", &[]); - let msg = ExecuteMsg::UnbondGateway {}; - - assert!(execute(deps.as_mut(), mock_env(), info, msg).is_ok()); - - assert!(storage::gateways() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("gateway-owner")) - .unwrap() - .is_none()); - - // and since it's removed, it can be reclaimed - let info = mock_info("gateway-owner", &tests::fixtures::good_gateway_pledge()); - - assert!(execute(deps.as_mut(), mock_env(), info, bond_msg).is_ok()); - assert_eq!( - &identity, - storage::gateways() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("gateway-owner")) - .unwrap() - .unwrap() - .1 - .identity() - ); - } - - #[test] - fn validating_gateway_bond() { - // you must send SOME funds - let result = validate_gateway_pledge( - Vec::new(), - INITIAL_GATEWAY_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::NoBondFound { - mix_denom: TEST_COIN_DENOM.to_string() - }) - ); - - // you must send at least 100 coins... - let mut bond = tests::fixtures::good_gateway_pledge(); - bond[0].amount = INITIAL_GATEWAY_PLEDGE.checked_sub(Uint128::new(1)).unwrap(); - let result = validate_gateway_pledge( - bond.clone(), - INITIAL_GATEWAY_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::InsufficientGatewayBond { - received: Into::::into(INITIAL_GATEWAY_PLEDGE) - 1, - minimum: INITIAL_GATEWAY_PLEDGE.into(), - }) - ); - - // more than that is still fine - let mut bond = tests::fixtures::good_gateway_pledge(); - bond[0].amount = INITIAL_GATEWAY_PLEDGE + Uint128::new(1); - let result = validate_gateway_pledge( - bond.clone(), - INITIAL_GATEWAY_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert!(result.is_ok()); - - // it must be sent in the defined denom! - let mut bond = tests::fixtures::good_gateway_pledge(); - bond[0].denom = "baddenom".to_string(); - let result = validate_gateway_pledge( - bond.clone(), - INITIAL_GATEWAY_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::WrongDenom { - mix_denom: TEST_COIN_DENOM.to_string() - }) - ); - - let mut bond = tests::fixtures::good_gateway_pledge(); - bond[0].denom = "foomp".to_string(); - let result = validate_gateway_pledge( - bond.clone(), - INITIAL_GATEWAY_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::WrongDenom { - mix_denom: TEST_COIN_DENOM.to_string() - }) - ); - } } diff --git a/contracts/mixnet/src/interval/helpers.rs b/contracts/mixnet/src/interval/helpers.rs new file mode 100644 index 0000000000..dd45c7511a --- /dev/null +++ b/contracts/mixnet/src/interval/helpers.rs @@ -0,0 +1,100 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::interval::storage; +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::{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, +) -> Result { + current_interval.change_epoch_length(Duration::from_secs(epoch_duration_secs)); + + let mut rewarding_params = rewards_storage::REWARDING_PARAMS.load(store)?; + rewarding_params.apply_epochs_in_interval_change(epochs_in_interval); + rewards_storage::REWARDING_PARAMS.save(store, &rewarding_params)?; + + current_interval.force_change_epochs_in_interval(epochs_in_interval); + 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, + ))) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_helpers; + use cosmwasm_std::Decimal; + + #[test] + fn changing_interval_config() { + let two = Decimal::from_atomics(2u32, 0).unwrap(); + let mut deps = test_helpers::init_contract(); + + let initial_interval = storage::current_interval(&deps.storage).unwrap(); + let initial_params = rewards_storage::REWARDING_PARAMS + .load(&deps.storage) + .unwrap(); + + // 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(), + ) + .unwrap(); + let updated_interval = storage::current_interval(&deps.storage).unwrap(); + let updated_params = rewards_storage::REWARDING_PARAMS + .load(&deps.storage) + .unwrap(); + + assert_eq!( + updated_interval.epochs_in_interval(), + initial_interval.epochs_in_interval() / 2 + ); + assert_eq!( + updated_params.interval.epoch_reward_budget, + initial_params.interval.epoch_reward_budget * two + ); + + // 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(), + ) + .unwrap(); + + // if we half the number of epochs, the reward budget should get doubled + let updated_interval = storage::current_interval(&deps.storage).unwrap(); + let updated_params = rewards_storage::REWARDING_PARAMS + .load(&deps.storage) + .unwrap(); + + assert_eq!( + updated_interval.epochs_in_interval(), + initial_interval.epochs_in_interval() * 2 + ); + assert_eq!( + updated_params.interval.epoch_reward_budget, + initial_params.interval.epoch_reward_budget / two + ); + } +} diff --git a/contracts/mixnet/src/interval/mod.rs b/contracts/mixnet/src/interval/mod.rs index 11f07ae776..9f81da03d1 100644 --- a/contracts/mixnet/src/interval/mod.rs +++ b/contracts/mixnet/src/interval/mod.rs @@ -1,6 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub mod helpers; +pub(crate) mod pending_events; pub mod queries; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs new file mode 100644 index 0000000000..6383169f97 --- /dev/null +++ b/contracts/mixnet/src/interval/pending_events.rs @@ -0,0 +1,1299 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::delegations; +use crate::delegations::storage as delegations_storage; +use crate::interval::helpers::change_interval_config; +use crate::interval::storage; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::helpers::{cleanup_post_unbond_mixnode_storage, get_mixnode_details_by_id}; +use crate::rewards::storage as rewards_storage; +use crate::support::helpers::send_to_proxy_or_owner; +use cosmwasm_std::{wasm_execute, Addr, Coin, DepsMut, Env, Response}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::events::{ + new_active_set_update_event, new_delegation_event, new_delegation_on_unbonded_node_event, + new_mixnode_cost_params_update_event, new_mixnode_unbonding_event, + new_rewarding_params_update_event, new_undelegation_event, +}; +use mixnet_contract_common::mixnode::MixNodeCostParams; +use mixnet_contract_common::pending_events::{ + PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData, + PendingIntervalEventKind, +}; +use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; +use mixnet_contract_common::{BlockHeight, Delegation, MixId}; +use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; + +pub(crate) trait ContractExecutableEvent { + // note: the error only means a HARD error like we failed to read from storage. + // if, for example, delegating fails because mixnode no longer exists, we return an Ok(()), + // because it's not a hard error and we don't want to fail the entire transaction + fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result; +} + +pub(crate) fn delegate( + deps: DepsMut<'_>, + env: &Env, + created_at: BlockHeight, + owner: Addr, + mix_id: MixId, + amount: Coin, + proxy: Option, +) -> Result { + // check if the target node still exists (it might have unbonded between this event getting created + // and being executed). Do note that it's absolutely possible for a mixnode to get immediately + // unbonded at this very block (if the event was pending), but that's tough luck, then it's up + // to the delegator to click the undelegate button + let mixnode_details = match get_mixnode_details_by_id(deps.storage, mix_id)? { + Some(details) + if details.rewarding_details.still_bonded() + && !details.bond_information.is_unbonding => + { + details + } + _ => { + // if mixnode is no longer bonded or in the process of unbonding, return the tokens back to the + // delegator; + let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![amount.clone()]); + let mut response = Response::new().add_message(return_tokens).add_event( + new_delegation_on_unbonded_node_event(&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 + // otherwise, we don't care + let vesting_contract = + mixnet_params_storage::vesting_contract_address(deps.storage)?; + if proxy == &vesting_contract { + let msg = VestingContractExecuteMsg::TrackUndelegation { + owner: owner.into_string(), + mix_id, + amount, + }; + + let track_undelegate_message = wasm_execute(proxy, &msg, vec![])?; + response = response.add_message(track_undelegate_message); + } + } + + return Ok(response); + } + }; + + let new_delegation_amount = amount.clone(); + let mut mix_rewarding = mixnode_details.rewarding_details; + + // the delegation_amount might get increased if there's already a pre-existing delegation on this mixnode + // (in that case we just create a fresh delegation with the sum of both) + let mut stored_delegation_amount = amount; + + // if there's an existing delegation, then withdraw the full reward and create a new delegation + // with the sum of both + let storage_key = Delegation::generate_storage_key(mix_id, &owner, proxy.as_ref()); + let old_delegation = if let Some(existing_delegation) = + delegations_storage::delegations().may_load(deps.storage, storage_key.clone())? + { + // completely remove the delegation from the node + let og_with_reward = mix_rewarding.undelegate(&existing_delegation)?; + + // and adjust the new value by the amount removed (which contains the original delegation + // alongside any earned rewards) + stored_delegation_amount.amount += og_with_reward.amount; + + Some(existing_delegation) + } else { + None + }; + + // 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( + created_at, + &owner, + &proxy, + &new_delegation_amount, + mix_id, + mix_rewarding.total_unit_reward, + ); + + let delegation = Delegation::new( + owner, + mix_id, + mix_rewarding.total_unit_reward, + stored_delegation_amount, + env.block.height, + proxy, + ); + + // save on reading since `.save()` would have attempted to read old data that we already have on hand + delegations_storage::delegations().replace( + deps.storage, + storage_key, + Some(&delegation), + old_delegation.as_ref(), + )?; + rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +pub(crate) fn undelegate( + deps: DepsMut<'_>, + created_at: BlockHeight, + owner: Addr, + mix_id: MixId, + proxy: Option, +) -> Result { + // see if the delegation still exists (in case of impatient user who decided to send multiple + // undelegation requests in an epoch) + let storage_key = Delegation::generate_storage_key(mix_id, &owner, proxy.as_ref()); + let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { + None => return Ok(Response::default()), + Some(delegation) => delegation, + }; + let mix_rewarding = + rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::InconsistentState { + comment: "mixnode rewarding got removed from the storage whilst there's still an existing delegation" + .into(), + })?; + // this also appropriately adjusts the storage + let tokens_to_return = + delegations::helpers::undelegate(deps.storage, delegation, mix_rewarding)?; + + 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(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 + // otherwise, we don't care + let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; + if proxy == &vesting_contract { + let msg = VestingContractExecuteMsg::TrackUndelegation { + owner: owner.into_string(), + mix_id, + amount: tokens_to_return, + }; + + let track_undelegate_message = wasm_execute(proxy, &msg, vec![])?; + response = response.add_message(track_undelegate_message); + } + } + + Ok(response) +} + +pub(crate) fn unbond_mixnode( + deps: DepsMut<'_>, + env: &Env, + 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) + // thus the node with all its associated information MUST exist in the storage. + let node_details = get_mixnode_details_by_id(deps.storage, mix_id)?.ok_or( + MixnetContractError::InconsistentState { + comment: "mixnode getting processed to get unbonded doesn't exist in the storage" + .into(), + }, + )?; + + // the denom on the original pledge was validated at the time of bonding so we can safely reuse it here + let rewarding_denom = &node_details.bond_information.original_pledge.denom; + let tokens = node_details + .rewarding_details + .operator_pledge_with_reward(rewarding_denom); + + let proxy = &node_details.bond_information.proxy; + let owner = &node_details.bond_information.owner; + + // send bonded funds (alongside all earned rewards) to the bond owner + let return_tokens = send_to_proxy_or_owner(proxy, owner, vec![tokens.clone()]); + + // remove the bond and if there are no delegations left, also the rewarding information + // decrement the associated layer count + cleanup_post_unbond_mixnode_storage(deps.storage, env, &node_details)?; + + let mut response = Response::new() + .add_message(return_tokens) + .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 + // otherwise, we don't care + let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; + if proxy == &vesting_contract { + let msg = VestingContractExecuteMsg::TrackUnbondMixnode { + owner: owner.clone().into_string(), + amount: tokens, + }; + + let track_unbond_message = wasm_execute(proxy, &msg, vec![])?; + response = response.add_message(track_unbond_message); + } + } + + Ok(response) +} + +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 + // by the authorized entity. + // Furthermore, we don't need to check whether the epoch is finished as the + // queue is only emptied upon the epoch finishing. + // Also, we know the update is valid as we checked for that before pushing the event onto the queue. + + let mut rewarding_params = rewards_storage::REWARDING_PARAMS.load(deps.storage)?; + 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(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.kind { + PendingEpochEventKind::Delegate { + owner, + mix_id, + amount, + proxy, + } => delegate(deps, env, self.created_at, owner, mix_id, amount, proxy), + PendingEpochEventKind::Undelegate { + owner, + mix_id, + proxy, + } => 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) + } + } + } +} + +pub(crate) fn change_mix_cost_params( + deps: DepsMut<'_>, + 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 + // node still exists + // + // note: there's no check if the bond is in "unbonding" state, as epoch actions would get + // cleared before touching interval actions + let mut mix_rewarding = + match rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { + Some(mix_rewarding) if mix_rewarding.still_bonded() => mix_rewarding, + // if node doesn't exist anymore, don't do anything, simple as that. + _ => return Ok(Response::default()), + }; + + 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) + mix_rewarding.cost_params = new_costs; + rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +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 + // by the authorized entity. + // Furthermore, we don't need to check whether the interval is finished as the + // queue is only emptied upon the interval finishing. + // Also, we know the update is valid as we checked for that before pushing the event onto the queue. + let interval = storage::current_interval(deps.storage)?; + + let mut rewarding_params = rewards_storage::REWARDING_PARAMS.load(deps.storage)?; + rewarding_params.try_apply_updates(updated_params, interval.epochs_in_interval())?; + 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, + ))) +} + +pub(crate) fn update_interval_config( + deps: DepsMut, + created_at: BlockHeight, + epochs_in_interval: u32, + epoch_duration_secs: u64, +) -> Result { + // We don't have to check for authorization as this event can only be pushed + // by the authorized entity. + // 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, + ) +} + +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.kind { + PendingIntervalEventKind::ChangeMixCostParams { + mix_id: mix, + new_costs, + } => change_mix_cost_params(deps, self.created_at, mix, new_costs), + PendingIntervalEventKind::UpdateRewardingParams { update } => { + update_rewarding_params(deps, self.created_at, update) + } + PendingIntervalEventKind::UpdateIntervalConfig { + epochs_in_interval, + epoch_duration_secs, + } => update_interval_config( + deps, + self.created_at, + epochs_in_interval, + epoch_duration_secs, + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_helpers; + use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; + use cosmwasm_std::Decimal; + use mixnet_contract_common::Percent; + use std::time::Duration; + + // note that authorization and basic validation has already been performed for all of those + // before being pushed onto the event queues + + #[cfg(test)] + mod delegating { + use super::*; + use crate::mixnodes::transactions::try_remove_mixnode; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::get_bank_send_msg; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{coin, to_binary, CosmosMsg, Decimal, WasmMsg}; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; + + #[test] + fn returns_the_tokens_if_mixnode_has_unbonded() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let delegation = 120_000_000u128; + let delegation_coin = coin(delegation, TEST_COIN_DENOM); + let owner1 = "delegator1"; + let owner2 = "delegator2"; + + // add pre-existing delegation + test.add_immediate_delegation(owner1, delegation, mix_id); + + let env = test.env(); + 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(), + None, + ) + .unwrap(); + + // delegation wasn't increased + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner1), None); + let amount = delegations_storage::delegations() + .load(test.deps().storage, storage_key) + .unwrap() + .amount; + assert_eq!(amount, delegation_coin); + + // and all tokens are returned back to the delegator + let (receiver, sent_amount) = get_bank_send_msg(&res_increase).unwrap(); + assert_eq!(receiver, owner1); + assert_eq!(sent_amount[0], delegation_coin); + + // for a fresh delegation, nothing was added to the storage either + let res_fresh = delegate( + test.deps_mut(), + &env, + 123, + Addr::unchecked(owner2), + mix_id, + delegation_coin.clone(), + None, + ) + .unwrap(); + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner2), None); + assert!(delegations_storage::delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + + // and all tokens are returned back to the delegator + let (receiver, sent_amount) = get_bank_send_msg(&res_fresh).unwrap(); + assert_eq!(receiver, owner2); + assert_eq!(sent_amount[0], delegation_coin); + } + + #[test] + fn returns_the_tokens_is_mixnode_is_unbonding() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let delegation = 120_000_000u128; + let delegation_coin = coin(delegation, TEST_COIN_DENOM); + let owner1 = "delegator1"; + let owner2 = "delegator2"; + + // add pre-existing delegation + test.add_immediate_delegation(owner1, delegation, mix_id); + + let env = test.env(); + 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(), + None, + ) + .unwrap(); + + // delegation wasn't increased + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner1), None); + let amount = delegations_storage::delegations() + .load(test.deps().storage, storage_key) + .unwrap() + .amount; + assert_eq!(amount, delegation_coin); + + // and all tokens are returned back to the delegator + let (receiver, sent_amount) = get_bank_send_msg(&res_increase).unwrap(); + assert_eq!(receiver, owner1); + assert_eq!(sent_amount[0], delegation_coin); + + // for a fresh delegation, nothing was added to the storage either + let res_fresh = delegate( + test.deps_mut(), + &env, + 123, + Addr::unchecked(owner2), + mix_id, + delegation_coin.clone(), + None, + ) + .unwrap(); + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner2), None); + assert!(delegations_storage::delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + + // and all tokens are returned back to the delegator + let (receiver, sent_amount) = get_bank_send_msg(&res_fresh).unwrap(); + assert_eq!(receiver, owner2); + assert_eq!(sent_amount[0], delegation_coin); + } + + #[test] + fn if_delegation_already_exists_a_fresh_one_with_sum_of_both_is_created() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + + let delegation_og = 120_000_000u128; + let delegation_new = 543_000_000u128; + let delegation_coin_new = coin(delegation_new, TEST_COIN_DENOM); + + let owner = "delegator"; + test.add_immediate_delegation(owner, delegation_og, mix_id); + + let env = test.env(); + let res = delegate( + test.deps_mut(), + &env, + 123, + Addr::unchecked(owner), + mix_id, + delegation_coin_new, + None, + ) + .unwrap(); + + let expected_amount = delegation_og + delegation_new; + let expected_amount_dec = Decimal::from_atomics(expected_amount, 0).unwrap(); + + // no refunds here! + assert!(get_bank_send_msg(&res).is_none()); + + let rewarding = rewards_storage::MIXNODE_REWARDING + .load(test.deps().storage, mix_id) + .unwrap(); + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None); + let delegation = delegations_storage::delegations() + .load(test.deps().storage, storage_key) + .unwrap(); + + assert_eq!(rewarding.unique_delegations, 1); + assert_eq!(rewarding.delegates, expected_amount_dec); + + assert_eq!(delegation.amount.amount.u128(), expected_amount) + } + + #[test] + fn if_delegation_already_exists_with_unclaimed_rewards_fresh_one_is_created() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + + let delegation_og = 120_000_000u128; + let delegation_new = 543_000_000u128; + let delegation_coin_new = coin(delegation_new, TEST_COIN_DENOM); + + // perform some rewarding here to advance the unit delegation beyond the initial value + test.update_rewarded_set(vec![mix_id]); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let owner = "delegator"; + test.add_immediate_delegation(owner, delegation_og, mix_id); + + test.skip_to_next_epoch_end(); + let dist1 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + let dist2 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None); + let delegation_pre = delegations_storage::delegations() + .load(test.deps().storage, storage_key.clone()) + .unwrap(); + + let env = test.env(); + let res = delegate( + test.deps_mut(), + &env, + 123, + Addr::unchecked(owner), + mix_id, + delegation_coin_new, + None, + ) + .unwrap(); + + let earned_before_update = dist1.delegates + dist2.delegates; + let truncated_reward = truncate_reward_amount(earned_before_update); + + let expected_amount = delegation_og + delegation_new + truncated_reward.u128(); + let expected_amount_dec = Decimal::from_atomics(expected_amount, 0).unwrap(); + + // no refunds here! + assert!(get_bank_send_msg(&res).is_none()); + + let rewarding = test.mix_rewarding(mix_id); + let delegation_post = delegations_storage::delegations() + .load(test.deps().storage, storage_key) + .unwrap(); + + assert_ne!( + delegation_pre.cumulative_reward_ratio, + delegation_post.cumulative_reward_ratio + ); + assert_eq!( + delegation_post.cumulative_reward_ratio, + rewarding.total_unit_reward + ); + + assert_eq!(rewarding.unique_delegations, 1); + assert_eq!(rewarding.delegates, expected_amount_dec); + + assert_eq!(delegation_post.amount.amount.u128(), expected_amount) + } + + #[test] + fn appropriately_updates_state_for_fresh_delegation() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let owner = "delegator"; + + let delegation = 120_000_000u128; + let delegation_coin = coin(120_000_000u128, TEST_COIN_DENOM); + + // perform some rewarding here to advance the unit delegation beyond the initial value + test.update_rewarded_set(vec![mix_id]); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None); + let delegation_pre = delegations_storage::delegations() + .may_load(test.deps().storage, storage_key.clone()) + .unwrap(); + let rewarding_pre = test.mix_rewarding(mix_id); + assert!(delegation_pre.is_none()); + assert!(rewarding_pre.delegates.is_zero()); + + let env = test.env(); + let res = delegate( + test.deps_mut(), + &env, + 123, + Addr::unchecked(owner), + mix_id, + delegation_coin.clone(), + None, + ) + .unwrap(); + assert!(get_bank_send_msg(&res).is_none()); + + let delegation_post = delegations_storage::delegations() + .load(test.deps().storage, storage_key) + .unwrap(); + let rewarding_post = test.mix_rewarding(mix_id); + assert_eq!(delegation_post.amount, delegation_coin); + assert_eq!( + delegation_post.cumulative_reward_ratio, + rewarding_post.total_unit_reward + ); + assert_eq!( + rewarding_post.delegates, + Decimal::from_atomics(delegation, 0).unwrap() + ) + } + + #[test] + fn attaches_vesting_contract_track_message_if_tokens_are_returned() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let delegation = 120_000_000u128; + let delegation_coin = coin(delegation, TEST_COIN_DENOM); + let owner1 = "delegator1"; + let owner2 = "delegator2"; + + let env = test.env(); + unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); + + let vesting_contract = test.vesting_contract(); + let dummy_proxy = Addr::unchecked("not-vesting-contract"); + + // for a fresh delegation, nothing was added to the storage either + let res_vesting = delegate( + test.deps_mut(), + &env, + 123, + Addr::unchecked(owner1), + mix_id, + delegation_coin.clone(), + Some(vesting_contract.clone()), + ) + .unwrap(); + let storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(owner1), + Some(vesting_contract.clone()).as_ref(), + ); + assert!(delegations_storage::delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + // and all tokens are returned back to the proxy + let (receiver, sent_amount) = get_bank_send_msg(&res_vesting).unwrap(); + assert_eq!(receiver, vesting_contract.as_str()); + assert_eq!(sent_amount[0], delegation_coin); + + // and we get appropriate track message + let mut found_track = true; + for msg in &res_vesting.messages { + if let CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + msg, + funds, + }) = &msg.msg + { + found_track = true; + assert_eq!(contract_addr, vesting_contract.as_str()); + let expected_msg = to_binary(&VestingContractExecuteMsg::TrackUndelegation { + owner: owner1.to_string(), + mix_id, + amount: delegation_coin.clone(), + }) + .unwrap(); + assert_eq!(&expected_msg, msg); + assert!(funds.is_empty()) + } + } + assert!(found_track); + + let res_other_proxy = delegate( + test.deps_mut(), + &env, + 123, + Addr::unchecked(owner1), + mix_id, + delegation_coin.clone(), + Some(dummy_proxy.clone()), + ) + .unwrap(); + let storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(owner2), + Some(dummy_proxy.clone()).as_ref(), + ); + assert!(delegations_storage::delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + // and all tokens are returned back to the proxy + let (receiver, sent_amount) = get_bank_send_msg(&res_other_proxy).unwrap(); + assert_eq!(receiver, dummy_proxy.as_str()); + assert_eq!(sent_amount[0], delegation_coin); + + // no track messages here! + for msg in &res_other_proxy.messages { + if let CosmosMsg::Wasm(WasmMsg::Execute { .. }) = &msg.msg { + panic!("we shouldn't have attempted to call any other contract!") + } + } + } + } + + #[cfg(test)] + mod undelegating { + use super::*; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::get_bank_send_msg; + use cosmwasm_std::{coin, to_binary, CosmosMsg, WasmMsg}; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; + + #[test] + fn doesnt_return_any_tokens_if_it_doesnt_exist() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let owner = Addr::unchecked("delegator"); + + let res = undelegate(test.deps_mut(), 123, owner, mix_id, None).unwrap(); + assert!(get_bank_send_msg(&res).is_none()); + } + + #[test] + fn errors_out_if_mix_rewarding_doesnt_exist() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let owner = Addr::unchecked("delegator"); + test.add_immediate_delegation(owner.as_str(), 100_000_000u32, mix_id); + + // 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(), 123, owner, mix_id, None); + assert!(matches!( + res, + Err(MixnetContractError::InconsistentState { .. }) + )); + } + + #[test] + fn returns_all_delegated_tokens_with_earned_rewards() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + + let owner = "delegator"; + let delegation = 120_000_000u128; + + // perform some rewarding here to advance the unit delegation beyond the initial value + test.update_rewarded_set(vec![mix_id]); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + test.add_immediate_delegation(owner, delegation, mix_id); + + test.skip_to_next_epoch_end(); + let dist1 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + let dist2 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let expected_reward = dist1.delegates + dist2.delegates; + let truncated_reward = truncate_reward_amount(expected_reward); + + let expected_return = delegation + truncated_reward.u128(); + + 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); + + // make sure delegation no longer exists + let storage_key = + Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None); + assert!(delegations_storage::delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + + // and mix rewarding no longer contains any information about the delegation + let rewarding = test.mix_rewarding(mix_id); + assert!(rewarding.delegates.is_zero()); + assert_eq!(rewarding.unique_delegations, 0); + } + + #[test] + fn attaches_vesting_contract_track_message_if_tokens_are_returned() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let delegation = 120_000_000u128; + let delegation_coin = coin(delegation, TEST_COIN_DENOM); + let owner1 = "delegator1"; + let owner2 = "delegator2"; + + let vesting_contract = test.vesting_contract(); + let dummy_proxy = Addr::unchecked("not-vesting-contract"); + + test.add_immediate_delegation_with_proxy( + owner1, + delegation, + mix_id, + vesting_contract.clone(), + ); + test.add_immediate_delegation_with_proxy( + owner2, + delegation, + mix_id, + dummy_proxy.clone(), + ); + + // 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()), + ) + .unwrap(); + let storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(owner1), + Some(vesting_contract.clone()).as_ref(), + ); + assert!(delegations_storage::delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + + // and all tokens are returned back to the proxy + let (receiver, sent_amount) = get_bank_send_msg(&res_vesting).unwrap(); + assert_eq!(receiver, vesting_contract.as_str()); + assert_eq!(sent_amount[0], delegation_coin); + + // and we get appropriate track message + let mut found_track = true; + for msg in &res_vesting.messages { + if let CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + msg, + funds, + }) = &msg.msg + { + found_track = true; + assert_eq!(contract_addr, vesting_contract.as_str()); + let expected_msg = to_binary(&VestingContractExecuteMsg::TrackUndelegation { + owner: owner1.to_string(), + mix_id, + amount: delegation_coin.clone(), + }) + .unwrap(); + assert_eq!(&expected_msg, msg); + assert!(funds.is_empty()) + } + } + assert!(found_track); + + let res_other_proxy = undelegate( + test.deps_mut(), + 123, + Addr::unchecked(owner2), + mix_id, + Some(dummy_proxy.clone()), + ) + .unwrap(); + let storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(owner2), + Some(dummy_proxy.clone()).as_ref(), + ); + assert!(delegations_storage::delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + // and all tokens are returned back to the proxy + let (receiver, sent_amount) = get_bank_send_msg(&res_other_proxy).unwrap(); + assert_eq!(receiver, dummy_proxy.as_str()); + assert_eq!(sent_amount[0], delegation_coin); + + // no track messages here! + for msg in &res_other_proxy.messages { + if let CosmosMsg::Wasm(WasmMsg::Execute { .. }) = &msg.msg { + panic!("we shouldn't have attempted to call any other contract!") + } + } + } + } + + #[cfg(test)] + mod mixnode_unbonding { + use super::*; + use crate::mixnodes::storage as mixnodes_storage; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::get_bank_send_msg; + use cosmwasm_std::{coin, to_binary, CosmosMsg, Uint128, WasmMsg}; + use mixnet_contract_common::mixnode::UnbondedMixnode; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; + + #[test] + fn returns_hard_error_if_mixnode_doesnt_exist() { + // this should have never happened so hard error MUST be thrown here + let mut test = TestSetup::new(); + let env = test.env(); + + let res = unbond_mixnode(test.deps_mut(), &env, 123, 1); + assert!(matches!( + res, + Err(MixnetContractError::InconsistentState { .. }) + )); + } + + #[test] + fn returns_original_pledge_alongside_any_earned_rewards() { + let mut test = TestSetup::new(); + + let owner = "mix-owner"; + let pledge = Uint128::new(250_000_000); + let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_details = mixnodes_storage::mixnode_bonds() + .load(test.deps().storage, mix_id) + .unwrap(); + let layer = mix_details.layer; + + test.update_rewarded_set(vec![mix_id]); + test.skip_to_next_epoch_end(); + let dist1 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + let dist2 = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let expected_reward = dist1.operator + dist2.operator; + let truncated_reward = truncate_reward_amount(expected_reward); + let expected_return = pledge + truncated_reward; + + let env = test.env(); + 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); + + assert!(rewards_storage::MIXNODE_REWARDING + .may_load(test.deps().storage, mix_id) + .unwrap() + .is_none()); + assert!(mixnodes_storage::mixnode_bonds() + .may_load(test.deps().storage, mix_id) + .unwrap() + .is_none()); + let expected = UnbondedMixnode { + identity_key: mix_details.identity().to_string(), + owner: Addr::unchecked(owner), + proxy: None, + unbonding_height: env.block.height, + }; + assert_eq!( + expected, + mixnodes_storage::unbonded_mixnodes() + .load(test.deps().storage, mix_id) + .unwrap() + ); + assert_eq!( + mixnodes_storage::LAYERS.load(test.deps().storage).unwrap()[layer], + 0 + ) + } + + #[test] + fn attaches_vesting_contract_track_message_if_tokens_are_returned() { + let mut test = TestSetup::new(); + + let vesting_contract = test.vesting_contract(); + let dummy_proxy = Addr::unchecked("not-vesting-contract"); + + let pledge = Uint128::new(250_000_000); + let pledge_coin = coin(250_000_000, TEST_COIN_DENOM); + let owner1 = "mix-owner1"; + let owner2 = "mix-owner2"; + let mix_id_vesting = + test.add_dummy_mixnode_with_proxy(owner1, Some(pledge), vesting_contract.clone()); + let mix_id_other_proxy = + 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, 123, mix_id_vesting).unwrap(); + + assert!(mixnodes_storage::mixnode_bonds() + .may_load(test.deps().storage, mix_id_vesting) + .unwrap() + .is_none()); + + // and all tokens are returned back to the proxy + let (receiver, sent_amount) = get_bank_send_msg(&res_vesting).unwrap(); + assert_eq!(receiver, vesting_contract.as_str()); + assert_eq!(sent_amount[0], pledge_coin); + + // and we get appropriate track message + let mut found_track = true; + for msg in &res_vesting.messages { + if let CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + msg, + funds, + }) = &msg.msg + { + found_track = true; + assert_eq!(contract_addr, vesting_contract.as_str()); + let expected_msg = to_binary(&VestingContractExecuteMsg::TrackUnbondMixnode { + owner: owner1.to_string(), + amount: pledge_coin.clone(), + }) + .unwrap(); + assert_eq!(&expected_msg, msg); + assert!(funds.is_empty()) + } + } + assert!(found_track); + + let res_other_proxy = + 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() + .is_none()); + // and all tokens are returned back to the proxy + let (receiver, sent_amount) = get_bank_send_msg(&res_other_proxy).unwrap(); + assert_eq!(receiver, dummy_proxy.as_str()); + assert_eq!(sent_amount[0], pledge_coin); + + // no track messages here! + for msg in &res_other_proxy.messages { + if let CosmosMsg::Wasm(WasmMsg::Execute { .. }) = &msg.msg { + panic!("we shouldn't have attempted to call any other contract!") + } + } + } + } + + #[test] + fn updating_active_set_updates_rewarding_params() { + let mut test = TestSetup::new(); + let current = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + + update_active_set_size(test.deps_mut(), 123, 50).unwrap(); + let updated = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + assert_ne!(current.active_set_size, updated.active_set_size); + assert_eq!(updated.active_set_size, 50) + } + + #[cfg(test)] + mod changing_mix_cost_params { + use super::*; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::coin; + use mixnet_contract_common::Percent; + + #[test] + fn doesnt_do_anything_if_mixnode_has_unbonded() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let env = test.env(); + 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(), 123, mix_id, new_params); + assert_eq!(res, Ok(Response::default())); + } + + #[test] + fn for_bonded_mixnode_updates_saved_value() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let before = test.mix_rewarding(mix_id).cost_params; + + 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(), 123, mix_id, new_params.clone()); + assert_eq!( + res, + 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; + assert_ne!(before, new_params); + assert_eq!(after, new_params); + } + } + + #[test] + fn updating_interval_rewarding_params_appropriately_recomputes_state() { + let mut test = TestSetup::new(); + + let before = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + + let two = Decimal::from_atomics(2u32, 0).unwrap(); + let four = Decimal::from_atomics(4u32, 0).unwrap(); + + // TODO: be more fuzzy about it and try to vary other fields that can cause + // re-computation like pool emission or rewarded set size update + let update = IntervalRewardingParamsUpdate { + reward_pool: Some(before.interval.reward_pool / two), + staking_supply: Some(before.interval.staking_supply * four), + staking_supply_scale_factor: None, + sybil_resistance_percent: Some(Percent::from_percentage_value(42).unwrap()), + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: None, + }; + + let res = update_rewarding_params(test.deps_mut(), 123, update); + assert!(res.is_ok()); + let after = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + + // with half the reward pool, our reward budget is also halved + assert_decimals( + before.interval.epoch_reward_budget, + two * after.interval.epoch_reward_budget, + ); + + // and with 4x the staking supply, the saturation point is also increased 4-folds + assert_decimals( + four * before.interval.stake_saturation_point, + after.interval.stake_saturation_point, + ); + + assert_eq!( + after.interval.sybil_resistance, + Percent::from_percentage_value(42).unwrap() + ) + } + + #[test] + fn updating_interval_config_recomputes_rewarding_params() { + let mut test = TestSetup::new(); + + let two = Decimal::from_atomics(2u32, 0).unwrap(); + + let params_before = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + + // skip few epochs just for the sake of it + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + + let interval_before = + crate::interval::storage::current_interval(test.deps().storage).unwrap(); + + // half the number of epochs (thus double reward budget) + // and change epoch length + update_interval_config( + test.deps_mut(), + 123, + interval_before.epochs_in_interval() / 2, + 1234, + ) + .unwrap(); + + let interval_after = + crate::interval::storage::current_interval(test.deps().storage).unwrap(); + let params_after = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + assert_eq!( + interval_after.epochs_in_interval(), + interval_before.epochs_in_interval() / 2 + ); + assert_eq!( + params_after.interval.epoch_reward_budget, + params_before.interval.epoch_reward_budget * two + ); + assert_eq!(interval_after.epoch_length(), Duration::from_secs(1234)) + } +} diff --git a/contracts/mixnet/src/interval/queries.rs b/contracts/mixnet/src/interval/queries.rs index dfa58f5e23..a0fe28ed28 100644 --- a/contracts/mixnet/src/interval/queries.rs +++ b/contracts/mixnet/src/interval/queries.rs @@ -1,365 +1,523 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::storage; -use crate::error::ContractError; -use cosmwasm_std::{Env, Order, StdResult, Storage}; +use crate::constants::{ + EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT, EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT, + INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT, INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT, + REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT, REWARDED_SET_MAX_RETRIEVAL_LIMIT, +}; +use crate::interval::storage; +use cosmwasm_std::{Deps, Env, Order, StdResult}; use cw_storage_plus::Bound; +use mixnet_contract_common::pending_events::{PendingEpochEvent, PendingIntervalEvent}; use mixnet_contract_common::{ - IdentityKey, Interval, PagedRewardedSetResponse, RewardedSetNodeStatus, - RewardedSetUpdateDetails, + CurrentIntervalResponse, EpochEventId, IntervalEventId, MixId, PagedRewardedSetResponse, + PendingEpochEventsResponse, PendingIntervalEventsResponse, }; -pub fn query_current_epoch(storage: &dyn Storage) -> Result { - storage::current_epoch(storage) +pub fn query_current_interval_details( + deps: Deps<'_>, + env: Env, +) -> StdResult { + let interval = storage::current_interval(deps.storage)?; + + Ok(CurrentIntervalResponse::new(interval, env)) } -pub(crate) fn query_rewarded_set_refresh_minimum_blocks() -> u64 { - crate::constants::REWARDED_SET_REFRESH_BLOCKS -} +pub fn query_rewarded_set_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT) + .min(REWARDED_SET_MAX_RETRIEVAL_LIMIT) as usize; -// note: I have removed the `query_rewarded_set_for_interval`, because I don't think it's appropriate -// for the contract to go through so much data (i.e. all "rewarded" sets of particular interval) in one go. -// To achieve the same result, the client would have to instead first call `query_rewarded_set_heights_for_interval` -// to learn the heights used in given interval and then for each of them `query_rewarded_set` for that particular height. - -pub fn query_current_rewarded_set_height(storage: &dyn Storage) -> Result { - Ok(storage::CURRENT_REWARDED_SET_HEIGHT.load(storage)?) -} - -fn query_rewarded_set_at_height( - storage: &dyn Storage, - height: u64, - start_after: Option, - limit: u32, -) -> Result, ContractError> { let start = start_after.map(Bound::exclusive); - let rewarded_set = storage::REWARDED_SET - .prefix(height) - .range(storage, start, None, Order::Ascending) - .take(limit as usize) - .collect::>()?; - Ok(rewarded_set) + let nodes = storage::REWARDED_SET + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>>()?; + + let start_next_after = nodes.last().map(|node| node.0); + + Ok(PagedRewardedSetResponse { + nodes, + start_next_after, + }) } -pub fn query_rewarded_set( - storage: &dyn Storage, - height: Option, - start_after: Option, - limit: Option, -) -> Result { - let height = match height { - Some(height) => height, - None => query_current_rewarded_set_height(storage)?, - }; - let limit = limit - .unwrap_or(storage::REWARDED_NODE_DEFAULT_PAGE_LIMIT) - .min(storage::REWARDED_NODE_MAX_PAGE_LIMIT); - - // query for an additional element to determine paging requirements - let mut paged_result = query_rewarded_set_at_height(storage, height, start_after, limit + 1)?; - - if paged_result.len() > limit as usize { - paged_result.truncate(limit as usize); - Ok(PagedRewardedSetResponse { - start_next_after: paged_result.last().map(|res| res.0.clone()), - identities: paged_result, - at_height: height, - }) - } else { - Ok(PagedRewardedSetResponse { - identities: paged_result, - start_next_after: None, - at_height: height, - }) - } -} - -// this was all put together into the same query so that all information would be synced together -pub fn query_rewarded_set_update_details( +pub fn query_pending_epoch_events_paged( + deps: Deps<'_>, env: Env, - storage: &dyn Storage, -) -> Result { - Ok(RewardedSetUpdateDetails { - refresh_rate_blocks: query_rewarded_set_refresh_minimum_blocks(), - last_refreshed_block: query_current_rewarded_set_height(storage)?, - current_height: env.block.height, + start_after: Option, + limit: Option, +) -> StdResult { + let interval = storage::current_interval(deps.storage)?; + + let limit = limit + .unwrap_or(EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT) + .min(EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let events = storage::PENDING_EPOCH_EVENTS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|row| row.into())) + .collect::>>()?; + + let start_next_after = events.last().map(|event| event.id); + + Ok(PendingEpochEventsResponse { + seconds_until_executable: interval.secs_until_current_epoch_end(&env), + events, + start_next_after, + }) +} + +pub fn query_pending_interval_events_paged( + deps: Deps<'_>, + env: Env, + start_after: Option, + limit: Option, +) -> StdResult { + let interval = storage::current_interval(deps.storage)?; + + let limit = limit + .unwrap_or(INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT) + .min(INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let events = storage::PENDING_INTERVAL_EVENTS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|row| row.into())) + .collect::>>()?; + + let start_next_after = events.last().map(|event| event.id); + + Ok(PendingIntervalEventsResponse { + seconds_until_executable: interval.secs_until_current_interval_end(&env), + events, + start_next_after, }) } #[cfg(test)] mod tests { use super::*; - use crate::interval::storage::REWARDED_NODE_MAX_PAGE_LIMIT; - use crate::support::tests::test_helpers; - use cosmwasm_std::testing::mock_env; - - fn store_rewarded_nodes( - storage: &mut dyn Storage, - height: u64, - active_set: u32, - rewarded_set: u32, - ) -> Vec { - let identities = (0..rewarded_set) - .map(|i| format!("identity{:04}", i)) - .collect::>(); - storage::save_rewarded_set(storage, height, active_set, identities.clone()).unwrap(); - identities - } + use crate::support::tests::test_helpers::TestSetup; #[test] - fn querying_for_rewarded_set_at_height() { - let mut deps = test_helpers::init_contract(); + fn querying_for_current_interval_details() { + let mut test = TestSetup::new(); - // store some nodes - let identities1 = store_rewarded_nodes(deps.as_mut().storage, 1, 100, 200); - let identities2 = store_rewarded_nodes(deps.as_mut().storage, 2, 50, 200); - let identities3 = store_rewarded_nodes(deps.as_mut().storage, 3, 150, 200); - let identities4 = store_rewarded_nodes(deps.as_mut().storage, 4, 300, 500); - let identities5 = store_rewarded_nodes(deps.as_mut().storage, 5, 500, 500); + let interval = test.current_interval(); + let env = test.env(); + let res = query_current_interval_details(test.deps(), env.clone()).unwrap(); - // expected2 and 3 are basically sanity checks to ensure changing active set size (increase or decrease) - // doesn't affect the ordering + assert_eq!(res.interval, interval); + assert!(!res.is_current_interval_over); + assert!(!res.is_current_epoch_over); + assert_eq!(res.current_blocktime, env.block.time.seconds()); - let expected1 = identities1 - .into_iter() - .enumerate() - .map(|(i, identity)| { - if i < 100 { - (identity, RewardedSetNodeStatus::Active) - } else { - (identity, RewardedSetNodeStatus::Standby) - } - }) - .collect::>(); + test.skip_to_current_epoch_end(); + let interval = test.current_interval(); + let env = test.env(); + let res = query_current_interval_details(test.deps(), env.clone()).unwrap(); - assert_eq!( - expected1, - query_rewarded_set_at_height(deps.as_ref().storage, 1, None, 1000).unwrap() - ); + assert_eq!(res.interval, interval); + assert!(!res.is_current_interval_over); + assert!(res.is_current_epoch_over); + assert_eq!(res.current_blocktime, env.block.time.seconds()); - let expected2 = identities2 - .into_iter() - .enumerate() - .map(|(i, identity)| { - if i < 50 { - (identity, RewardedSetNodeStatus::Active) - } else { - (identity, RewardedSetNodeStatus::Standby) - } - }) - .collect::>(); + test.skip_to_current_interval_end(); + let interval = test.current_interval(); + let env = test.env(); + let res = query_current_interval_details(test.deps(), env.clone()).unwrap(); - assert_eq!( - expected2, - query_rewarded_set_at_height(deps.as_ref().storage, 2, None, 1000).unwrap() - ); - - let expected3 = identities3 - .into_iter() - .enumerate() - .map(|(i, identity)| { - if i < 150 { - (identity, RewardedSetNodeStatus::Active) - } else { - (identity, RewardedSetNodeStatus::Standby) - } - }) - .collect::>(); - - assert_eq!( - expected3, - query_rewarded_set_at_height(deps.as_ref().storage, 3, None, 1000).unwrap() - ); - - // check limit and paging - // active: 300, rewarded: 500 - let first_100 = identities4 - .iter() - .take(100) - .map(|identity| (identity.clone(), RewardedSetNodeStatus::Active)) - .collect::>(); - assert_eq!( - first_100, - query_rewarded_set_at_height(deps.as_ref().storage, 4, None, 100).unwrap() - ); - - let expected_single1 = vec![("identity0299".to_string(), RewardedSetNodeStatus::Active)]; - let expected_single2 = vec![("identity0300".to_string(), RewardedSetNodeStatus::Standby)]; - assert_eq!( - expected_single1, - query_rewarded_set_at_height( - deps.as_ref().storage, - 4, - Some("identity0298".to_string()), - 1 - ) - .unwrap() - ); - assert_eq!( - expected_single2, - query_rewarded_set_at_height( - deps.as_ref().storage, - 4, - Some("identity0299".to_string()), - 1 - ) - .unwrap() - ); - - let last_100 = identities4 - .iter() - .skip(400) - .map(|identity| (identity.clone(), RewardedSetNodeStatus::Standby)) - .collect::>(); - assert_eq!( - last_100, - query_rewarded_set_at_height( - deps.as_ref().storage, - 4, - Some("identity0399".to_string()), - 100 - ) - .unwrap() - ); - - // all nodes are in the active set - let expected5 = identities5 - .into_iter() - .map(|identity| (identity, RewardedSetNodeStatus::Active)) - .collect::>(); - - assert_eq!( - expected5, - query_rewarded_set_at_height(deps.as_ref().storage, 5, None, 1000).unwrap() - ); + assert_eq!(res.interval, interval); + assert!(res.is_current_interval_over); + assert!(res.is_current_epoch_over); + assert_eq!(res.current_blocktime, env.block.time.seconds()); } - #[test] - fn querying_for_rewarded_set() { - let mut deps = test_helpers::init_contract(); + #[cfg(test)] + mod rewarded_set { + use super::*; - let current_height = 123; - let other_height = 456; - let different_height = 789; + fn set_rewarded_set_to_n_nodes(test: &mut TestSetup, n: usize) { + let set = (1u32..).take(n).collect::>(); + test.update_rewarded_set(set) + } - storage::CURRENT_REWARDED_SET_HEIGHT - .save(deps.as_mut().storage, ¤t_height) + #[test] + fn obeys_limits() { + let mut test = TestSetup::new(); + set_rewarded_set_to_n_nodes(&mut test, 200); + + let limit = 2; + let page1 = query_rewarded_set_paged(test.deps(), None, Some(limit)).unwrap(); + assert_eq!(limit, page1.nodes.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut test = TestSetup::new(); + set_rewarded_set_to_n_nodes(&mut test, 2000); + + // query without explicitly setting a limit + let page1 = query_rewarded_set_paged(test.deps(), None, None).unwrap(); + + assert_eq!( + REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut test = TestSetup::new(); + set_rewarded_set_to_n_nodes(&mut test, 2000); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 10000; + let page1 = query_rewarded_set_paged(test.deps(), None, Some(crazy_limit)).unwrap(); + + assert_eq!(REWARDED_SET_MAX_RETRIEVAL_LIMIT, page1.nodes.len() as u32); + } + + #[test] + fn pagination_works() { + let mut test = TestSetup::new(); + + set_rewarded_set_to_n_nodes(&mut test, 1); + + let per_page = 2; + let page1 = query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.nodes.len()); + + set_rewarded_set_to_n_nodes(&mut test, 2); + + // page1 should have 2 results on it + let page1 = query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); + assert_eq!(2, page1.nodes.len()); + + set_rewarded_set_to_n_nodes(&mut test, 3); + + // page1 still has the same 2 results + let another_page1 = + query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); + assert_eq!(2, another_page1.nodes.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = + query_rewarded_set_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); + + assert_eq!(1, page2.nodes.len()); + + // save another one + set_rewarded_set_to_n_nodes(&mut test, 4); + + let page2 = + query_rewarded_set_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.nodes.len()); + } + } + + #[cfg(test)] + mod pending_epoch_events { + use super::*; + use cosmwasm_std::Addr; + use mixnet_contract_common::pending_events::PendingEpochEventKind; + use rand_chacha::rand_core::RngCore; + + fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) { + for _ in 0..n { + push_dummy_epoch_action(test) + } + } + + fn push_dummy_epoch_action(test: &mut TestSetup) { + let dummy_action = PendingEpochEventKind::Undelegate { + owner: Addr::unchecked("foomp"), + mix_id: test.rng.next_u32(), + proxy: None, + }; + let env = test.env(); + storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); + } + + #[test] + fn obeys_limits() { + let mut test = TestSetup::new(); + push_n_dummy_epoch_actions(&mut test, 100); + let env = test.env(); + + let limit = 2; + + let page1 = + query_pending_epoch_events_paged(test.deps(), env, None, Some(limit)).unwrap(); + assert_eq!(limit, page1.events.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut test = TestSetup::new(); + push_n_dummy_epoch_actions(&mut test, 1000); + let env = test.env(); + + // query without explicitly setting a limit + let page1 = query_pending_epoch_events_paged(test.deps(), env, None, None).unwrap(); + + assert_eq!( + EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT, + page1.events.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut test = TestSetup::new(); + push_n_dummy_epoch_actions(&mut test, 1000); + let env = test.env(); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 10000; + let page1 = query_pending_epoch_events_paged(test.deps(), env, None, Some(crazy_limit)) + .unwrap(); + + assert_eq!(EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT, page1.events.len() as u32); + } + + #[test] + fn pagination_works() { + let mut test = TestSetup::new(); + let env = test.env(); + push_dummy_epoch_action(&mut test); + + let per_page = 2; + let page1 = + query_pending_epoch_events_paged(test.deps(), env.clone(), None, Some(per_page)) + .unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.events.len()); + + // save another + push_dummy_epoch_action(&mut test); + + // page1 should have 2 results on it + let page1 = + query_pending_epoch_events_paged(test.deps(), env.clone(), None, Some(per_page)) + .unwrap(); + assert_eq!(2, page1.events.len()); + + push_dummy_epoch_action(&mut test); + + // page1 still has the same 2 results + let another_page1 = + query_pending_epoch_events_paged(test.deps(), env.clone(), None, Some(per_page)) + .unwrap(); + assert_eq!(2, another_page1.events.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_pending_epoch_events_paged( + test.deps(), + env.clone(), + Some(start_after), + Some(per_page), + ) .unwrap(); - let identities1 = store_rewarded_nodes(deps.as_mut().storage, current_height, 50, 100); - let identities2 = store_rewarded_nodes(deps.as_mut().storage, other_height, 100, 200); - let identities3 = store_rewarded_nodes( - deps.as_mut().storage, - different_height, - storage::REWARDED_NODE_MAX_PAGE_LIMIT, - storage::REWARDED_NODE_MAX_PAGE_LIMIT * 2, - ); + assert_eq!(1, page2.events.len()); - // if height is not set, current height is used, else it's just passed - let expected1 = PagedRewardedSetResponse { - identities: identities1 - .into_iter() - .enumerate() - .map(|(i, identity)| { - if i < 50 { - (identity, RewardedSetNodeStatus::Active) - } else { - (identity, RewardedSetNodeStatus::Standby) - } - }) - .collect::>(), - start_next_after: None, - at_height: current_height, - }; - let expected2 = PagedRewardedSetResponse { - identities: identities2 - .into_iter() - .enumerate() - .map(|(i, identity)| { - if i < 100 { - (identity, RewardedSetNodeStatus::Active) - } else { - (identity, RewardedSetNodeStatus::Standby) - } - }) - .collect::>(), - start_next_after: None, - at_height: other_height, - }; + // save another one + push_dummy_epoch_action(&mut test); - assert_eq!( - Ok(expected1), - query_rewarded_set(deps.as_ref().storage, None, None, None) - ); - assert_eq!( - Ok(expected2), - query_rewarded_set(deps.as_ref().storage, Some(other_height), None, None) - ); - - // if limit is not set, a default one is used instead - let expected3 = PagedRewardedSetResponse { - identities: identities3 - .iter() - .take(storage::REWARDED_NODE_DEFAULT_PAGE_LIMIT as usize) - .cloned() - .map(|identity| (identity, RewardedSetNodeStatus::Active)) - .collect::>(), - start_next_after: Some(format!( - "identity{:04}", - storage::REWARDED_NODE_DEFAULT_PAGE_LIMIT - 1 - )), - at_height: different_height, - }; - assert_eq!( - Ok(expected3), - query_rewarded_set(deps.as_ref().storage, Some(different_height), None, None) - ); - - // limit cannot be larger that pre-defined maximum - let expected4 = PagedRewardedSetResponse { - identities: identities3 - .iter() - .take(storage::REWARDED_NODE_MAX_PAGE_LIMIT as usize) - .cloned() - .map(|identity| (identity, RewardedSetNodeStatus::Active)) - .collect::>(), - start_next_after: Some(format!( - "identity{:04}", - storage::REWARDED_NODE_MAX_PAGE_LIMIT - 1 - )), - at_height: different_height, - }; - assert_eq!( - Ok(expected4), - query_rewarded_set( - deps.as_ref().storage, - Some(different_height), - None, - Some(REWARDED_NODE_MAX_PAGE_LIMIT * 100) + let page2 = query_pending_epoch_events_paged( + test.deps(), + env, + Some(start_after), + Some(per_page), ) - ); - } - - #[test] - fn querying_for_rewarded_set_update_details() { - let env = mock_env(); - let mut deps = test_helpers::init_contract(); - - let current_height = 123; - storage::CURRENT_REWARDED_SET_HEIGHT - .save(deps.as_mut().storage, ¤t_height) .unwrap(); - // returns whatever is in the correct environment - assert_eq!( - RewardedSetUpdateDetails { - refresh_rate_blocks: crate::constants::REWARDED_SET_REFRESH_BLOCKS, - last_refreshed_block: current_height, - current_height: env.block.height - }, - query_rewarded_set_update_details(env, deps.as_ref().storage).unwrap() - ) + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.events.len()); + } + + #[test] + fn shows_correct_time_until_possible_execution() { + let mut test = TestSetup::new(); + let env = test.env(); + push_dummy_epoch_action(&mut test); + + let res = + query_pending_epoch_events_paged(test.deps(), env.clone(), None, None).unwrap(); + let interval = test.current_interval(); + + // it's essentially always the time until the epoch end + assert_eq!( + res.seconds_until_executable, + interval.secs_until_current_epoch_end(&env) + ) + } + } + + #[cfg(test)] + mod pending_interval_events { + use super::*; + use crate::support::tests::fixtures; + use mixnet_contract_common::pending_events::PendingIntervalEventKind; + use rand_chacha::rand_core::RngCore; + + fn push_n_dummy_interval_actions(test: &mut TestSetup, n: usize) { + for _ in 0..n { + push_dummy_interval_action(test) + } + } + + fn push_dummy_interval_action(test: &mut TestSetup) { + let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { + mix_id: test.rng.next_u32(), + new_costs: fixtures::mix_node_cost_params_fixture(), + }; + let env = test.env(); + storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); + } + + #[test] + fn obeys_limits() { + let mut test = TestSetup::new(); + push_n_dummy_interval_actions(&mut test, 100); + let env = test.env(); + + let limit = 2; + + let page1 = + query_pending_interval_events_paged(test.deps(), env, None, Some(limit)).unwrap(); + assert_eq!(limit, page1.events.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut test = TestSetup::new(); + push_n_dummy_interval_actions(&mut test, 1000); + let env = test.env(); + + // query without explicitly setting a limit + let page1 = query_pending_interval_events_paged(test.deps(), env, None, None).unwrap(); + + assert_eq!( + INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT, + page1.events.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut test = TestSetup::new(); + push_n_dummy_interval_actions(&mut test, 1000); + let env = test.env(); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 10000; + let page1 = + query_pending_interval_events_paged(test.deps(), env, None, Some(crazy_limit)) + .unwrap(); + + assert_eq!( + INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT, + page1.events.len() as u32 + ); + } + + #[test] + fn pagination_works() { + let mut test = TestSetup::new(); + let env = test.env(); + push_dummy_interval_action(&mut test); + + let per_page = 2; + let page1 = + query_pending_interval_events_paged(test.deps(), env.clone(), None, Some(per_page)) + .unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.events.len()); + + // save another + push_dummy_interval_action(&mut test); + + // page1 should have 2 results on it + let page1 = + query_pending_interval_events_paged(test.deps(), env.clone(), None, Some(per_page)) + .unwrap(); + assert_eq!(2, page1.events.len()); + + push_dummy_interval_action(&mut test); + + // page1 still has the same 2 results + let another_page1 = + query_pending_interval_events_paged(test.deps(), env.clone(), None, Some(per_page)) + .unwrap(); + assert_eq!(2, another_page1.events.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_pending_interval_events_paged( + test.deps(), + env.clone(), + Some(start_after), + Some(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.events.len()); + + // save another one + push_dummy_interval_action(&mut test); + + let page2 = query_pending_interval_events_paged( + test.deps(), + env, + Some(start_after), + Some(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.events.len()); + } + + #[test] + fn shows_correct_time_until_possible_execution() { + let mut test = TestSetup::new(); + let env = test.env(); + push_dummy_interval_action(&mut test); + + let res = + query_pending_interval_events_paged(test.deps(), env.clone(), None, None).unwrap(); + let interval = test.current_interval(); + + // it's essentially always the time until the interval end + assert_eq!( + res.seconds_until_executable, + interval.secs_until_current_interval_end(&env) + ) + } } } diff --git a/contracts/mixnet/src/interval/storage.rs b/contracts/mixnet/src/interval/storage.rs index 31f506d33b..282876f5f8 100644 --- a/contracts/mixnet/src/interval/storage.rs +++ b/contracts/mixnet/src/interval/storage.rs @@ -1,73 +1,109 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{StdResult, Storage}; -use cw_storage_plus::{Item, Map}; -use mixnet_contract_common::{ - reward_params::EpochRewardParams, IdentityKey, Interval, RewardedSetNodeStatus, +use crate::constants::{ + CURRENT_INTERVAL_KEY, EPOCH_EVENT_ID_COUNTER_KEY, INTERVAL_EVENT_ID_COUNTER_KEY, + LAST_EPOCH_EVENT_ID_KEY, LAST_INTERVAL_EVENT_ID_KEY, PENDING_EPOCH_EVENTS_NAMESPACE, + PENDING_INTERVAL_EVENTS_NAMESPACE, REWARDED_SET_KEY, }; +use cosmwasm_std::{Env, Order, StdResult, Storage}; +use cw_storage_plus::{Item, Map}; +use mixnet_contract_common::pending_events::{ + PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData, +}; +use mixnet_contract_common::{ + EpochEventId, Interval, IntervalEventId, MixId, PendingIntervalEventKind, RewardedSetNodeStatus, +}; +use std::collections::HashMap; -use crate::{error::ContractError, support::helpers::epoch_reward_params}; +pub(crate) const CURRENT_INTERVAL: Item<'_, Interval> = Item::new(CURRENT_INTERVAL_KEY); +pub(crate) const REWARDED_SET: Map = Map::new(REWARDED_SET_KEY); -// type aliases for better reasoning for storage keys -// (I found it helpful) -type BlockHeight = u64; -type IntervalId = u32; +pub(crate) const EPOCH_EVENT_ID_COUNTER: Item = Item::new(EPOCH_EVENT_ID_COUNTER_KEY); +pub(crate) const INTERVAL_EVENT_ID_COUNTER: Item = + Item::new(INTERVAL_EVENT_ID_COUNTER_KEY); -// TODO: those values need to be verified -pub(crate) const REWARDED_NODE_DEFAULT_PAGE_LIMIT: u32 = 1000; -pub(crate) const REWARDED_NODE_MAX_PAGE_LIMIT: u32 = 1500; +pub(crate) const LAST_PROCESSED_EPOCH_EVENT: Item = + Item::new(LAST_EPOCH_EVENT_ID_KEY); +pub(crate) const LAST_PROCESSED_INTERVAL_EVENT: Item = + Item::new(LAST_INTERVAL_EVENT_ID_KEY); -pub(crate) const CURRENT_EPOCH: Item<'_, Interval> = Item::new("ceph"); -pub(crate) const CURRENT_EPOCH_REWARD_PARAMS: Item<'_, EpochRewardParams> = Item::new("erp"); -pub(crate) const CURRENT_REWARDED_SET_HEIGHT: Item<'_, BlockHeight> = Item::new("crh"); +// we're indexing the events by an increasing ID so that we'd execute them in the order they were created +// (we can't use block height as it's very possible multiple requests might be created in the same block height, +// and composite keys would be more complex than just using an increasing ID) +/// Contains operations that should get resolved at the end of the current epoch. +pub(crate) const PENDING_EPOCH_EVENTS: Map = + Map::new(PENDING_EPOCH_EVENTS_NAMESPACE); -// I've changed the `()` data to an `u8` as after serializing `()` is represented as "null", -// taking more space than a single digit u8. If we don't care about what's there, why not go with more efficient approach? : ) -// pub(crate) const REWARDED_SET_HEIGHTS_FOR_INTERVAL: Map<'_, (IntervalId, BlockHeight), u8> = -// Map::new("rsh"); +/// Contains operations that should get resolved at the end of the current interval. +pub(crate) const PENDING_INTERVAL_EVENTS: Map = + Map::new(PENDING_INTERVAL_EVENTS_NAMESPACE); -// pub(crate) const REWARDED_SET: Map<(u64, IdentityKey), NodeStatus> = Map::new("rs"); -pub(crate) const REWARDED_SET: Map<'_, (BlockHeight, IdentityKey), RewardedSetNodeStatus> = - Map::new("rs"); - -pub(crate) const EPOCHS: Map<'_, IntervalId, Interval> = Map::new("ephs"); - -pub fn save_epoch(storage: &mut dyn Storage, epoch: &Interval) -> Result<(), ContractError> { - CURRENT_EPOCH.save(storage, epoch)?; - EPOCHS.save(storage, epoch.id(), epoch)?; - Ok(()) +pub(crate) fn current_interval(storage: &dyn Storage) -> StdResult { + CURRENT_INTERVAL.load(storage) } -pub fn current_epoch_reward_params( - storage: &dyn Storage, -) -> Result { - Ok(CURRENT_EPOCH_REWARD_PARAMS.load(storage)?) +pub(crate) fn save_interval(storage: &mut dyn Storage, interval: &Interval) -> StdResult<()> { + CURRENT_INTERVAL.save(storage, interval) } -pub fn save_epoch_reward_params( - epoch_id: u32, +pub(crate) fn next_epoch_event_id_counter(store: &mut dyn Storage) -> StdResult { + let id: EpochEventId = EPOCH_EVENT_ID_COUNTER.may_load(store)?.unwrap_or_default() + 1; + EPOCH_EVENT_ID_COUNTER.save(store, &id)?; + Ok(id) +} + +pub(crate) fn next_interval_event_id_counter( + store: &mut dyn Storage, +) -> StdResult { + let id: IntervalEventId = INTERVAL_EVENT_ID_COUNTER + .may_load(store)? + .unwrap_or_default() + + 1; + INTERVAL_EVENT_ID_COUNTER.save(store, &id)?; + Ok(id) +} + +pub(crate) fn push_new_epoch_event( storage: &mut dyn Storage, -) -> Result<(), ContractError> { - let epoch_reward_params = epoch_reward_params(storage)?; - CURRENT_EPOCH_REWARD_PARAMS.save(storage, &epoch_reward_params)?; - crate::rewards::storage::EPOCH_REWARD_PARAMS.save(storage, epoch_id, &epoch_reward_params)?; - Ok(()) -} - -pub fn current_epoch(storage: &dyn Storage) -> Result { - CURRENT_EPOCH - .load(storage) - .map_err(|_| ContractError::EpochNotInitialized) -} - -pub(crate) fn save_rewarded_set( - storage: &mut dyn Storage, - height: BlockHeight, - active_set_size: u32, - entries: Vec, + env: &Env, + event: PendingEpochEventKind, ) -> StdResult<()> { - for (i, identity) in entries.into_iter().enumerate() { + let event_id = next_epoch_event_id_counter(storage)?; + 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, + env: &Env, + event: PendingIntervalEventKind, +) -> StdResult<()> { + let event_id = next_interval_event_id_counter(storage)?; + 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, +) -> StdResult<()> { + // our goal is to reduce the number of reads and writes to the underlying storage, + // whilst completely overwriting the current rewarded set. + // the naive implementation would be to read the entire current rewarded set, + // remove all of those entries + // and write the new one in its place. + // However, very often it might turn out that a node hasn't changed its status in the updated epoch, + // and in those cases we can save on having to remove the entry and writing a new one. + + // Note: so far it seems the contract compiles (and stores) fine with a `HashMap`, but if we ever + // run into any issues due to any randomness? we can switch it up for a BTreeMap + let mut old_nodes = REWARDED_SET + .range(storage, None, None, Order::Ascending) + .collect::, _>>()?; + + for (i, node_id) in new_set.into_iter().enumerate() { // first k nodes are active let set_status = if i < active_set_size as usize { RewardedSetNodeStatus::Active @@ -75,47 +111,77 @@ pub(crate) fn save_rewarded_set( RewardedSetNodeStatus::Standby }; - REWARDED_SET.save(storage, (height, identity), &set_status)?; + if !matches!(old_nodes.get(&node_id), Some(status) if status == &set_status) { + // if the status changed, or didn't exist, write it down: + REWARDED_SET.save(storage, node_id, &set_status)?; + } + + old_nodes.remove(&node_id); + } + + // finally remove the entries for nodes that no longer exist [in the rewarded set] + for old_node_id in old_nodes.keys() { + REWARDED_SET.remove(storage, *old_node_id) } Ok(()) } +pub(crate) fn initialise_storage( + storage: &mut dyn Storage, + starting_interval: Interval, +) -> StdResult<()> { + save_interval(storage, &starting_interval)?; + LAST_PROCESSED_EPOCH_EVENT.save(storage, &0)?; + LAST_PROCESSED_INTERVAL_EVENT.save(storage, &0)?; + EPOCH_EVENT_ID_COUNTER.save(storage, &0)?; + INTERVAL_EVENT_ID_COUNTER.save(storage, &0) +} + #[cfg(test)] mod tests { use super::*; - use crate::support::tests::test_helpers; + use cosmwasm_std::testing::mock_dependencies; + use cosmwasm_std::Order; + + fn read_entire_set(storage: &mut dyn Storage) -> HashMap { + REWARDED_SET + .range(storage, None, None, Order::Ascending) + .map(|r| r.unwrap()) + .collect() + } #[test] - fn saving_rewarded_set() { - let mut deps = test_helpers::init_contract(); + fn updating_rewarded_set() { + // just some variables to keep test assertions more concise + let active = &RewardedSetNodeStatus::Active; + let standby = &RewardedSetNodeStatus::Standby; - let active_set_size = 100; - let mut nodes = Vec::new(); - for i in 0..1000 { - nodes.push(format!("identity{:04}", i)) - } + let mut deps = mock_dependencies(); + let store = deps.as_mut().storage; + assert!(read_entire_set(store).is_empty()); - save_rewarded_set(deps.as_mut().storage, 1234, active_set_size, nodes).unwrap(); + // writing initial rewarded set shouldn't do anything fancy + update_rewarded_set(store, 2, vec![6, 2, 7, 4, 1]).unwrap(); + let current_set = read_entire_set(store); + assert_eq!(current_set.len(), 5); + assert_eq!(active, current_set.get(&6).unwrap()); + assert_eq!(active, current_set.get(&2).unwrap()); + assert_eq!(standby, current_set.get(&7).unwrap()); + assert_eq!(standby, current_set.get(&4).unwrap()); + assert_eq!(standby, current_set.get(&1).unwrap()); + assert!(current_set.get(&42).is_none()); - // first k nodes MUST BE active - for i in 0..1000 { - let identity = format!("identity{:04}", i); - if i < active_set_size { - assert_eq!( - RewardedSetNodeStatus::Active, - REWARDED_SET - .load(deps.as_ref().storage, (1234, identity)) - .unwrap() - ) - } else { - assert_eq!( - RewardedSetNodeStatus::Standby, - REWARDED_SET - .load(deps.as_ref().storage, (1234, identity)) - .unwrap() - ) - } - } + update_rewarded_set(store, 2, vec![2, 5, 6, 3, 4]).unwrap(); + let current_set = read_entire_set(store); + assert_eq!(current_set.len(), 5); + assert_eq!(active, current_set.get(&2).unwrap()); + assert_eq!(active, current_set.get(&5).unwrap()); + assert_eq!(standby, current_set.get(&6).unwrap()); + assert_eq!(standby, current_set.get(&3).unwrap()); + assert_eq!(standby, current_set.get(&4).unwrap()); + // those no longer are in the rewarded set + assert!(current_set.get(&7).is_none()); + assert!(current_set.get(&1).is_none()); } } diff --git a/contracts/mixnet/src/interval/transactions.rs b/contracts/mixnet/src/interval/transactions.rs index c9340846b7..705f36804a 100644 --- a/contracts/mixnet/src/interval/transactions.rs +++ b/contracts/mixnet/src/interval/transactions.rs @@ -2,271 +2,1594 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::error::ContractError; -use crate::error::ContractError::EpochInProgress; -use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::support::helpers::is_authorized; +use crate::interval::helpers::change_interval_config; +use crate::interval::pending_events::ContractExecutableEvent; +use crate::interval::storage::push_new_interval_event; +use crate::rewards; +use crate::rewards::storage as rewards_storage; +use crate::support::helpers::{ensure_is_authorized, ensure_is_owner}; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Storage}; -use mixnet_contract_common::events::{new_advance_interval_event, new_change_rewarded_set_event}; -use mixnet_contract_common::{IdentityKey, Interval}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::events::{ + new_advance_epoch_event, new_pending_epoch_events_execution_event, + new_pending_interval_config_update_event, new_pending_interval_events_execution_event, + new_reconcile_pending_events, +}; +use mixnet_contract_common::pending_events::PendingIntervalEventKind; +use mixnet_contract_common::MixId; +use std::collections::BTreeSet; -// We've distributed the rewards to the rewarded set from the validator api before making this call (implicit order, should be solved in the future) -// We now write the new rewarded set, snapshot the mixnodes and finally reconcile all delegations and undelegations. That way the rewards for the previous -// epoch will be calculated correctly as the delegations and undelegations from the previous epoch will only take effect in the next (current) one. -pub fn try_write_rewarded_set( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - rewarded_set: Vec, - active_set_size: u32, -) -> Result { - let state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; +// those two should be called in separate tx (from advancing epoch), +// since there might be a lot of events to execute. +// however, it should also be called when advancing epoch itself in case somebody +// managed to sneak in a transaction between those two operations +// (but then the amount of work is going to be minimal) +pub(crate) fn perform_pending_epoch_actions( + mut deps: DepsMut<'_>, + env: &Env, + limit: Option, +) -> Result<(Response, u32), MixnetContractError> { + let last_executed = storage::LAST_PROCESSED_EPOCH_EVENT.load(deps.storage)?; + let last_inserted = storage::EPOCH_EVENT_ID_COUNTER.load(deps.storage)?; - // check if this is executed by the permitted validator, if not reject the transaction - if info.sender != state.rewarding_validator_address { - return Err(ContractError::Unauthorized); + // no pending events + if last_executed == last_inserted { + return Ok((Response::new(), 0)); } - // We don't want more then we need, less should be fine, as we could have less nodes bonded overall - if active_set_size > state.params.mixnode_active_set_size { - return Err(ContractError::UnexpectedActiveSetSize { - received: active_set_size, - expected: state.params.mixnode_active_set_size, - }); + let pending = last_inserted - last_executed; + let last = limit + .map(|limit| { + if limit >= pending { + last_inserted + } else { + last_executed + limit + } + }) + .unwrap_or(last_inserted); + + let mut response = Response::new(); + // no need to use the [cosmwasm] range iterator as we know the exact keys in order + for event_id in last_executed + 1..=last { + let event = storage::PENDING_EPOCH_EVENTS.load(deps.storage, event_id)?; + let mut sub_response = event.execute(deps.branch(), env)?; + response.messages.append(&mut sub_response.messages); + response.attributes.append(&mut sub_response.attributes); + response.events.append(&mut sub_response.events); + // response.data.append(&mut sub_response.data); + + storage::PENDING_EPOCH_EVENTS.remove(deps.storage, event_id); } - if rewarded_set.len() as u32 > state.params.mixnode_rewarded_set_size { - return Err(ContractError::UnexpectedRewardedSetSize { - received: rewarded_set.len() as u32, - expected: state.params.mixnode_rewarded_set_size, - }); - } + storage::LAST_PROCESSED_EPOCH_EVENT.save(deps.storage, &last)?; - let block_height = env.block.height; - let num_nodes = rewarded_set.len(); - - storage::save_rewarded_set(deps.storage, block_height, active_set_size, rewarded_set)?; - storage::CURRENT_REWARDED_SET_HEIGHT.save(deps.storage, &block_height)?; - - Ok(Response::new().add_event(new_change_rewarded_set_event( - state.params.mixnode_active_set_size, - state.params.mixnode_rewarded_set_size, - num_nodes as u32, - ))) + Ok((response, last - last_executed)) } -pub fn try_init_epoch( +pub(crate) fn perform_pending_interval_actions( + mut deps: DepsMut<'_>, + env: &Env, + limit: Option, +) -> Result<(Response, u32), MixnetContractError> { + let last_executed = storage::LAST_PROCESSED_INTERVAL_EVENT.load(deps.storage)?; + let last_inserted = storage::INTERVAL_EVENT_ID_COUNTER.load(deps.storage)?; + + // no pending events + if last_executed == last_inserted { + return Ok((Response::new(), 0)); + } + + let pending = last_inserted - last_executed; + let last = limit + .map(|limit| { + if limit >= pending { + last_inserted + } else { + last_executed + limit + } + }) + .unwrap_or(last_inserted); + + let mut response = Response::new(); + // no need to use the [cosmwasm] range iterator as we know the exact keys in order + for event_id in last_executed + 1..=last { + let event = storage::PENDING_INTERVAL_EVENTS.load(deps.storage, event_id)?; + let mut sub_response = event.execute(deps.branch(), env)?; + response.messages.append(&mut sub_response.messages); + response.attributes.append(&mut sub_response.attributes); + response.events.append(&mut sub_response.events); + // response.data.append(&mut sub_response.data); + + storage::PENDING_INTERVAL_EVENTS.remove(deps.storage, event_id); + } + + storage::LAST_PROCESSED_INTERVAL_EVENT.save(deps.storage, &last)?; + + Ok((response, last - last_executed)) +} + +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()); + + 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 + return Err(MixnetContractError::EpochInProgress { + current_block_time: env.block.time.seconds(), + epoch_start: interval.current_epoch_start_unix_timestamp(), + epoch_end: interval.current_epoch_end_unix_timestamp(), + }); + } else { + 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) + } + + if interval.is_current_interval_over(&env) { + // first clear epoch events queue and then touch the interval actions + 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) +} + +fn update_rewarded_set( storage: &mut dyn Storage, - env: Env, -) -> Result { - is_authorized(info.sender.as_str().to_string(), storage)?; + new_rewarded_set: Vec, + expected_active_set_size: u32, +) -> Result<(), MixnetContractError> { + let reward_params = rewards_storage::REWARDING_PARAMS.load(storage)?; - init_epoch(storage, env)?; + // the rewarded set has been determined based off active set size taken from the contract, + // thus the expected value HAS TO match + if expected_active_set_size != reward_params.active_set_size { + return Err(MixnetContractError::UnexpectedActiveSetSize { + received: expected_active_set_size, + expected: reward_params.active_set_size, + }); + } - Ok(Response::default()) -} + if new_rewarded_set.len() as u32 > reward_params.rewarded_set_size { + return Err(MixnetContractError::UnexpectedRewardedSetSize { + received: new_rewarded_set.len() as u32, + expected: reward_params.rewarded_set_size, + }); + } -pub fn init_epoch(storage: &mut dyn Storage, env: Env) -> Result { - let epoch = Interval::init_epoch(env); - storage::save_epoch(storage, &epoch)?; - Ok(epoch) + // check for duplicates + let mut tmp_set = BTreeSet::new(); + for node_id in &new_rewarded_set { + if !tmp_set.insert(node_id) { + return Err(MixnetContractError::DuplicateRewardedSetNode { mix_id: *node_id }); + } + } + + Ok(storage::update_rewarded_set( + storage, + expected_active_set_size, + new_rewarded_set, + )?) } pub fn try_advance_epoch( + mut deps: DepsMut<'_>, env: Env, - storage: &mut dyn Storage, - sender: String, -) -> Result { - // in theory, we could have just changed the state and relied on its reversal upon failed - // execution, but better safe than sorry and do not modify the state at all unless we know - // all checks have succeeded. - + info: MessageInfo, + new_rewarded_set: Vec, + expected_active_set_size: u32, +) -> Result { // Only rewarding validator can attempt to advance epoch + ensure_is_authorized(info.sender, deps.storage)?; - is_authorized(sender, storage)?; + let mut response = Response::new(); - let current_epoch = storage::current_epoch(storage)?; - if current_epoch.is_over(env.clone()) { - let next_epoch = current_epoch.next_on_chain(env); - - storage::save_epoch(storage, &next_epoch)?; - storage::save_epoch_reward_params(next_epoch.id(), storage)?; - - return Ok(Response::new().add_event(new_advance_interval_event(next_epoch))); + // we must make sure that we roll into new epoch / interval with up to date state + // with no pending actions (like somebody wanting to update their profit margin) + let current_interval = storage::current_interval(deps.storage)?; + if !current_interval.is_current_epoch_over(&env) { + return Err(MixnetContractError::EpochInProgress { + current_block_time: env.block.time.seconds(), + epoch_start: current_interval.current_epoch_start_unix_timestamp(), + epoch_end: current_interval.current_epoch_end_unix_timestamp(), + }); + } else { + let (mut sub_response, executed) = + perform_pending_epoch_actions(deps.branch(), &env, None)?; + 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)); + } + + // first clear epoch events queue and then touch the interval actions + if current_interval.is_current_interval_over(&env) { + // the interval has finished -> we can change things such as the profit margin + let (mut sub_response, executed) = + perform_pending_interval_actions(deps.branch(), &env, None)?; + 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)); + + // this one is a very important one! + rewards::helpers::apply_reward_pool_changes(deps.storage)?; + } + + let updated_interval = current_interval.advance_epoch(); + let num_nodes = new_rewarded_set.len(); + + // finally save updated interval and the rewarded set + storage::save_interval(deps.storage, &updated_interval)?; + update_rewarded_set(deps.storage, new_rewarded_set, expected_active_set_size)?; + + Ok(response.add_event(new_advance_epoch_event(updated_interval, num_nodes as u32))) +} + +pub(crate) fn try_update_interval_config( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + epochs_in_interval: u32, + epoch_duration_secs: u64, + force_immediately: bool, +) -> Result { + ensure_is_owner(info.sender, deps.storage)?; + + let interval = storage::current_interval(deps.storage)?; + 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 = PendingIntervalEventKind::UpdateIntervalConfig { + epochs_in_interval, + epoch_duration_secs, + }; + 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( + epochs_in_interval, + epoch_duration_secs, + time_left, + )), + ) } - Err(EpochInProgress { - current_block_time: env.block.time.seconds(), - epoch_start: current_epoch.start_unix_timestamp(), - epoch_end: current_epoch.end_unix_timestamp(), - }) } #[cfg(test)] mod tests { use super::*; - use crate::support::tests::test_helpers; - use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::Timestamp; - use mixnet_contract_common::RewardedSetNodeStatus; - use mixnet_params_storage::rewarding_validator_address; + use crate::support::tests::fixtures; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::Addr; + use mixnet_contract_common::pending_events::PendingEpochEventKind; + use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; - #[test] - fn writing_rewarded_set() { - let mut env = mock_env(); - let mut deps = test_helpers::init_contract(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let authorised_sender = mock_info(current_state.rewarding_validator_address.as_str(), &[]); - let full_rewarded_set = (0..current_state.params.mixnode_rewarded_set_size) - .map(|i| format!("identity{:04}", i)) - .collect::>(); - let last_update = 123; - storage::CURRENT_REWARDED_SET_HEIGHT - .save(deps.as_mut().storage, &last_update) - .unwrap(); - - // can only be performed by the permitted validator - let dummy_sender = mock_info("dummy_sender", &[]); - assert_eq!( - Err(ContractError::Unauthorized), - try_write_rewarded_set( - deps.as_mut(), - env.clone(), - dummy_sender, - full_rewarded_set.clone(), - current_state.params.mixnode_active_set_size - ) - ); - - // the sender must use the same active set size as the one defined in the contract - assert_eq!( - Err(ContractError::UnexpectedActiveSetSize { - received: 123, - expected: current_state.params.mixnode_active_set_size - }), - try_write_rewarded_set( - deps.as_mut(), - env.clone(), - authorised_sender.clone(), - full_rewarded_set.clone(), - 123 - ) - ); - - // the sender cannot provide more nodes than the rewarded set size - let mut bigger_set = full_rewarded_set.clone(); - bigger_set.push("another_node".to_string()); - assert_eq!( - Err(ContractError::UnexpectedRewardedSetSize { - received: current_state.params.mixnode_rewarded_set_size + 1, - expected: current_state.params.mixnode_rewarded_set_size - }), - try_write_rewarded_set( - deps.as_mut(), - env.clone(), - authorised_sender.clone(), - bigger_set, - current_state.params.mixnode_active_set_size - ) - ); - - // cannot be performed too soon after a previous update - env.block.height = last_update + 1; - // after successful rewarded set write, all internal storage structures are updated appropriately - env.block.height = last_update + crate::constants::REWARDED_SET_REFRESH_BLOCKS; - let expected_response = Response::new().add_event(new_change_rewarded_set_event( - current_state.params.mixnode_active_set_size, - current_state.params.mixnode_rewarded_set_size, - full_rewarded_set.len() as u32, - )); - - assert_eq!( - Ok(expected_response), - try_write_rewarded_set( - deps.as_mut(), - env.clone(), - authorised_sender, - full_rewarded_set.clone(), - current_state.params.mixnode_active_set_size - ) - ); - - for (i, rewarded_node) in full_rewarded_set.into_iter().enumerate() { - if (i as u32) < current_state.params.mixnode_active_set_size { - assert_eq!( - RewardedSetNodeStatus::Active, - storage::REWARDED_SET - .load(deps.as_ref().storage, (env.block.height, rewarded_node)) - .unwrap() - ) - } else { - assert_eq!( - RewardedSetNodeStatus::Standby, - storage::REWARDED_SET - .load(deps.as_ref().storage, (env.block.height, rewarded_node)) - .unwrap() - ) - } + 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 = PendingEpochEventKind::Undelegate { + owner: Addr::unchecked("foomp"), + mix_id: i as MixId, + proxy: None, + }; + 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 = PendingIntervalEventKind::ChangeMixCostParams { + mix_id: i as MixId, + new_costs: fixtures::mix_node_cost_params_fixture(), + }; + storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); + } + } + + #[cfg(test)] + mod performing_pending_epoch_actions { + use super::*; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::{coin, coins, wasm_execute, Addr, BankMsg, Empty, SubMsg}; + use mixnet_contract_common::events::{ + new_active_set_update_event, new_delegation_on_unbonded_node_event, + new_undelegation_event, + }; + use mixnet_contract_common::pending_events::PendingEpochEventKind; + + #[test] + fn without_limit_executes_all_actions() { + let mut test = TestSetup::new(); + assert_eq!( + 0, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + let env = test.env(); + // no events are pending, nothing should get done + let (res, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); + assert_eq!(res, Response::new()); + assert_eq!(executed, 0); + assert_eq!( + 0, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_epoch_actions(&mut test, 42); + let (res, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); + // dummy actions don't emit any events + assert_eq!(res, Response::new()); + assert_eq!(executed, 42); + assert_eq!( + 42, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .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()); + assert_eq!(executed, 10); + assert_eq!( + 52, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_epoch_actions(&mut test, 1); + let (res, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); + assert_eq!(res, Response::new()); + assert_eq!(executed, 1); + assert_eq!( + 53, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_epoch_actions(&mut test, 10); + 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(env.block.height, 50)) + ); + assert_eq!(executed, 21); + assert_eq!( + 74, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + + #[test] + fn catches_all_events_and_messages_from_executed_actions() { + let mut test = TestSetup::new(); + let vesting_contract = test.vesting_contract(); + + let env = test.env(); + let legit_mix = test.add_dummy_mixnode("mix-owner", None); + let delegator = Addr::unchecked("delegator"); + let amount = 123_456_789u128; + test.add_immediate_delegation(delegator.as_str(), amount, legit_mix); + + let mut expected_events = Vec::new(); + let mut expected_messages: Vec> = Vec::new(); + + // delegate to node that doesn't exist, + // we expect to receive BankMsg with tokens being returned, + // and event regarding delegation + 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, &env, non_existent_delegation) + .unwrap(); + expected_events.push(new_delegation_on_unbonded_node_event( + &Addr::unchecked("foomp"), + &None, + 123, + )); + expected_messages.push(SubMsg::new(BankMsg::Send { + to_address: "foomp".to_string(), + amount: coins(123, TEST_COIN_DENOM), + })); + + // delegation to node that doesn't exist with vesting contract + // we expect the same as above PLUS TrackUndelegation message + 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, &env, non_existent_delegation) + .unwrap(); + expected_events.push(new_delegation_on_unbonded_node_event( + &Addr::unchecked("foomp2"), + &Some(vesting_contract.clone()), + 123, + )); + expected_messages.push(SubMsg::new(BankMsg::Send { + to_address: vesting_contract.clone().into_string(), + amount: coins(123, TEST_COIN_DENOM), + })); + let msg = VestingContractExecuteMsg::TrackUndelegation { + owner: "foomp2".to_string(), + mix_id: 123, + amount: coin(123, TEST_COIN_DENOM), + }; + let track_undelegate_message = wasm_execute(vesting_contract, &msg, vec![]).unwrap(); + expected_messages.push(SubMsg::new(track_undelegate_message)); + + // updating active set should only emit events and no cosmos messages + 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 = PendingEpochEventKind::Undelegate { + owner: delegator.clone(), + mix_id: legit_mix, + proxy: None, + }; + 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), + })); + + let (res, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); + let mut expected = Response::new().add_events(expected_events); + expected.messages = expected_messages; + assert_eq!(res, expected); + assert_eq!(executed, 4); + assert_eq!( + 4, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + + #[test] + fn respects_limit() { + let mut test = TestSetup::new(); + + let env = test.env(); + + push_n_dummy_epoch_actions(&mut test, 20); + + // no events are pending, nothing should get done + let (_, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, Some(0)).unwrap(); + assert_eq!(executed, 0); + assert_eq!( + 0, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + let (_, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, Some(10)).unwrap(); + assert_eq!(executed, 10); + assert_eq!( + 10, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + let (_, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, Some(10)).unwrap(); + assert_eq!(executed, 10); + assert_eq!( + 20, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_epoch_actions(&mut test, 20); + let (_, executed) = + perform_pending_epoch_actions(test.deps_mut(), &env, Some(100)).unwrap(); + assert_eq!(executed, 20); + assert_eq!( + 40, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + } + + #[cfg(test)] + mod performing_pending_interval_actions { + use super::*; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::{coin, Empty, SubMsg}; + use mixnet_contract_common::events::{ + new_interval_config_update_event, new_mixnode_cost_params_update_event, + new_rewarding_params_update_event, + }; + use mixnet_contract_common::mixnode::MixNodeCostParams; + use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; + use mixnet_contract_common::Percent; + + #[test] + fn without_limit_executes_all_actions() { + let mut test = TestSetup::new(); + assert_eq!( + 0, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + let env = test.env(); + // no events are pending, nothing should get done + let (res, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, None).unwrap(); + assert_eq!(res, Response::new()); + assert_eq!(executed, 0); + assert_eq!( + 0, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_interval_actions(&mut test, 42); + let (res, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, None).unwrap(); + // dummy actions don't emit any events + assert_eq!(res, Response::new()); + assert_eq!(executed, 42); + assert_eq!( + 42, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_interval_actions(&mut test, 10); + let (res, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, None).unwrap(); + assert_eq!(res, Response::new()); + assert_eq!(executed, 10); + assert_eq!( + 52, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_interval_actions(&mut test, 1); + let (res, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, None).unwrap(); + assert_eq!(res, Response::new()); + assert_eq!(executed, 1); + assert_eq!( + 53, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_interval_actions(&mut test, 10); + let update = IntervalRewardingParamsUpdate { + rewarded_set_size: Some(500), + ..Default::default() + }; + 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( + env.block.height, + update, + updated_params + )) + ); + assert_eq!(executed, 21); + assert_eq!( + 74, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + + #[test] + fn catches_all_events_and_messages_from_executed_actions() { + let mut test = TestSetup::new(); + let env = test.env(); + + let mut expected_events = Vec::new(); + let expected_messages: Vec> = Vec::new(); + + let legit_mix = test.add_dummy_mixnode("mix-owner", None); + let new_costs = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(12).unwrap(), + interval_operating_cost: coin(123_000, TEST_COIN_DENOM), + }; + let cost_change = PendingIntervalEventKind::ChangeMixCostParams { + mix_id: legit_mix, + new_costs: new_costs.clone(), + }; + + 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 = 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 = 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, &env, change_interval) + .unwrap(); + expected_events.push(new_interval_config_update_event( + env.block.height, + 123, + 1000, + expected_updated2.interval, + )); + + let (res, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, None).unwrap(); + let mut expected = Response::new().add_events(expected_events); + expected.messages = expected_messages; + assert_eq!(res, expected); + assert_eq!(executed, 3); + assert_eq!( + 3, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + + #[test] + fn respects_limit() { + let mut test = TestSetup::new(); + + let env = test.env(); + + push_n_dummy_interval_actions(&mut test, 20); + + // no events are pending, nothing should get done + let (_, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, Some(0)).unwrap(); + assert_eq!(executed, 0); + assert_eq!( + 0, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + let (_, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, Some(10)).unwrap(); + assert_eq!(executed, 10); + assert_eq!( + 10, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + let (_, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, Some(10)).unwrap(); + assert_eq!(executed, 10); + assert_eq!( + 20, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + + push_n_dummy_interval_actions(&mut test, 20); + let (_, executed) = + perform_pending_interval_actions(test.deps_mut(), &env, Some(100)).unwrap(); + assert_eq!(executed, 20); + assert_eq!( + 40, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + } + + #[cfg(test)] + 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::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, 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(); + + push_n_dummy_epoch_actions(&mut test, 10); + push_n_dummy_interval_actions(&mut test, 10); + test.skip_to_current_epoch_end(); + + let env = test.env(); + 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(); + assert!(epoch_events.is_empty()); + assert_eq!(interval_events.len(), 10); + } + + #[test] + fn clears_both_epoch_and_interval_events_if_interval_has_finished() { + let mut test = TestSetup::new(); + + 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, rewarding_validator, None).unwrap(); + + let epoch_events = test.pending_epoch_events(); + let interval_events = test.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert!(interval_events.is_empty()); + } + + #[test] + fn with_limit_prioritises_epoch_events() { + let mut test1 = TestSetup::new(); + let mut test2 = TestSetup::new(); + let mut test3 = TestSetup::new(); + let mut test4 = TestSetup::new(); + + for test in [&mut test1, &mut test2, &mut test3, &mut test4].iter_mut() { + push_n_dummy_epoch_actions(test, 10); + push_n_dummy_interval_actions(test, 10); + test.skip_to_current_interval_end(); + } + + 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(), + 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(), + 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(), + 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(), + 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(), + 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(), + 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(), + 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, rewarding_validator, Some(100)) + .unwrap(); + let epoch_events = test4.pending_epoch_events(); + let interval_events = test4.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert!(interval_events.is_empty()); + } + + #[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 = 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, &env, non_existent_delegation) + .unwrap(); + expected_events.push(new_delegation_on_unbonded_node_event( + &Addr::unchecked("foomp"), + &None, + 123, + )); + expected_messages.push(SubMsg::new(BankMsg::Send { + to_address: "foomp".to_string(), + amount: coins(123, TEST_COIN_DENOM), + })); + expected_events.push(new_pending_epoch_events_execution_event(1)); + + // interval event + let update = IntervalRewardingParamsUpdate { + rewarded_set_size: Some(500), + ..Default::default() + }; + 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, + )); + expected_events.push(new_pending_interval_events_execution_event(1)); + + test.skip_to_current_interval_end(); + let env = test.env(); + 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); + assert_eq!( + 1, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + assert_eq!( + 1, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); } - assert_eq!( - env.block.height, - storage::CURRENT_REWARDED_SET_HEIGHT - .load(deps.as_ref().storage) - .unwrap() - ); } #[test] - fn advancing_epoch() { - let mut env = mock_env(); - let mut deps = test_helpers::init_contract(); - let sender = rewarding_validator_address(&deps.storage).unwrap(); + fn updating_rewarded_set() { + // the actual logic behind writing stuff to the storage has been tested in + // different unit test + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + let current_rewarded_set = test.rewarding_params().rewarded_set_size; - let _current_epoch = init_epoch(&mut deps.storage, env.clone()).unwrap(); - - // Works as its after the current epoch - env.block.time = Timestamp::from_seconds(1641081600); - assert!(try_advance_epoch(env.clone(), deps.as_mut().storage, sender.clone()).is_ok()); - - let current_epoch = crate::interval::storage::current_epoch(&mut deps.storage).unwrap(); - - // same if the current blocktime is set to BEFORE the first interval has even begun - // (say we decided to set the first interval to be some time in the future at initialisation) - env.block.time = Timestamp::from_seconds(1609459200); + // active set size has to match the expectation + let err = update_rewarded_set( + test.deps_mut().storage, + vec![1, 2, 3], + current_active_set - 10, + ) + .unwrap_err(); assert_eq!( - Err(ContractError::EpochInProgress { - current_block_time: 1609459200, - epoch_start: current_epoch.start_unix_timestamp(), - epoch_end: current_epoch.end_unix_timestamp() - }), - try_advance_epoch(env.clone(), deps.as_mut().storage, sender.clone(),) + err, + MixnetContractError::UnexpectedActiveSetSize { + received: current_active_set - 10, + expected: current_active_set, + } ); - // works otherwise + // number of nodes provided has to be equal or smaller than the current rewarded set size - // interval that has just finished - env.block.time = - Timestamp::from_seconds(current_epoch.start_unix_timestamp() as u64 + 10000); - let expected_new_epoch = current_epoch.next_on_chain(env.clone()); - let expected_response = - Response::new().add_event(new_advance_interval_event(expected_new_epoch)); + // fewer nodes + let res = update_rewarded_set(test.deps_mut().storage, vec![1, 2, 3], current_active_set); + assert!(res.is_ok()); + + let exact_num = (1u32..) + .take(current_rewarded_set as usize) + .collect::>(); + let res = update_rewarded_set(test.deps_mut().storage, exact_num, current_active_set); + assert!(res.is_ok()); + + // one more + let too_many = (1u32..) + .take((current_rewarded_set + 1) as usize) + .collect::>(); + let err = + update_rewarded_set(test.deps_mut().storage, too_many, current_active_set).unwrap_err(); assert_eq!( - Ok(expected_response), - try_advance_epoch(env.clone(), deps.as_mut().storage, sender) + err, + MixnetContractError::UnexpectedRewardedSetSize { + received: current_rewarded_set + 1, + expected: current_rewarded_set, + } ); - // interval way back in the past (i.e. 'somebody' failed to advance it for a long time) - env.block.time = Timestamp::from_seconds(1672531200); - storage::save_epoch(deps.as_mut().storage, ¤t_epoch).unwrap(); - let expected_new_epoch = current_epoch.next_on_chain(env.clone()); - let expected_response = - Response::new().add_event(new_advance_interval_event(expected_new_epoch)); - let sender = rewarding_validator_address(&deps.storage).unwrap(); + // doesn't allow for duplicates + let nodes_with_duplicate = vec![1, 2, 3, 4, 5, 1]; + let err = update_rewarded_set( + test.deps_mut().storage, + nodes_with_duplicate, + current_active_set, + ) + .unwrap_err(); assert_eq!( - Ok(expected_response), - try_advance_epoch(env.clone(), deps.as_mut().storage, sender) + err, + MixnetContractError::DuplicateRewardedSetNode { mix_id: 1 } ); + let nodes_with_duplicate = vec![1, 2, 3, 5, 4, 5]; + let err = update_rewarded_set( + test.deps_mut().storage, + nodes_with_duplicate, + current_active_set, + ) + .unwrap_err(); + assert_eq!( + err, + MixnetContractError::DuplicateRewardedSetNode { mix_id: 5 } + ); + } + + #[cfg(test)] + mod advancing_epoch { + use super::*; + use crate::rewards::models::RewardPoolChange; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{coin, coins, BankMsg, Decimal, Empty, SubMsg}; + use mixnet_contract_common::events::{ + new_delegation_on_unbonded_node_event, new_rewarding_params_update_event, + }; + use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; + use mixnet_contract_common::RewardedSetNodeStatus; + + #[test] + fn can_only_be_performed_by_specified_rewarding_validator() { + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + let some_sender = mock_info("foomper", &[]); + + test.skip_to_current_epoch_end(); + + let env = test.env(); + let res = try_advance_epoch( + test.deps_mut(), + env, + some_sender, + vec![1, 2, 3], + current_active_set, + ); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + // good address (sanity check) + let env = test.env(); + let sender = test.rewarding_validator(); + let res = try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ); + assert!(res.is_ok()) + } + + #[test] + fn can_only_be_performed_if_epoch_is_over() { + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + + let env = test.env(); + let sender = test.rewarding_validator(); + let res = try_advance_epoch( + test.deps_mut(), + env, + sender.clone(), + vec![1, 2, 3], + current_active_set, + ); + assert!(matches!( + res, + Err(MixnetContractError::EpochInProgress { .. }) + )); + + // sanity check + test.skip_to_current_epoch_end(); + let env = test.env(); + let res = try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ); + assert!(res.is_ok()) + } + + #[test] + fn only_clears_epoch_events_if_interval_is_in_progress() { + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + + push_n_dummy_epoch_actions(&mut test, 10); + push_n_dummy_interval_actions(&mut test, 10); + test.skip_to_current_epoch_end(); + + let env = test.env(); + let sender = test.rewarding_validator(); + try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ) + .unwrap(); + + let epoch_events = test.pending_epoch_events(); + let interval_events = test.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert_eq!(interval_events.len(), 10); + } + + #[test] + fn clears_both_epoch_and_interval_events_if_interval_has_finished() { + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + + push_n_dummy_epoch_actions(&mut test, 10); + push_n_dummy_interval_actions(&mut test, 10); + test.skip_to_current_interval_end(); + + let env = test.env(); + let sender = test.rewarding_validator(); + try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ) + .unwrap(); + + let epoch_events = test.pending_epoch_events(); + let interval_events = test.pending_interval_events(); + assert!(epoch_events.is_empty()); + assert!(interval_events.is_empty()); + } + + #[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 = 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, &env, non_existent_delegation) + .unwrap(); + expected_events.push(new_delegation_on_unbonded_node_event( + &Addr::unchecked("foomp"), + &None, + 123, + )); + expected_messages.push(SubMsg::new(BankMsg::Send { + to_address: "foomp".to_string(), + amount: coins(123, TEST_COIN_DENOM), + })); + expected_events.push(new_pending_epoch_events_execution_event(1)); + + // interval event + let update = IntervalRewardingParamsUpdate { + rewarded_set_size: Some(500), + ..Default::default() + }; + 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, + )); + expected_events.push(new_pending_interval_events_execution_event(1)); + let current_interval = test.current_interval(); + let expected = current_interval.advance_epoch(); + expected_events.push(new_advance_epoch_event(expected, 3)); + + test.skip_to_current_interval_end(); + + let env = test.env(); + let sender = test.rewarding_validator(); + let res = try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ) + .unwrap(); + + let mut expected = Response::new().add_events(expected_events); + expected.messages = expected_messages; + assert_eq!(res, expected); + assert_eq!( + 1, + storage::LAST_PROCESSED_EPOCH_EVENT + .load(test.deps().storage) + .unwrap() + ); + assert_eq!( + 1, + storage::LAST_PROCESSED_INTERVAL_EVENT + .load(test.deps().storage) + .unwrap() + ); + } + + #[test] + fn if_interval_is_over_applies_reward_pool_changes() { + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + + let start_params = test.rewarding_params(); + + let pool_update = Decimal::from_atomics(100_000_000u32, 0).unwrap(); + // push some changes + rewards_storage::PENDING_REWARD_POOL_CHANGE + .save( + test.deps_mut().storage, + &RewardPoolChange { + removed: pool_update, + added: Default::default(), + }, + ) + .unwrap(); + + // end of epoch - nothing has happened + let sender = test.rewarding_validator(); + test.skip_to_current_epoch_end(); + let env = test.env(); + try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ) + .unwrap(); + + let params = test.rewarding_params(); + let pool_change = rewards_storage::PENDING_REWARD_POOL_CHANGE + .load(test.deps().storage) + .unwrap(); + assert_eq!(params, start_params); + assert_eq!(pool_change.removed, pool_update); + + let sender = test.rewarding_validator(); + test.skip_to_current_interval_end(); + let env = test.env(); + try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ) + .unwrap(); + + let epochs_in_interval = test.current_interval().epochs_in_interval(); + let update = IntervalRewardingParamsUpdate { + reward_pool: Some(start_params.interval.reward_pool - pool_update), + staking_supply: Some(start_params.interval.staking_supply + pool_update), + ..Default::default() + }; + let mut expected = start_params; + expected + .try_apply_updates(update, epochs_in_interval) + .unwrap(); + + let params = test.rewarding_params(); + let pool_change = rewards_storage::PENDING_REWARD_POOL_CHANGE + .load(test.deps().storage) + .unwrap(); + assert_eq!(params, expected); + assert_eq!(pool_change.removed, Decimal::zero()); + } + + #[test] + fn updates_rewarded_set_and_interval_data() { + let mut test = TestSetup::new(); + let current_active_set = test.rewarding_params().active_set_size; + + let interval_pre = test.current_interval(); + let rewarded_set_pre = test.rewarded_set(); + assert!(rewarded_set_pre.is_empty()); + + let sender = test.rewarding_validator(); + test.skip_to_current_interval_end(); + let env = test.env(); + try_advance_epoch( + test.deps_mut(), + env, + sender, + vec![1, 2, 3], + current_active_set, + ) + .unwrap(); + + let interval_post = test.current_interval(); + let rewarded_set = test.rewarded_set(); + + let expected_id = interval_pre.current_epoch_absolute_id() + 1; + assert_eq!(interval_post.current_epoch_absolute_id(), expected_id); + assert_eq!( + rewarded_set, + vec![ + (1, RewardedSetNodeStatus::Active), + (2, RewardedSetNodeStatus::Active), + (3, RewardedSetNodeStatus::Active) + ] + ); + } + } + + #[cfg(test)] + mod updating_interval_config { + use super::*; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Decimal; + use std::time::Duration; + + #[test] + fn can_only_be_done_by_contract_owner() { + let mut test = TestSetup::new(); + + let rewarding_validator = test.rewarding_validator(); + let owner = test.owner(); + let random = mock_info("random-guy", &[]); + + let env = test.env(); + let res = try_update_interval_config( + test.deps_mut(), + env.clone(), + rewarding_validator, + 100, + 1000, + false, + ); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + let res = + try_update_interval_config(test.deps_mut(), env.clone(), random, 100, 1000, false); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + let res = try_update_interval_config(test.deps_mut(), env, owner, 100, 1000, false); + assert!(res.is_ok()) + } + + #[test] + fn if_interval_is_finished_change_happens_immediately() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = test.rewarding_params(); + test.skip_to_current_interval_end(); + + let env = test.env(); + let res = + try_update_interval_config(test.deps_mut(), env, owner.clone(), 100, 1000, false); + assert!(res.is_ok()); + let new = test.rewarding_params(); + let interval = test.current_interval(); + assert_ne!(old, new); + assert_eq!(interval.epochs_in_interval(), 100); + + // sanity check for "normal" case + let mut test = TestSetup::new(); + let env = test.env(); + let res = try_update_interval_config(test.deps_mut(), env, owner, 100, 1000, false); + assert!(res.is_ok()); + let new = test.rewarding_params(); + assert_eq!(old, new); + } + + #[test] + fn if_update_is_forced_it_happens_immediately() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = test.rewarding_params(); + let env = test.env(); + let res = try_update_interval_config(test.deps_mut(), env, owner, 100, 1000, true); + assert!(res.is_ok()); + let new = test.rewarding_params(); + let interval = test.current_interval(); + assert_ne!(old, new); + assert_eq!(interval.epochs_in_interval(), 100); + } + + #[test] + fn without_forcing_it_change_happens_upon_clearing_interval_events() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = test.rewarding_params(); + let env = test.env(); + let res = try_update_interval_config(test.deps_mut(), env, owner, 100, 1000, false); + assert!(res.is_ok()); + let new = test.rewarding_params(); + assert_eq!(old, new); + + // make sure it's actually saved to pending events + let events = test.pending_interval_events(); + 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(); + let new = test.rewarding_params(); + let interval = test.current_interval(); + assert_ne!(old, new); + assert_eq!(interval.epochs_in_interval(), 100); + } + + #[test] + fn upon_update_fields_are_recomputed_accordingly() { + let mut test = TestSetup::new(); + let owner = test.owner(); + let two = Decimal::from_atomics(2u32, 0).unwrap(); + + let params_before = test.rewarding_params(); + + // skip few epochs just for the sake of it + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + test.skip_to_next_epoch(); + + let interval_before = + crate::interval::storage::current_interval(test.deps().storage).unwrap(); + + // half the number of epochs (thus double reward budget) + // and change epoch length + let env = test.env(); + try_update_interval_config( + test.deps_mut(), + env, + owner, + interval_before.epochs_in_interval() / 2, + 1234, + true, + ) + .unwrap(); + + let interval_after = + crate::interval::storage::current_interval(test.deps().storage).unwrap(); + let params_after = rewards_storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap(); + assert_eq!( + interval_after.epochs_in_interval(), + interval_before.epochs_in_interval() / 2 + ); + assert_eq!( + params_after.interval.epoch_reward_budget, + params_before.interval.epoch_reward_budget * two + ); + assert_eq!(interval_after.epoch_length(), Duration::from_secs(1234)) + } } } diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 5b0d0c8d0c..3cff5ed93f 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -4,11 +4,12 @@ mod constants; pub mod contract; mod delegations; -mod error; mod gateways; mod interval; mod mixnet_contract_settings; mod mixnodes; -mod queued_migrations; mod rewards; mod support; + +#[cfg(feature = "contract-testing")] +mod testing; diff --git a/contracts/mixnet/src/mixnet_contract_settings/mod.rs b/contracts/mixnet/src/mixnet_contract_settings/mod.rs index 9898f4e305..3bb874a2ec 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/mod.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/mod.rs @@ -1,7 +1,6 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod models; pub mod queries; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/mixnet_contract_settings/models.rs b/contracts/mixnet/src/mixnet_contract_settings/models.rs deleted file mode 100644 index d271972be7..0000000000 --- a/contracts/mixnet/src/mixnet_contract_settings/models.rs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use cosmwasm_std::Addr; -use mixnet_contract_common::ContractStateParams; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -pub struct ContractState { - pub owner: Addr, // only the owner account can update state - pub mix_denom: String, - pub rewarding_validator_address: Addr, - pub params: ContractStateParams, -} diff --git a/contracts/mixnet/src/mixnet_contract_settings/queries.rs b/contracts/mixnet/src/mixnet_contract_settings/queries.rs index 845be0dc67..1181c87d2f 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/queries.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/queries.rs @@ -1,27 +1,31 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; use cosmwasm_std::{Deps, StdResult}; -use mixnet_contract_common::{ContractStateParams, MixnetContractVersion}; +use mixnet_contract_common::{ContractBuildInformation, ContractState, ContractStateParams}; -pub fn query_contract_settings_params(deps: Deps<'_>) -> StdResult { +pub(crate) fn query_contract_state(deps: Deps<'_>) -> StdResult { + storage::CONTRACT_STATE.load(deps.storage) +} + +pub(crate) fn query_contract_settings_params(deps: Deps<'_>) -> StdResult { storage::CONTRACT_STATE .load(deps.storage) .map(|settings| settings.params) } -pub fn query_rewarding_validator_address(deps: Deps<'_>) -> StdResult { +pub(crate) fn query_rewarding_validator_address(deps: Deps<'_>) -> StdResult { storage::CONTRACT_STATE .load(deps.storage) .map(|settings| settings.rewarding_validator_address.to_string()) } -pub(crate) fn query_contract_version() -> MixnetContractVersion { +pub(crate) fn query_contract_version() -> ContractBuildInformation { // as per docs // env! macro will expand to the value of the named environment variable at // compile time, yielding an expression of type `&'static str` - MixnetContractVersion { + ContractBuildInformation { build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(), build_version: env!("VERGEN_BUILD_SEMVER").to_string(), commit_sha: env!("VERGEN_GIT_SHA").to_string(), @@ -34,10 +38,8 @@ pub(crate) fn query_contract_version() -> MixnetContractVersion { #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::mixnet_contract_settings::models::ContractState; use crate::support::tests::test_helpers; - - use cosmwasm_std::Addr; + use cosmwasm_std::{coin, Addr}; #[test] fn query_for_contract_settings_works() { @@ -45,14 +47,13 @@ pub(crate) mod tests { let dummy_state = ContractState { owner: Addr::unchecked("someowner"), - mix_denom: String::from("unym"), rewarding_validator_address: Addr::unchecked("monitor"), + vesting_contract_address: Addr::unchecked("foomp"), + rewarding_denom: "unym".to_string(), params: ContractStateParams { - minimum_mixnode_pledge: 123u128.into(), - minimum_gateway_pledge: 456u128.into(), - mixnode_rewarded_set_size: 1000, - mixnode_active_set_size: 500, - staking_supply: 1000000u128.into(), + minimum_mixnode_delegation: None, + minimum_mixnode_pledge: coin(123u128, "unym"), + minimum_gateway_pledge: coin(456u128, "unym"), }, }; diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 5f4c736d13..990c5e604d 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -1,69 +1,58 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::mixnet_contract_settings::models::ContractState; -use cosmwasm_std::StdResult; -use cosmwasm_std::Storage; +use crate::constants::CONTRACT_STATE_KEY; +use cosmwasm_std::{Addr, Storage}; +use cosmwasm_std::{Coin, StdResult}; use cw_storage_plus::Item; -use mixnet_contract_common::{Layer, LayerDistribution}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::ContractState; -pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new("config"); -pub(crate) const LAYERS: Item<'_, LayerDistribution> = Item::new("layers"); +pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new(CONTRACT_STATE_KEY); -pub fn rewarding_validator_address(storage: &dyn Storage) -> StdResult { - CONTRACT_STATE +pub fn rewarding_validator_address(storage: &dyn Storage) -> Result { + Ok(CONTRACT_STATE .load(storage) - .map(|state| state.rewarding_validator_address.to_string()) + .map(|state| state.rewarding_validator_address)?) } -pub fn mix_denom(storage: &dyn Storage) -> StdResult { - CONTRACT_STATE.load(storage).map(|state| state.mix_denom) +pub(crate) fn minimum_mixnode_pledge(storage: &dyn Storage) -> Result { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.params.minimum_mixnode_pledge)?) } -pub fn increment_layer_count(storage: &mut dyn Storage, layer: Layer) -> StdResult<()> { - LAYERS - .update(storage, |mut distribution| { - match layer { - Layer::Gateway => distribution.gateways += 1, - Layer::One => distribution.layer1 += 1, - Layer::Two => distribution.layer2 += 1, - Layer::Three => distribution.layer3 += 1, - } - Ok(distribution) - }) - .map(|_| ()) +pub(crate) fn minimum_gateway_pledge(storage: &dyn Storage) -> Result { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.params.minimum_gateway_pledge)?) } -pub fn decrement_layer_count(storage: &mut dyn Storage, layer: Layer) -> StdResult<()> { - LAYERS - .update(storage, |mut distribution| { - match layer { - Layer::Gateway => { - distribution.gateways = distribution - .gateways - .checked_sub(1) - .expect("tried to subtract from unsigned zero!") - } - Layer::One => { - distribution.layer1 = distribution - .layer1 - .checked_sub(1) - .expect("tried to subtract from unsigned zero!") - } - Layer::Two => { - distribution.layer2 = distribution - .layer2 - .checked_sub(1) - .expect("tried to subtract from unsigned zero!") - } - Layer::Three => { - distribution.layer3 = distribution - .layer3 - .checked_sub(1) - .expect("tried to subtract from unsigned zero!") - } - } - Ok(distribution) - }) - .map(|_| ()) +#[allow(unused)] +pub(crate) fn minimum_delegation_stake( + storage: &dyn Storage, +) -> Result, MixnetContractError> { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.params.minimum_mixnode_delegation)?) +} + +#[allow(unused)] +pub(crate) fn rewarding_denom(storage: &dyn Storage) -> Result { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.rewarding_denom)?) +} + +pub(crate) fn vesting_contract_address(storage: &dyn Storage) -> Result { + Ok(CONTRACT_STATE + .load(storage) + .map(|state| state.vesting_contract_address)?) +} + +pub(crate) fn initialise_storage( + storage: &mut dyn Storage, + initial_state: ContractState, +) -> StdResult<()> { + CONTRACT_STATE.save(storage, &initial_state) } diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index 2a4cef7af7..1413d492ca 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -1,56 +1,51 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::error::ContractError; -use cosmwasm_std::Addr; use cosmwasm_std::DepsMut; use cosmwasm_std::MessageInfo; use cosmwasm_std::Response; -use mixnet_contract_common::events::new_settings_update_event; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::events::{ + new_rewarding_validator_address_update_event, new_settings_update_event, +}; use mixnet_contract_common::ContractStateParams; pub fn try_update_rewarding_validator_address( deps: DepsMut<'_>, info: MessageInfo, address: String, -) -> Result { +) -> Result { let mut state = storage::CONTRACT_STATE.load(deps.storage)?; if info.sender != state.owner { - return Err(ContractError::Unauthorized); + return Err(MixnetContractError::Unauthorized); } - state.rewarding_validator_address = Addr::unchecked(address); + let new_address = deps.api.addr_validate(&address)?; + let old_address = state.rewarding_validator_address; + + state.rewarding_validator_address = new_address.clone(); storage::CONTRACT_STATE.save(deps.storage, &state)?; - Ok(Response::default()) + Ok( + Response::new().add_event(new_rewarding_validator_address_update_event( + old_address, + new_address, + )), + ) } pub(crate) fn try_update_contract_settings( deps: DepsMut<'_>, info: MessageInfo, params: ContractStateParams, -) -> Result { +) -> Result { let mut state = storage::CONTRACT_STATE.load(deps.storage)?; // check if this is executed by the owner, if not reject the transaction if info.sender != state.owner { - return Err(ContractError::Unauthorized); - } - - if params.mixnode_rewarded_set_size == 0 { - return Err(ContractError::ZeroRewardedSet); - } - - if params.mixnode_active_set_size == 0 { - return Err(ContractError::ZeroActiveSet); - } - - // note: rewarded_set = active_set + idle_set - // hence rewarded set must always be bigger than (or equal to) the active set - if params.mixnode_rewarded_set_size < params.mixnode_active_set_size { - return Err(ContractError::InvalidActiveSetSize); + return Err(MixnetContractError::Unauthorized); } let response = Response::new().add_event(new_settings_update_event(&state.params, ¶ms)); @@ -64,13 +59,14 @@ pub(crate) fn try_update_contract_settings( #[cfg(test)] pub mod tests { use super::*; - use crate::contract::{INITIAL_GATEWAY_PLEDGE, INITIAL_MIXNODE_PLEDGE, INITIAL_STAKING_SUPPLY}; - use crate::error::ContractError; + use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; use crate::mixnet_contract_settings::queries::query_rewarding_validator_address; + use crate::mixnet_contract_settings::storage::rewarding_denom; use crate::mixnet_contract_settings::transactions::try_update_contract_settings; use crate::support::tests::test_helpers; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::Response; + use cosmwasm_std::{Addr, Coin, Response, Uint128}; + use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::ContractStateParams; #[test] @@ -83,7 +79,7 @@ pub mod tests { info, "not-the-creator".to_string(), ); - assert_eq!(res, Err(ContractError::Unauthorized)); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); let info = mock_info("creator", &[]); let res = try_update_rewarding_validator_address( @@ -91,7 +87,15 @@ pub mod tests { info, "new-good-address".to_string(), ); - assert_eq!(res, Ok(Response::default())); + assert_eq!( + res, + Ok( + Response::default().add_event(new_rewarding_validator_address_update_event( + Addr::unchecked("rewarder"), + Addr::unchecked("new-good-address") + )) + ) + ); let state = storage::CONTRACT_STATE.load(&deps.storage).unwrap(); assert_eq!( @@ -108,13 +112,18 @@ pub mod tests { #[test] fn updating_contract_settings() { let mut deps = test_helpers::init_contract(); + let denom = rewarding_denom(deps.as_ref().storage).unwrap(); let new_params = ContractStateParams { - minimum_mixnode_pledge: INITIAL_MIXNODE_PLEDGE, - minimum_gateway_pledge: INITIAL_GATEWAY_PLEDGE, - mixnode_rewarded_set_size: 100, - mixnode_active_set_size: 50, - staking_supply: INITIAL_STAKING_SUPPLY, + minimum_mixnode_delegation: None, + minimum_mixnode_pledge: Coin { + denom: denom.clone(), + amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, + }, + minimum_gateway_pledge: Coin { + denom, + amount: INITIAL_GATEWAY_PLEDGE_AMOUNT + Uint128::new(1234), + }, }; let initial_params = storage::CONTRACT_STATE @@ -128,7 +137,7 @@ pub mod tests { // cannot be updated from non-owner account let info = mock_info("not-the-creator", &[]); let res = try_update_contract_settings(deps.as_mut(), info, new_params.clone()); - assert_eq!(res, Err(ContractError::Unauthorized)); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); // but works fine from the creator account let info = mock_info("creator", &[]); @@ -142,25 +151,25 @@ pub mod tests { let current_state = storage::CONTRACT_STATE.load(deps.as_ref().storage).unwrap(); assert_eq!(current_state.params, new_params); - // error is thrown if rewarded set is smaller than the active set - let info = mock_info("creator", &[]); - let mut new_params = current_state.params.clone(); - new_params.mixnode_rewarded_set_size = new_params.mixnode_active_set_size - 1; - let res = try_update_contract_settings(deps.as_mut(), info, new_params); - assert_eq!(Err(ContractError::InvalidActiveSetSize), res); - - // error is thrown for 0 size rewarded set - let info = mock_info("creator", &[]); - let mut new_params = current_state.params.clone(); - new_params.mixnode_rewarded_set_size = 0; - let res = try_update_contract_settings(deps.as_mut(), info, new_params); - assert_eq!(Err(ContractError::ZeroRewardedSet), res); - - // error is thrown for 0 size active set - let info = mock_info("creator", &[]); - let mut new_params = current_state.params; - new_params.mixnode_active_set_size = 0; - let res = try_update_contract_settings(deps.as_mut(), info, new_params); - assert_eq!(Err(ContractError::ZeroActiveSet), res); + // // error is thrown if rewarded set is smaller than the active set + // let info = mock_info("creator", &[]); + // let mut new_params = current_state.params.clone(); + // new_params.mixnode_rewarded_set_size = new_params.mixnode_active_set_size - 1; + // let res = try_update_contract_settings(deps.as_mut(), info, new_params); + // assert_eq!(Err(MixnetContractError::InvalidActiveSetSize), res); + // + // // error is thrown for 0 size rewarded set + // let info = mock_info("creator", &[]); + // let mut new_params = current_state.params.clone(); + // new_params.mixnode_rewarded_set_size = 0; + // let res = try_update_contract_settings(deps.as_mut(), info, new_params); + // assert_eq!(Err(MixnetContractError::ZeroRewardedSet), res); + // + // // error is thrown for 0 size active set + // let info = mock_info("creator", &[]); + // let mut new_params = current_state.params; + // new_params.mixnode_active_set_size = 0; + // let res = try_update_contract_settings(deps.as_mut(), info, new_params); + // assert_eq!(Err(MixnetContractError::ZeroActiveSet), res); } } diff --git a/contracts/mixnet/src/mixnodes/bonding_queries.rs b/contracts/mixnet/src/mixnodes/bonding_queries.rs deleted file mode 100644 index 3c5085e04e..0000000000 --- a/contracts/mixnet/src/mixnodes/bonding_queries.rs +++ /dev/null @@ -1,276 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use super::storage::{self, StoredMixnodeBond}; -use cosmwasm_std::{Deps, Order, StdResult}; -use cw_storage_plus::Bound; -use mixnet_contract_common::{ - IdentityKey, MixNodeBond, MixOwnershipResponse, MixnodeBondResponse, PagedMixnodeResponse, -}; - -pub fn get_blacklisted_nodes(deps: Deps<'_>) -> Vec { - storage::MIXNODES_BOND_BLACKLIST - .keys(deps.storage, None, None, Order::Ascending) - .filter_map(|i| i.ok()) - .collect() -} - -pub fn query_mixnode_at_height( - deps: Deps<'_>, - mix_identity: String, - height: u64, -) -> StdResult> { - storage::mixnodes().may_load_at_height(deps.storage, &mix_identity, height) -} - -pub fn query_checkpoints_for_mixnode( - deps: Deps<'_>, - mix_identity: IdentityKey, -) -> StdResult> { - Ok(storage::mixnodes() - .changelog() - .prefix(&mix_identity) - .keys(deps.storage, None, None, Order::Ascending) - .filter_map(|x| x.ok()) - .collect()) -} - -pub fn query_mixnodes_paged( - deps: Deps<'_>, - start_after: Option, - limit: Option, -) -> StdResult { - let limit = limit - .unwrap_or(storage::BOND_PAGE_DEFAULT_LIMIT) - .min(storage::BOND_PAGE_MAX_LIMIT) as usize; - - let start = start_after.as_deref().map(Bound::exclusive); - - let nodes = storage::mixnodes() - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .map(|res| res.map(|item| item.1)) - .map(|stored_bond| { - // I really don't like this additional read per entry, but I don't see an obvious way to remove it - stored_bond.map(|stored_bond| { - let total_delegation = - storage::TOTAL_DELEGATION.load(deps.storage, stored_bond.identity()); - total_delegation - .map(|total_delegation| stored_bond.attach_delegation(total_delegation)) - }) - }) - .collect::>>>()??; - - let start_next_after = nodes.last().map(|node| node.identity().clone()); - - Ok(PagedMixnodeResponse::new(nodes, limit, start_next_after)) -} - -pub fn query_owns_mixnode(deps: Deps<'_>, address: String) -> StdResult { - let validated_addr = deps.api.addr_validate(&address)?; - let stored_bond = storage::mixnodes() - .idx - .owner - .item(deps.storage, validated_addr.clone())? - .map(|record| record.1); - - let mixnode = match stored_bond { - None => None, - Some(bond) => { - let total_delegation = - storage::TOTAL_DELEGATION.may_load(deps.storage, bond.identity())?; - Some(bond.attach_delegation(total_delegation.unwrap_or_default())) - } - }; - - Ok(MixOwnershipResponse { - address: validated_addr, - mixnode, - }) -} - -pub fn query_mixnode_bond(deps: Deps<'_>, identity: IdentityKey) -> StdResult { - Ok(MixnodeBondResponse { - mixnode: storage::read_full_mixnode_bond(deps.storage, &identity)?, - identity, - }) -} - -#[cfg(test)] -pub(crate) mod tests { - use super::storage; - use super::*; - use crate::mixnodes::storage::BOND_PAGE_DEFAULT_LIMIT; - use crate::support::tests::test_helpers; - use crate::{contract::execute, support::tests}; - use cosmwasm_std::testing::{mock_env, mock_info}; - - #[test] - fn mixnodes_empty_on_init() { - let deps = test_helpers::init_contract(); - let response = query_mixnodes_paged(deps.as_ref(), None, Option::from(2)).unwrap(); - assert_eq!(0, response.nodes.len()); - } - - #[test] - fn mixnodes_paged_retrieval_obeys_limits() { - let mut deps = test_helpers::init_contract(); - let limit = 2; - for n in 0..1000 { - let key = format!("bond{}", n); - test_helpers::add_mixnode(&key, tests::fixtures::good_mixnode_pledge(), deps.as_mut()); - } - - let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(limit)).unwrap(); - assert_eq!(limit, page1.nodes.len() as u32); - } - - #[test] - fn mixnodes_paged_retrieval_has_default_limit() { - let mut deps = test_helpers::init_contract(); - for n in 0..1000 { - let key = format!("bond{}", n); - test_helpers::add_mixnode(&key, tests::fixtures::good_mixnode_pledge(), deps.as_mut()); - } - - // query without explicitly setting a limit - let page1 = query_mixnodes_paged(deps.as_ref(), None, None).unwrap(); - - assert_eq!(BOND_PAGE_DEFAULT_LIMIT, page1.nodes.len() as u32); - } - - #[test] - fn mixnodes_paged_retrieval_has_max_limit() { - let mut deps = test_helpers::init_contract(); - for n in 0..1000 { - let key = format!("bond{}", n); - test_helpers::add_mixnode(&key, tests::fixtures::good_mixnode_pledge(), deps.as_mut()); - } - - // query with a crazily high limit in an attempt to use too many resources - let crazy_limit = 1000; - let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap(); - - // we default to a decent sized upper bound instead - let expected_limit = storage::BOND_PAGE_MAX_LIMIT; - assert_eq!(expected_limit, page1.nodes.len() as u32); - } - - #[test] - fn pagination_works() { - // prepare 4 messages and identities that are sorted by the generated identities - // (because we query them in an ascended manner) - let mut exec_data = (0..4) - .map(|i| { - let sender = format!("nym-addr{}", i); - let (msg, identity) = tests::messages::valid_bond_mixnode_msg(&sender); - (msg, (sender, identity)) - }) - .collect::>(); - exec_data.sort_by(|(_, (_, id1)), (_, (_, id2))| id1.cmp(id2)); - let (messages, sender_identities): (Vec<_>, Vec<_>) = exec_data.into_iter().unzip(); - - let mut deps = test_helpers::init_contract(); - - let info = mock_info( - &sender_identities[0].0.clone(), - &tests::fixtures::good_mixnode_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[0].clone()).unwrap(); - - let per_page = 2; - let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); - - // page should have 1 result on it - assert_eq!(1, page1.nodes.len()); - - // save another - let info = mock_info( - &sender_identities[1].0.clone(), - &tests::fixtures::good_mixnode_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[1].clone()).unwrap(); - - // page1 should have 2 results on it - let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); - assert_eq!(2, page1.nodes.len()); - - let info = mock_info( - &sender_identities[2].0.clone(), - &tests::fixtures::good_mixnode_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[2].clone()).unwrap(); - - // page1 still has 2 results - let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); - assert_eq!(2, page1.nodes.len()); - - // retrieving the next page should start after the last key on this page - let start_after = page1.start_next_after.unwrap(); - let page2 = query_mixnodes_paged( - deps.as_ref(), - Option::from(start_after.clone()), - Option::from(per_page), - ) - .unwrap(); - - assert_eq!(1, page2.nodes.len()); - - // save another one - let info = mock_info( - &sender_identities[3].0.clone(), - &tests::fixtures::good_mixnode_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[3].clone()).unwrap(); - - let page2 = query_mixnodes_paged( - deps.as_ref(), - Option::from(start_after), - Option::from(per_page), - ) - .unwrap(); - - // now we have 2 pages, with 2 results on the second page - assert_eq!(2, page2.nodes.len()); - } - - #[test] - fn query_for_mixnode_owner_works() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - - // "fred" does not own a mixnode if there are no mixnodes - let res = query_owns_mixnode(deps.as_ref(), "fred".to_string()).unwrap(); - assert!(res.mixnode.is_none()); - - // mixnode was added to "bob", "fred" still does not own one - test_helpers::add_mixnode("bob", tests::fixtures::good_mixnode_pledge(), deps.as_mut()); - - let res = query_owns_mixnode(deps.as_ref(), "fred".to_string()).unwrap(); - assert!(res.mixnode.is_none()); - - // "fred" now owns a mixnode! - test_helpers::add_mixnode( - "fred", - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - let res = query_owns_mixnode(deps.as_ref(), "fred".to_string()).unwrap(); - assert!(res.mixnode.is_some()); - - let api = deps.api.clone(); - - // but after unbonding it, he doesn't own one anymore - crate::mixnodes::transactions::try_remove_mixnode( - &env, - deps.as_mut().storage, - &api, - mock_info("fred", &[]), - ) - .unwrap(); - - let res = query_owns_mixnode(deps.as_ref(), "fred".to_string()).unwrap(); - assert!(res.mixnode.is_none()); - } -} diff --git a/contracts/mixnet/src/mixnodes/helpers.rs b/contracts/mixnet/src/mixnodes/helpers.rs new file mode 100644 index 0000000000..15e5e82f98 --- /dev/null +++ b/contracts/mixnet/src/mixnodes/helpers.rs @@ -0,0 +1,476 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::storage; +use crate::interval::storage as interval_storage; +use crate::mixnodes::storage::{assign_layer, next_mixnode_id_counter}; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Addr, Coin, Decimal, Env, StdResult, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::mixnode::{ + MixNodeCostParams, MixNodeDetails, MixNodeRewarding, UnbondedMixnode, +}; +use mixnet_contract_common::{Layer, MixId, MixNode, MixNodeBond}; + +pub(crate) fn must_get_mixnode_bond_by_owner( + store: &dyn Storage, + owner: &Addr, +) -> Result { + Ok(storage::mixnode_bonds() + .idx + .owner + .item(store, owner.clone())? + .ok_or(MixnetContractError::NoAssociatedMixNodeBond { + owner: owner.clone(), + })? + .1) +} + +pub(crate) fn get_mixnode_details_by_id( + store: &dyn Storage, + mix_id: MixId, +) -> StdResult> { + 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.mix_id)?; + Ok(Some(MixNodeDetails::new( + bond_information, + rewarding_details, + ))) + } else { + Ok(None) + } +} + +pub(crate) fn get_mixnode_details_by_owner( + store: &dyn Storage, + address: Addr, +) -> StdResult> { + if let Some(bond_information) = storage::mixnode_bonds() + .idx + .owner + .item(store, address)? + .map(|record| record.1) + { + // if bond exists, rewarding details MUST also exist + let rewarding_details = + rewards_storage::MIXNODE_REWARDING.load(store, bond_information.mix_id)?; + Ok(Some(MixNodeDetails::new( + bond_information, + rewarding_details, + ))) + } else { + Ok(None) + } +} + +pub(crate) fn save_new_mixnode( + storage: &mut dyn Storage, + env: Env, + mixnode: MixNode, + cost_params: MixNodeCostParams, + owner: Addr, + proxy: Option, + pledge: Coin, +) -> Result<(MixId, Layer), MixnetContractError> { + let layer = assign_layer(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( + mix_id, + owner, + pledge, + layer, + mixnode, + proxy, + env.block.height, + ); + + // save mixnode bond data + // note that this implicitly checks for uniqueness on identity key, sphinx key and owner + storage::mixnode_bonds().save(storage, mix_id, &mixnode_bond)?; + + // save rewarding data + rewards_storage::MIXNODE_REWARDING.save(storage, mix_id, &mixnode_rewarding)?; + + Ok((mix_id, layer)) +} + +pub(crate) fn cleanup_post_unbond_mixnode_storage( + storage: &mut dyn Storage, + env: &Env, + current_details: &MixNodeDetails, +) -> Result<(), MixnetContractError> { + 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, + mix_id, + None, + Some(¤t_details.bond_information), + )?; + + // 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, 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, mix_id, &zeroed)?; + } + + let identity = current_details.bond_information.identity().to_owned(); + let owner = current_details.bond_information.owner().to_owned(); + let proxy = current_details.bond_information.proxy.to_owned(); + + // save minimal information about this mixnode + storage::unbonded_mixnodes().save( + storage, + mix_id, + &UnbondedMixnode { + identity_key: identity, + owner, + proxy, + unbonding_height: env.block.height, + }, + )?; + storage::decrement_layer_count(storage, current_details.bond_information.layer) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::fixtures::{ + mix_node_cost_params_fixture, mix_node_fixture, TEST_COIN_DENOM, + }; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::coin; + + const OWNER_EXISTS: &str = "mix-owner-existing"; + const OWNER_UNBONDING: &str = "mix-owner-unbonding"; + const OWNER_UNBONDED: &str = "mix-owner-unbonded"; + 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 { + 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); + let mix_id_unbonded_leftover = test.add_dummy_mixnode(OWNER_UNBONDED_LEFTOVER, None); + + // manually adjust delegation info as to indicate the rewarding information shouldnt get removed + let mut rewarding_details = test.mix_rewarding(mix_id_unbonded_leftover); + rewarding_details.delegates = Decimal::raw(12345); + rewarding_details.unique_delegations = 1; + rewards_storage::MIXNODE_REWARDING + .save( + test.deps_mut().storage, + mix_id_unbonded_leftover, + &rewarding_details, + ) + .unwrap(); + + test.immediately_unbond_mixnode(mix_id_unbonded); + test.immediately_unbond_mixnode(mix_id_unbonded_leftover); + test.start_unbonding_mixnode(mix_id_unbonding); + + vec![ + mix_id_exists, + mix_id_unbonding, + mix_id_unbonded, + mix_id_unbonded_leftover, + ] + } + + #[test] + fn getting_mixnode_bond_by_owner() { + let mut test = TestSetup::new(); + + let owner_exists = Addr::unchecked(OWNER_EXISTS); + let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); + let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); + let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); + + let ids = setup_mix_combinations(&mut test); + let mix_id_exists = ids[0]; + let mix_id_unbonding = ids[1]; + + // 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.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.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); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: owner_unbonded + }) + ); + + let res = must_get_mixnode_bond_by_owner(test.deps().storage, &owner_unbonded_leftover); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: owner_unbonded_leftover + }) + ); + } + + #[test] + fn getting_mixnode_details_by_id() { + let mut test = TestSetup::new(); + + let ids = setup_mix_combinations(&mut test); + let mix_id_exists = ids[0]; + let mix_id_unbonding = ids[1]; + let mix_id_unbonded = ids[2]; + let mix_id_unbonded_leftover = ids[3]; + + let res = get_mixnode_details_by_id(test.deps().storage, mix_id_exists) + .unwrap() + .unwrap(); + 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.mix_id, mix_id_unbonding); + + let res = get_mixnode_details_by_id(test.deps().storage, mix_id_unbonded).unwrap(); + assert!(res.is_none()); + + let res = get_mixnode_details_by_id(test.deps().storage, mix_id_unbonded_leftover).unwrap(); + assert!(res.is_none()) + } + + #[test] + fn getting_mixnode_details_by_owner() { + let mut test = TestSetup::new(); + + let owner_exists = Addr::unchecked(OWNER_EXISTS); + let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); + let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); + let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); + + let ids = setup_mix_combinations(&mut test); + let mix_id_exists = ids[0]; + let mix_id_unbonding = ids[1]; + + // if this is a normally bonded mixnode, all should be fine + let res = get_mixnode_details_by_owner(test.deps().storage, owner_exists) + .unwrap() + .unwrap(); + 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.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(); + assert!(res.is_none()); + + let res = + get_mixnode_details_by_owner(test.deps().storage, owner_unbonded_leftover).unwrap(); + assert!(res.is_none()); + } + + #[test] + fn saving_new_mixnode() { + let mut test = TestSetup::new(); + + // get some mixnodes in + test.add_dummy_mixnode("owner1", None); + test.add_dummy_mixnode("owner2", None); + test.add_dummy_mixnode("owner3", None); + test.add_dummy_mixnode("owner4", None); + test.add_dummy_mixnode("owner5", None); + + let env = test.env(); + let id_key = "identity-key"; + let sphinx_key = "sphinx-key"; + let mut mixnode = mix_node_fixture(); + mixnode.identity_key = id_key.into(); + mixnode.sphinx_key = sphinx_key.into(); + let cost_params = mix_node_cost_params_fixture(); + let owner = Addr::unchecked("mix-owner"); + let pledge = coin(100_000_000, TEST_COIN_DENOM); + + let (id, layer) = save_new_mixnode( + test.deps_mut().storage, + env.clone(), + mixnode, + cost_params.clone(), + owner.clone(), + None, + pledge.clone(), + ) + .unwrap(); + assert_eq!(id, 6); + assert_eq!(layer, Layer::Three); + + assert_eq!( + storage::MIXNODE_ID_COUNTER + .load(test.deps().storage) + .unwrap(), + 6 + ); + assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer3, 2); + let mix_details = get_mixnode_details_by_id(test.deps().storage, id) + .unwrap() + .unwrap(); + assert_eq!(mix_details.mix_id(), id); + assert_eq!(mix_details.original_pledge(), &pledge); + assert_eq!( + mix_details.bond_information.bonding_height, + env.block.height + ); + + // try to add node with duplicate identity... + let mut mixnode = mix_node_fixture(); + mixnode.identity_key = id_key.into(); + let res = save_new_mixnode( + test.deps_mut().storage, + env.clone(), + mixnode, + cost_params.clone(), + Addr::unchecked("different-owner"), + None, + pledge.clone(), + ); + assert!(res.is_err()); + + // and duplicate owner... + let mixnode = mix_node_fixture(); + let res = save_new_mixnode( + test.deps_mut().storage, + env.clone(), + mixnode, + cost_params.clone(), + owner, + None, + pledge.clone(), + ); + assert!(res.is_err()); + + // and duplicate sphinx key... + let mut mixnode = mix_node_fixture(); + mixnode.sphinx_key = sphinx_key.into(); + let res = save_new_mixnode( + test.deps_mut().storage, + env, + mixnode, + cost_params, + Addr::unchecked("different-owner"), + None, + pledge, + ); + assert!(res.is_err()); + } + + #[test] + fn cleaning_post_unbond_storage() { + let mut test = TestSetup::new(); + + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id_leftover = test.add_dummy_mixnode("mix-owner-leftover", None); + + // manually adjust delegation info as to indicate the rewarding information shouldnt get removed + let mut rewarding_details = test.mix_rewarding(mix_id_leftover); + rewarding_details.delegates = Decimal::raw(12345); + rewarding_details.unique_delegations = 1; + rewards_storage::MIXNODE_REWARDING + .save(test.deps_mut().storage, mix_id_leftover, &rewarding_details) + .unwrap(); + + let bond1 = storage::mixnode_bonds() + .load(test.deps().storage, mix_id) + .unwrap(); + let bond2 = storage::mixnode_bonds() + .load(test.deps().storage, mix_id_leftover) + .unwrap(); + + let env = test.env(); + let details1 = get_mixnode_details_by_id(test.deps().storage, mix_id) + .unwrap() + .unwrap(); + cleanup_post_unbond_mixnode_storage(test.deps_mut().storage, &env, &details1).unwrap(); + + // bond information is gone + let bond = storage::mixnode_bonds() + .may_load(test.deps().storage, mix_id) + .unwrap(); + assert!(bond.is_none()); + + // rewarding information is gone + let mix_rewarding = rewards_storage::MIXNODE_REWARDING + .may_load(test.deps().storage, mix_id) + .unwrap(); + assert!(mix_rewarding.is_none()); + + // unbonded details are inserted + let unbonded_details = storage::unbonded_mixnodes() + .load(test.deps().storage, mix_id) + .unwrap(); + let expected = UnbondedMixnode { + identity_key: bond1.mix_node.identity_key, + owner: bond1.owner, + proxy: None, + unbonding_height: env.block.height, + }; + assert_eq!(unbonded_details, expected); + + // layers are decremented + assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer1, 0); + + let details2 = get_mixnode_details_by_id(test.deps().storage, mix_id_leftover) + .unwrap() + .unwrap(); + cleanup_post_unbond_mixnode_storage(test.deps_mut().storage, &env, &details2).unwrap(); + + // bond information is gone + let bond = storage::mixnode_bonds() + .may_load(test.deps().storage, mix_id_leftover) + .unwrap(); + assert!(bond.is_none()); + + // rewarding information is NOT gone, but operator field is reset + let mix_rewarding = rewards_storage::MIXNODE_REWARDING + .may_load(test.deps().storage, mix_id_leftover) + .unwrap() + .unwrap(); + assert!(mix_rewarding.operator.is_zero()); + assert_eq!(mix_rewarding.unique_delegations, 1); + + // unbonded details are inserted + let unbonded_details = storage::unbonded_mixnodes() + .load(test.deps().storage, mix_id_leftover) + .unwrap(); + let expected = UnbondedMixnode { + identity_key: bond2.mix_node.identity_key, + owner: bond2.owner, + proxy: None, + unbonding_height: env.block.height, + }; + assert_eq!(unbonded_details, expected); + + // layers are decremented + assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer2, 0); + } +} diff --git a/contracts/mixnet/src/mixnodes/layer_queries.rs b/contracts/mixnet/src/mixnodes/layer_queries.rs deleted file mode 100644 index 1764e7211e..0000000000 --- a/contracts/mixnet/src/mixnodes/layer_queries.rs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use cosmwasm_std::{Deps, StdResult}; -use mixnet_contract_common::LayerDistribution; - -pub(crate) fn query_layer_distribution(deps: Deps<'_>) -> StdResult { - mixnet_params_storage::LAYERS.load(deps.storage) -} diff --git a/contracts/mixnet/src/mixnodes/mod.rs b/contracts/mixnet/src/mixnodes/mod.rs index 8f80d5b408..f3a3c9941f 100644 --- a/contracts/mixnet/src/mixnodes/mod.rs +++ b/contracts/mixnet/src/mixnodes/mod.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -pub mod bonding_queries; -pub mod layer_queries; +pub mod helpers; +pub mod queries; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/mixnodes/queries.rs b/contracts/mixnet/src/mixnodes/queries.rs new file mode 100644 index 0000000000..5411b108ff --- /dev/null +++ b/contracts/mixnet/src/mixnodes/queries.rs @@ -0,0 +1,1330 @@ +// Copyright 2021-2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::storage; +use crate::constants::{ + MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT, MIXNODE_BOND_MAX_RETRIEVAL_LIMIT, + MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT, MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT, + UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT, UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT, +}; +use crate::mixnodes::helpers::{get_mixnode_details_by_id, get_mixnode_details_by_owner}; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Deps, Order, StdResult, Storage}; +use cw_storage_plus::Bound; +use mixnet_contract_common::mixnode::{ + MixNodeBond, MixNodeDetails, MixnodeRewardingDetailsResponse, PagedMixnodesDetailsResponse, + PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, +}; +use mixnet_contract_common::{ + IdentityKey, LayerDistribution, MixId, MixOwnershipResponse, MixnodeDetailsResponse, + PagedMixnodeBondsResponse, +}; + +pub fn query_mixnode_bonds_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT) + .min(MIXNODE_BOND_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::mixnode_bonds() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|node| node.mix_id); + + Ok(PagedMixnodeBondsResponse::new( + nodes, + limit, + start_next_after, + )) +} + +fn attach_rewarding_info( + storage: &dyn Storage, + 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.mix_id)?; + Ok(MixNodeDetails::new(bond, mix_rewarding)) + } + Err(err) => Err(err), + } +} + +pub fn query_mixnodes_details_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT) + .min(MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::mixnode_bonds() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| attach_rewarding_info(deps.storage, res)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|details| details.mix_id()); + + Ok(PagedMixnodesDetailsResponse::new( + nodes, + limit, + start_next_after, + )) +} + +pub fn query_unbonded_mixnodes_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_mixnodes() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.0); + + Ok(PagedUnbondedMixnodesResponse::new( + nodes, + limit, + start_next_after, + )) +} + +pub fn query_unbonded_mixnodes_by_owner_paged( + deps: Deps<'_>, + owner: String, + start_after: Option, + limit: Option, +) -> StdResult { + let owner = deps.api.addr_validate(&owner)?; + + let limit = limit + .unwrap_or(UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_mixnodes() + .idx + .owner + .prefix(owner) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.0); + + Ok(PagedUnbondedMixnodesResponse::new( + nodes, + limit, + start_next_after, + )) +} + +pub fn query_unbonded_mixnodes_by_identity_paged( + deps: Deps<'_>, + identity_key: String, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_mixnodes() + .idx + .identity_key + .prefix(identity_key) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.0); + + Ok(PagedUnbondedMixnodesResponse::new( + nodes, + limit, + start_next_after, + )) +} + +pub fn query_owned_mixnode(deps: Deps<'_>, address: String) -> StdResult { + let validated_addr = deps.api.addr_validate(&address)?; + let mixnode_details = get_mixnode_details_by_owner(deps.storage, validated_addr.clone())?; + + Ok(MixOwnershipResponse { + address: validated_addr, + mixnode_details, + }) +} + +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 { + mix_id, + mixnode_details, + }) +} + +pub fn query_mixnode_details_by_identity( + deps: Deps<'_>, + mix_identity: IdentityKey, +) -> StdResult> { + if let Some(bond_information) = storage::mixnode_bonds() + .idx + .identity_key + .item(deps.storage, mix_identity)? + .map(|record| record.1) + { + // if bond exists, rewarding details MUST also exist + let rewarding_details = + rewards_storage::MIXNODE_REWARDING.load(deps.storage, bond_information.mix_id)?; + Ok(Some(MixNodeDetails::new( + bond_information, + rewarding_details, + ))) + } else { + Ok(None) + } +} + +pub fn query_mixnode_rewarding_details( + deps: Deps<'_>, + mix_id: MixId, +) -> StdResult { + let rewarding_details = rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?; + + Ok(MixnodeRewardingDetailsResponse { + mix_id, + rewarding_details, + }) +} + +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 { + mix_id, + unbonded_info, + }) +} + +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 => { + return Ok(StakeSaturationResponse { + mix_id, + current_saturation: None, + uncapped_saturation: None, + }) + } + }; + + let rewarding_params = rewards_storage::REWARDING_PARAMS.load(deps.storage)?; + + Ok(StakeSaturationResponse { + mix_id, + current_saturation: Some(mix_rewarding.bond_saturation(&rewarding_params)), + uncapped_saturation: Some(mix_rewarding.uncapped_bond_saturation(&rewarding_params)), + }) +} + +pub(crate) fn query_layer_distribution(deps: Deps<'_>) -> StdResult { + storage::LAYERS.load(deps.storage) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::interval::pending_events; + use crate::support::tests::fixtures::good_mixnode_pledge; + use crate::support::tests::test_helpers::TestSetup; + use crate::support::tests::{fixtures, test_helpers}; + use cosmwasm_std::testing::mock_env; + use cosmwasm_std::Decimal; + + #[cfg(test)] + mod mixnode_bonds { + use super::*; + use crate::support::tests::fixtures::good_mixnode_pledge; + + #[test] + fn obeys_limits() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + let limit = 2; + + test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, Some(limit)).unwrap(); + assert_eq!(limit, page1.nodes.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + + // query without explicitly setting a limit + let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, None).unwrap(); + + assert_eq!( + MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, Some(crazy_limit)).unwrap(); + + // we default to a decent sized upper bound instead + assert_eq!(MIXNODE_BOND_MAX_RETRIEVAL_LIMIT, page1.nodes.len() as u32); + } + + #[test] + fn pagination_works() { + // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + "addr1", + good_mixnode_pledge(), + ); + + let per_page = 2; + let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.nodes.len()); + + // save another + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + "addr2", + good_mixnode_pledge(), + ); + + // page1 should have 2 results on it + let page1 = query_mixnode_bonds_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + assert_eq!(2, page1.nodes.len()); + + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + "addr3", + good_mixnode_pledge(), + ); + + // page1 still has the same 2 results + let another_page1 = + query_mixnode_bonds_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + assert_eq!(2, another_page1.nodes.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_mixnode_bonds_paged(deps.as_ref(), Some(start_after), Some(per_page)) + .unwrap(); + + assert_eq!(1, page2.nodes.len()); + + // save another one + test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "addr4", good_mixnode_pledge()); + + let page2 = query_mixnode_bonds_paged(deps.as_ref(), Some(start_after), Some(per_page)) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.nodes.len()); + } + } + + #[cfg(test)] + mod mixnode_details { + use super::*; + use crate::support::tests::fixtures::good_mixnode_pledge; + + #[test] + fn obeys_limits() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + let limit = 2; + + test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + let page1 = query_mixnodes_details_paged(deps.as_ref(), None, Some(limit)).unwrap(); + assert_eq!(limit, page1.nodes.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + + // query without explicitly setting a limit + let page1 = query_mixnodes_details_paged(deps.as_ref(), None, None).unwrap(); + + assert_eq!( + MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + test_helpers::add_dummy_mixnodes(&mut rng, deps.as_mut(), env, 1000); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = + query_mixnodes_details_paged(deps.as_ref(), None, Some(crazy_limit)).unwrap(); + + // we default to a decent sized upper bound instead + assert_eq!( + MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn pagination_works() { + // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + "addr1", + good_mixnode_pledge(), + ); + + let per_page = 2; + let page1 = query_mixnodes_details_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.nodes.len()); + + // save another + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + "addr2", + good_mixnode_pledge(), + ); + + // page1 should have 2 results on it + let page1 = query_mixnodes_details_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + assert_eq!(2, page1.nodes.len()); + + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + "addr3", + good_mixnode_pledge(), + ); + + // page1 still has the same 2 results + let another_page1 = + query_mixnodes_details_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + assert_eq!(2, another_page1.nodes.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = + query_mixnodes_details_paged(deps.as_ref(), Some(start_after), Some(per_page)) + .unwrap(); + + assert_eq!(1, page2.nodes.len()); + + // save another one + test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "addr4", good_mixnode_pledge()); + + let page2 = + query_mixnodes_details_paged(deps.as_ref(), Some(start_after), Some(per_page)) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.nodes.len()); + } + } + + #[cfg(test)] + mod unbonded_mixnodes { + use super::*; + use cosmwasm_std::Addr; + use mixnet_contract_common::mixnode::UnbondedMixnode; + + #[test] + fn obeys_limits() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let limit = 2; + + test_helpers::add_dummy_unbonded_mixnodes(&mut rng, deps.as_mut(), 1000); + let page1 = query_unbonded_mixnodes_paged(deps.as_ref(), None, Some(limit)).unwrap(); + assert_eq!(limit, page1.nodes.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + test_helpers::add_dummy_unbonded_mixnodes(&mut rng, deps.as_mut(), 1000); + + // query without explicitly setting a limit + let page1 = query_unbonded_mixnodes_paged(deps.as_ref(), None, None).unwrap(); + + assert_eq!( + UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + test_helpers::add_dummy_unbonded_mixnodes(&mut rng, deps.as_mut(), 1000); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = + query_unbonded_mixnodes_paged(deps.as_ref(), None, Some(crazy_limit)).unwrap(); + + // we default to a decent sized upper bound instead + assert_eq!( + UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn pagination_works() { + fn add_unbonded(storage: &mut dyn Storage, id: MixId) { + storage::unbonded_mixnodes() + .save( + storage, + id, + &UnbondedMixnode { + identity_key: format!("dummy{}", id), + owner: Addr::unchecked(format!("dummy{}", id)), + proxy: None, + unbonding_height: 123, + }, + ) + .unwrap(); + } + + // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id + let mut deps = test_helpers::init_contract(); + + add_unbonded(deps.as_mut().storage, 1); + + let per_page = 2; + let page1 = query_unbonded_mixnodes_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.nodes.len()); + + // save another + add_unbonded(deps.as_mut().storage, 2); + + // page1 should have 2 results on it + let page1 = query_unbonded_mixnodes_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + assert_eq!(2, page1.nodes.len()); + + add_unbonded(deps.as_mut().storage, 3); + + // page1 still has the same 2 results + let another_page1 = + query_unbonded_mixnodes_paged(deps.as_ref(), None, Some(per_page)).unwrap(); + assert_eq!(2, another_page1.nodes.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = + query_unbonded_mixnodes_paged(deps.as_ref(), Some(start_after), Some(per_page)) + .unwrap(); + + assert_eq!(1, page2.nodes.len()); + + // save another one + add_unbonded(deps.as_mut().storage, 4); + let page2 = + query_unbonded_mixnodes_paged(deps.as_ref(), Some(start_after), Some(per_page)) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.nodes.len()); + } + } + + #[cfg(test)] + mod unbonded_mixnodes_by_owner { + use super::*; + use cosmwasm_std::Addr; + use mixnet_contract_common::mixnode::UnbondedMixnode; + + fn add_unbonded_with_owner(storage: &mut dyn Storage, id: MixId, owner: &str) { + storage::unbonded_mixnodes() + .save( + storage, + id, + &UnbondedMixnode { + identity_key: format!("dummy{}", id), + owner: Addr::unchecked(owner), + proxy: None, + unbonding_height: 123, + }, + ) + .unwrap(); + } + + #[test] + fn obeys_limits() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let limit = 2; + let owner = "owner"; + + test_helpers::add_dummy_unbonded_mixnodes_with_owner( + &mut rng, + deps.as_mut(), + owner, + 1000, + ); + let page1 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + None, + Some(limit), + ) + .unwrap(); + assert_eq!(limit, page1.nodes.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let owner = "owner"; + + test_helpers::add_dummy_unbonded_mixnodes_with_owner( + &mut rng, + deps.as_mut(), + owner, + 1000, + ); + + // query without explicitly setting a limit + let page1 = + query_unbonded_mixnodes_by_owner_paged(deps.as_ref(), owner.into(), None, None) + .unwrap(); + + assert_eq!( + UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let owner = "owner"; + + test_helpers::add_dummy_unbonded_mixnodes_with_owner( + &mut rng, + deps.as_mut(), + owner, + 1000, + ); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + None, + Some(crazy_limit), + ) + .unwrap(); + + // we default to a decent sized upper bound instead + assert_eq!( + UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn pagination_works() { + // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id + let mut deps = test_helpers::init_contract(); + let owner = "owner"; + add_unbonded_with_owner(deps.as_mut().storage, 1, owner); + + let per_page = 2; + let page1 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + None, + Some(per_page), + ) + .unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.nodes.len()); + + // save another + add_unbonded_with_owner(deps.as_mut().storage, 2, owner); + + // page1 should have 2 results on it + let page1 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + None, + Some(per_page), + ) + .unwrap(); + assert_eq!(2, page1.nodes.len()); + + add_unbonded_with_owner(deps.as_mut().storage, 3, owner); + + // page1 still has the same 2 results + let another_page1 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + None, + Some(per_page), + ) + .unwrap(); + assert_eq!(2, another_page1.nodes.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + Some(start_after), + Some(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.nodes.len()); + + // save another one + add_unbonded_with_owner(deps.as_mut().storage, 4, owner); + let page2 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + owner.into(), + Some(start_after), + Some(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.nodes.len()); + } + + #[test] + fn only_retrieves_nodes_with_specific_owner() { + let mut deps = test_helpers::init_contract(); + let owner1 = "owner1"; + let owner2 = "owner2"; + let owner3 = "owner3"; + let owner4 = "owner4"; + + add_unbonded_with_owner(deps.as_mut().storage, 1, owner1); + add_unbonded_with_owner(deps.as_mut().storage, 2, owner1); + add_unbonded_with_owner(deps.as_mut().storage, 3, owner2); + add_unbonded_with_owner(deps.as_mut().storage, 4, owner1); + add_unbonded_with_owner(deps.as_mut().storage, 5, owner3); + add_unbonded_with_owner(deps.as_mut().storage, 6, owner3); + add_unbonded_with_owner(deps.as_mut().storage, 7, owner4); + add_unbonded_with_owner(deps.as_mut().storage, 8, owner2); + add_unbonded_with_owner(deps.as_mut().storage, 9, owner1); + add_unbonded_with_owner(deps.as_mut().storage, 10, owner3); + + let expected_ids1 = vec![1, 2, 4, 9]; + let expected_ids2 = vec![3, 8]; + let expected_ids3 = vec![5, 6, 10]; + let expected_ids4 = vec![7]; + + let res1 = + query_unbonded_mixnodes_by_owner_paged(deps.as_ref(), owner1.into(), None, None) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res1, expected_ids1); + + let res2 = + query_unbonded_mixnodes_by_owner_paged(deps.as_ref(), owner2.into(), None, None) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res2, expected_ids2); + + let res3 = + query_unbonded_mixnodes_by_owner_paged(deps.as_ref(), owner3.into(), None, None) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res3, expected_ids3); + + let res4 = + query_unbonded_mixnodes_by_owner_paged(deps.as_ref(), owner4.into(), None, None) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res4, expected_ids4); + + let res5 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + "doesnt-exist".into(), + None, + None, + ) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert!(res5.is_empty()); + } + } + + #[cfg(test)] + mod unbonded_mixnodes_by_identity { + use super::*; + use cosmwasm_std::Addr; + use mixnet_contract_common::mixnode::UnbondedMixnode; + + fn add_unbonded_with_identity(storage: &mut dyn Storage, id: MixId, identity: &str) { + storage::unbonded_mixnodes() + .save( + storage, + id, + &UnbondedMixnode { + identity_key: identity.to_string(), + owner: Addr::unchecked(format!("dummy{}", id)), + proxy: None, + unbonding_height: 123, + }, + ) + .unwrap(); + } + + #[test] + fn obeys_limits() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let limit = 2; + let identity = "foomp123"; + + test_helpers::add_dummy_unbonded_mixnodes_with_identity( + &mut rng, + deps.as_mut(), + identity, + 1000, + ); + let page1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + None, + Some(limit), + ) + .unwrap(); + assert_eq!(limit, page1.nodes.len() as u32); + } + + #[test] + fn has_default_limit() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let identity = "foomp123"; + test_helpers::add_dummy_unbonded_mixnodes_with_identity( + &mut rng, + deps.as_mut(), + identity, + 1000, + ); + + // query without explicitly setting a limit + let page1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + None, + None, + ) + .unwrap(); + + assert_eq!( + UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn has_max_limit() { + let mut deps = test_helpers::init_contract(); + let _env = mock_env(); + let mut rng = test_helpers::test_rng(); + let identity = "foomp123"; + test_helpers::add_dummy_unbonded_mixnodes_with_identity( + &mut rng, + deps.as_mut(), + identity, + 1000, + ); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000; + let page1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + None, + Some(crazy_limit), + ) + .unwrap(); + + // we default to a decent sized upper bound instead + assert_eq!( + UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT, + page1.nodes.len() as u32 + ); + } + + #[test] + fn pagination_works() { + // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id + let mut deps = test_helpers::init_contract(); + let identity = "foomp123"; + + add_unbonded_with_identity(deps.as_mut().storage, 1, identity); + + let per_page = 2; + let page1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + None, + Some(per_page), + ) + .unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.nodes.len()); + + // save another + add_unbonded_with_identity(deps.as_mut().storage, 2, identity); + + // page1 should have 2 results on it + let page1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + None, + Some(per_page), + ) + .unwrap(); + assert_eq!(2, page1.nodes.len()); + + add_unbonded_with_identity(deps.as_mut().storage, 3, identity); + + // page1 still has the same 2 results + let another_page1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + None, + Some(per_page), + ) + .unwrap(); + assert_eq!(2, another_page1.nodes.len()); + assert_eq!(page1, another_page1); + + // retrieving the next page should start after the last key on this page + let start_after = page1.start_next_after.unwrap(); + let page2 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + Some(start_after), + Some(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.nodes.len()); + + // save another one + add_unbonded_with_identity(deps.as_mut().storage, 4, identity); + let page2 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity.into(), + Some(start_after), + Some(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.nodes.len()); + } + + #[test] + fn only_retrieves_nodes_with_specific_identity_key() { + let mut deps = test_helpers::init_contract(); + let identity1 = "identity1"; + let identity2 = "identity2"; + let identity3 = "identity3"; + let identity4 = "identity4"; + + add_unbonded_with_identity(deps.as_mut().storage, 1, identity1); + add_unbonded_with_identity(deps.as_mut().storage, 2, identity1); + add_unbonded_with_identity(deps.as_mut().storage, 3, identity2); + add_unbonded_with_identity(deps.as_mut().storage, 4, identity1); + add_unbonded_with_identity(deps.as_mut().storage, 5, identity3); + add_unbonded_with_identity(deps.as_mut().storage, 6, identity3); + add_unbonded_with_identity(deps.as_mut().storage, 7, identity4); + add_unbonded_with_identity(deps.as_mut().storage, 8, identity2); + add_unbonded_with_identity(deps.as_mut().storage, 9, identity1); + add_unbonded_with_identity(deps.as_mut().storage, 10, identity3); + + let expected_ids1 = vec![1, 2, 4, 9]; + let expected_ids2 = vec![3, 8]; + let expected_ids3 = vec![5, 6, 10]; + let expected_ids4 = vec![7]; + + let res1 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity1.into(), + None, + None, + ) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res1, expected_ids1); + + let res2 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity2.into(), + None, + None, + ) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res2, expected_ids2); + + let res3 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity3.into(), + None, + None, + ) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res3, expected_ids3); + + let res4 = query_unbonded_mixnodes_by_identity_paged( + deps.as_ref(), + identity4.into(), + None, + None, + ) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert_eq!(res4, expected_ids4); + + let res5 = query_unbonded_mixnodes_by_owner_paged( + deps.as_ref(), + "doesnt-exist".into(), + None, + None, + ) + .unwrap() + .nodes + .into_iter() + .map(|r| r.0) + .collect::>(); + assert!(res5.is_empty()); + } + } + + #[test] + fn query_for_owned_mixnode() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + let address = "mix-owner".to_string(); + + // when it doesnt exist + let res = query_owned_mixnode(deps.as_ref(), address.clone()).unwrap(); + assert!(res.mixnode_details.is_none()); + assert_eq!(address, res.address); + + // when it [fully] exists + let id = test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env, + &address, + good_mixnode_pledge(), + ); + let res = query_owned_mixnode(deps.as_ref(), address.clone()).unwrap(); + let details = res.mixnode_details.unwrap(); + assert_eq!(address, details.bond_information.owner); + assert_eq!( + good_mixnode_pledge()[0], + details.bond_information.original_pledge + ); + assert_eq!(address, res.address); + + // when it partially exists, i.e. case when the operator unbonded, but there are still some pending delegates + // TODO: perhaps this should work slightly differently, to return the underlying mixnode rewarding? + + // manually adjust delegation info as to indicate the rewarding information shouldnt get removed + let mut rewarding_details = details.rewarding_details; + rewarding_details.delegates = Decimal::raw(12345); + rewarding_details.unique_delegations = 1; + rewards_storage::MIXNODE_REWARDING + .save(deps.as_mut().storage, id, &rewarding_details) + .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); + } + + #[test] + fn query_for_mixnode_details() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + // no node under this id + let res = query_mixnode_details(deps.as_ref(), 42).unwrap(); + assert!(res.mixnode_details.is_none()); + assert_eq!(42, res.mix_id); + + // it exists + let mix_id = + 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.mix_id); + assert_eq!( + good_mixnode_pledge()[0], + details.bond_information.original_pledge + ); + assert_eq!(mix_id, res.mix_id); + } + + #[test] + fn query_for_mixnode_details_by_identity() { + let mut test = TestSetup::new(); + + // no node under this identity + let res = query_mixnode_details_by_identity(test.deps(), "foomp".into()).unwrap(); + assert!(res.is_none()); + + // it exists + let mix_id = test.add_dummy_mixnode("owner", None); + // this was already tested to be working : ) + let expected = query_mixnode_details(test.deps(), mix_id) + .unwrap() + .mixnode_details + .unwrap(); + let mix_identity = expected.bond_information.identity(); + + let res = query_mixnode_details_by_identity(test.deps(), mix_identity.into()).unwrap(); + assert_eq!(expected, res.unwrap()); + } + + #[test] + fn query_for_mixnode_rewarding_details() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + // no node under this id + let res = query_mixnode_rewarding_details(deps.as_ref(), 42).unwrap(); + assert!(res.rewarding_details.is_none()); + assert_eq!(42, res.mix_id); + + let mix_id = + test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "foomp", good_mixnode_pledge()); + let res = query_mixnode_rewarding_details(deps.as_ref(), mix_id).unwrap(); + let details = res.rewarding_details.unwrap(); + assert_eq!( + fixtures::mix_node_cost_params_fixture(), + details.cost_params + ); + assert_eq!(mix_id, res.mix_id); + } + + #[test] + fn query_for_unbonded_mixnode() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + let sender = "mix-owner"; + + // no node under this id + let res = query_unbonded_mixnode(deps.as_ref(), 42).unwrap(); + assert!(res.unbonded_info.is_none()); + assert_eq!(42, res.mix_id); + + // 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(), 123, mix_id).unwrap(); + + let res = query_unbonded_mixnode(deps.as_ref(), mix_id).unwrap(); + assert_eq!(res.unbonded_info.unwrap().owner, sender); + assert_eq!(mix_id, res.mix_id); + } + + #[test] + fn query_for_stake_saturation() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + // no node under this id + let res = query_stake_saturation(deps.as_ref(), 42).unwrap(); + assert!(res.current_saturation.is_none()); + assert!(res.uncapped_saturation.is_none()); + assert_eq!(42, res.mix_id); + + let rewarding_params = rewards_storage::REWARDING_PARAMS + .load(deps.as_ref().storage) + .unwrap(); + let saturation_point = rewarding_params.interval.stake_saturation_point; + + let mix_id = + test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, "foomp", good_mixnode_pledge()); + + // below saturation point + // there's only the base pledge without any delegation + let expected = + Decimal::from_atomics(good_mixnode_pledge()[0].amount, 0).unwrap() / saturation_point; + let res = query_stake_saturation(deps.as_ref(), mix_id).unwrap(); + assert_eq!(expected, res.current_saturation.unwrap()); + assert_eq!(expected, res.uncapped_saturation.unwrap()); + assert_eq!(mix_id, res.mix_id); + + // exactly at saturation point + let mut mix_rewarding = rewards_storage::MIXNODE_REWARDING + .load(deps.as_ref().storage, mix_id) + .unwrap(); + mix_rewarding.operator = saturation_point; + rewards_storage::MIXNODE_REWARDING + .save(deps.as_mut().storage, mix_id, &mix_rewarding) + .unwrap(); + + let res = query_stake_saturation(deps.as_ref(), mix_id).unwrap(); + assert_eq!(Decimal::one(), res.current_saturation.unwrap()); + assert_eq!(Decimal::one(), res.uncapped_saturation.unwrap()); + assert_eq!(mix_id, res.mix_id); + + // above the saturation point + let mut mix_rewarding = rewards_storage::MIXNODE_REWARDING + .load(deps.as_ref().storage, mix_id) + .unwrap(); + mix_rewarding.delegates = mix_rewarding.operator * Decimal::percent(150); + rewards_storage::MIXNODE_REWARDING + .save(deps.as_mut().storage, mix_id, &mix_rewarding) + .unwrap(); + + let res = query_stake_saturation(deps.as_ref(), mix_id).unwrap(); + assert_eq!(Decimal::one(), res.current_saturation.unwrap()); + assert_eq!(Decimal::percent(250), res.uncapped_saturation.unwrap()); + assert_eq!(mix_id, res.mix_id); + } +} diff --git a/contracts/mixnet/src/mixnodes/storage.rs b/contracts/mixnet/src/mixnodes/storage.rs index ad54790b8b..dd9ba56212 100644 --- a/contracts/mixnet/src/mixnodes/storage.rs +++ b/contracts/mixnet/src/mixnodes/storage.rs @@ -1,258 +1,213 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{StdResult, Storage, Uint128}; -use cw_storage_plus::{Index, IndexList, IndexedSnapshotMap, Map, Strategy, UniqueIndex}; -use mixnet_contract_common::{ - reward_params::NodeEpochRewards, Addr, Coin, IdentityKeyRef, Layer, MixNode, MixNodeBond, +use crate::constants::{ + LAYER_DISTRIBUTION_KEY, MIXNODES_IDENTITY_IDX_NAMESPACE, MIXNODES_OWNER_IDX_NAMESPACE, + MIXNODES_PK_NAMESPACE, MIXNODES_SPHINX_IDX_NAMESPACE, NODE_ID_COUNTER_KEY, + UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE, UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE, + UNBONDED_MIXNODES_PK_NAMESPACE, }; -use mixnet_contract_common::{SphinxKey, U128}; -use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; +use cosmwasm_std::{StdResult, Storage}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, MultiIndex, UniqueIndex}; +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, MixId, MixNodeBond}; -use crate::mixnet_contract_settings::storage::mix_denom; +// 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 -// storage prefixes -const TOTAL_DELEGATION_NAMESPACE: &str = "td"; -const MIXNODES_PK_NAMESPACE: &str = "mn"; -const MIXNODES_PK_CHECKPOINTS: &str = "mn__check"; -const MIXNODES_PK_CHANGELOG: &str = "mn__change"; -const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno"; -const MIXNODES_SPHINX_IDX_NAMESPACE: &str = "mns"; -const MIXNODES_BOND_BLACKLIST_NAMESPACE: &str = "mbb"; +pub(crate) struct UnbondedMixnodeIndex<'a> { + pub(crate) owner: MultiIndex<'a, Addr, UnbondedMixnode, MixId>, -const LAST_PM_UPDATE_NAMESPACE: &str = "lpm"; - -// paged retrieval limits for all queries and transactions -pub(crate) const BOND_PAGE_MAX_LIMIT: u32 = 75; -pub(crate) const BOND_PAGE_DEFAULT_LIMIT: u32 = 50; - -pub(crate) const TOTAL_DELEGATION: Map<'_, IdentityKeyRef<'_>, Uint128> = - Map::new(TOTAL_DELEGATION_NAMESPACE); - -pub(crate) const LAST_PM_UPDATE_TIME: Map<'_, IdentityKeyRef<'_>, u64> = - Map::new(LAST_PM_UPDATE_NAMESPACE); - -pub(crate) const MIXNODES_BOND_BLACKLIST: Map<'_, IdentityKeyRef<'_>, u8> = - Map::new(MIXNODES_BOND_BLACKLIST_NAMESPACE); - -pub(crate) struct MixnodeBondIndex<'a> { - pub(crate) owner: UniqueIndex<'a, Addr, StoredMixnodeBond>, - - pub(crate) sphinx_key: UniqueIndex<'a, SphinxKey, StoredMixnodeBond>, + pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedMixnode, MixId>, } -// IndexList is just boilerplate code for fetching a struct's indexes -// note that from my understanding this will be converted into a macro at some point in the future -impl<'a> IndexList for MixnodeBondIndex<'a> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.owner, &self.sphinx_key]; +impl<'a> IndexList for UnbondedMixnodeIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key]; Box::new(v.into_iter()) } } -// mixnodes() is the storage access function. -pub(crate) fn mixnodes<'a>( -) -> IndexedSnapshotMap<'a, IdentityKeyRef<'a>, StoredMixnodeBond, MixnodeBondIndex<'a>> { +pub(crate) fn unbonded_mixnodes<'a>( +) -> IndexedMap<'a, MixId, UnbondedMixnode, UnbondedMixnodeIndex<'a>> { + let indexes = UnbondedMixnodeIndex { + owner: MultiIndex::new( + |d| d.owner.clone(), + UNBONDED_MIXNODES_PK_NAMESPACE, + UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE, + ), + identity_key: MultiIndex::new( + |d| d.identity_key.clone(), + UNBONDED_MIXNODES_PK_NAMESPACE, + UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE, + ), + }; + IndexedMap::new(UNBONDED_MIXNODES_PK_NAMESPACE, indexes) +} + +pub(crate) const LAYERS: Item<'_, LayerDistribution> = Item::new(LAYER_DISTRIBUTION_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>, + + pub(crate) identity_key: UniqueIndex<'a, IdentityKey, MixNodeBond>, + + pub(crate) sphinx_key: UniqueIndex<'a, SphinxKey, MixNodeBond>, +} + +// IndexList is just boilerplate code for fetching a struct's indexes +// note that from my understanding this will be converted into a macro at some point in the future +impl<'a> IndexList for MixnodeBondIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = + vec![&self.owner, &self.identity_key, &self.sphinx_key]; + Box::new(v.into_iter()) + } +} + +// mixnode_bonds() is the storage access function. +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( + |d| d.mix_node.identity_key.clone(), + MIXNODES_IDENTITY_IDX_NAMESPACE, + ), sphinx_key: UniqueIndex::new( |d| d.mix_node.sphinx_key.clone(), MIXNODES_SPHINX_IDX_NAMESPACE, ), }; - IndexedSnapshotMap::new( - MIXNODES_PK_NAMESPACE, - MIXNODES_PK_CHECKPOINTS, - MIXNODES_PK_CHANGELOG, - Strategy::Selected, - indexes, - ) + IndexedMap::new(MIXNODES_PK_NAMESPACE, indexes) } -#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] -pub struct StoredMixnodeBond { - pub pledge_amount: Coin, - pub owner: Addr, - pub layer: Layer, - pub block_height: u64, - pub mix_node: MixNode, - pub proxy: Option, - pub accumulated_rewards: Option, - pub epoch_rewards: Option, +pub fn decrement_layer_count( + storage: &mut dyn Storage, + layer: Layer, +) -> Result<(), MixnetContractError> { + let mut layers = LAYERS.load(storage)?; + layers.decrement_layer_count(layer)?; + Ok(LAYERS.save(storage, &layers)?) } -impl From for StoredMixnodeBond { - fn from(mixnode_bond: MixNodeBond) -> StoredMixnodeBond { - StoredMixnodeBond { - pledge_amount: mixnode_bond.pledge_amount, - owner: mixnode_bond.owner, - layer: mixnode_bond.layer, - block_height: mixnode_bond.block_height, - mix_node: mixnode_bond.mix_node, - proxy: mixnode_bond.proxy, - accumulated_rewards: mixnode_bond.accumulated_rewards, - epoch_rewards: None, - } - } +pub(crate) fn assign_layer(store: &mut dyn Storage) -> StdResult { + // load current distribution + let mut layers = LAYERS.load(store)?; + + // choose the one with fewest nodes + let fewest = layers.choose_with_fewest(); + + // increment the existing count + layers.increment_layer_count(fewest); + + // and resave it + LAYERS.save(store, &layers)?; + Ok(fewest) } -impl StoredMixnodeBond { - #[allow(clippy::too_many_arguments)] - pub(crate) fn new( - pledge_amount: Coin, - owner: Addr, - layer: Layer, - block_height: u64, - mix_node: MixNode, - proxy: Option, - accumulated_rewards: Option, - epoch_rewards: Option, - ) -> Self { - StoredMixnodeBond { - pledge_amount, - owner, - layer, - block_height, - mix_node, - proxy, - accumulated_rewards, - epoch_rewards, - } - } - - pub(crate) fn accumulated_rewards(&self) -> Uint128 { - self.accumulated_rewards.unwrap_or_else(Uint128::zero) - } - - pub(crate) fn attach_delegation(self, total_delegation: Uint128) -> MixNodeBond { - MixNodeBond { - total_delegation: Coin { - denom: self.pledge_amount.denom.clone(), - amount: total_delegation, - }, - pledge_amount: self.pledge_amount, - owner: self.owner, - layer: self.layer, - block_height: self.block_height, - mix_node: self.mix_node, - proxy: self.proxy, - accumulated_rewards: self.accumulated_rewards, - } - } - - pub(crate) fn identity(&self) -> &String { - &self.mix_node.identity_key - } - - pub(crate) fn pledge_amount(&self) -> Coin { - self.pledge_amount.clone() - } - - pub fn profit_margin(&self) -> U128 { - U128::from_num(self.mix_node.profit_margin_percent) / U128::from_num(100) - } +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) } -impl Display for StoredMixnodeBond { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "amount: {}, owner: {}, identity: {}", - self.pledge_amount, self.owner, self.mix_node.identity_key - ) - } -} - -pub(crate) fn read_full_mixnode_bond( - storage: &dyn Storage, - mix_identity: IdentityKeyRef<'_>, -) -> StdResult> { - let stored_bond = mixnodes().may_load(storage, mix_identity)?; - match stored_bond { - None => Ok(None), - Some(stored_bond) => { - let total_delegation = TOTAL_DELEGATION.may_load(storage, mix_identity)?; - Ok(Some(MixNodeBond { - pledge_amount: stored_bond.pledge_amount, - total_delegation: Coin { - denom: mix_denom(storage)?, - amount: total_delegation.unwrap_or_default(), - }, - owner: stored_bond.owner, - layer: stored_bond.layer, - block_height: stored_bond.block_height, - mix_node: stored_bond.mix_node, - proxy: stored_bond.proxy, - accumulated_rewards: stored_bond.accumulated_rewards, - })) - } - } +pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> StdResult<()> { + LAYERS.save(storage, &LayerDistribution::default()) } #[cfg(test)] mod tests { - use super::super::storage; use super::*; - use crate::support::tests; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers::init_contract; - use cosmwasm_std::testing::MockStorage; - use cosmwasm_std::{coin, Addr, Uint128}; - use mixnet_contract_common::{IdentityKey, MixNode}; + use crate::support::tests::test_helpers; + use cosmwasm_std::testing::mock_dependencies; #[test] - fn mixnode_single_read_retrieval() { - let mut storage = MockStorage::new(); - let bond1 = tests::fixtures::stored_mixnode_bond_fixture("owner1"); - let bond2 = tests::fixtures::stored_mixnode_bond_fixture("owner2"); - mixnodes().save(&mut storage, "bond1", &bond1, 1).unwrap(); - mixnodes().save(&mut storage, "bond2", &bond2, 1).unwrap(); + fn decrementing_layer() { + let mut deps = test_helpers::init_contract(); - let res1 = mixnodes().load(&storage, "bond1").unwrap(); - let res2 = mixnodes().load(&storage, "bond2").unwrap(); - assert_eq!(bond1, res1); - assert_eq!(bond2, res2); + // we never underflow, if it were to happen we're going to return an error instead + assert_eq!( + Err(MixnetContractError::OverflowSubtraction { + minuend: 0, + subtrahend: 1 + }), + decrement_layer_count(deps.as_mut().storage, Layer::One) + ); + + LAYERS + .save( + deps.as_mut().storage, + &LayerDistribution { + layer1: 3, + layer2: 2, + layer3: 1, + }, + ) + .unwrap(); + + assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_ok()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_ok()); + + assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_ok()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); + + assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_err()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); + + assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_err()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_err()); + assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); } #[test] - fn reading_mixnode_bond() { - let mut mock_storage = init_contract().storage; - let node_owner: Addr = Addr::unchecked("node-owner"); - let node_identity: IdentityKey = "nodeidentity".into(); + fn assigning_layer() { + let mut deps = test_helpers::init_contract(); - // produces a None if target mixnode doesn't exist - let res = storage::read_full_mixnode_bond(&mock_storage, &node_identity).unwrap(); - assert!(res.is_none()); - - // returns appropriate value otherwise - let pledge_value = 1000000000; - - let mixnode_bond = StoredMixnodeBond { - pledge_amount: coin(pledge_value, TEST_COIN_DENOM), - owner: node_owner, - layer: Layer::One, - block_height: 12_345, - mix_node: MixNode { - identity_key: node_identity.clone(), - ..tests::fixtures::mix_node_fixture() - }, - proxy: None, - accumulated_rewards: None, - epoch_rewards: None, + let layers = LayerDistribution { + layer1: 3, + layer2: 2, + layer3: 1, }; + LAYERS.save(deps.as_mut().storage, &layers).unwrap(); - storage::mixnodes() - .save(&mut mock_storage, &node_identity, &mixnode_bond, 1) - .unwrap(); + // always assigns layer with fewest nodes + assert_eq!(Layer::Three, assign_layer(deps.as_mut().storage).unwrap()); + assert_eq!(2, LAYERS.load(deps.as_ref().storage).unwrap().layer3); + // we have 3, 2, 2, so the 2nd layer should get chosen now + assert_eq!(Layer::Two, assign_layer(deps.as_mut().storage).unwrap()); + assert_eq!(3, LAYERS.load(deps.as_ref().storage).unwrap().layer2); + + // 3, 3, 2, so 3rd one again + assert_eq!(Layer::Three, assign_layer(deps.as_mut().storage).unwrap()); + assert_eq!(3, LAYERS.load(deps.as_ref().storage).unwrap().layer3); + } + + #[test] + fn next_id() { + let mut deps = test_helpers::init_contract(); + + for i in 1u32..1000 { + assert_eq!(i, next_mixnode_id_counter(deps.as_mut().storage).unwrap()); + } + } + + #[test] + fn initialising() { + let mut deps = mock_dependencies(); + assert!(LAYERS.load(deps.as_ref().storage).is_err()); + + initialise_storage(deps.as_mut().storage).unwrap(); assert_eq!( - Uint128::new(pledge_value), - storage::read_full_mixnode_bond(&mock_storage, node_identity.as_str()) - .unwrap() - .unwrap() - .pledge_amount - .amount + LayerDistribution::default(), + LAYERS.load(deps.as_ref().storage).unwrap() ); } } diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 0b8b6153e3..0a1e816ffd 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -1,60 +1,40 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::storage::{self, LAST_PM_UPDATE_TIME}; -use crate::error::ContractError; -use crate::mixnet_contract_settings::storage::{self as mixnet_params_storage, mix_denom}; -use crate::mixnodes::layer_queries::query_layer_distribution; -use crate::mixnodes::storage::StoredMixnodeBond; -use crate::support::helpers::{ensure_no_existing_bond, validate_node_identity_signature}; -use cosmwasm_std::{ - wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Storage, Uint128, +use super::storage; +use crate::interval::storage as interval_storage; +use crate::interval::storage::push_new_interval_event; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::helpers::{must_get_mixnode_bond_by_owner, save_new_mixnode}; +use crate::support::helpers::{ + ensure_bonded, ensure_no_existing_bond, ensure_proxy_match, validate_node_identity_signature, + validate_pledge, }; +use cosmwasm_std::{Addr, Coin, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_checkpoint_mixnodes_event, new_mixnode_bonding_event, new_mixnode_unbonding_event, + new_mixnode_bonding_event, new_mixnode_config_update_event, + new_mixnode_pending_cost_params_update_event, new_pending_mixnode_unbonding_event, }; -use mixnet_contract_common::{IdentityKeyRef, MixNode}; -use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; -use vesting_contract_common::one_ucoin; - -const MIN_PM_UPDATE_INTERVAL: u64 = 60 * 60 * 24 * 30; // one month roughly - -pub fn try_checkpoint_mixnodes( - storage: &mut dyn Storage, - block_height: u64, - info: MessageInfo, -) -> Result { - let state = mixnet_params_storage::CONTRACT_STATE.load(storage)?; - // check if this is executed by the permitted validator, if not reject the transaction - if info.sender != state.rewarding_validator_address { - return Err(ContractError::Unauthorized); - } - - crate::mixnodes::storage::mixnodes().add_checkpoint(storage, block_height)?; - - Ok(Response::new().add_event(new_checkpoint_mixnodes_event(block_height))) -} +use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; +use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; +use mixnet_contract_common::MixNode; pub fn try_add_mixnode( deps: DepsMut<'_>, env: Env, info: MessageInfo, mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, -) -> Result { - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::CONTRACT_STATE - .load(deps.storage)? - .params - .minimum_mixnode_pledge; - let pledge = validate_mixnode_pledge(info.funds, minimum_pledge, mix_denom(deps.storage)?)?; - +) -> Result { _try_add_mixnode( deps, env, mix_node, - pledge, - info.sender.as_str(), + cost_params, + info.funds, + info.sender, owner_signature, None, ) @@ -65,872 +45,599 @@ pub fn try_add_mixnode_on_behalf( env: Env, info: MessageInfo, mix_node: MixNode, + cost_params: MixNodeCostParams, owner: String, owner_signature: String, -) -> Result { - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::CONTRACT_STATE - .load(deps.storage)? - .params - .minimum_mixnode_pledge; - let pledge = validate_mixnode_pledge(info.funds, minimum_pledge, mix_denom(deps.storage)?)?; - +) -> Result { let proxy = info.sender; + let owner = deps.api.addr_validate(&owner)?; _try_add_mixnode( deps, env, mix_node, - pledge, - &owner, + cost_params, + info.funds, + owner, owner_signature, Some(proxy), ) } -pub fn is_blacklisted( - storage: &dyn Storage, - identity: IdentityKeyRef, -) -> Result { - Ok(storage::MIXNODES_BOND_BLACKLIST - .may_load(storage, identity)? - .is_some()) -} - +// I'm not entirely sure how to deal with this warning at the current moment +#[allow(clippy::too_many_arguments)] fn _try_add_mixnode( deps: DepsMut<'_>, env: Env, - mix_node: MixNode, - pledge_amount: Coin, - owner: &str, + mixnode: MixNode, + cost_params: MixNodeCostParams, + pledge: Vec, + owner: Addr, owner_signature: String, proxy: Option, -) -> Result { - let owner = deps.api.addr_validate(owner)?; +) -> Result { + // check if the pledge contains any funds of the appropriate denomination + let minimum_pledge = mixnet_params_storage::minimum_mixnode_pledge(deps.storage)?; + let pledge = validate_pledge(pledge, minimum_pledge)?; + // if the client has an active bonded mixnode or gateway, don't allow bonding + // note that this has to be done explicitly as `UniqueIndex` constraint would not protect us + // against attempting to use different node types (i.e. gateways and mixnodes) ensure_no_existing_bond(deps.storage, &owner)?; - if is_blacklisted(deps.storage, &mix_node.identity_key)? { - return Err(ContractError::MixnodeBlacklisted { - identity: mix_node.identity_key, - }); - }; - - // We don't have to check lower bound as its an u8 - if mix_node.profit_margin_percent > 100 { - return Err(ContractError::InvalidProfitMarginPercent( - mix_node.profit_margin_percent, - )); - } - - // check if somebody else has already bonded a mixnode with this identity - if let Some(existing_bond) = - storage::mixnodes().may_load(deps.storage, &mix_node.identity_key)? - { - if existing_bond.owner != owner { - return Err(ContractError::DuplicateMixnode { - owner: existing_bond.owner, - }); - } - } + // there's no need to explicitly check whether there already exists mixnode with the same + // identity or sphinx keys as this is going to be done implicitly when attempting to save + // the bond information due to `UniqueIndex` constraint defined on those fields. // check if this sender actually owns the mixnode by checking the signature validate_node_identity_signature( deps.as_ref(), &owner, owner_signature, - &mix_node.identity_key, + &mixnode.identity_key, )?; - let layer_distribution = query_layer_distribution(deps.as_ref())?; - let layer = layer_distribution.choose_with_fewest(); - - let stored_bond = StoredMixnodeBond::new( - pledge_amount.clone(), + let node_identity = mixnode.identity_key.clone(); + let (node_id, layer) = save_new_mixnode( + deps.storage, + env, + mixnode, + cost_params, owner.clone(), - layer, - env.block.height, - mix_node, proxy.clone(), - None, - None, - ); - - // technically we don't have to set the total_delegation bucket, but it makes things easier - // in different places that we can guarantee that if node exists, so does the data behind the total delegation - let identity = stored_bond.identity(); - storage::mixnodes().save(deps.storage, identity, &stored_bond, env.block.height)?; - - // if this is a fresh mixnode - write 0 total delegation, otherwise, don't touch it since the node has just rebonded - if storage::TOTAL_DELEGATION - .may_load(deps.storage, identity)? - .is_none() - { - storage::TOTAL_DELEGATION.save(deps.storage, identity, &Uint128::zero())?; - } - - storage::LAST_PM_UPDATE_TIME.save(deps.storage, identity, &env.block.time.seconds())?; - - mixnet_params_storage::increment_layer_count(deps.storage, stored_bond.layer)?; + pledge.clone(), + )?; Ok(Response::new().add_event(new_mixnode_bonding_event( &owner, &proxy, - &pledge_amount, - identity, - stored_bond.layer, + &pledge, + &node_identity, + node_id, + layer, ))) } pub fn try_remove_mixnode_on_behalf( - env: &Env, - storage: &mut dyn Storage, - api: &dyn Api, + deps: DepsMut<'_>, + env: Env, info: MessageInfo, owner: String, -) -> Result { +) -> Result { let proxy = info.sender; - _try_remove_mixnode(env, storage, api, &owner, Some(proxy), true) + let owner = deps.api.addr_validate(&owner)?; + _try_remove_mixnode(deps, env, owner, Some(proxy)) } pub fn try_remove_mixnode( - env: &Env, - storage: &mut dyn Storage, - api: &dyn Api, + deps: DepsMut<'_>, + env: Env, info: MessageInfo, -) -> Result { - _try_remove_mixnode(env, storage, api, info.sender.as_ref(), None, true) +) -> Result { + _try_remove_mixnode(deps, env, info.sender, None) } pub(crate) fn _try_remove_mixnode( - env: &Env, - storage: &mut dyn Storage, - api: &dyn Api, - owner: &str, + deps: DepsMut<'_>, + env: Env, + owner: Addr, proxy: Option, - collect_rewards: bool, -) -> Result { - let owner = api.addr_validate(owner)?; +) -> Result { + let existing_bond = storage::mixnode_bonds() + .idx + .owner + .item(deps.storage, owner.clone())? + .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner })? + .1; - if collect_rewards { - crate::rewards::transactions::_try_compound_operator_reward( - storage, - api, - env.block.height, - &owner, - None, - )?; - } + // see if the proxy matches + ensure_proxy_match(&proxy, &existing_bond.proxy)?; + ensure_bonded(&existing_bond)?; - // try to find the node of the sender - let mixnode_bond = match storage::mixnodes().idx.owner.item(storage, owner.clone())? { - Some(record) => record.1, - None => return Err(ContractError::NoAssociatedMixNodeBond { owner }), + // set `is_unbonding` field + let mut updated_bond = existing_bond.clone(); + updated_bond.is_unbonding = true; + storage::mixnode_bonds().replace( + deps.storage, + 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 = PendingEpochEventKind::UnbondMixnode { + mix_id: existing_bond.mix_id, }; + interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; - if proxy != mixnode_bond.proxy { - return Err(ContractError::ProxyMismatch { - existing: mixnode_bond - .proxy - .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - incoming: proxy.map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - }); - } - // send bonded funds back to the bond owner - let return_tokens = BankMsg::Send { - to_address: proxy.as_ref().unwrap_or(&owner).to_string(), - amount: vec![mixnode_bond.pledge_amount()], - }; - - // remove the bond - storage::mixnodes().remove(storage, mixnode_bond.identity(), env.block.height)?; - - // decrement layer count - mixnet_params_storage::decrement_layer_count(storage, mixnode_bond.layer)?; - - let mut response = Response::new(); - - if let Some(proxy) = &proxy { - let msg = VestingContractExecuteMsg::TrackUnbondMixnode { - owner: owner.as_str().to_string(), - amount: mixnode_bond.pledge_amount(), - }; - - let track_unbond_message = wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom(storage)?)])?; - response = response.add_message(track_unbond_message); - } - - let response = response.add_message(return_tokens); - - Ok(response.add_event(new_mixnode_unbonding_event( - &owner, - &proxy, - &mixnode_bond.pledge_amount, - mixnode_bond.identity(), - ))) + Ok( + Response::new().add_event(new_pending_mixnode_unbonding_event( + &existing_bond.owner, + &existing_bond.proxy, + existing_bond.identity(), + existing_bond.mix_id, + )), + ) } pub(crate) fn try_update_mixnode_config( deps: DepsMut<'_>, - env: Env, info: MessageInfo, - profit_margin_percent: u8, -) -> Result { - let owner = deps.api.addr_validate(info.sender.as_ref())?; - _try_update_mixnode_config(deps, env, profit_margin_percent, owner, None) + new_config: MixNodeConfigUpdate, +) -> Result { + let owner = info.sender; + _try_update_mixnode_config(deps, new_config, owner, None) } pub(crate) fn try_update_mixnode_config_on_behalf( deps: DepsMut, - env: Env, info: MessageInfo, - profit_margin_percent: u8, + new_config: MixNodeConfigUpdate, owner: String, -) -> Result { +) -> Result { let owner = deps.api.addr_validate(&owner)?; - let proxy = deps.api.addr_validate(info.sender.as_ref())?; - _try_update_mixnode_config(deps, env, profit_margin_percent, owner, Some(proxy)) + let proxy = info.sender; + _try_update_mixnode_config(deps, new_config, owner, Some(proxy)) } pub(crate) fn _try_update_mixnode_config( deps: DepsMut, - env: Env, - profit_margin_percent: u8, + new_config: MixNodeConfigUpdate, owner: Addr, proxy: Option, -) -> Result { - let mixnode_bond = storage::mixnodes() - .idx - .owner - .item(deps.storage, owner.clone())? - .ok_or(ContractError::NoAssociatedMixNodeBond { owner })? - .1; - let mix_denom = mix_denom(deps.storage)?; +) -> Result { + let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &owner)?; - if proxy != mixnode_bond.proxy { - return Err(ContractError::ProxyMismatch { - existing: mixnode_bond - .proxy - .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - incoming: proxy.map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - }); - } + ensure_bonded(&existing_bond)?; + ensure_proxy_match(&proxy, &existing_bond.proxy)?; - let last_update_time = storage::LAST_PM_UPDATE_TIME - .load(deps.storage, mixnode_bond.identity()) - .unwrap_or(0); + let cfg_update_event = + new_mixnode_config_update_event(existing_bond.mix_id, &owner, &proxy, &new_config); - let current_block_time = env.block.time.seconds(); + let mut updated_bond = existing_bond.clone(); + updated_bond.mix_node.host = new_config.host; + updated_bond.mix_node.mix_port = new_config.mix_port; + updated_bond.mix_node.verloc_port = new_config.verloc_port; + updated_bond.mix_node.http_api_port = new_config.http_api_port; + updated_bond.mix_node.version = new_config.version; - if current_block_time - last_update_time < MIN_PM_UPDATE_INTERVAL { - return Err(ContractError::UpdatePMTooSoon { - last_update_time, - current_block_time, - }); - } - - // We don't have to check lower bound as its an u8 - if profit_margin_percent > 100 { - return Err(ContractError::InvalidProfitMarginPercent( - profit_margin_percent, - )); - } - - storage::mixnodes().update( + storage::mixnode_bonds().replace( deps.storage, - mixnode_bond.identity(), - env.block.height, - |mixnode_bond_opt| { - mixnode_bond_opt - .map(|mut mixnode_bond| { - mixnode_bond.mix_node.profit_margin_percent = profit_margin_percent; - mixnode_bond.block_height = env.block.height; - mixnode_bond - }) - .ok_or(ContractError::NoBondFound { - mix_denom: mix_denom.clone(), - }) - }, + existing_bond.mix_id, + Some(&updated_bond), + Some(&existing_bond), )?; - LAST_PM_UPDATE_TIME.save(deps.storage, mixnode_bond.identity(), ¤t_block_time)?; - - let mut response = Response::new(); - - if let Some(proxy) = proxy { - // Returns one_ucoin proxy had to send in order to execute the contract to contract transaction, this is potentially leaky as anyone can say that they're a proxy, - // and they could potentially leak 1 unym per transaction, altough I'm pretty sure transaction fees make that silly. - let return_one_ucoint = BankMsg::Send { - to_address: proxy.as_str().to_string(), - amount: vec![one_ucoin(mix_denom)], - }; - response = response.add_message(return_one_ucoint); - } - - Ok(response) + Ok(Response::new().add_event(cfg_update_event)) } -fn validate_mixnode_pledge( - mut pledge: Vec, - minimum_pledge: Uint128, - mix_denom: String, -) -> Result { - // check if anything was put as bond - if pledge.is_empty() { - return Err(ContractError::NoBondFound { mix_denom }); - } +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, env, new_costs, owner, None) +} - if pledge.len() > 1 { - return Err(ContractError::MultipleDenoms); - } +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, env, new_costs, owner, Some(proxy)) +} - // check that the denomination is correct - if pledge[0].denom != mix_denom { - return Err(ContractError::WrongDenom { mix_denom }); - } +pub(crate) fn _try_update_mixnode_cost_params( + deps: DepsMut, + env: Env, + new_costs: MixNodeCostParams, + owner: Addr, + proxy: Option, +) -> Result { + // see if the node still exists + let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &owner)?; - // check that we have at least MIXNODE_BOND coins in our pledge - if pledge[0].amount < minimum_pledge { - return Err(ContractError::InsufficientMixNodeBond { - received: pledge[0].amount.into(), - minimum: minimum_pledge.into(), - }); - } + ensure_proxy_match(&proxy, &existing_bond.proxy)?; + ensure_bonded(&existing_bond)?; - Ok(pledge.pop().unwrap()) + 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 = PendingIntervalEventKind::ChangeMixCostParams { + mix_id: existing_bond.mix_id, + new_costs, + }; + push_new_interval_event(deps.storage, &env, interval_event)?; + + Ok(Response::new().add_event(cosmos_event)) } #[cfg(test)] pub mod tests { use super::*; - use crate::contract::{execute, query, INITIAL_MIXNODE_PLEDGE}; - use crate::error::ContractError; - use crate::mixnodes::transactions::validate_mixnode_pledge; - use crate::support::tests; - use crate::support::tests::fixtures::TEST_COIN_DENOM; - use crate::support::tests::test_helpers; + use crate::contract::execute; + use crate::mixnet_contract_settings::storage::minimum_mixnode_pledge; + use crate::mixnodes::helpers::get_mixnode_details_by_id; + use crate::support::tests::fixtures::{good_mixnode_pledge, TEST_COIN_DENOM}; + use crate::support::tests::{fixtures, test_helpers}; use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::{coins, BankMsg, Response}; - use cosmwasm_std::{from_binary, Addr, Uint128}; - use mixnet_contract_common::{ - ExecuteMsg, Layer, LayerDistribution, MixNode, PagedMixnodeResponse, QueryMsg, - }; - use rand::thread_rng; + use cosmwasm_std::{Order, StdResult, Uint128}; + use mixnet_contract_common::{ExecuteMsg, Layer, LayerDistribution, Percent}; #[test] fn mixnode_add() { let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + let sender = "alice"; + let minimum_pledge = minimum_mixnode_pledge(deps.as_ref().storage).unwrap(); + let mut insufficient_pledge = minimum_pledge.clone(); + insufficient_pledge.amount -= Uint128::new(1000); // if we don't send enough funds - let insufficient_bond = Into::::into(INITIAL_MIXNODE_PLEDGE) - 1; - let info = mock_info("anyone", &coins(insufficient_bond, TEST_COIN_DENOM)); - let (msg, _) = tests::messages::valid_bond_mixnode_msg("anyone"); + let info = mock_info(sender, &[insufficient_pledge.clone()]); + let (mixnode, sig) = test_helpers::mixnode_with_signature(&mut rng, sender); + let cost_params = fixtures::mix_node_cost_params_fixture(); // we are informed that we didn't send enough funds - let result = execute(deps.as_mut(), mock_env(), info, msg); + let result = try_add_mixnode( + deps.as_mut(), + env.clone(), + info, + mixnode.clone(), + cost_params.clone(), + sig.clone(), + ); assert_eq!( result, - Err(ContractError::InsufficientMixNodeBond { - received: insufficient_bond, - minimum: INITIAL_MIXNODE_PLEDGE.into(), + Err(MixnetContractError::InsufficientPledge { + received: insufficient_pledge, + minimum: minimum_pledge.clone(), }) ); - // no mixnode was inserted into the topology - let res = query( - deps.as_ref(), - mock_env(), - QueryMsg::GetMixNodes { - start_after: None, - limit: Option::from(2), - }, - ) - .unwrap(); - let page: PagedMixnodeResponse = from_binary(&res).unwrap(); - assert_eq!(0, page.nodes.len()); + // if the signature provided is invalid, the bonding also fails + let info = mock_info(sender, &[minimum_pledge]); - // if we send enough funds - let info = mock_info("anyone", &tests::fixtures::good_mixnode_pledge()); - let (msg, (identity, sphinx)) = tests::messages::valid_bond_mixnode_msg("anyone"); - - // we get back a message telling us everything was OK - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert!(execute_response.is_ok()); - - // we can query topology and the new node is there - let query_response = query( - deps.as_ref(), - mock_env(), - QueryMsg::GetMixNodes { - start_after: None, - limit: Option::from(2), - }, - ) - .unwrap(); - let page: PagedMixnodeResponse = from_binary(&query_response).unwrap(); - assert_eq!(1, page.nodes.len()); - assert_eq!( - &MixNode { - identity_key: identity, - sphinx_key: sphinx, - ..tests::fixtures::mix_node_fixture() - }, - page.nodes[0].mix_node() + let result = try_add_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + mixnode.clone(), + cost_params.clone(), + "bad-signature".into(), ); + assert!(matches!( + result, + Err(MixnetContractError::MalformedEd25519Signature(..)) + )); // if there was already a mixnode bonded by particular user - let info = mock_info("foomper", &tests::fixtures::good_mixnode_pledge()); - let (msg, _) = tests::messages::valid_bond_mixnode_msg("foomper"); - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); - - let info = mock_info("foomper", &tests::fixtures::good_mixnode_pledge()); - let (msg, _) = tests::messages::valid_bond_mixnode_msg("foomper"); + test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + sender, + fixtures::good_mixnode_pledge(), + ); // it fails - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(Err(ContractError::AlreadyOwnsMixnode), execute_response); - - // bonding fails if the user already owns a gateway - test_helpers::add_gateway( - "gateway-owner", - tests::fixtures::good_gateway_pledge(), + let result = try_add_mixnode( deps.as_mut(), + env.clone(), + info, + mixnode, + cost_params.clone(), + sig, + ); + assert_eq!(Err(MixnetContractError::AlreadyOwnsMixnode), result); + + // the same holds if the user already owns a gateway + let sender2 = "gateway-owner"; + + test_helpers::add_gateway( + &mut rng, + deps.as_mut(), + env.clone(), + sender2, + tests::fixtures::good_gateway_pledge(), ); - let info = mock_info("gateway-owner", &tests::fixtures::good_mixnode_pledge()); - let (msg, _) = tests::messages::valid_bond_mixnode_msg("gateway-owner"); + let info = mock_info(sender2, &tests::fixtures::good_mixnode_pledge()); + let (mixnode, sig) = test_helpers::mixnode_with_signature(&mut rng, sender2); - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(execute_response, Err(ContractError::AlreadyOwnsGateway)); + let result = try_add_mixnode( + deps.as_mut(), + env.clone(), + info.clone(), + mixnode.clone(), + cost_params.clone(), + sig.clone(), + ); + assert_eq!(Err(MixnetContractError::AlreadyOwnsGateway), result); // but after he unbonds it, it's all fine again - let info = mock_info("gateway-owner", &[]); let msg = ExecuteMsg::UnbondGateway {}; - execute(deps.as_mut(), mock_env(), info, msg).unwrap(); + execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - let info = mock_info("gateway-owner", &tests::fixtures::good_mixnode_pledge()); - let (msg, _) = tests::messages::valid_bond_mixnode_msg("gateway-owner"); + let result = try_add_mixnode(deps.as_mut(), env, info, mixnode, cost_params, sig); + assert!(result.is_ok()); - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert!(execute_response.is_ok()); + // 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.mix_id); - // adding another node from another account, but with the same IP, should fail (or we would have a weird state). Is that right? Think about this, not sure yet. - // if we attempt to register a second node from the same address, should we get an error? It would probably be polite. - } + // and make sure we're on layer 2 (because it was the next empty one) + assert_eq!(Layer::Two, bond.layer); - #[test] - fn adding_mixnode_without_existing_owner_succeeds() { - let mut deps = test_helpers::init_contract(); - - let info = mock_info("mix-owner", &tests::fixtures::good_mixnode_pledge()); - - // before the execution the node had no associated owner - assert!(storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("mix-owner")) - .unwrap() - .is_none()); - - let (msg, (identity, _)) = tests::messages::valid_bond_mixnode_msg("mix-owner"); - - // it's all fine, owner is saved - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); - assert!(execute_response.is_ok()); - - assert_eq!( - &identity, - storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("mix-owner")) - .unwrap() - .unwrap() - .1 - .identity() - ); - } - - #[test] - fn adding_mixnode_with_existing_owner_fails() { - let mut deps = test_helpers::init_contract(); - - let identity = test_helpers::add_mixnode( - "mix-owner", - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - // request fails giving the existing owner address in the message - let info = mock_info( - "mix-owner-pretender", - &tests::fixtures::good_mixnode_pledge(), - ); - let msg = ExecuteMsg::BondMixnode { - mix_node: MixNode { - identity_key: identity, - ..tests::fixtures::mix_node_fixture() - }, - owner_signature: "foomp".to_string(), + // and see if the layer distribution matches our expectation + let expected = LayerDistribution { + layer1: 1, + layer2: 1, + layer3: 0, }; - - let execute_response = execute(deps.as_mut(), mock_env(), info, msg); assert_eq!( - Err(ContractError::DuplicateMixnode { - owner: Addr::unchecked("mix-owner") - }), - execute_response - ); - } - - #[test] - fn adding_mixnode_with_existing_unchanged_owner_fails() { - let mut deps = test_helpers::init_contract(); - - test_helpers::add_mixnode( - "mix-owner", - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - let info = mock_info("mix-owner", &tests::fixtures::good_mixnode_pledge()); - let (msg, _) = tests::messages::valid_bond_mixnode_msg("mix-owner"); - - let res = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!(Err(ContractError::AlreadyOwnsMixnode), res); - } - - #[test] - fn adding_mixnode_updates_layer_distribution() { - let mut deps = test_helpers::init_contract(); - - assert_eq!( - LayerDistribution::default(), - mixnet_params_storage::LAYERS.load(&deps.storage).unwrap(), - ); - - test_helpers::add_mixnode( - "mix1", - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - assert_eq!( - LayerDistribution { - layer1: 1, - ..Default::default() - }, - mixnet_params_storage::LAYERS.load(&deps.storage).unwrap() - ); + expected, + storage::LAYERS.load(deps.as_ref().storage).unwrap() + ) } #[test] fn mixnode_remove() { let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); - // try un-registering when no nodes exist yet - let info = mock_info("anyone", &[]); - let msg = ExecuteMsg::UnbondMixnode {}; - let result = execute(deps.as_mut(), mock_env(), info, msg); - - // we're told that there is no node for our address - assert_eq!( - result, - Err(ContractError::NoAssociatedMixNodeBond { - owner: Addr::unchecked("anyone") - }) - ); - - // let's add a node owned by bob - test_helpers::add_mixnode("bob", tests::fixtures::good_mixnode_pledge(), deps.as_mut()); - - // attempt to un-register fred's node, which doesn't exist - let info = mock_info("fred", &[]); - let msg = ExecuteMsg::UnbondMixnode {}; - let result = execute(deps.as_mut(), mock_env(), info, msg); - assert_eq!( - result, - Err(ContractError::NoAssociatedMixNodeBond { - owner: Addr::unchecked("fred") - }) - ); - - // bob's node is still there - let nodes = tests::queries::get_mix_nodes(&mut deps); - assert_eq!(1, nodes.len()); - assert_eq!("bob", nodes[0].owner().clone()); - - // add a node owned by fred - let fred_identity = test_helpers::add_mixnode( - "fred", - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - // let's make sure we now have 2 nodes: - assert_eq!(2, tests::queries::get_mix_nodes(&mut deps).len()); - - // un-register fred's node - let info = mock_info("fred", &[]); - let msg = ExecuteMsg::UnbondMixnode {}; - let remove_fred = execute(deps.as_mut(), mock_env(), info.clone(), msg).unwrap(); - - // we should see a funds transfer from the contract back to fred - let expected_message = BankMsg::Send { - to_address: String::from(info.sender), - amount: tests::fixtures::good_mixnode_pledge(), - }; - - // run the executor and check that we got back the correct results - let expected_response = - Response::new() - .add_message(expected_message) - .add_event(new_mixnode_unbonding_event( - &Addr::unchecked("fred"), - &None, - &tests::fixtures::good_mixnode_pledge()[0], - &fred_identity, - )); - - assert_eq!(expected_response, remove_fred); - - // only 1 node now exists, owned by bob: - let mix_node_bonds = tests::queries::get_mix_nodes(&mut deps); - - assert_eq!(1, mix_node_bonds.len()); - assert_eq!(&Addr::unchecked("bob"), mix_node_bonds[0].owner()); - } - - #[test] - fn removing_mixnode_clears_ownership() { - let mut deps = test_helpers::init_contract(); - - let info = mock_info("mix-owner", &tests::fixtures::good_mixnode_pledge()); - let (bond_msg, (identity, _)) = tests::messages::valid_bond_mixnode_msg("mix-owner"); - execute(deps.as_mut(), mock_env(), info, bond_msg.clone()).unwrap(); - - assert_eq!( - &identity, - storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("mix-owner")) - .unwrap() - .unwrap() - .1 - .identity() - ); - - let info = mock_info("mix-owner", &[]); - let msg = ExecuteMsg::UnbondMixnode {}; - - let response = execute(deps.as_mut(), mock_env(), info, msg); - - assert!(response.is_ok()); - - assert!(storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("mix-owner")) - .unwrap() - .is_none()); - - // and since it's removed, it can be reclaimed - let info = mock_info("mix-owner", &tests::fixtures::good_mixnode_pledge()); - - assert!(execute(deps.as_mut(), mock_env(), info, bond_msg).is_ok()); - assert_eq!( - &identity, - storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("mix-owner")) - .unwrap() - .unwrap() - .1 - .identity() - ); - } - - #[test] - fn updating_mixnode_config() { - let sender = "bob"; - let mut env = mock_env(); - let mut deps = test_helpers::init_contract(); + let sender = "alice"; let info = mock_info(sender, &[]); - // try updating a non existing mixnode bond - let msg = ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent: 10, - }; - let ret = execute(deps.as_mut(), env.clone(), info.clone(), msg); + // trying to remove your mixnode fails if you never had one in the first place + let res = try_remove_mixnode(deps.as_mut(), env.clone(), info.clone()); assert_eq!( - ret, - Err(ContractError::NoAssociatedMixNodeBond { + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { owner: Addr::unchecked(sender) }) ); - test_helpers::add_mixnode( + 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(), + ); + assert_eq!( + res, + Err(MixnetContractError::ProxyMismatch { + existing: "None".to_string(), + incoming: "proxy".to_string() + }) + ); + + // "normal" unbonding succeeds and unbonding event is pushed to the pending epoch events + 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) + .collect::>>() + .unwrap(); + assert_eq!(pending_events.len(), 1); + let event = pending_events.pop().unwrap(); + assert_eq!(1, event.0); + 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(), env, info); + assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id })) + } + + #[test] + fn updating_mixnode_config() { + let mut deps = test_helpers::init_contract(); + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + let sender = "alice"; + let info = mock_info(sender, &[]); + let update = MixNodeConfigUpdate { + host: "1.1.1.1:1234".to_string(), + mix_port: 1234, + verloc_port: 1235, + http_api_port: 1236, + version: "v1.2.3".to_string(), + }; + + // try updating a non existing mixnode bond + let res = try_update_mixnode_config(deps.as_mut(), info.clone(), update.clone()); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: Addr::unchecked(sender) + }) + ); + + let mix_id = test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), sender, tests::fixtures::good_mixnode_pledge(), + ); + + // attempted to remove on behalf with invalid proxy (current is `None`) + let res = try_update_mixnode_config_on_behalf( deps.as_mut(), + mock_info("proxy", &[]), + update.clone(), + sender.to_string(), ); - - // check the initial profit margin is set to the fixture value - let fixture_profit_margin = tests::fixtures::mix_node_fixture().profit_margin_percent; assert_eq!( - fixture_profit_margin, - storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("bob")) - .unwrap() - .unwrap() - .1 - .mix_node - .profit_margin_percent + res, + Err(MixnetContractError::ProxyMismatch { + existing: "None".to_string(), + incoming: "proxy".to_string() + }) ); + // "normal" update succeeds + let res = try_update_mixnode_config(deps.as_mut(), info.clone(), update.clone()); + assert!(res.is_ok()); - env.block.time = env.block.time.plus_seconds(MIN_PM_UPDATE_INTERVAL + 1); + // and the config has actually been updated + let mix = must_get_mixnode_bond_by_owner(deps.as_ref().storage, &Addr::unchecked(sender)) + .unwrap(); + assert_eq!(mix.mix_node.host, update.host); + assert_eq!(mix.mix_node.mix_port, update.mix_port); + assert_eq!(mix.mix_node.verloc_port, update.verloc_port); + assert_eq!(mix.mix_node.http_api_port, update.http_api_port); + assert_eq!(mix.mix_node.version, update.version); - // try updating with an invalid value - let profit_margin_percent = 101; - let msg = ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }; - let ret = execute(deps.as_mut(), env.clone(), info.clone(), msg); - assert_eq!( - ret, - Err(ContractError::InvalidProfitMarginPercent( - profit_margin_percent - )) - ); - - let profit_margin_percent = fixture_profit_margin + 10; - let msg = ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }; - execute(deps.as_mut(), env, info, msg).unwrap(); - assert_eq!( - profit_margin_percent, - storage::mixnodes() - .idx - .owner - .item(deps.as_ref().storage, Addr::unchecked("bob")) - .unwrap() - .unwrap() - .1 - .mix_node - .profit_margin_percent - ); + // but we cannot perform any updates whilst the mixnode is already unbonding + 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 { mix_id })) } #[test] - fn validating_mixnode_bond() { - // you must send SOME funds - let result = validate_mixnode_pledge( - Vec::new(), - INITIAL_MIXNODE_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::NoBondFound { - mix_denom: TEST_COIN_DENOM.to_string() - }) - ); - - // you must send at least 100 coins... - let mut bond = tests::fixtures::good_mixnode_pledge(); - bond[0].amount = INITIAL_MIXNODE_PLEDGE.checked_sub(Uint128::new(1)).unwrap(); - let result = validate_mixnode_pledge( - bond.clone(), - INITIAL_MIXNODE_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::InsufficientMixNodeBond { - received: Into::::into(INITIAL_MIXNODE_PLEDGE) - 1, - minimum: INITIAL_MIXNODE_PLEDGE.into(), - }) - ); - - // more than that is still fine - let mut bond = tests::fixtures::good_mixnode_pledge(); - bond[0].amount = INITIAL_MIXNODE_PLEDGE + Uint128::new(1); - let result = validate_mixnode_pledge( - bond.clone(), - INITIAL_MIXNODE_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert!(result.is_ok()); - - // it must be sent in the defined denom! - let mut bond = tests::fixtures::good_mixnode_pledge(); - bond[0].denom = "baddenom".to_string(); - let result = validate_mixnode_pledge( - bond.clone(), - INITIAL_MIXNODE_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::WrongDenom { - mix_denom: TEST_COIN_DENOM.to_string() - }) - ); - - let mut bond = tests::fixtures::good_mixnode_pledge(); - bond[0].denom = "foomp".to_string(); - let result = validate_mixnode_pledge( - bond.clone(), - INITIAL_MIXNODE_PLEDGE, - TEST_COIN_DENOM.to_string(), - ); - assert_eq!( - result, - Err(ContractError::WrongDenom { - mix_denom: TEST_COIN_DENOM.to_string() - }) - ); - } - - #[test] - fn choose_layer_mix_node() { + fn updating_mixnode_cost_params() { let mut deps = test_helpers::init_contract(); - let alice_identity = test_helpers::add_mixnode( - "alice", - tests::fixtures::good_mixnode_pledge(), + let env = mock_env(); + let mut rng = test_helpers::test_rng(); + + let sender = "alice"; + let info = mock_info(sender, &[]); + let update = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(42).unwrap(), + interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM), + }; + + // try updating a non existing mixnode bond + let res = try_update_mixnode_cost_params( deps.as_mut(), + env.clone(), + info.clone(), + update.clone(), + ); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: Addr::unchecked(sender) + }) ); - let bob_identity = - test_helpers::add_mixnode("bob", tests::fixtures::good_mixnode_pledge(), deps.as_mut()); - let bonded_mix_nodes = tests::queries::get_mix_nodes(&mut deps); - let alice_node = bonded_mix_nodes - .iter() - .find(|m| m.owner == "alice") - .cloned() - .unwrap(); - let bob_node = bonded_mix_nodes - .iter() - .find(|m| m.owner == "bob") - .cloned() - .unwrap(); + let mix_id = test_helpers::add_mixnode( + &mut rng, + deps.as_mut(), + env.clone(), + sender, + tests::fixtures::good_mixnode_pledge(), + ); - assert_eq!(alice_node.mix_node.identity_key, alice_identity); - assert_eq!(alice_node.layer, Layer::One); - assert_eq!(bob_node.mix_node.identity_key, bob_identity); - assert_eq!(bob_node.layer, mixnet_contract_common::Layer::Two); + // 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(), + ); + assert_eq!( + res, + Err(MixnetContractError::ProxyMismatch { + existing: "None".to_string(), + incoming: "proxy".to_string() + }) + ); + // "normal" update succeeds + 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 + let mut pending_events = interval_storage::PENDING_INTERVAL_EVENTS + .range(deps.as_ref().storage, None, None, Order::Ascending) + .collect::>>() + .unwrap(); + assert_eq!(pending_events.len(), 1); + let event = pending_events.pop().unwrap(); + assert_eq!(1, event.0); + assert_eq!( + PendingIntervalEventKind::ChangeMixCostParams { + mix_id, + new_costs: update.clone() + }, + event.1.kind + ); + + // execute the event + 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) + .unwrap() + .unwrap(); + 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(), 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] fn adding_mixnode_with_duplicate_sphinx_key_errors_out() { let mut deps = test_helpers::init_contract(); + let mut rng = test_helpers::test_rng(); - let keypair1 = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); - let keypair2 = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); + let keypair1 = crypto::asymmetric::identity::KeyPair::new(&mut rng); + let keypair2 = crypto::asymmetric::identity::KeyPair::new(&mut rng); let sig1 = keypair1.private_key().sign_text("alice"); let sig2 = keypair1.private_key().sign_text("bob"); @@ -942,68 +649,35 @@ pub mod tests { mix_port: 1234, verloc_port: 1234, http_api_port: 1234, - sphinx_key: crypto::asymmetric::encryption::KeyPair::new(&mut thread_rng()) + sphinx_key: crypto::asymmetric::encryption::KeyPair::new(&mut rng) .public_key() .to_base58_string(), identity_key: keypair1.public_key().to_base58_string(), version: "v0.1.2.3".to_string(), - profit_margin_percent: 10, - }; - - assert!( - try_add_mixnode(deps.as_mut(), mock_env(), info_alice, mixnode.clone(), sig1).is_ok() - ); - - mixnode.identity_key = keypair2.public_key().to_base58_string(); - - // change identity but reuse sphinx key - assert!(try_add_mixnode(deps.as_mut(), mock_env(), info_bob, mixnode, sig2).is_err()); - } - - #[test] - fn updating_pm_too_often_fails() { - use super::MIN_PM_UPDATE_INTERVAL; - - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - - let keypair1 = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); - let sig1 = keypair1.private_key().sign_text("alice"); - - let info_alice = mock_info("alice", &tests::fixtures::good_mixnode_pledge()); - - let mixnode = MixNode { - host: "1.2.3.4".to_string(), - mix_port: 1234, - verloc_port: 1234, - http_api_port: 1234, - sphinx_key: crypto::asymmetric::encryption::KeyPair::new(&mut thread_rng()) - .public_key() - .to_base58_string(), - identity_key: keypair1.public_key().to_base58_string(), - version: "v0.1.2.3".to_string(), - profit_margin_percent: 10, }; + let cost_params = fixtures::mix_node_cost_params_fixture(); assert!(try_add_mixnode( deps.as_mut(), mock_env(), - info_alice.clone(), + info_alice, mixnode.clone(), + cost_params.clone(), sig1 ) .is_ok()); - env.block.time = env.block.time.plus_seconds(MIN_PM_UPDATE_INTERVAL - 1); + mixnode.identity_key = keypair2.public_key().to_base58_string(); - // fails if too soon after bonding - assert!( - try_update_mixnode_config(deps.as_mut(), env.clone(), info_alice.clone(), 20).is_err() - ); - - env.block.time = env.block.time.plus_seconds(2); - - // succeds after some time - assert!(try_update_mixnode_config(deps.as_mut(), env, info_alice, 20).is_ok()); + // change identity but reuse sphinx key + assert!(try_add_mixnode( + deps.as_mut(), + mock_env(), + info_bob, + mixnode, + cost_params, + sig2 + ) + .is_err()); } } diff --git a/contracts/mixnet/src/queued_migrations.rs b/contracts/mixnet/src/queued_migrations.rs index a0d329fdf6..87d5c392c8 100644 --- a/contracts/mixnet/src/queued_migrations.rs +++ b/contracts/mixnet/src/queued_migrations.rs @@ -1,38 +1,2 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 - -use cosmwasm_std::{Addr, Response, Storage}; -use cw_storage_plus::Item; -use mixnet_contract_common::{ContractStateParams, MigrateMsg}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::error::ContractError; -use crate::mixnet_contract_settings::models::ContractState; -use crate::mixnet_contract_settings::storage::CONTRACT_STATE; - -pub fn migrate_config_from_env( - storage: &mut dyn Storage, - msg: &MigrateMsg, -) -> Result { - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] - pub struct OldContractState { - pub owner: Addr, - pub mix_denom: String, - pub rewarding_validator_address: Addr, - pub params: ContractStateParams, - } - const OLD_CONTRACT_STATE: Item<'_, OldContractState> = Item::new("config"); - - let old_state = OLD_CONTRACT_STATE.load(storage)?; - let new_state = ContractState { - owner: old_state.owner, - mix_denom: msg.mixnet_denom.clone(), - rewarding_validator_address: old_state.rewarding_validator_address, - params: old_state.params, - }; - - CONTRACT_STATE.save(storage, &new_state)?; - - Ok(Default::default()) -} diff --git a/contracts/mixnet/src/rewards/helpers.rs b/contracts/mixnet/src/rewards/helpers.rs index bd0fc5df7d..894c260e6f 100644 --- a/contracts/mixnet/src/rewards/helpers.rs +++ b/contracts/mixnet/src/rewards/helpers.rs @@ -1,22 +1,279 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::error::ContractError; -use cosmwasm_std::Storage; -use mixnet_contract_common::{IdentityKey, RewardingResult, RewardingStatus}; +use crate::delegations::storage as delegations_storage; +use crate::interval::storage as interval_storage; +use cosmwasm_std::{Coin, Decimal, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::mixnode::{MixNodeDetails, MixNodeRewarding}; +use mixnet_contract_common::Delegation; -pub(crate) fn update_rewarding_status( - storage: &mut dyn Storage, - interval_id: u32, - mix_identity: IdentityKey, - rewarding_result: RewardingResult, -) -> Result<(), ContractError> { - storage::REWARDING_STATUS.save( - storage, - (interval_id, mix_identity), - &RewardingStatus::Complete(rewarding_result), - )?; +/// Recomputes rewarding parameters (such as staking supply, saturation point, etc) based on +/// pending changes currently stored in `PENDING_REWARD_POOL_CHANGE`. +pub(crate) fn apply_reward_pool_changes( + store: &mut dyn Storage, +) -> Result<(), MixnetContractError> { + let mut rewarding_params = storage::REWARDING_PARAMS.load(store)?; + let pending_pool_change = storage::PENDING_REWARD_POOL_CHANGE.load(store)?; + let interval = interval_storage::current_interval(store)?; + + let reward_pool = rewarding_params.interval.reward_pool - pending_pool_change.removed + + pending_pool_change.added; + let staking_supply = rewarding_params.interval.staking_supply + + rewarding_params.interval.staking_supply_scale_factor * pending_pool_change.removed; + let epoch_reward_budget = reward_pool + / Decimal::from_atomics(interval.epochs_in_interval(), 0).unwrap() + * rewarding_params.interval.interval_pool_emission; + let stake_saturation_point = + staking_supply / Decimal::from_atomics(rewarding_params.rewarded_set_size, 0).unwrap(); + + rewarding_params.interval.reward_pool = reward_pool; + rewarding_params.interval.staking_supply = staking_supply; + rewarding_params.interval.epoch_reward_budget = epoch_reward_budget; + rewarding_params.interval.stake_saturation_point = stake_saturation_point; + + storage::PENDING_REWARD_POOL_CHANGE.save(store, &Default::default())?; + storage::REWARDING_PARAMS.save(store, &rewarding_params)?; Ok(()) } + +pub(crate) fn withdraw_operator_reward( + store: &mut dyn Storage, + mix_details: MixNodeDetails, +) -> Result { + let mix_id = mix_details.mix_id(); + let mut mix_rewarding = mix_details.rewarding_details; + let original_pledge = mix_details.bond_information.original_pledge; + let reward = mix_rewarding.withdraw_operator_reward(&original_pledge); + + // save updated rewarding info + storage::MIXNODE_REWARDING.save(store, mix_id, &mix_rewarding)?; + Ok(reward) +} + +pub(crate) fn withdraw_delegator_reward( + store: &mut dyn Storage, + delegation: Delegation, + mut mix_rewarding: MixNodeRewarding, +) -> Result { + let mix_id = delegation.mix_id; + let mut updated_delegation = delegation.clone(); + let reward = mix_rewarding.withdraw_delegator_reward(&mut updated_delegation)?; + + // save updated delegation and mix rewarding info + delegations_storage::delegations().replace( + store, + delegation.storage_key(), + Some(&updated_delegation), + Some(&delegation), + )?; + storage::MIXNODE_REWARDING.save(store, mix_id, &mix_rewarding)?; + Ok(reward) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mixnodes::helpers::get_mixnode_details_by_id; + use crate::rewards::models::RewardPoolChange; + use crate::support::tests::test_helpers::{assert_decimals, performance, TestSetup}; + use cosmwasm_std::Uint128; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; + + #[test] + fn applying_reward_pool_changes() { + let mut test = TestSetup::new(); + + let epochs_in_interval = test.current_interval().epochs_in_interval(); + let epochs_in_interval_dec = Decimal::from_atomics(epochs_in_interval, 0).unwrap(); + let start_rewarding_params = test.rewarding_params(); + + // nothing changes if pending changes are empty + apply_reward_pool_changes(test.deps_mut().storage).unwrap(); + assert_eq!(start_rewarding_params, test.rewarding_params()); + + // normal case of having distributed some rewards + let distributed_rewards = Decimal::from_atomics(100_000_000u32, 0).unwrap(); + storage::PENDING_REWARD_POOL_CHANGE + .save( + test.deps_mut().storage, + &RewardPoolChange { + removed: distributed_rewards, + added: Default::default(), + }, + ) + .unwrap(); + apply_reward_pool_changes(test.deps_mut().storage).unwrap(); + + let updated_rewarding_params = test.rewarding_params(); + + // updates reward pool + assert_eq!( + updated_rewarding_params.interval.reward_pool, + start_rewarding_params.interval.reward_pool - distributed_rewards + ); + + // updates staking supply + assert_eq!( + updated_rewarding_params.interval.staking_supply, + start_rewarding_params.interval.staking_supply + distributed_rewards + ); + + // updates epoch rewarding budget + assert_eq!( + updated_rewarding_params.interval.epoch_reward_budget, + updated_rewarding_params.interval.reward_pool / epochs_in_interval_dec + * updated_rewarding_params.interval.interval_pool_emission + ); + + // updates stake saturation point + assert_eq!( + updated_rewarding_params.interval.stake_saturation_point, + updated_rewarding_params.interval.staking_supply + / Decimal::from_atomics(updated_rewarding_params.rewarded_set_size, 0).unwrap() + ); + + // resets changes back to 0 + assert_eq!( + storage::PENDING_REWARD_POOL_CHANGE + .load(test.deps().storage) + .unwrap(), + Default::default() + ); + + // future case of having to also increase the reward pool + let added_credentials = Decimal::from_atomics(50_000_000u32, 0).unwrap(); + storage::PENDING_REWARD_POOL_CHANGE + .save( + test.deps_mut().storage, + &RewardPoolChange { + removed: distributed_rewards, + added: added_credentials, + }, + ) + .unwrap(); + apply_reward_pool_changes(test.deps_mut().storage).unwrap(); + + let updated_rewarding_params2 = test.rewarding_params(); + + // updates reward pool + assert_eq!( + updated_rewarding_params2.interval.reward_pool, + updated_rewarding_params.interval.reward_pool - distributed_rewards + added_credentials + ); + + // updates staking supply + assert_eq!( + updated_rewarding_params2.interval.staking_supply, + updated_rewarding_params.interval.staking_supply + distributed_rewards + ); + + // updates epoch rewarding budget + assert_eq!( + updated_rewarding_params2.interval.epoch_reward_budget, + updated_rewarding_params2.interval.reward_pool / epochs_in_interval_dec + * updated_rewarding_params2.interval.interval_pool_emission + ); + + // updates stake saturation point + assert_eq!( + updated_rewarding_params2.interval.stake_saturation_point, + updated_rewarding_params2.interval.staking_supply + / Decimal::from_atomics(updated_rewarding_params2.rewarded_set_size, 0).unwrap() + ); + + // resets changes back to 0 + assert_eq!( + storage::PENDING_REWARD_POOL_CHANGE + .load(test.deps().storage) + .unwrap(), + Default::default() + ); + } + + #[test] + fn withdrawing_operator_reward() { + let mut test = TestSetup::new(); + + let pledge = Uint128::new(250_000_000); + let pledge_dec = Decimal::from_atomics(250_000_000u32, 0).unwrap(); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(pledge)); + + // no rewards + let mix_details = get_mixnode_details_by_id(test.deps().storage, mix_id) + .unwrap() + .unwrap(); + let res = withdraw_operator_reward(test.deps_mut().storage, mix_details).unwrap(); + assert_eq!(res.amount, Uint128::zero()); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + let dist1 = test.reward_with_distribution(mix_id, performance(100.0)); + + test.skip_to_next_epoch_end(); + let dist2 = test.reward_with_distribution(mix_id, performance(100.0)); + + test.skip_to_next_epoch_end(); + let dist3 = test.reward_with_distribution(mix_id, performance(100.0)); + + let mix_details = get_mixnode_details_by_id(test.deps().storage, mix_id) + .unwrap() + .unwrap(); + let res = withdraw_operator_reward(test.deps_mut().storage, mix_details).unwrap(); + assert_eq!( + res.amount, + truncate_reward_amount(dist1.operator + dist2.operator + dist3.operator) + ); + let updated = test.mix_rewarding(mix_id); + assert_eq!(updated.operator, pledge_dec) + } + + #[test] + fn withdrawing_delegator_reward() { + let mut test = TestSetup::new(); + + let delegation_amount = Uint128::new(2500_000_000); + let delegation_dec = Decimal::from_atomics(2500_000_000u32, 0).unwrap(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let delegator = "delegator"; + test.add_immediate_delegation(delegator, delegation_amount, mix_id); + + // no rewards + let delegation = test.delegation(mix_id, delegator, &None); + let mix_rewarding = test.mix_rewarding(mix_id); + let res = + withdraw_delegator_reward(test.deps_mut().storage, delegation, mix_rewarding).unwrap(); + assert_eq!(res.amount, Uint128::zero()); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + let dist1 = test.reward_with_distribution(mix_id, performance(100.0)); + + test.skip_to_next_epoch_end(); + let dist2 = test.reward_with_distribution(mix_id, performance(100.0)); + + test.skip_to_next_epoch_end(); + let dist3 = test.reward_with_distribution(mix_id, performance(100.0)); + + let delegation_pre = test.delegation(mix_id, delegator, &None); + let mix_rewarding = test.mix_rewarding(mix_id); + let res = withdraw_delegator_reward( + test.deps_mut().storage, + delegation_pre.clone(), + mix_rewarding, + ) + .unwrap(); + let reward = dist1.delegates + dist2.delegates + dist3.delegates; + assert_eq!(res.amount, truncate_reward_amount(reward)); + let updated = test.mix_rewarding(mix_id); + assert_decimals(updated.delegates, delegation_dec); + + let delegation_post = test.delegation(mix_id, delegator, &None); + assert_ne!( + delegation_pre.cumulative_reward_ratio, + delegation_post.cumulative_reward_ratio + ) + } +} diff --git a/contracts/mixnet/src/rewards/mod.rs b/contracts/mixnet/src/rewards/mod.rs index f3a3c9941f..2bf969c292 100644 --- a/contracts/mixnet/src/rewards/mod.rs +++ b/contracts/mixnet/src/rewards/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod helpers; +pub mod models; pub mod queries; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/rewards/models.rs b/contracts/mixnet/src/rewards/models.rs new file mode 100644 index 0000000000..77a7342291 --- /dev/null +++ b/contracts/mixnet/src/rewards/models.rs @@ -0,0 +1,18 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::Decimal; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Default, Debug, Clone, Copy, Eq, PartialEq)] +pub(crate) struct RewardPoolChange { + /// Indicates amount that shall get moved from the reward pool to the staking supply + /// upon the current interval finishing. + pub removed: Decimal, + + // this will be used once coconut credentials are in use; + /// Indicates amount that shall get added to the reward pool and not touch the staking supply + /// upon the current interval finishing. + #[allow(unused)] + pub added: Decimal, +} diff --git a/contracts/mixnet/src/rewards/queries.rs b/contracts/mixnet/src/rewards/queries.rs index 9abcd66a0f..2b969c410b 100644 --- a/contracts/mixnet/src/rewards/queries.rs +++ b/contracts/mixnet/src/rewards/queries.rs @@ -1,226 +1,1076 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::error::ContractError; -use cosmwasm_std::Uint128; -use cosmwasm_std::{Deps, StdResult}; -use mixnet_contract_common::{IdentityKey, MixnodeRewardingStatusResponse}; +use crate::delegations::storage as delegations_storage; +use crate::interval::storage as interval_storage; +use crate::mixnodes; +use crate::mixnodes::storage as mixnodes_storage; +use cosmwasm_std::{coin, Coin, Decimal, Deps, StdResult}; +use mixnet_contract_common::mixnode::MixNodeDetails; +use mixnet_contract_common::reward_params::{NodeRewardParams, Performance, RewardingParams}; +use mixnet_contract_common::rewarding::helpers::truncate_reward; +use mixnet_contract_common::rewarding::{ + EstimatedCurrentEpochRewardResponse, PendingRewardResponse, +}; +use mixnet_contract_common::{Delegation, MixId}; -pub(crate) fn query_reward_pool(deps: Deps<'_>) -> StdResult { - storage::REWARD_POOL.load(deps.storage) +pub(crate) fn query_rewarding_params(deps: Deps<'_>) -> StdResult { + storage::REWARDING_PARAMS.load(deps.storage) } -pub(crate) fn query_circulating_supply(deps: Deps<'_>) -> StdResult { - storage::circulating_supply(deps.storage) +fn pending_operator_reward(mix_details: Option) -> PendingRewardResponse { + match mix_details { + Some(mix_details) => PendingRewardResponse { + amount_staked: Some(mix_details.original_pledge().clone()), + amount_earned: Some(mix_details.pending_operator_reward()), + amount_earned_detailed: Some(mix_details.pending_detailed_operator_reward()), + mixnode_still_fully_bonded: !mix_details.is_unbonding(), + }, + None => PendingRewardResponse::default(), + } } -pub(crate) fn query_staking_supply(deps: Deps<'_>) -> StdResult { - storage::staking_supply(deps.storage) -} - -pub(crate) fn query_rewarding_status( - deps: Deps<'_>, - mix_identity: IdentityKey, - interval_id: u32, -) -> StdResult { - let status = storage::REWARDING_STATUS.may_load(deps.storage, (interval_id, mix_identity))?; - - Ok(MixnodeRewardingStatusResponse { status }) -} - -pub fn query_operator_reward(deps: Deps, owner: String) -> Result { - let owner_address = deps.api.addr_validate(&owner)?; - let bond = match crate::mixnodes::storage::mixnodes() - .idx - .owner - .item(deps.storage, owner_address.clone())? - { - Some(record) => record.1, - None => { - // Return if bond does not exist - return Ok(Uint128::zero()); - } - }; - - super::transactions::calculate_operator_reward(deps.storage, deps.api, &owner_address, &bond) -} - -pub fn query_delegator_reward( +pub fn query_pending_operator_reward( deps: Deps, owner: String, - mix_identity: IdentityKey, +) -> StdResult { + let owner_address = deps.api.addr_validate(&owner)?; + // in order to determine operator's reward we need to know its original pledge and thus + // we have to load the entire thing + let mix_details = mixnodes::helpers::get_mixnode_details_by_owner(deps.storage, owner_address)?; + Ok(pending_operator_reward(mix_details)) +} + +pub fn query_pending_mixnode_operator_reward( + deps: Deps, + 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 + let mix_details = mixnodes::helpers::get_mixnode_details_by_id(deps.storage, mix_id)?; + Ok(pending_operator_reward(mix_details)) +} + +pub fn query_pending_delegator_reward( + deps: Deps, + owner: String, + mix_id: MixId, proxy: Option, -) -> Result { - let proxy = match proxy { - Some(proxy) => Some( - deps.api - .addr_validate(&proxy) - .map_err(|_| ContractError::InvalidAddress(proxy))?, - ), - None => None, +) -> StdResult { + let owner_address = deps.api.addr_validate(&owner)?; + let proxy = proxy + .map(|proxy| deps.api.addr_validate(&proxy)) + .transpose()?; + + let mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { + Some(mix_rewarding) => mix_rewarding, + None => return Ok(PendingRewardResponse::default()), }; - let key = mixnet_contract_common::delegation::generate_storage_key( - &deps.api.addr_validate(&owner)?, - proxy.as_ref(), + let storage_key = Delegation::generate_storage_key(mix_id, &owner_address, proxy.as_ref()); + let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { + Some(delegation) => delegation, + None => return Ok(PendingRewardResponse::default()), + }; + + let detailed_reward = mix_rewarding.determine_delegation_reward(&delegation); + let delegator_reward = mix_rewarding.pending_delegator_reward(&delegation); + + // check if the mixnode isnt in the process of unbonding (or has already unbonded) + let is_bonded = matches!(mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)?, Some(mix_bond) if !mix_bond.is_unbonding); + + Ok(PendingRewardResponse { + amount_staked: Some(delegation.amount), + amount_earned: Some(delegator_reward), + amount_earned_detailed: Some(detailed_reward), + mixnode_still_fully_bonded: is_bonded, + }) +} + +fn zero_reward( + original_stake: Coin, + current_value: Decimal, +) -> EstimatedCurrentEpochRewardResponse { + EstimatedCurrentEpochRewardResponse { + estimation: Some(coin(0, &original_stake.denom)), + detailed_estimation_amount: Some(Decimal::zero()), + current_stake_value: Some(truncate_reward(current_value, &original_stake.denom)), + current_stake_value_detailed_amount: Some(current_value), + original_stake: Some(original_stake), + } +} + +pub(crate) fn query_estimated_current_epoch_operator_reward( + deps: Deps<'_>, + mix_id: MixId, + estimated_performance: Performance, +) -> StdResult { + let mix_details = match mixnodes::helpers::get_mixnode_details_by_id(deps.storage, mix_id)? { + None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), + Some(mix_details) => mix_details, + }; + + let amount_staked = mix_details.original_pledge().clone(); + let mix_rewarding = mix_details.rewarding_details; + let current_value = mix_rewarding.operator; + + // if node is currently not in the rewarded set, the performance is 0, + // or the node has either unbonded or is in the process of unbonding, + // the calculations are trivial - the rewards are 0 + if mix_details.bond_information.is_unbonding { + return Ok(zero_reward(amount_staked, current_value)); + } + + let node_status = match interval_storage::REWARDED_SET.may_load(deps.storage, mix_id)? { + None => return Ok(zero_reward(amount_staked, current_value)), + Some(node_status) => node_status, + }; + + if estimated_performance.is_zero() { + return Ok(zero_reward(amount_staked, current_value)); + } + + let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + let interval = interval_storage::current_interval(deps.storage)?; + + let node_reward_params = NodeRewardParams::new(estimated_performance, node_status.is_active()); + let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); + let reward_distribution = mix_rewarding.determine_reward_split( + node_reward, + estimated_performance, + interval.epochs_in_interval(), ); - super::transactions::calculate_delegator_reward(deps.storage, deps.api, key, &mix_identity) + + Ok(EstimatedCurrentEpochRewardResponse { + estimation: Some(truncate_reward( + reward_distribution.operator, + &amount_staked.denom, + )), + detailed_estimation_amount: Some(reward_distribution.operator), + current_stake_value: Some(truncate_reward(current_value, &amount_staked.denom)), + current_stake_value_detailed_amount: Some(current_value), + original_stake: Some(amount_staked), + }) +} + +pub(crate) fn query_estimated_current_epoch_delegator_reward( + deps: Deps<'_>, + owner: String, + mix_id: MixId, + proxy: Option, + estimated_performance: Performance, +) -> StdResult { + let owner_address = deps.api.addr_validate(&owner)?; + let proxy = proxy + .map(|proxy| deps.api.addr_validate(&proxy)) + .transpose()?; + + let mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { + Some(mix_rewarding) => mix_rewarding, + None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), + }; + + let storage_key = Delegation::generate_storage_key(mix_id, &owner_address, proxy.as_ref()); + let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { + Some(delegation) => delegation, + None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), + }; + + let staked_dec = Decimal::from_atomics(delegation.amount.amount, 0).unwrap(); + let current_value = staked_dec + mix_rewarding.determine_delegation_reward(&delegation); + let amount_staked = delegation.amount; + + // check if the mixnode isnt in the process of unbonding (or has already unbonded) + let is_bonded = matches!(mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)?, Some(mix_bond) if !mix_bond.is_unbonding); + + if !is_bonded { + return Ok(zero_reward(amount_staked, current_value)); + } + + // if node is currently not in the rewarded set, the performance is 0, + // or the node has either unbonded or is in the process of unbonding, + // the calculations are trivial - the rewards are 0 + let node_status = match interval_storage::REWARDED_SET.may_load(deps.storage, mix_id)? { + None => return Ok(zero_reward(amount_staked, current_value)), + Some(node_status) => node_status, + }; + + if estimated_performance.is_zero() { + return Ok(zero_reward(amount_staked, current_value)); + } + + let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + let interval = interval_storage::current_interval(deps.storage)?; + + let node_reward_params = NodeRewardParams::new(estimated_performance, node_status.is_active()); + let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); + let reward_distribution = mix_rewarding.determine_reward_split( + node_reward, + estimated_performance, + interval.epochs_in_interval(), + ); + + if reward_distribution.delegates.is_zero() { + return Ok(zero_reward(amount_staked, current_value)); + } + + let reward_share = current_value / mix_rewarding.delegates * reward_distribution.delegates; + + Ok(EstimatedCurrentEpochRewardResponse { + estimation: Some(truncate_reward(reward_share, &amount_staked.denom)), + detailed_estimation_amount: Some(reward_share), + current_stake_value: Some(truncate_reward(current_value, &amount_staked.denom)), + current_stake_value_detailed_amount: Some(current_value), + original_stake: Some(amount_staked), + }) } #[cfg(test)] -pub(crate) mod tests { +mod tests { use super::*; - use crate::mixnet_contract_settings::storage as mixnet_params_storage; - use crate::support::tests; use crate::support::tests::test_helpers; - use cosmwasm_std::testing::{mock_env, mock_info}; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::Uint128; + + #[test] + fn querying_for_rewarding_params() { + // not much to test here. after contract is initialised, the query must always be valid + let deps = test_helpers::init_contract(); + let res = query_rewarding_params(deps.as_ref()); + + assert!(res.is_ok()) + } #[cfg(test)] - mod querying_for_rewarding_status { + mod querying_for_pending_operator_reward { use super::*; - use crate::constants; - use crate::delegations::transactions::try_delegate_to_mixnode; - use crate::interval::storage::{save_epoch, save_epoch_reward_params}; - use crate::rewards::transactions::try_reward_mixnode; + use crate::mixnodes::transactions::try_remove_mixnode; use crate::support::tests::fixtures::TEST_COIN_DENOM; - use cosmwasm_std::{coin, Addr}; - use mixnet_contract_common::{ - Interval, RewardingResult, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT, - }; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{coin, Decimal}; + use mixnet_contract_common::rewarding::helpers::truncate_reward; #[test] - fn returns_empty_status_for_unrewarded_nodes() { - let mut deps = test_helpers::init_contract(); - let env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; + fn for_non_existent_node() { + let test = TestSetup::new(); + let owner = "mix-owner"; - let node_identity = test_helpers::add_mixnode( - "bob", - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); + let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); + let res2 = query_pending_mixnode_operator_reward(test.deps(), 42).unwrap(); + assert_eq!(res, res2); - assert!( - query_rewarding_status(deps.as_ref(), node_identity.clone(), 1) - .unwrap() - .status - .is_none() - ); - - // node was rewarded but for different interval - let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_reward_mixnode( - deps.as_mut(), - env, - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ) - .unwrap(); - - assert!(query_rewarding_status(deps.as_ref(), node_identity, 2) - .unwrap() - .status - .is_none()); + assert!(res.amount_earned.is_none()); + assert!(res.amount_earned_detailed.is_none()); + assert!(res.amount_staked.is_none()); + assert!(!res.mixnode_still_fully_bonded); } #[test] + fn for_unrewarded_node() { + let mut test = TestSetup::new(); + let owner = "mix-owner"; - fn returns_complete_status_for_fully_rewarded_node() { - // with single page - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); - let node_owner: Addr = Addr::unchecked("bob"); - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); + let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); + let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap(); + assert_eq!(res, res2); - env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING; + let expected_actual = coin(0, TEST_COIN_DENOM); - let info = mock_info(rewarding_validator_address.as_ref(), &[]); + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), Decimal::zero()); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(res.mixnode_still_fully_bonded); + } - let epoch = Interval::init_epoch(env.clone()); - save_epoch(&mut deps.storage, &epoch).unwrap(); - save_epoch_reward_params(epoch.id(), &mut deps.storage).unwrap(); + #[test] + fn for_node_with_pending_reward() { + let mut test = TestSetup::new(); + let owner = "mix-owner"; + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); - try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + let mut total_earned = Decimal::zero(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.operator; + + let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); + let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap(); + assert_eq!(res, res2); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(res.mixnode_still_fully_bonded); + + // reward it few more times for good measure + for _ in 0..10 { + test.skip_to_next_epoch_end(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.operator; + + let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); + let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap(); + assert_eq!(res, res2); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(res.mixnode_still_fully_bonded); + } + } + + #[test] + fn for_node_that_is_unbonding() { + let mut test = TestSetup::new(); + let owner = "mix-owner"; + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + let mut total_earned = Decimal::zero(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.operator; + + let sender = mock_info(owner, &[]); + 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(); + assert_eq!(res, res2); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(!res.mixnode_still_fully_bonded); + } + + #[test] + fn for_node_that_has_unbonded() { + let mut test = TestSetup::new(); + let owner = "mix-owner"; + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let sender = mock_info(owner, &[]); + 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(); + let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap(); + assert_eq!(res, res2); + + // if you unbonded, you don't have any pending stuff as you've already claimed it + // by unbonding + assert!(res.amount_earned.is_none()); + assert!(res.amount_earned_detailed.is_none()); + assert!(res.amount_staked.is_none()); + assert!(!res.mixnode_still_fully_bonded); + } + } + + #[cfg(test)] + mod querying_for_pending_delegator_reward { + use super::*; + use crate::mixnodes::transactions::try_remove_mixnode; + use crate::rewards::transactions::try_withdraw_delegator_reward; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::get_bank_send_msg; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{coin, Decimal}; + use mixnet_contract_common::rewarding::helpers::truncate_reward; + + #[test] + fn for_non_existent_delegation() { + let test = TestSetup::new(); + let delegator = "delegator"; + + let res = + query_pending_delegator_reward(test.deps(), delegator.into(), 42, None).unwrap(); + + assert!(res.amount_earned.is_none()); + assert!(res.amount_earned_detailed.is_none()); + assert!(res.amount_staked.is_none()); + assert!(!res.mixnode_still_fully_bonded); + } + + #[test] + fn for_unrewarded_delegator() { + let mut test = TestSetup::new(); + let owner = "delegator"; + + let initial_stake = Uint128::new(100_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + test.add_immediate_delegation(owner, initial_stake, mix_id); + + let res = + query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None).unwrap(); + + let expected_actual = coin(0, TEST_COIN_DENOM); + + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), Decimal::zero()); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(res.mixnode_still_fully_bonded); + } + + #[test] + fn for_delegator_with_pending_reward() { + let mut test = TestSetup::new(); + let owner = "delegator"; + + let initial_stake = Uint128::new(100_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + let mut total_earned = Decimal::zero(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.delegates; + + let res = + query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None).unwrap(); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(res.mixnode_still_fully_bonded); + + // reward it few more times for good measure + for _ in 0..10 { + test.skip_to_next_epoch_end(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.delegates; + + let res = query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None) + .unwrap(); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(res.mixnode_still_fully_bonded); + } + } + + #[test] + fn for_node_that_is_unbonding() { + let mut test = TestSetup::new(); + let owner = "delegator"; + + let initial_stake = Uint128::new(100_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + let mut total_earned = Decimal::zero(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.delegates; + + let sender = mock_info("mix-owner", &[]); + 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(); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(!res.mixnode_still_fully_bonded); + } + + #[test] + fn for_node_that_has_unbonded() { + let mut test = TestSetup::new(); + let owner = "delegator"; + + let initial_stake = Uint128::new(100_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + let mut total_earned = Decimal::zero(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + total_earned += dist.delegates; + + let sender = mock_info("mix-owner", &[]); + let env = test.env(); + try_remove_mixnode(test.deps_mut(), env, sender).unwrap(); + test.execute_all_pending_events(); + + let res = + query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None).unwrap(); + + let expected_actual = truncate_reward(total_earned, TEST_COIN_DENOM); + assert_eq!(res.amount_earned.unwrap(), expected_actual); + assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); + assert_eq!(res.amount_staked.unwrap().amount, initial_stake); + assert!(!res.mixnode_still_fully_bonded); + } + + #[test] + fn always_equals_to_what_can_be_withdrawn() { + // we've already tested withdraw reward to calculate values correctly + // even if there are multiple delegators joined at different times when the reward has to be split + let mut test = TestSetup::new(); + let del1 = "delegator1"; + let del2 = "delegator2"; + let del3 = "delegator3"; + let del4 = "delegator4"; + + let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + test.add_immediate_delegation(del1, 123_456_789u32, mix_id); + test.add_immediate_delegation(del2, 150_000_000u32, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + test.add_immediate_delegation(del3, 500_000_000u32, mix_id); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(85.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(5.0)); + + test.add_immediate_delegation(del4, 5_000_000u32, mix_id); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + test.add_immediate_delegation(del2, 250_000_000u32, mix_id); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(98.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + test.remove_immediate_delegation(del3, mix_id); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(98.0)); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let pending1 = + query_pending_delegator_reward(test.deps(), del1.into(), mix_id, None).unwrap(); + let pending2 = + query_pending_delegator_reward(test.deps(), del2.into(), mix_id, None).unwrap(); + let pending3 = + query_pending_delegator_reward(test.deps(), del3.into(), mix_id, None).unwrap(); + let pending4 = + query_pending_delegator_reward(test.deps(), del4.into(), mix_id, None).unwrap(); + + let actual1_res = + try_withdraw_delegator_reward(test.deps_mut(), mock_info(del1, &[]), mix_id) + .unwrap(); + let (_, actual1) = get_bank_send_msg(&actual1_res).unwrap(); + assert_eq!(pending1.amount_earned.unwrap(), actual1[0]); + + let actual2_res = + try_withdraw_delegator_reward(test.deps_mut(), mock_info(del2, &[]), mix_id) + .unwrap(); + let (_, actual2) = get_bank_send_msg(&actual2_res).unwrap(); + assert_eq!(pending2.amount_earned.unwrap(), actual2[0]); + + // the amount is none because we have removed our delegation! + assert!(pending3.amount_earned.is_none()); + + let actual4_res = + try_withdraw_delegator_reward(test.deps_mut(), mock_info(del4, &[]), mix_id) + .unwrap(); + let (_, actual4) = get_bank_send_msg(&actual4_res).unwrap(); + assert_eq!(pending4.amount_earned.unwrap(), actual4[0]); + } + } + + #[cfg(test)] + mod querying_for_estimated_current_epoch_operator_reward { + use super::*; + use crate::mixnodes::transactions::try_remove_mixnode; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::testing::mock_info; + + fn expected_current_operator( + test: &TestSetup, + mix_id: MixId, + initial_stake: Uint128, + ) -> EstimatedCurrentEpochRewardResponse { + let mix_rewarding = test.mix_rewarding(mix_id); + EstimatedCurrentEpochRewardResponse { + estimation: Some(coin(0, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(Decimal::zero()), + current_stake_value: Some(truncate_reward(mix_rewarding.operator, TEST_COIN_DENOM)), + current_stake_value_detailed_amount: Some(mix_rewarding.operator), + original_stake: Some(coin(initial_stake.u128(), TEST_COIN_DENOM)), + } + } + + #[test] + fn when_node_doesnt_exist() { + let test = TestSetup::new(); + let res = query_estimated_current_epoch_operator_reward( + test.deps(), + 42, + test_helpers::performance(100.0), + ) + .unwrap(); + assert_eq!(res, EstimatedCurrentEpochRewardResponse::empty_response()) + } + + #[test] + fn when_node_is_unbonding() { + let mut test = TestSetup::new(); + let initial_stake = Uint128::new(1_000_000_000_000); + let owner = "mix-owner"; + let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let sender = mock_info(owner, &[]); + let env = test.env(); + try_remove_mixnode(test.deps_mut(), env, sender).unwrap(); + + let res = query_estimated_current_epoch_operator_reward( + test.deps(), + mix_id, + test_helpers::performance(100.0), ) .unwrap(); - let res = query_rewarding_status(deps.as_ref(), node_identity, 0).unwrap(); - assert!(matches!(res.status, Some(RewardingStatus::Complete(..)))); + let expected = expected_current_operator(&test, mix_id, initial_stake); + assert_eq!(res, expected) + } - match res.status.unwrap() { - RewardingStatus::Complete(result) => { - assert_ne!(RewardingResult::default().node_reward, result.node_reward); - } - _ => unreachable!(), - } + #[test] + fn when_node_has_already_unbonded() { + let mut test = TestSetup::new(); + let initial_stake = Uint128::new(1_000_000_000_000); + let owner = "mix-owner"; + let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); - // with multiple pages - let node_owner: Addr = Addr::unchecked("alice"); - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); - for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT + 123 { - try_delegate_to_mixnode( - deps.as_mut(), - env.clone(), - mock_info( - &*format!("delegator{:04}", i), - &[coin(200_000000, TEST_COIN_DENOM)], - ), - node_identity.clone(), - ) - .unwrap(); - } + let sender = mock_info(owner, &[]); + let env = test.env(); + try_remove_mixnode(test.deps_mut(), env, sender).unwrap(); + test.execute_all_pending_events(); - env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING; - test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); + let res = query_estimated_current_epoch_operator_reward( + test.deps(), + mix_id, + test_helpers::performance(100.0), + ) + .unwrap(); + assert_eq!(res, EstimatedCurrentEpochRewardResponse::empty_response()) + } - let info = mock_info(rewarding_validator_address.as_ref(), &[]); + #[test] + fn when_node_is_not_in_the_rewarded_set() { + let mut test = TestSetup::new(); + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); - try_reward_mixnode( - deps.as_mut(), - env, - info, - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.update_rewarded_set(vec![]); + + let res = query_estimated_current_epoch_operator_reward( + test.deps(), + mix_id, + test_helpers::performance(100.0), ) .unwrap(); - // rewards all pending - // try_reward_next_mixnode_delegators(deps.as_mut(), info, node_identity.to_string(), 1) - // .unwrap(); + let expected = expected_current_operator(&test, mix_id, initial_stake); + assert_eq!(res, expected) + } - let res = query_rewarding_status(deps.as_ref(), node_identity, 1).unwrap(); - assert!(matches!(res.status, Some(RewardingStatus::Complete(..)))); + #[test] + fn when_estimated_performance_is_zero() { + let mut test = TestSetup::new(); + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); - match res.status.unwrap() { - RewardingStatus::Complete(result) => { - assert_ne!(RewardingResult::default().node_reward, result.node_reward); - } - _ => unreachable!(), + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let res = query_estimated_current_epoch_operator_reward( + test.deps(), + mix_id, + test_helpers::performance(0.0), + ) + .unwrap(); + + let expected = expected_current_operator(&test, mix_id, initial_stake); + assert_eq!(res, expected) + } + + #[test] + fn with_correct_parameters_matches_actual_distribution() { + let mut test = TestSetup::new(); + let initial_stake = Uint128::new(1_000_000_000_000); + let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let mix_rewarding = test.mix_rewarding(mix_id); + let res = query_estimated_current_epoch_operator_reward( + test.deps(), + mix_id, + test_helpers::performance(95.0), + ) + .unwrap(); + + test.skip_to_next_epoch_end(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(95.0)); + + let expected = EstimatedCurrentEpochRewardResponse { + original_stake: Some(coin(initial_stake.u128(), TEST_COIN_DENOM)), + current_stake_value: Some(truncate_reward(mix_rewarding.operator, TEST_COIN_DENOM)), + current_stake_value_detailed_amount: Some(mix_rewarding.operator), + estimation: Some(truncate_reward(dist.operator, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(dist.operator), + }; + + assert_eq!(res, expected) + } + } + + #[cfg(test)] + mod querying_for_estimated_current_epoch_delegator_reward { + use super::*; + use crate::mixnodes::transactions::try_remove_mixnode; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use cosmwasm_std::testing::mock_info; + + fn expected_current_delegator( + test: &TestSetup, + mix_id: MixId, + owner: &str, + ) -> EstimatedCurrentEpochRewardResponse { + let mix_rewarding = test.mix_rewarding(mix_id); + let delegation = test.delegation(mix_id, owner, &None); + + let staked_dec = Decimal::from_atomics(delegation.amount.amount, 0).unwrap(); + let current_value = staked_dec + mix_rewarding.determine_delegation_reward(&delegation); + let amount_staked = delegation.amount; + + EstimatedCurrentEpochRewardResponse { + estimation: Some(coin(0, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(Decimal::zero()), + current_stake_value: Some(truncate_reward(current_value, TEST_COIN_DENOM)), + current_stake_value_detailed_amount: Some(current_value), + original_stake: Some(amount_staked), } } + + #[test] + fn when_delegation_doesnt_exist() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let res = query_estimated_current_epoch_delegator_reward( + test.deps(), + "foomper".into(), + mix_id, + None, + test_helpers::performance(100.0), + ) + .unwrap(); + + assert_eq!(res, EstimatedCurrentEpochRewardResponse::empty_response()) + } + + #[test] + fn when_node_is_unbonding() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let initial_stake = Uint128::new(1_000_000_000); + let owner = "delegator"; + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let sender = mock_info("mix-owner", &[]); + let env = test.env(); + try_remove_mixnode(test.deps_mut(), env, sender).unwrap(); + + let res = query_estimated_current_epoch_delegator_reward( + test.deps(), + owner.into(), + mix_id, + None, + test_helpers::performance(100.0), + ) + .unwrap(); + + let expected = expected_current_delegator(&test, mix_id, owner); + assert_eq!(res, expected) + } + + #[test] + fn when_node_has_already_unbonded() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let initial_stake = Uint128::new(1_000_000_000); + let owner = "delegator"; + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let sender = mock_info("mix-owner", &[]); + 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( + test.deps(), + owner.into(), + mix_id, + None, + test_helpers::performance(100.0), + ) + .unwrap(); + + let expected = expected_current_delegator(&test, mix_id, owner); + assert_eq!(res, expected) + } + + #[test] + fn when_node_is_not_in_the_rewarded_set() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let initial_stake = Uint128::new(1_000_000_000); + let owner = "delegator"; + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.update_rewarded_set(vec![]); + + let res = query_estimated_current_epoch_delegator_reward( + test.deps(), + owner.into(), + mix_id, + None, + test_helpers::performance(100.0), + ) + .unwrap(); + + let expected = expected_current_delegator(&test, mix_id, owner); + assert_eq!(res, expected) + } + + #[test] + fn when_estimated_performance_is_zero() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let initial_stake = Uint128::new(1_000_000_000); + let owner = "delegator"; + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let res = query_estimated_current_epoch_delegator_reward( + test.deps(), + owner.into(), + mix_id, + None, + test_helpers::performance(0.0), + ) + .unwrap(); + + let expected = expected_current_delegator(&test, mix_id, owner); + assert_eq!(res, expected) + } + + #[test] + fn with_correct_parameters_matches_actual_distribution_for_single_delegator() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let initial_stake = Uint128::new(1_000_000_000); + let owner = "delegator"; + test.add_immediate_delegation(owner, initial_stake, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + + let mix_rewarding = test.mix_rewarding(mix_id); + let res = query_estimated_current_epoch_delegator_reward( + test.deps(), + owner.into(), + mix_id, + None, + test_helpers::performance(95.0), + ) + .unwrap(); + + test.skip_to_next_epoch_end(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(95.0)); + + let expected = EstimatedCurrentEpochRewardResponse { + original_stake: Some(coin(initial_stake.u128(), TEST_COIN_DENOM)), + current_stake_value: Some(truncate_reward( + mix_rewarding.delegates, + TEST_COIN_DENOM, + )), + current_stake_value_detailed_amount: Some(mix_rewarding.delegates), + estimation: Some(truncate_reward(dist.delegates, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(dist.delegates), + }; + + assert_eq!(res, expected) + } + + #[test] + fn with_correct_parameters_matches_actual_distribution_for_three_delegators() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + let initial_stake1 = Uint128::new(1_000_000_000); + let initial_stake2 = Uint128::new(45_000_000_000); + let initial_stake3 = Uint128::new(8_500_000_000); + + let initial_stake1_dec = Decimal::from_atomics(initial_stake1, 0).unwrap(); + let initial_stake2_dec = Decimal::from_atomics(initial_stake2, 0).unwrap(); + let initial_stake3_dec = Decimal::from_atomics(initial_stake3, 0).unwrap(); + let del1 = "delegator1"; + let del2 = "delegator2"; + let del3 = "delegator3"; + test.add_immediate_delegation(del1, initial_stake1, mix_id); + test.add_immediate_delegation(del2, initial_stake2, mix_id); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(95.0)); + + test.add_immediate_delegation(del3, initial_stake3, mix_id); + test.skip_to_next_epoch_end(); + test.reward_with_distribution(mix_id, test_helpers::performance(85.0)); + + let mix_rewarding = test.mix_rewarding(mix_id); + + let ress = [del1, del2, del3] + .iter() + .map(|owner| { + query_estimated_current_epoch_delegator_reward( + test.deps(), + owner.to_string(), + mix_id, + None, + test_helpers::performance(95.0), + ) + .unwrap() + }) + .collect::>(); + + // as verified by other tests those values are correct + let est1 = query_pending_delegator_reward(test.deps(), del1.into(), mix_id, None) + .unwrap() + .amount_earned_detailed + .unwrap(); + let est2 = query_pending_delegator_reward(test.deps(), del2.into(), mix_id, None) + .unwrap() + .amount_earned_detailed + .unwrap(); + let est3 = query_pending_delegator_reward(test.deps(), del3.into(), mix_id, None) + .unwrap() + .amount_earned_detailed + .unwrap(); + + let cur1 = initial_stake1_dec + est1; + let cur2 = initial_stake2_dec + est2; + let cur3 = initial_stake3_dec + est3; + + test.skip_to_next_epoch_end(); + let dist = test.reward_with_distribution(mix_id, test_helpers::performance(95.0)); + + let share1 = cur1 / mix_rewarding.delegates * dist.delegates; + let share2 = cur2 / mix_rewarding.delegates * dist.delegates; + let share3 = cur3 / mix_rewarding.delegates * dist.delegates; + + let expected1 = EstimatedCurrentEpochRewardResponse { + original_stake: Some(coin(initial_stake1.u128(), TEST_COIN_DENOM)), + current_stake_value: Some(truncate_reward(cur1, TEST_COIN_DENOM)), + current_stake_value_detailed_amount: Some(cur1), + estimation: Some(truncate_reward(share1, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(share1), + }; + assert_eq!(ress[0], expected1); + + let expected2 = EstimatedCurrentEpochRewardResponse { + original_stake: Some(coin(initial_stake2.u128(), TEST_COIN_DENOM)), + current_stake_value: Some(truncate_reward(cur2, TEST_COIN_DENOM)), + current_stake_value_detailed_amount: Some(cur2), + estimation: Some(truncate_reward(share2, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(share2), + }; + assert_eq!(ress[1], expected2); + + let expected3 = EstimatedCurrentEpochRewardResponse { + original_stake: Some(coin(initial_stake3.u128(), TEST_COIN_DENOM)), + current_stake_value: Some(truncate_reward(cur3, TEST_COIN_DENOM)), + current_stake_value_detailed_amount: Some(cur3), + estimation: Some(truncate_reward(share3, TEST_COIN_DENOM)), + detailed_estimation_amount: Some(share3), + }; + assert_eq!(ress[2], expected3); + } } } diff --git a/contracts/mixnet/src/rewards/storage.rs b/contracts/mixnet/src/rewards/storage.rs index b62e89ba0d..15ccd8e478 100644 --- a/contracts/mixnet/src/rewards/storage.rs +++ b/contracts/mixnet/src/rewards/storage.rs @@ -1,81 +1,39 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::ContractError; -use crate::mixnet_contract_settings::storage as settings_storage; -use config::defaults::TOTAL_SUPPLY; -use cosmwasm_std::{StdResult, Storage, Uint128}; +use crate::constants::{ + MIXNODES_REWARDING_PK_NAMESPACE, PENDING_REWARD_POOL_KEY, REWARDING_PARAMS_KEY, +}; +use crate::rewards::models::RewardPoolChange; +use cosmwasm_std::{Decimal, StdResult, Storage}; use cw_storage_plus::{Item, Map}; -use mixnet_contract_common::{reward_params::EpochRewardParams, IdentityKey, RewardingStatus}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::mixnode::MixNodeRewarding; +use mixnet_contract_common::reward_params::RewardingParams; +use mixnet_contract_common::MixId; -type BlockHeight = u64; -type Address = String; +// 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(crate) const REWARD_POOL: Item<'_, Uint128> = Item::new("pool"); -// TODO: Do we need a migration for this? -pub(crate) const REWARDING_STATUS: Map<'_, (u32, IdentityKey), RewardingStatus> = Map::new("rm"); +pub const MIXNODE_REWARDING: Map = + Map::new(MIXNODES_REWARDING_PK_NAMESPACE); -// This has to be a byte vector due to proxy delegastions and rewarding -pub(crate) const DELEGATOR_REWARD_CLAIMED_HEIGHT: Map<'_, (Vec, IdentityKey), BlockHeight> = - Map::new("drc"); -pub(crate) const OPERATOR_REWARD_CLAIMED_HEIGHT: Map<'_, (Address, IdentityKey), BlockHeight> = - Map::new("orc"); - -type EpochId = u32; - -pub(crate) const EPOCH_REWARD_PARAMS: Map<'_, EpochId, EpochRewardParams> = Map::new("epr"); - -pub fn epoch_reward_params_for_id( - storage: &dyn Storage, - id: EpochId, -) -> StdResult { - EPOCH_REWARD_PARAMS.load(storage, id) -} - -#[allow(dead_code)] -pub fn incr_reward_pool( - amount: Uint128, +pub fn reward_accounting( storage: &mut dyn Storage, -) -> Result { - REWARD_POOL.update(storage, |mut current_pool| { - current_pool += amount; - Ok(current_pool) - }) + amount: Decimal, +) -> Result<(), MixnetContractError> { + let mut pending_changes = PENDING_REWARD_POOL_CHANGE.load(storage)?; + pending_changes.removed += amount; + + Ok(PENDING_REWARD_POOL_CHANGE.save(storage, &pending_changes)?) } -pub fn reward_accounting(storage: &mut dyn Storage, amount: Uint128) -> Result<(), ContractError> { - decr_reward_pool(storage, amount)?; - incr_staking_supply(storage, amount)?; - Ok(()) -} - -fn incr_staking_supply(storage: &mut dyn Storage, amount: Uint128) -> Result<(), ContractError> { - let mut contract_state = - crate::mixnet_contract_settings::storage::CONTRACT_STATE.load(storage)?; - contract_state.params.staking_supply += amount; - crate::mixnet_contract_settings::storage::CONTRACT_STATE.save(storage, &contract_state)?; - Ok(()) -} - -fn decr_reward_pool(storage: &mut dyn Storage, amount: Uint128) -> Result { - REWARD_POOL.update(storage, |current_pool| { - let stake = current_pool - .checked_sub(amount) - .map_err(|_| ContractError::OutOfFunds { - to_remove: amount.u128(), - reward_pool: current_pool.u128(), - })?; - - Ok(stake) - }) -} - -pub fn circulating_supply(storage: &dyn Storage) -> StdResult { - let reward_pool = REWARD_POOL.load(storage)?; - Ok(Uint128::new(TOTAL_SUPPLY).saturating_sub(reward_pool)) -} - -pub fn staking_supply(storage: &dyn Storage) -> StdResult { - let state = settings_storage::CONTRACT_STATE.load(storage)?; - Ok(state.params.staking_supply) +pub(crate) fn initialise_storage( + storage: &mut dyn Storage, + reward_params: RewardingParams, +) -> StdResult<()> { + REWARDING_PARAMS.save(storage, &reward_params)?; + PENDING_REWARD_POOL_CHANGE.save(storage, &RewardPoolChange::default()) } diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index e0f59de6e9..342522ac9e 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -1,1884 +1,1892 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::storage::{ - self, epoch_reward_params_for_id, DELEGATOR_REWARD_CLAIMED_HEIGHT, - OPERATOR_REWARD_CLAIMED_HEIGHT, -}; -use crate::constants; -use crate::contract::debug_with_visibility; +use super::storage; use crate::delegations::storage as delegations_storage; -use crate::delegations::transactions::_try_delegate_to_mixnode; -use crate::error::ContractError; -use crate::mixnet_contract_settings::storage::mix_denom; -use crate::mixnodes::storage::mixnodes; -use crate::mixnodes::storage::{self as mixnodes_storage, StoredMixnodeBond}; -use crate::mixnodes::transactions::is_blacklisted; +use crate::interval::storage as interval_storage; +use crate::interval::storage::{push_new_epoch_event, push_new_interval_event}; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::helpers::get_mixnode_details_by_owner; +use crate::mixnodes::storage as mixnodes_storage; use crate::rewards::helpers; -use crate::support::helpers::{is_authorized, operator_cost_at_epoch}; -use cosmwasm_std::{ - coins, wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, MessageInfo, Order, Response, - Storage, Uint128, +use crate::support::helpers::{ + ensure_bonded, ensure_is_authorized, ensure_is_owner, ensure_proxy_match, + send_to_proxy_or_owner, }; -use cw_storage_plus::Bound; +use cosmwasm_std::{wasm_execute, Addr, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_claim_delegator_reward_event, new_claim_operator_reward_event, - new_compound_delegator_reward_event, new_compound_operator_reward_event, - new_mix_operator_rewarding_event, new_not_found_mix_operator_rewarding_event, - new_too_fresh_bond_mix_operator_rewarding_event, new_zero_uptime_mix_operator_rewarding_event, + new_active_set_update_event, new_mix_rewarding_event, + new_not_found_mix_operator_rewarding_event, new_pending_active_set_update_event, + new_pending_rewarding_params_update_event, new_rewarding_params_update_event, + new_withdraw_delegator_reward_event, new_withdraw_operator_reward_event, + new_zero_uptime_mix_operator_rewarding_event, }; -use mixnet_contract_common::mixnode::StoredNodeRewardResult; -use mixnet_contract_common::reward_params::{NodeEpochRewards, NodeRewardParams, RewardParams}; -use mixnet_contract_common::{Delegation, IdentityKey, RewardingStatus}; - -use mixnet_contract_common::RewardingResult; +use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; +use mixnet_contract_common::reward_params::{ + IntervalRewardingParamsUpdate, NodeRewardParams, Performance, +}; +use mixnet_contract_common::{Delegation, MixId}; use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; -use vesting_contract_common::one_ucoin; - -// All four of the below methods need to do the following things: -// 1. Calculate currently available rewards -// 2. Send the rewards back to whoever claimed them -// 3. Set the LAST_CLAIMED_HEIGHT to the current height -pub fn try_claim_operator_reward( - deps: DepsMut<'_>, - env: &Env, - info: &MessageInfo, -) -> Result { - _try_claim_operator_reward(deps.storage, deps.api, env, info.sender.as_ref(), None) -} - -pub fn try_claim_operator_reward_on_behalf( - deps: DepsMut<'_>, - env: &Env, - info: &MessageInfo, - owner: String, -) -> Result { - _try_claim_operator_reward( - deps.storage, - deps.api, - env, - &owner, - Some(info.sender.clone()), - ) -} - -fn _try_claim_operator_reward( - storage: &mut dyn Storage, - api: &dyn Api, - env: &Env, - owner: &str, - proxy: Option, -) -> Result { - let owner = api.addr_validate(owner)?; - let mix_denom = mix_denom(storage)?; - - let bond = match crate::mixnodes::storage::mixnodes() - .idx - .owner - .item(storage, owner.clone())? - { - Some(record) => record.1, - None => return Err(ContractError::NoAssociatedMixNodeBond { owner }), - }; - - if proxy != bond.proxy { - return Err(ContractError::ProxyMismatch { - existing: bond - .proxy - .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - incoming: proxy.map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), - }); - } - - let reward = calculate_operator_reward(storage, api, &owner, &bond)?; - - OPERATOR_REWARD_CLAIMED_HEIGHT.save( - storage, - (owner.to_string(), bond.identity().to_string()), - &env.block.height, - )?; - - if reward.is_zero() { - return Err(ContractError::NoRewardsToClaim { - identity: bond.identity().to_string(), - address: owner.to_string(), - }); - } - - let return_tokens = BankMsg::Send { - to_address: proxy.as_ref().unwrap_or(&owner).to_string(), - amount: coins(reward.u128(), mix_denom.clone()), - }; - - let mut response = Response::default() - .add_message(return_tokens) - .add_event(new_claim_operator_reward_event(&owner, reward)); - - if let Some(proxy) = proxy { - let msg = Some(VestingContractExecuteMsg::TrackReward { - address: owner.to_string(), - amount: Coin::new(reward.u128(), mix_denom.clone()), - }); - - let wasm_msg = wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom)])?; - response = response.add_message(wasm_msg); - } - - Ok(response) -} - -pub fn _try_claim_delegator_reward( - storage: &mut dyn Storage, - api: &dyn Api, - env: &Env, - owner: &str, - mix_identity: &str, - proxy: Option, -) -> Result { - let owner = api.addr_validate(owner)?; - let mix_denom = mix_denom(storage)?; - - let key = mixnet_contract_common::delegation::generate_storage_key(&owner, proxy.as_ref()); - let reward = calculate_delegator_reward(storage, api, key.clone(), mix_identity)?; - - DELEGATOR_REWARD_CLAIMED_HEIGHT.save( - storage, - (key, mix_identity.to_string()), - &env.block.height, - )?; - - if reward.is_zero() { - return Err(ContractError::NoRewardsToClaim { - identity: mix_identity.to_string(), - address: owner.to_string(), - }); - } - - let return_tokens = BankMsg::Send { - to_address: proxy.as_ref().unwrap_or(&owner).to_string(), - amount: coins(reward.u128(), mix_denom.clone()), - }; - - let mut response = - Response::default() - .add_message(return_tokens) - .add_event(new_claim_delegator_reward_event( - &owner, - &proxy, - reward, - mix_identity, - )); - - if let Some(proxy) = proxy { - let msg = Some(VestingContractExecuteMsg::TrackReward { - address: owner.to_string(), - amount: Coin::new(reward.u128(), mix_denom.clone()), - }); - - let wasm_msg = wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom)])?; - response = response.add_message(wasm_msg); - } - - Ok(response) -} - -pub fn try_claim_delegator_reward_on_behalf( - deps: DepsMut<'_>, - env: &Env, - info: &MessageInfo, - owner: String, - mix_identity: &str, -) -> Result { - _try_claim_delegator_reward( - deps.storage, - deps.api, - env, - &owner, - mix_identity, - Some(info.sender.clone()), - ) -} - -pub fn try_claim_delegator_reward( - deps: DepsMut<'_>, - env: &Env, - info: &MessageInfo, - mix_identity: &str, -) -> Result { - _try_claim_delegator_reward( - deps.storage, - deps.api, - env, - info.sender.as_ref(), - mix_identity, - None, - ) -} - -pub fn try_compound_operator_reward_on_behalf( - deps: DepsMut, - env: Env, - info: MessageInfo, - owner: String, -) -> Result { - let proxy = deps.api.addr_validate(info.sender.as_str())?; - let owner = deps.api.addr_validate(&owner)?; - - let reward = _try_compound_operator_reward( - deps.storage, - deps.api, - env.block.height, - &owner, - Some(proxy), - )?; - - Ok(Response::new().add_event(new_compound_operator_reward_event(&owner, reward))) -} - -pub fn try_compound_operator_reward( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, -) -> Result { - let owner = deps.api.addr_validate(info.sender.as_str())?; - let reward = - _try_compound_operator_reward(deps.storage, deps.api, env.block.height, &owner, None)?; - - Ok(Response::new().add_event(new_compound_operator_reward_event(&owner, reward))) -} - -pub fn _try_compound_operator_reward( - storage: &mut dyn Storage, - api: &dyn Api, - block_height: u64, - owner: &Addr, - proxy: Option, -) -> Result { - let bond = match mixnodes().idx.owner.item(storage, owner.to_owned())? { - Some(record) => record.1, - None => { - // Return if bond does not exist - return Ok(Uint128::zero()); - } - }; - - if bond.proxy != proxy { - // Return if proxy is not the same as the bond proxy - return Ok(Uint128::zero()); - } - - let mut updated_bond = bond.clone(); - let reward = calculate_operator_reward(storage, api, owner, &bond)?; - updated_bond.accumulated_rewards = - Some(updated_bond.accumulated_rewards().saturating_sub(reward)); - updated_bond.pledge_amount.amount += reward; - mixnodes().replace( - storage, - bond.identity(), - Some(&updated_bond), - Some(&bond), - block_height, - )?; - - OPERATOR_REWARD_CLAIMED_HEIGHT.save( - storage, - (owner.to_string(), bond.identity().to_string()), - &block_height, - )?; - - Ok(reward) -} - -pub fn calculate_operator_reward( - storage: &dyn Storage, - api: &dyn Api, - owner: &Addr, - bond: &StoredMixnodeBond, -) -> Result { - let last_claimed_height = storage::OPERATOR_REWARD_CLAIMED_HEIGHT - .load(storage, (owner.to_string(), bond.identity().to_string())) - .unwrap_or(0); - - let accumulated_rewards = mixnodes() - .changelog() - .prefix(bond.identity()) - .keys( - storage, - Some(Bound::inclusive(last_claimed_height)), - None, - Order::Ascending, - ) - .filter_map(|height| height.ok()) - .fold( - Ok(Uint128::zero()), - |acc, height| -> Result { - let accumulated_reward = acc?; - if let Some(bond) = mixnodes() - .may_load_at_height(storage, bond.identity().as_str(), height) - .ok() - .flatten() - { - if let Some(ref epoch_rewards) = bond.epoch_rewards { - // Compound rewards from previous heights - let operator_cost = - operator_cost_at_epoch(storage, epoch_rewards.epoch_id())?; - match epoch_rewards.operator_reward(bond.profit_margin(), operator_cost) { - Ok(reward) => return Ok(accumulated_reward + reward), - Err(err) => { - debug_with_visibility( - api, - format!("Failed to calculate operator reward: {:?}", err), - ); - } - }; - } - }; - Ok(accumulated_reward) - }, - )?; - Ok(accumulated_rewards) -} - -// calculate_delegator_reward -// - figure out when is the last time reward go claimed -// - calculate current rewards -// compound_delegator_reward -// - decrease node accumulated rewards -// - increase delegation - -pub fn try_compound_delegator_reward_on_behalf( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - owner: String, - mix_identity: IdentityKey, -) -> Result { - let proxy = deps.api.addr_validate(info.sender.as_str())?; - let owner = deps.api.addr_validate(&owner)?; - let reward = _try_compound_delegator_reward( - env.block.height, - deps, - owner.as_str(), - &mix_identity, - Some(proxy.clone()), - )?; - - Ok( - Response::new().add_event(new_compound_delegator_reward_event( - &owner, - &Some(proxy), - reward, - &mix_identity, - )), - ) -} - -pub fn try_compound_reward( - deps: DepsMut<'_>, - env: Env, - operator: Option, - delegator: Option, - mix_identity: Option, - proxy: Option, -) -> Result { - let proxy = proxy.and_then(|p| deps.api.addr_validate(&p).ok()); - if let Some(operator_address) = operator { - let operator_address = deps.api.addr_validate(&operator_address)?; - let reward = _try_compound_operator_reward( - deps.storage, - deps.api, - env.block.height, - &operator_address, - proxy, - )?; - Ok( - Response::default().add_event(new_compound_operator_reward_event( - &operator_address, - reward, - )), - ) - } else if let Some(delegator_address) = delegator { - if mix_identity.is_none() { - return Err(ContractError::MissingMixIdentity); - } - - let delegator_address = deps.api.addr_validate(&delegator_address)?; - let reward = _try_compound_delegator_reward( - env.block.height, - deps, - delegator_address.as_str(), - mix_identity.as_ref().unwrap(), - proxy, - )?; - Ok( - Response::default().add_event(new_compound_delegator_reward_event( - &delegator_address, - &None, - reward, - &mix_identity.unwrap(), - )), - ) - } else { - Ok(Response::default()) - } -} - -pub fn try_compound_delegator_reward( - deps: DepsMut<'_>, - env: Env, - info: MessageInfo, - mix_identity: IdentityKey, -) -> Result { - let owner = deps.api.addr_validate(info.sender.as_str())?; - let reward = _try_compound_delegator_reward( - env.block.height, - deps, - owner.as_str(), - &mix_identity, - None, - )?; - - Ok( - Response::new().add_event(new_compound_delegator_reward_event( - &info.sender, - &None, - reward, - &mix_identity, - )), - ) -} - -pub fn _try_compound_delegator_reward( - block_height: u64, - mut deps: DepsMut<'_>, - owner_address: &str, - mix_identity: &str, - proxy: Option, -) -> Result { - let delegation_map = crate::delegations::storage::delegations(); - let mix_denom = mix_denom(deps.storage)?; - - let key = mixnet_contract_common::delegation::generate_storage_key( - &deps.api.addr_validate(owner_address)?, - proxy.as_ref(), - ); - let reward = calculate_delegator_reward(deps.storage, deps.api, key.clone(), mix_identity)?; - let mut total_delegation_delegate = Uint128::zero(); - - // Might want to introduce paging here - let delegation_heights = delegation_map - .prefix((mix_identity.to_string(), key.clone())) - .keys(deps.storage, None, None, cosmwasm_std::Order::Ascending) - .filter_map(|v| v.ok()) - .collect::>(); - - for h in delegation_heights { - let delegation = - delegation_map.load(deps.storage, (mix_identity.to_string(), key.clone(), h))?; - total_delegation_delegate += delegation.amount.amount; - delegation_map.replace( - deps.storage, - (mix_identity.to_string(), key.clone(), h), - None, - Some(&delegation), - )?; - } - - let compounded_delegation = total_delegation_delegate + reward; - - if compounded_delegation != Uint128::zero() { - mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>( - deps.storage, - mix_identity, - |total_delegation| { - // since we know that the target node exists and because the total_delegation bucket - // entry is created whenever the node itself is added, the unwrap here is fine - // as the entry MUST exist - Ok(total_delegation - .unwrap() - .saturating_sub(total_delegation_delegate)) - }, - )?; - - _try_delegate_to_mixnode( - deps.branch(), - block_height, - mix_identity, - owner_address, - Coin { - amount: compounded_delegation, - denom: mix_denom, - }, - proxy, - )?; - } - - { - if let Some(mut bond) = mixnodes().may_load(deps.storage, mix_identity)? { - bond.accumulated_rewards = Some(bond.accumulated_rewards().saturating_sub(reward)); - mixnodes().save(deps.storage, mix_identity, &bond, block_height)?; - } - } - - DELEGATOR_REWARD_CLAIMED_HEIGHT.save( - deps.storage, - (key, mix_identity.to_string()), - &block_height, - )?; - - Ok(reward) -} - -// TODO: Test -// + last_reward_claimed_height is updated -// + last_reward_claimed height is correctly used -pub fn calculate_delegator_reward( - storage: &dyn Storage, - api: &dyn Api, - key: Vec, - mix_identity: &str, -) -> Result { - if is_blacklisted(storage, mix_identity)? { - return Ok(Uint128::zero()); - }; - - let last_claimed_height = storage::DELEGATOR_REWARD_CLAIMED_HEIGHT - .load(storage, (key.clone(), mix_identity.to_string())) - .unwrap_or(0); - - // Get delegations newer then last_claimed_height, it would be nice to also fold this into the iteration bellow but it should be ok for now, as - // I doubt folks refresh their delegations often - let mut delegations = delegations_storage::delegations() - .prefix((mix_identity.to_string(), key)) - .range( - storage, - Some(Bound::inclusive(last_claimed_height)), - None, - Order::Descending, - ) - .filter_map(|record| record.ok()) - .map(|(_, delegation)| delegation) - .collect::>(); - - // Accumulate outside of the loop to gain some speed, on a log of checkpoints - let mut delegation_at_height = Uint128::zero(); - - // This is a bit gnarly, but we want to avoid loading all heights, the loading mixnodes, so we're doing it all in the iterator - let accumulated_rewards = mixnodes() - .changelog() - .prefix(mix_identity) - .keys( - storage, - Some(Bound::inclusive(last_claimed_height)), - None, - Order::Ascending, - ) - .filter_map(|height| height.ok()) - // Get all checkpoints greater then last claimed delegation height - .fold( - Ok(Uint128::zero()), - |acc, height| -> Result { - let accumulated_reward = acc?; - delegation_at_height = delegations - .iter() - .filter(|d| d.block_height <= height) - .fold(delegation_at_height, |total, delegation| { - total + delegation.amount.amount - }); - // Drop what we've processed - // This should be replaced with drain_filter once it stabilizes - delegations.retain(|d| d.block_height > height); - // debug_with_visibility( - // api, - // format!("delegation at height {} - {}", height, delegation_at_height), - // ); - if delegation_at_height != Uint128::zero() { - // debug_with_visibility( - // api, - // format!("Loading bond {} at height {}", mix_identity, height), - // ); - if let Some(bond) = mixnodes() - .may_load_at_height(storage, mix_identity, height) - .ok() - .flatten() - { - if let Some(ref epoch_rewards) = bond.epoch_rewards { - let operator_cost = - operator_cost_at_epoch(storage, epoch_rewards.epoch_id()).unwrap(); - // Compound rewards from previous heights - match epoch_reward_params_for_id(storage, epoch_rewards.epoch_id()) { - Ok(params) => { - let reward_at_height = match epoch_rewards.delegation_reward( - delegation_at_height + accumulated_reward, - bond.profit_margin(), - operator_cost, - params, - ) { - Ok(reward) => { - // debug_with_visibility( - // api, - // format!("Reward at height {} - {}", height, reward), - // ); - reward - } - Err(err) => { - debug_with_visibility( - api, - format!( - "Error calculating reward at {} - {}", - height, err - ), - ); - Uint128::zero() - } - }; - return Ok(accumulated_reward + reward_at_height); - } - Err(_err) => { - debug_with_visibility( - api, - format!("No epoch reward params for epoch {}", height), - ); - } - } - } - } - }; - Ok(accumulated_reward) - }, - )?; - - Ok(accumulated_rewards) -} pub(crate) fn try_reward_mixnode( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mix_identity: IdentityKey, - params: NodeRewardParams, -) -> Result { - is_authorized(info.sender.to_string(), deps.storage)?; - // verify_rewarding_state(deps.storage, info, epoch_id)?; - let epoch = crate::interval::storage::current_epoch(deps.storage)?; + mix_id: MixId, + node_performance: Performance, +) -> Result { + ensure_is_authorized(info.sender, deps.storage)?; - // check if the mixnode hasn't been rewarded in this rewarding interval already - match storage::REWARDING_STATUS.may_load(deps.storage, (epoch.id(), mix_identity.clone()))? { - None => (), - Some(RewardingStatus::Complete(_)) => { - return Err(ContractError::MixnodeAlreadyRewarded { - identity: mix_identity, - }) + // see if the epoch has finished + let interval = interval_storage::current_interval(deps.storage)?; + if !interval.is_current_epoch_over(&env) { + return Err(MixnetContractError::EpochInProgress { + current_block_time: env.block.time.seconds(), + epoch_start: interval.current_epoch_start_unix_timestamp(), + epoch_end: interval.current_epoch_end_unix_timestamp(), + }); + } + + // 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, 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, mix_id))); } - Some(RewardingStatus::PendingNextDelegatorPage(_)) => { - return Err(ContractError::DelegatorsPendingReward { - identity: mix_identity, - }) - } - } - - // check if the bond even exists - let mut current_bond = - match mixnodes_storage::read_full_mixnode_bond(deps.storage, &mix_identity)? { - Some(bond) => bond, - None => { - return Ok( - Response::new().add_event(new_not_found_mix_operator_rewarding_event( - epoch.id(), - &mix_identity, - )), - ) - } - }; - - // check if node is old enough for rewarding - if current_bond.block_height + constants::MINIMUM_BLOCK_AGE_FOR_REWARDING > env.block.height { - storage::REWARDING_STATUS.save( - deps.storage, - (epoch.id(), mix_identity.clone()), - &RewardingStatus::Complete(Default::default()), - )?; - - return Ok( - Response::new().add_event(new_too_fresh_bond_mix_operator_rewarding_event( - epoch.id(), - &mix_identity, - )), - ); - } - - let node_pledge = current_bond.pledge_amount.amount; - let node_delegation = current_bond.total_delegation.amount; - - // check if it has non-zero uptime - if params.uptime() == Uint128::zero() { - storage::REWARDING_STATUS.save( - deps.storage, - (epoch.id(), mix_identity.clone()), - &RewardingStatus::Complete(Default::default()), - )?; - - return Ok( - Response::new().add_event(new_zero_uptime_mix_operator_rewarding_event( - epoch.id(), - &mix_identity, - )), - ); - } - - let mut node_reward_params = params; - node_reward_params.set_reward_blockstamp(env.block.height); - - let epoch_reward_params = crate::interval::storage::current_epoch_reward_params(deps.storage)?; - let reward_params = RewardParams::new(epoch_reward_params, node_reward_params); - let node_reward_result = current_bond.reward(&reward_params); - let stored_node_result: StoredNodeRewardResult = node_reward_result.try_into()?; - - current_bond.accumulated_rewards = - Some(current_bond.accumulated_rewards() + stored_node_result.reward()); - let mut stored_bond: StoredMixnodeBond = current_bond.into(); - // technically we don't have to set the total_delegation bucket, but it makes things easier - // in different places that we can guarantee that if node exists, so does the data behind the total delegation - - stored_bond.epoch_rewards = Some(NodeEpochRewards::new( - node_reward_params, - stored_node_result, - epoch.id(), - )); - - let identity = stored_bond.identity(); - - crate::mixnodes::storage::mixnodes().save( - deps.storage, - identity, - &stored_bond, - env.block.height, - )?; - - // Take rewards out of the rewarding pool - storage::reward_accounting(deps.storage, stored_node_result.reward())?; - - let rewarding_result = RewardingResult { - node_reward: stored_node_result.reward(), }; - helpers::update_rewarding_status( - deps.storage, - epoch.id(), - mix_identity.clone(), - rewarding_result, - )?; + let prior_delegates = mix_rewarding.delegates; + let prior_unit_reward = mix_rewarding.full_reward_ratio(); - Ok(Response::new().add_event(new_mix_operator_rewarding_event( - epoch.id(), - &mix_identity, - node_reward_result, - node_pledge, - node_delegation, + // 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 { + 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, mix_id) + .map_err(|_| MixnetContractError::MixnodeNotInRewardedSet { + mix_id, + absolute_epoch_id, + })?; + + // no need to calculate anything as rewards are going to be 0 for everything + // 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, mix_id, &mix_rewarding)?; + return Ok( + Response::new().add_event(new_zero_uptime_mix_operator_rewarding_event( + interval, mix_id, + )), + ); + } + + let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + let node_reward_params = NodeRewardParams::new(node_performance, node_status.is_active()); + + // calculate each step separate for easier accounting + let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); + let reward_distribution = mix_rewarding.determine_reward_split( + node_reward, + node_performance, + interval.epochs_in_interval(), + ); + mix_rewarding.distribute_rewards(reward_distribution, absolute_epoch_id); + + // persist changes happened to the storage + 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, + mix_id, + reward_distribution, + prior_delegates, + prior_unit_reward, ))) } +pub(crate) fn try_withdraw_operator_reward( + deps: DepsMut<'_>, + info: MessageInfo, +) -> Result { + _try_withdraw_operator_reward(deps, info.sender, None) +} + +pub(crate) fn try_withdraw_operator_reward_on_behalf( + deps: DepsMut<'_>, + info: MessageInfo, + owner: String, +) -> Result { + let proxy = info.sender; + let owner = deps.api.addr_validate(&owner)?; + _try_withdraw_operator_reward(deps, owner, Some(proxy)) +} + +pub(crate) fn _try_withdraw_operator_reward( + deps: DepsMut<'_>, + owner: Addr, + proxy: Option, +) -> Result { + // we need to grab all of the node's details so we'd known original pledge alongside + // all the earned rewards (and obviously to know if this node even exists and is still + // in the bonded state) + let mix_details = get_mixnode_details_by_owner(deps.storage, owner.clone())?.ok_or( + MixnetContractError::NoAssociatedMixNodeBond { + owner: owner.clone(), + }, + )?; + let mix_id = mix_details.mix_id(); + + ensure_proxy_match(&proxy, &mix_details.bond_information.proxy)?; + ensure_bonded(&mix_details.bond_information)?; + + let reward = helpers::withdraw_operator_reward(deps.storage, mix_details)?; + let mut response = Response::new(); + + // if the reward is zero, don't track or send anything - there's no point + if !reward.amount.is_zero() { + let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![reward.clone()]); + response = response.add_message(return_tokens); + + if let Some(proxy) = &proxy { + // we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract + // otherwise, we don't care + let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; + if proxy == &vesting_contract { + let msg = VestingContractExecuteMsg::TrackReward { + amount: reward.clone(), + address: owner.clone().into_string(), + }; + let track_reward_message = wasm_execute(proxy, &msg, vec![])?; + response = response.add_message(track_reward_message); + } + } + } + + Ok(response.add_event(new_withdraw_operator_reward_event( + &owner, &proxy, reward, mix_id, + ))) +} + +pub(crate) fn try_withdraw_delegator_reward( + deps: DepsMut<'_>, + info: MessageInfo, + mix_id: MixId, +) -> Result { + _try_withdraw_delegator_reward(deps, mix_id, info.sender, None) +} + +pub(crate) fn try_withdraw_delegator_reward_on_behalf( + deps: DepsMut<'_>, + info: MessageInfo, + mix_id: MixId, + owner: String, +) -> Result { + let proxy = info.sender; + let owner = deps.api.addr_validate(&owner)?; + _try_withdraw_delegator_reward(deps, mix_id, owner, Some(proxy)) +} + +pub(crate) fn _try_withdraw_delegator_reward( + deps: DepsMut<'_>, + mix_id: MixId, + owner: Addr, + proxy: Option, +) -> Result { + // see if the delegation even exists + let storage_key = Delegation::generate_storage_key(mix_id, &owner, proxy.as_ref()); + let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { + None => { + return Err(MixnetContractError::NoMixnodeDelegationFound { + mix_id, + address: owner.into_string(), + proxy: proxy.map(Addr::into_string), + }) + } + Some(delegation) => delegation, + }; + + // grab associated mixnode rewarding details + let mix_rewarding = + storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::InconsistentState { + comment: "mixnode rewarding got removed from the storage whilst there's still an existing delegation" + .into(), + })?; + + // see if the mixnode is not in the process of unbonding or whether it has already unbonded + // (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 { mix_id }) + } + None => return Err(MixnetContractError::MixnodeHasUnbonded { mix_id }), + _ => (), + }; + + ensure_proxy_match(&proxy, &delegation.proxy)?; + + let reward = helpers::withdraw_delegator_reward(deps.storage, delegation, mix_rewarding)?; + let mut response = Response::new(); + + // if the reward is zero, don't track or send anything - there's no point + if !reward.amount.is_zero() { + let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![reward.clone()]); + response = response.add_message(return_tokens); + + if let Some(proxy) = &proxy { + // we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract + // otherwise, we don't care + let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; + if proxy == &vesting_contract { + let msg = VestingContractExecuteMsg::TrackReward { + amount: reward.clone(), + address: owner.clone().into_string(), + }; + let track_reward_message = wasm_execute(proxy, &msg, vec![])?; + response = response.add_message(track_reward_message); + } + } + } + + Ok(response.add_event(new_withdraw_delegator_reward_event( + &owner, &proxy, reward, mix_id, + ))) +} + +pub(crate) fn try_update_active_set_size( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + active_set_size: u32, + force_immediately: bool, +) -> Result { + ensure_is_owner(info.sender, deps.storage)?; + + let mut rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + if active_set_size == 0 { + return Err(MixnetContractError::ZeroActiveSet); + } + + if active_set_size > rewarding_params.rewarded_set_size { + return Err(MixnetContractError::InvalidActiveSetSize); + } + + let interval = interval_storage::current_interval(deps.storage)?; + 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( + env.block.height, + active_set_size, + ))) + } else { + // push the epoch event + let epoch_event = PendingEpochEventKind::UpdateActiveSetSize { + new_size: active_set_size, + }; + 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( + active_set_size, + time_left, + )), + ) + } +} + +pub(crate) fn try_update_rewarding_params( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + updated_params: IntervalRewardingParamsUpdate, + force_immediately: bool, +) -> Result { + ensure_is_owner(info.sender, deps.storage)?; + + if !updated_params.contains_updates() { + return Err(MixnetContractError::EmptyParamsChangeMsg); + } + + let interval = interval_storage::current_interval(deps.storage)?; + if force_immediately || interval.is_current_interval_over(&env) { + let mut rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + 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 = PendingIntervalEventKind::UpdateRewardingParams { + update: updated_params, + }; + 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( + updated_params, + time_left, + )), + ) + } +} + #[cfg(test)] pub mod tests { use super::*; - use crate::delegations::transactions::{ - _try_remove_delegation_from_mixnode, try_delegate_to_mixnode, - }; - use crate::error::ContractError; - use crate::interval::storage::{ - current_epoch_reward_params, save_epoch, save_epoch_reward_params, - }; - use crate::interval::transactions::try_advance_epoch; - use crate::mixnet_contract_settings::storage::{ - self as mixnet_params_storage, rewarding_validator_address, - }; use crate::mixnodes::storage as mixnodes_storage; - use crate::mixnodes::storage::StoredMixnodeBond; - use crate::rewards::transactions::try_reward_mixnode; - use crate::support::helpers::{current_operator_epoch_cost, epochs_in_interval}; - use crate::support::tests; - use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::test_helpers; - use az::CheckedCast; - use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::{coin, coins, Addr, StdError, Timestamp, Uint128}; - use mixnet_contract_common::events::{ - must_find_attribute, BOND_TOO_FRESH_VALUE, NO_REWARD_REASON_KEY, - OPERATOR_REWARDING_EVENT_TYPE, - }; - use mixnet_contract_common::reward_params::{ - EpochRewardParams, NodeRewardParams, RewardParams, - }; - use mixnet_contract_common::{Delegation, IdentityKey, Interval, Layer, MixNode}; - use time::OffsetDateTime; + use cosmwasm_std::testing::mock_info; - #[test] - fn rewarding_mixnodes_with_incorrect_interval_id() { - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - let sender = rewarding_validator_address(&deps.storage).unwrap(); - let info = mock_info(&sender, &coins(1000, TEST_COIN_DENOM)); - crate::interval::transactions::init_epoch(&mut deps.storage, env.clone()).unwrap(); - - // bond the node - let node_owner: Addr = Addr::unchecked("node-owner"); - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - // Reward once - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ); - assert!(res.is_ok()); - - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ); - // Fails since mixnode was already rewarded in this epoch - assert!(res.is_err()); - - let rewarding_validator_address = rewarding_validator_address(&deps.storage).unwrap(); - - // Advance epoch - let premature_advance = try_advance_epoch( - env.clone(), - &mut deps.storage, - rewarding_validator_address.clone(), - ); - assert!(premature_advance.is_err()); - - env.block.time = Timestamp::from_seconds(env.block.time.seconds() + 3600); - - let timely_advance = - try_advance_epoch(env.clone(), &mut deps.storage, rewarding_validator_address); - assert!(timely_advance.is_ok()); - - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ); - assert!(res.is_ok()); - - test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); - - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity, - tests::fixtures::node_reward_params_fixture(100), - ); - assert!(res.is_ok()); - } - - #[test] - fn attempting_rewarding_mixnode_multiple_times_per_interval() { - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - // bond the node - let node_owner: Addr = Addr::unchecked("node-owner"); - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - tests::fixtures::good_mixnode_pledge(), - deps.as_mut(), - ); - - let info = mock_info(rewarding_validator_address.as_ref(), &[]); - - // first reward goes through just fine - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ); - assert!(res.is_ok()); - - // but the other one fails - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ); - assert_eq!( - Err(ContractError::MixnodeAlreadyRewarded { - identity: node_identity.clone() - }), - res - ); - - // but rewarding the same node in the following interval is fine again - test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); - - let res = try_reward_mixnode( - deps.as_mut(), - env, - info.clone(), - node_identity, - tests::fixtures::node_reward_params_fixture(100), - ); - assert!(res.is_ok()); - } - - #[test] - fn rewarding_mixnode_blockstamp_based() { - let mut deps = test_helpers::init_contract(); - - let mut env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - let node_owner: Addr = Addr::unchecked("node-owner"); - let node_identity: IdentityKey = "nodeidentity".into(); - - let initial_bond = 10000_000000; - let initial_delegation = 20000_000000; - let mixnode_bond = StoredMixnodeBond { - pledge_amount: coin(initial_bond, TEST_COIN_DENOM), - owner: node_owner, - layer: Layer::One, - block_height: env.block.height, - mix_node: MixNode { - identity_key: node_identity.clone(), - ..tests::fixtures::mix_node_fixture() - }, - proxy: None, - accumulated_rewards: None, - epoch_rewards: None, + #[cfg(test)] + mod mixnode_rewarding { + use super::*; + use crate::interval::pending_events; + use crate::support::tests::test_helpers::{find_attribute, TestSetup}; + 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, PRIOR_DELEGATES_KEY, PRIOR_UNIT_REWARD_KEY, + ZERO_PERFORMANCE_VALUE, }; + use mixnet_contract_common::helpers::compare_decimals; + use mixnet_contract_common::RewardedSetNodeStatus; - mixnodes_storage::mixnodes() - .save( - deps.as_mut().storage, - &node_identity, - &mixnode_bond, - env.block.height, - ) - .unwrap(); - mixnodes_storage::TOTAL_DELEGATION - .save( - deps.as_mut().storage, - &node_identity, - &Uint128::new(initial_delegation), - ) - .unwrap(); + #[test] + fn can_only_be_performed_by_specified_rewarding_validator() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let some_sender = mock_info("foomper", &[]); - // delegation happens later, but not later enough - env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING - 1; - env.block.time = Timestamp::from_seconds(OffsetDateTime::now_utc().unix_timestamp() as u64); + // skip time to when the following epoch is over (since mixnodes are not eligible for rewarding + // in the same epoch they're bonded and we need the rewarding epoch to be over) + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + let performance = test_helpers::performance(100.); - delegations_storage::delegations() - .save( - deps.as_mut().storage, - (node_identity.clone(), "delegator".into(), env.block.height), - &Delegation::new( - Addr::unchecked("delegator"), - node_identity.clone(), - coin(initial_delegation, TEST_COIN_DENOM), - env.block.height, - None, - ), - ) - .unwrap(); + let env = test.env(); + let res = try_reward_mixnode(test.deps_mut(), env, some_sender, mix_id, performance); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); - let info = mock_info(rewarding_validator_address.as_ref(), &[]); + // good address (sanity check) + let env = test.env(); + let sender = test.rewarding_validator(); + let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + assert!(res.is_ok()); + } - let epoch = Interval::init_epoch(env.clone()); - save_epoch(&mut deps.storage, &epoch).unwrap(); - save_epoch_reward_params(epoch.id(), &mut deps.storage).unwrap(); + #[test] + fn can_only_be_performed_if_node_is_fully_bonded() { + let mut test = TestSetup::new(); + let mix_id_never_existed = 42; + let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None); + let mix_id_unbonded_leftover = + test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![ + mix_id_never_existed, + mix_id_unbonded, + mix_id_unbonded_leftover, + ]); - let epoch_from_storage = crate::interval::storage::current_epoch(&deps.storage).unwrap(); - assert_eq!(epoch_from_storage.id(), 0); + let env = test.env(); - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ) - .unwrap(); + // note: we don't have to test for cases where `is_unbonding` is set to true on a mixnode + // since before performing the validator-api should clear out the event queue - assert_eq!( - initial_bond, - test_helpers::read_mixnode_pledge_amount(deps.as_ref().storage, &node_identity) - .unwrap() - .u128() - ); - assert_eq!( - initial_delegation, - mixnodes_storage::TOTAL_DELEGATION - .load(deps.as_ref().storage, &node_identity) - .unwrap() - .u128() - ); - assert_eq!(1, res.events.len()); - assert_eq!(OPERATOR_REWARDING_EVENT_TYPE, res.events[0].ty); - assert_eq!( - BOND_TOO_FRESH_VALUE, - must_find_attribute(&res.events[0], NO_REWARD_REASON_KEY) - ); - - // reward can happen now, but only for bonded node - env.block.height += 1; - env.block.time = Timestamp::from_seconds(epoch.next().start_unix_timestamp() as u64); - let sender = - crate::mixnet_contract_settings::storage::rewarding_validator_address(&deps.storage) + // manually adjust delegation info as to indicate the rewarding information shouldnt get removed + let mut rewarding_details = storage::MIXNODE_REWARDING + .load(test.deps().storage, mix_id_unbonded_leftover) .unwrap(); - try_advance_epoch(env.clone(), &mut deps.storage, sender).unwrap(); - - let info = mock_info(rewarding_validator_address.as_ref(), &[]); - - let res = try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ) - .unwrap(); - - let mixnode = crate::mixnodes::storage::mixnodes() - .load(&deps.storage, &node_identity) - .unwrap(); - - assert!(mixnode.accumulated_rewards > Some(Uint128::zero()),); - assert_eq!( - initial_delegation, - mixnodes_storage::TOTAL_DELEGATION - .load(deps.as_ref().storage, &node_identity) - .unwrap() - .u128() - ); - - assert_eq!(1, res.events.len()); - let event = &res.events[0]; - assert_eq!(OPERATOR_REWARDING_EVENT_TYPE, event.ty); - // assert_ne!("0", must_find_attribute(event, TOTAL_MIXNODE_REWARD_KEY)); - // assert_ne!("0", must_find_attribute(event, OPERATOR_REWARD_KEY)); - // assert_eq!( - // "0", - // must_find_attribute(event, DISTRIBUTED_DELEGATION_REWARDS_KEY) - // ); - // assert_eq!( - // false.to_string(), - // must_find_attribute(event, FURTHER_DELEGATIONS_TO_REWARD_KEY) - // ); - - // reward happens now, both for node owner and delegators - env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING - 1; - test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); - - let pledge_before_rewarding = - test_helpers::read_mixnode_pledge_amount(deps.as_ref().storage, &node_identity) - .unwrap() - .u128(); - - let info = mock_info(rewarding_validator_address.as_ref(), &[]); - let res = try_reward_mixnode( - deps.as_mut(), - env, - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ) - .unwrap(); - - // We are in a lazy system, rewarding will not increase pledge or delegations - assert_eq!( - test_helpers::read_mixnode_pledge_amount(deps.as_ref().storage, &node_identity) - .unwrap() - .u128(), - pledge_before_rewarding - ); - assert_eq!( - mixnodes_storage::TOTAL_DELEGATION - .load(deps.as_ref().storage, &node_identity) - .unwrap() - .u128(), - initial_delegation - ); - - assert_eq!(1, res.events.len()); - let event = &res.events[0]; - assert_eq!(OPERATOR_REWARDING_EVENT_TYPE, event.ty); - // assert_ne!("0", must_find_attribute(event, TOTAL_MIXNODE_REWARD_KEY)); - // assert_ne!("0", must_find_attribute(event, OPERATOR_REWARD_KEY)); - // assert_ne!( - // "0", - // must_find_attribute(event, DISTRIBUTED_DELEGATION_REWARDS_KEY) - // ); - // assert_eq!( - // false.to_string(), - // must_find_attribute(event, FURTHER_DELEGATIONS_TO_REWARD_KEY) - // ); - } - - #[test] - fn test_reward_additivity_and_snapshots() { - use crate::constants::INTERVAL_REWARD_PERCENT; - use crate::contract::INITIAL_REWARD_POOL; - use crate::mixnodes::transactions::try_add_mixnode; - use rand::thread_rng; - - let mixnodes = crate::mixnodes::storage::mixnodes(); - - type U128 = fixed::types::U75F53; - - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_ref().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - - let info = mock_info(rewarding_validator_address.as_str(), &[]); - - crate::mixnodes::transactions::try_checkpoint_mixnodes( - &mut deps.storage, - env.block.height, - info.clone(), - ) - .unwrap(); - let checkpoints = mixnodes - .changelog() - .keys(&deps.storage, None, None, Order::Ascending) - .filter_map(|x| x.ok()) - .collect::>(); - assert_eq!(0, checkpoints.len()); - - let period_reward_pool = - (INITIAL_REWARD_POOL / 100 / epochs_in_interval(&deps.storage).unwrap() as u128) - * INTERVAL_REWARD_PERCENT as u128; - assert_eq!(period_reward_pool, 6_944_444_444); - let circulating_supply = storage::circulating_supply(&deps.storage).unwrap().u128(); - assert_eq!(circulating_supply, 750_000_000_000_000u128); - - let staking_supply = storage::staking_supply(&deps.storage).unwrap().u128(); - assert_eq!(staking_supply, 100_000_000_000_000u128); - - let sender = Addr::unchecked("alice"); - let stake = coins(10_000_000_000, TEST_COIN_DENOM); - - let keypair = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); - let owner_signature = keypair - .private_key() - .sign(sender.as_bytes()) - .to_base58_string(); - - let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut thread_rng()); - - let info = mock_info(sender.as_str(), &stake); - - let node_identity_1 = keypair.public_key().to_base58_string(); - - try_add_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - MixNode { - identity_key: node_identity_1.clone(), - sphinx_key: legit_sphinx_key.public_key().to_base58_string(), - ..tests::fixtures::mix_node_fixture() - }, - owner_signature, - ) - .unwrap(); - - // tick - - env.block.height += 1; - - let info = mock_info(rewarding_validator_address.as_str(), &[]); - crate::mixnodes::transactions::try_checkpoint_mixnodes( - &mut deps.storage, - env.block.height, - info.clone(), - ) - .unwrap(); - mixnodes - .assert_checkpointed(&deps.storage, env.block.height) - .unwrap(); - let checkpoints = mixnodes - .changelog() - .keys(&deps.storage, None, None, Order::Ascending) - .filter_map(|x| x.ok()) - .collect::>(); - assert_eq!(checkpoints.len(), 1); - - let node_owner: Addr = Addr::unchecked("johnny"); - let node_identity_2 = test_helpers::add_mixnode( - node_owner.as_str(), - coins(10_000_000_000, TEST_COIN_DENOM), - deps.as_mut(), - ); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("alice_d1", &[coin(8000_000000, TEST_COIN_DENOM)]), - node_identity_1.clone(), - ) - .unwrap(); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("alice_d2", &[coin(2000_000000, TEST_COIN_DENOM)]), - node_identity_1.clone(), - ) - .unwrap(); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("bob_d1", &[coin(8000_000000, TEST_COIN_DENOM)]), - node_identity_2.clone(), - ) - .unwrap(); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("bob_d2", &[coin(2000_000000, TEST_COIN_DENOM)]), - node_identity_2.clone(), - ) - .unwrap(); - - let node_owner: Addr = Addr::unchecked("alicebob"); - let node_identity_3 = test_helpers::add_mixnode( - node_owner.as_str(), - coins(10_000_000_000 * 2, TEST_COIN_DENOM), - deps.as_mut(), - ); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("alicebob_d1", &[coin(8000_000000 * 2, TEST_COIN_DENOM)]), - node_identity_3.clone(), - ) - .unwrap(); - - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("alicebob_d2", &[coin(2000_000000 * 2, TEST_COIN_DENOM)]), - node_identity_3.clone(), - ) - .unwrap(); - - crate::delegations::transactions::_try_reconcile_all_delegation_events( - &mut deps.storage, - &deps.api, - ) - .unwrap(); - - env.block.height += 2 * constants::MINIMUM_BLOCK_AGE_FOR_REWARDING; - - let mix_1 = mixnodes_storage::read_full_mixnode_bond(&deps.storage, &node_identity_1) - .unwrap() - .unwrap(); - let mix_1_uptime = 100; - - let mix_2 = mixnodes_storage::read_full_mixnode_bond(&deps.storage, &node_identity_2) - .unwrap() - .unwrap(); - let mix_2_uptime = 50; - - let mix_3 = mixnodes_storage::read_full_mixnode_bond(&deps.storage, &node_identity_3) - .unwrap() - .unwrap(); - - // average of 1 and 2 - let mix_3_uptime = 75; - - let epoch = Interval::init_epoch(env.clone()); - save_epoch(&mut deps.storage, &epoch).unwrap(); - save_epoch_reward_params(epoch.id(), &mut deps.storage).unwrap(); - - let interval_reward_params = current_epoch_reward_params(&deps.storage).unwrap(); - - let node_reward_params = NodeRewardParams::new(0, mix_1_uptime, true); - let node_reward_params_2 = NodeRewardParams::new(0, mix_2_uptime, true); - let node_reward_params_3 = NodeRewardParams::new(0, mix_3_uptime, true); - - let mut params = RewardParams::new(interval_reward_params, node_reward_params); - let mut params2 = RewardParams::new(interval_reward_params, node_reward_params_2); - let mut params3 = RewardParams::new(interval_reward_params, node_reward_params_3); - - params.set_reward_blockstamp(env.block.height); - params2.set_reward_blockstamp(env.block.height); - params3.set_reward_blockstamp(env.block.height); - - assert_eq!(params.performance(), U128::from_num(1u32)); - assert_eq!(params2.performance(), U128::from_num(0.5f32)); - assert_eq!(params3.performance(), U128::from_num(0.75f32)); - - let mix_1_reward_result = mix_1.reward(¶ms); - - let info = mock_info(rewarding_validator_address.as_str(), &[]); - crate::mixnodes::transactions::try_checkpoint_mixnodes( - &mut deps.storage, - env.block.height, - info.clone(), - ) - .unwrap(); - mixnodes - .assert_checkpointed(&deps.storage, env.block.height) - .unwrap(); - - try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity_1.clone(), - node_reward_params, - ) - .unwrap(); - - let mix_after_reward = mixnodes.may_load(&deps.storage, &node_identity_1).unwrap(); - let accumulated1 = mix_after_reward - .as_ref() - .unwrap() - .accumulated_rewards - .unwrap() - .u128(); - // stupid one-off error, but @DU says it's fine - assert_eq!(accumulated1, 1948911 + 1); - - let checkpoints = mixnodes - .changelog() - .prefix(&node_identity_1) - .keys(&deps.storage, None, None, Order::Ascending) - .filter_map(|x| x.ok()) - .count(); - assert_eq!(checkpoints, 2); - - env.block.height += 10000; - env.block.time = env.block.time.plus_seconds(3601); - - try_advance_epoch( - env.clone(), - &mut deps.storage, - rewarding_validator_address.to_string(), - ) - .unwrap(); - - // After two snapshots we should see an increase in delegation - try_delegate_to_mixnode( - deps.as_mut(), - env.clone(), - mock_info("alice_d1", &[coin(8000_000000, TEST_COIN_DENOM)]), - node_identity_1.clone(), - ) - .unwrap(); - - crate::delegations::transactions::_try_reconcile_all_delegation_events( - &mut deps.storage, - &deps.api, - ) - .unwrap(); - - let info = mock_info(rewarding_validator_address.as_str(), &[]); - crate::mixnodes::transactions::try_checkpoint_mixnodes( - &mut deps.storage, - env.block.height, - info.clone(), - ) - .unwrap(); - mixnodes - .assert_checkpointed(&deps.storage, env.block.height) - .unwrap(); - - try_reward_mixnode( - deps.as_mut(), - env.clone(), - info, - node_identity_1.clone(), - node_reward_params, - ) - .unwrap(); - let mix_after_reward_2 = mixnodes.may_load(&deps.storage, &node_identity_1).unwrap(); - let accumulated2 = mix_after_reward_2 - .as_ref() - .unwrap() - .accumulated_rewards - .unwrap() - .u128(); - assert_eq!(accumulated2, accumulated1 + 2728477); - assert_ne!(mix_after_reward, mix_after_reward_2); - - let checkpoints = mixnodes - .changelog() - .prefix(&node_identity_1) - .keys(&deps.storage, None, None, Order::Ascending) - .collect::>>(); - assert_eq!(checkpoints.len(), 3); - - env.block.height += 10000; - env.block.time = env.block.time.plus_seconds(3601); - - try_advance_epoch( - env.clone(), - &mut deps.storage, - rewarding_validator_address.to_string(), - ) - .unwrap(); - - let info = mock_info(rewarding_validator_address.as_str(), &[]); - crate::mixnodes::transactions::try_checkpoint_mixnodes( - &mut deps.storage, - env.block.height, - info.clone(), - ) - .unwrap(); - mixnodes - .assert_checkpointed(&deps.storage, env.block.height) - .unwrap(); - - try_reward_mixnode( - deps.as_mut(), - env.clone(), - info, - node_identity_1.clone(), - node_reward_params, - ) - .unwrap(); - let mix_after_reward_3 = mixnodes.may_load(&deps.storage, &node_identity_1).unwrap(); - let accumulated3 = mix_after_reward_3 - .as_ref() - .unwrap() - .accumulated_rewards - .unwrap() - .u128(); - // off by one : ) - assert_eq!(accumulated3 + 1, accumulated2 + 2728477); - - let checkpoints = mixnodes - .changelog() - .prefix(&node_identity_1) - .keys(&deps.storage, None, None, Order::Ascending) - .filter_map(|x| x.ok()) - .collect::>(); - assert_eq!(checkpoints.len(), 4); - - let delegation_map = crate::delegations::storage::delegations(); - let key = "alice_d1".as_bytes().to_vec(); - - let last_claimed_height = storage::DELEGATOR_REWARD_CLAIMED_HEIGHT - .load(&deps.storage, (key.clone(), node_identity_1.to_string())) - .unwrap_or(0); - - assert_eq!(last_claimed_height, 0); - - let viable_delegations = delegation_map - .prefix((node_identity_1.to_string(), key.clone())) - .range(&deps.storage, None, None, Order::Descending) - .filter_map(|record| record.ok()) - .filter(|(height, _)| last_claimed_height <= *height) - .map(|(_, delegation)| delegation) - .collect::>(); - - assert_eq!(viable_delegations.len(), 2); - - let viable_heights = mixnodes - .changelog() - .prefix(&node_identity_1) - .keys(&deps.storage, None, None, Order::Ascending) - .filter_map(|height| height.ok()) - .filter(|height| last_claimed_height <= *height) - .collect::>(); - - // Should be equal to the number of checkpoints - assert_eq!(viable_heights.len(), 4); - - for (i, h) in viable_heights.into_iter().enumerate() { - let delegation_at_height = viable_delegations - .iter() - .filter(|d| d.block_height <= h) - .fold(Uint128::zero(), |total, delegation| { - total + delegation.amount.amount - }); - if i < 2 { - assert_eq!(delegation_at_height, Uint128::new(8000000000)); - } else { - assert_eq!(delegation_at_height, Uint128::new(16000000000)); + rewarding_details.delegates = Decimal::raw(12345); + rewarding_details.unique_delegations = 1; + storage::MIXNODE_REWARDING + .save( + test.deps_mut().storage, + mix_id_unbonded_leftover, + &rewarding_details, + ) + .unwrap(); + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded).unwrap(); + + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) + .unwrap(); + + let env = test.env(); + let sender = test.rewarding_validator(); + let performance = test_helpers::performance(100.0); + + for &mix_id in &[ + mix_id_never_existed, + mix_id_unbonded, + mix_id_unbonded_leftover, + ] { + let res = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id, + performance, + ) + .unwrap(); + + let reason = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + NO_REWARD_REASON_KEY, + &res, + ); + assert_eq!(BOND_NOT_FOUND_VALUE, reason); } } - let alice_reward = - calculate_delegator_reward(&deps.storage, &deps.api, key.clone(), &node_identity_1) + #[test] + fn can_only_be_performed_once_epoch_is_over() { + let mut test = TestSetup::new(); + + let mix_id = test.add_dummy_mixnode("mix-owner", None); + let sender = test.rewarding_validator(); + + // node is in the active set BUT the current epoch has just begun + test.skip_to_next_epoch(); + test.update_rewarded_set(vec![mix_id]); + let performance = test_helpers::performance(100.); + + let env = test.env(); + let res = try_reward_mixnode(test.deps_mut(), env, sender.clone(), mix_id, performance); + assert!(matches!( + res, + Err(MixnetContractError::EpochInProgress { .. }) + )); + + // epoch is over (sanity check) + test.skip_to_current_epoch_end(); + let env = test.env(); + let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + assert!(res.is_ok()); + } + + #[test] + fn can_only_be_performed_for_nodes_in_rewarded_set() { + let mut test = TestSetup::new(); + + let active_mix_id = test.add_dummy_mixnode("mix-owner-active", None); + let standby_mix_id = test.add_dummy_mixnode("mix-owner-standby", None); + let inactive_mix_id = test.add_dummy_mixnode("mix-owner-inactive", None); + let sender = test.rewarding_validator(); + + test.skip_to_next_epoch_end(); + + // manually set the rewarded set so that we'd have 1 active node, 1 standby and 1 inactive + interval_storage::REWARDED_SET + .save( + test.deps_mut().storage, + active_mix_id, + &RewardedSetNodeStatus::Active, + ) + .unwrap(); + interval_storage::REWARDED_SET + .save( + test.deps_mut().storage, + standby_mix_id, + &RewardedSetNodeStatus::Standby, + ) .unwrap(); - // TODO: perform deeper investigation into this number as it seem to not have compounded - // reward on the initial 8000 delegation and only have done it starting from 16000 - assert_eq!(alice_reward, Uint128::new(2737979)); + let performance = test_helpers::performance(100.); + let env = test.env(); + let res_active = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + active_mix_id, + performance, + ); + let res_standby = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + standby_mix_id, + performance, + ); + let res_inactive = + try_reward_mixnode(test.deps_mut(), env, sender, inactive_mix_id, performance); - let mix_0 = mixnodes.load(&deps.storage, &node_identity_1).unwrap(); + assert!(res_active.is_ok()); + assert!(res_standby.is_ok()); + assert!(matches!( + res_inactive, + Err(MixnetContractError::MixnodeNotInRewardedSet { mix_id, .. }) if mix_id == inactive_mix_id + )); + } - _try_compound_delegator_reward( - env.block.height, - deps.as_mut(), - "alice_d1", - &node_identity_1, - None, - ) - .unwrap(); + #[test] + fn can_only_be_performed_once_per_node_per_epoch() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); - crate::delegations::transactions::_try_reconcile_all_delegation_events( - &mut deps.storage, - &deps.api, - ) - .unwrap(); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + let performance = test_helpers::performance(100.); + let env = test.env(); + let sender = test.rewarding_validator(); - let delegations = crate::delegations::storage::delegations() - .prefix((node_identity_1.to_string(), key.clone())) - .range(&deps.storage, None, None, Order::Ascending) - .filter_map(|x| x.ok()) - .map(|(_, delegation)| delegation) - .collect::>(); - assert_eq!(delegations.len(), 1); + // first rewarding + let res = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id, + performance, + ); + assert!(res.is_ok()); - let delegation = delegations.first().unwrap(); - assert_eq!( - delegation.amount.amount, - Uint128::new(16000000000 + 2737979) - ); + // second rewarding + let res = try_reward_mixnode(test.deps_mut(), env, sender.clone(), mix_id, performance); + assert!(matches!( + res, + Err(MixnetContractError::MixnodeAlreadyRewarded { mix_id, .. }) if mix_id == mix_id + )); - let mix_1 = mixnodes - .load(&deps.storage, &node_identity_1.clone()) + // in the following epoch we're good again + test.skip_to_next_epoch_end(); + let env = test.env(); + let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + assert!(res.is_ok()); + } + + #[test] + fn requires_nonzero_performance_score() { + let mut test = TestSetup::new(); + let mix_id = test.add_dummy_mixnode("mix-owner", None); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + let zero_performance = test_helpers::performance(0.); + let performance = test_helpers::performance(100.0); + let env = test.env(); + let sender = test.rewarding_validator(); + + // first rewarding + let res = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id, + zero_performance, + ) .unwrap(); + let reason = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + NO_REWARD_REASON_KEY, + &res, + ); + assert_eq!(ZERO_PERFORMANCE_VALUE, reason); - _try_remove_delegation_from_mixnode(deps.as_mut(), env, node_identity_1, "alice_d1", None) - .unwrap(); + // sanity check: it's still treated as rewarding, so you we can't reward the node again + // with different performance for the same epoch + let res = try_reward_mixnode( + test.deps_mut(), + env, + sender.clone(), + mix_id, + zero_performance, + ); + assert!(matches!( + res, + Err(MixnetContractError::MixnodeAlreadyRewarded { mix_id, .. }) if mix_id == mix_id + )); - crate::delegations::transactions::_try_reconcile_all_delegation_events( - &mut deps.storage, - &deps.api, - ) - .unwrap(); + // but in the next epoch, as always, we're good again + test.skip_to_next_epoch_end(); + let env = test.env(); + let res = + try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance).unwrap(); - assert_eq!( - mix_0.accumulated_rewards(), - mix_1.accumulated_rewards() + alice_reward - ); + // rewards got distributed (in this test we don't care what they were exactly, but they must be non-zero) + let operator = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + OPERATOR_REWARD_KEY, + &res, + ); + assert!(!operator.is_empty()); + assert_ne!("0", operator); + let delegates = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + DELEGATES_REWARD_KEY, + &res, + ); + assert_eq!("0", delegates); + } - let operator_reward = - calculate_operator_reward(&deps.storage, &deps.api, &Addr::unchecked("alice"), &mix_1) + #[test] + fn correctly_accounts_for_rewards_distributed() { + let mut test = TestSetup::new(); + let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); + let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); + let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id1, mix_id2, mix_id3]); + let performance = test_helpers::performance(98.0); + let env = test.env(); + let sender = test.rewarding_validator(); + + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + + 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) .unwrap(); - assert_eq!(operator_reward, Uint128::new(2278901)); + assert!(change.removed.is_zero()); + assert!(change.added.is_zero()); - assert_eq!(mix_1_reward_result.sigma(), U128::from_num(0.0002f64)); - assert_eq!(mix_1_reward_result.lambda(), U128::from_num(0.0001f64)); - assert_eq!(mix_1_reward_result.reward().int(), accumulated1); + let mut total_operator = Decimal::zero(); + let mut total_delegates = Decimal::zero(); - let mix_2_reward_result = mix_2.reward(¶ms2); + for &mix_id in &[mix_id1, mix_id2, mix_id3] { + let before = storage::MIXNODE_REWARDING + .load(test.deps().storage, mix_id) + .unwrap(); - assert_eq!(mix_2_reward_result.sigma(), U128::from_num(0.0002f64)); - assert_eq!(mix_2_reward_result.lambda(), U128::from_num(0.0001f64)); - assert_eq!(mix_2_reward_result.reward().int(), 974456u128); + let res = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id, + performance, + ) + .unwrap(); + let operator: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + OPERATOR_REWARD_KEY, + &res, + ) + .parse() + .unwrap(); + let delegates: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + DELEGATES_REWARD_KEY, + &res, + ) + .parse() + .unwrap(); - let _mix_3_reward_result = mix_3.reward(¶ms3); + let after = storage::MIXNODE_REWARDING + .load(test.deps().storage, mix_id) + .unwrap(); - // assert_eq!(mix_3_reward_result.reward().int(), mix_1_reward_result.reward().int() + mix_2_reward_result.reward().int()); - } + // also the values emitted via events are consistent with the actual values! + let actual_operator = after.operator - before.operator; + let actual_delegates = after.delegates - before.delegates; + assert_eq!(actual_operator, operator); + assert_eq!(actual_delegates, delegates); - #[test] - fn test_tokenomics_rewarding() { - use crate::constants::INTERVAL_REWARD_PERCENT; - use crate::contract::INITIAL_REWARD_POOL; - use crate::support::helpers::epochs_in_interval; + total_operator += operator; + total_delegates += delegates; - type U128 = fixed::types::U75F53; + let change = storage::PENDING_REWARD_POOL_CHANGE + .load(test.deps().storage) + .unwrap(); + assert_eq!(change.removed, total_operator + total_delegates); + assert!(change.added.is_zero()); + } + } - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_ref().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; - let period_reward_pool = - (INITIAL_REWARD_POOL / 100 / epochs_in_interval(&deps.storage).unwrap() as u128) - * INTERVAL_REWARD_PERCENT as u128; - assert_eq!(period_reward_pool, 6_944_444_444); - let circulating_supply = storage::circulating_supply(&deps.storage).unwrap().u128(); - assert_eq!(circulating_supply, 750_000_000_000_000u128); + #[test] + fn correctly_splits_the_rewards() { + // we're basing this test on our simulator that we have separate unit tests for + // to determine whether values it spits are equal to what we expect - let node_owner: Addr = Addr::unchecked("alice"); - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - coins(10_000_000_000, TEST_COIN_DENOM), - deps.as_mut(), - ); + let operator1 = Uint128::new(100_000_000); + let operator2 = Uint128::new(2_570_000_000); + let operator3 = Uint128::new(12_345_000_000); - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("alice_d1", &[coin(8000_000000, TEST_COIN_DENOM)]), - node_identity.clone(), - ) - .unwrap(); + let mut test = TestSetup::new(); + let mix_id1 = test.add_dummy_mixnode("mix-owner1", Some(operator1)); + let mix_id2 = test.add_dummy_mixnode("mix-owner2", Some(operator2)); + let mix_id3 = test.add_dummy_mixnode("mix-owner3", Some(operator3)); - try_delegate_to_mixnode( - deps.as_mut(), - mock_env(), - mock_info("alice_d2", &[coin(2000_000000, TEST_COIN_DENOM)]), - node_identity.clone(), - ) - .unwrap(); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id1, mix_id2, mix_id3]); + let performance = test_helpers::performance(98.0); - crate::delegations::transactions::_try_reconcile_all_delegation_events( - &mut deps.storage, - &deps.api, - ) - .unwrap(); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); - let info = mock_info(rewarding_validator_address.as_ref(), &[]); - env.block.height += 2 * constants::MINIMUM_BLOCK_AGE_FOR_REWARDING; + 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 mix_1 = mixnodes_storage::read_full_mixnode_bond(&deps.storage, &node_identity) - .unwrap() - .unwrap(); - let mix_1_uptime = 90; + // repeat the rewarding the same set of delegates for few epochs + for _ in 0..10 { + for &mix_id in &[mix_id1, mix_id2, mix_id3] { + let mut sim = test.instantiate_simulator(mix_id); + let dist = test.reward_with_distribution(mix_id, performance); + let node_params = NodeRewardParams { + performance, + in_active_set: true, + }; + let sim_res = sim.simulate_epoch_single_node(node_params); + assert_eq!(sim_res, dist); + } + test.skip_to_next_epoch_end(); + } - let epoch = Interval::init_epoch(env.clone()); - save_epoch(&mut deps.storage, &epoch).unwrap(); - save_epoch_reward_params(epoch.id(), &mut deps.storage).unwrap(); + // 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_immediate_delegation("delegator1", Uint128::new(50_000_000), mix_id1); + test.add_immediate_delegation("delegator1", Uint128::new(200_000_000), mix_id2); - let interval_reward_params = current_epoch_reward_params(&deps.storage).unwrap(); + test.add_immediate_delegation("delegator5", Uint128::new(123_000_000), mix_id3); + test.add_immediate_delegation("delegator6", Uint128::new(456_000_000), mix_id3); - // ======================= - // TODO: this is a temporary 'workaround' until we can produce "good" numbers for if the rewards - // were calculated based of 100M staking supply as opposed to original 750M circulating supply - let interval_reward_params = EpochRewardParams::new( - interval_reward_params.epoch_reward_pool(), - interval_reward_params.rewarded_set_size(), - interval_reward_params.active_set_size(), - circulating_supply, - interval_reward_params.sybil_resistance_percent(), - interval_reward_params.active_set_work_factor(), - ); + let performance = test_helpers::performance(12.3); + for _ in 0..10 { + for &mix_id in &[mix_id1, mix_id2, mix_id3] { + let mut sim = test.instantiate_simulator(mix_id); + let dist = test.reward_with_distribution(mix_id, performance); + let node_params = NodeRewardParams { + performance, + in_active_set: true, + }; + let sim_res = sim.simulate_epoch_single_node(node_params); + assert_eq!(sim_res, dist); + } + test.skip_to_next_epoch_end(); + } + } - // this one repeats internals of `save_epoch_reward_params` but with 750M staking supply as opposed to 100M - crate::interval::storage::CURRENT_EPOCH_REWARD_PARAMS - .save(&mut deps.storage, &interval_reward_params) - .unwrap(); - crate::rewards::storage::EPOCH_REWARD_PARAMS - .save(&mut deps.storage, epoch.id(), &interval_reward_params) - .unwrap(); - // ======================= + #[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 node_reward_params = NodeRewardParams::new(0, mix_1_uptime, true); + let mut test = TestSetup::new(); + let sender = test.rewarding_validator(); - let mut params = RewardParams::new(interval_reward_params, node_reward_params); + let mix_id1 = test.add_dummy_mixnode("mix-owner1", Some(operator1)); + let mix_id2 = test.add_dummy_mixnode("mix-owner2", Some(operator2)); - params.set_reward_blockstamp(env.block.height); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id1, mix_id2]); + let performance = test_helpers::performance(98.0); - assert_eq!(params.performance(), U128::from_num(0.8999999999999999f64)); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id1); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); - let mix_1_reward_result = mix_1.reward(¶ms); + test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id1); - assert_eq!( - mix_1_reward_result.sigma(), - U128::from_num(0.0000266666666666f64) - ); - assert_eq!( - mix_1_reward_result.lambda(), - U128::from_num(0.0000133333333333f64) - ); - assert_eq!(mix_1_reward_result.reward().int(), 233202u128); + let del11 = test.delegation(mix_id1, "delegator1", &None); + let del12 = test.delegation(mix_id1, "delegator2", &None); + let del21 = test.delegation(mix_id2, "delegator1", &None); - let base_operator_cost = current_operator_epoch_cost(&deps.storage).unwrap(); + 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); - assert_eq!( - mix_1.node_profit(¶ms, base_operator_cost).int(), - 183203u128 - ); + let node_params = NodeRewardParams { + performance, + in_active_set: true, + }; - assert_ne!( - mix_1_reward_result.reward(), - mix_1.node_profit(¶ms, base_operator_cost).int() - ); + let dist1 = sim1.simulate_epoch_single_node(node_params); + let dist2 = sim2.simulate_epoch_single_node(node_params); - let mix1_operator_reward = mix_1.operator_reward(¶ms, base_operator_cost); + let env = test.env(); - let mix1_delegator1_reward = - mix_1.reward_delegation(Uint128::new(8000_000000), ¶ms, base_operator_cost); + let actual_prior1 = test.mix_rewarding(mix_id1); + let actual_prior2 = test.mix_rewarding(mix_id2); - let mix1_delegator2_reward = - mix_1.reward_delegation(Uint128::new(2000_000000), ¶ms, base_operator_cost); + let res1 = try_reward_mixnode( + test.deps_mut(), + env.clone(), + sender.clone(), + mix_id1, + performance, + ) + .unwrap(); - assert_eq!(mix1_operator_reward, 150761); - assert_eq!(mix1_delegator1_reward, 65953); - assert_eq!(mix1_delegator2_reward, 16488); + let prior_delegates1: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + PRIOR_DELEGATES_KEY, + &res1, + ) + .parse() + .unwrap(); + assert_eq!(prior_delegates1, actual_prior1.delegates); - assert_eq!( - mix_1_reward_result.reward().int(), - mix1_operator_reward + mix1_delegator1_reward + mix1_delegator2_reward - ); + let delegates_reward1: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding), + DELEGATES_REWARD_KEY, + &res1, + ) + .parse() + .unwrap(); + assert_eq!(delegates_reward1, dist1.delegates); - assert_eq!( - mix1_operator_reward + mix1_delegator1_reward + mix1_delegator2_reward, - mix_1_reward_result.reward().int() - ); + 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); - let pre_reward_bond = - test_helpers::read_mixnode_pledge_amount(&deps.storage, &node_identity) - .unwrap() - .u128(); - assert_eq!(pre_reward_bond, 10_000_000_000); + // 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; - let pre_reward_delegation = mixnodes_storage::TOTAL_DELEGATION - .load(&deps.storage, &node_identity) - .unwrap() - .u128(); - assert_eq!(pre_reward_delegation, 10_000_000_000); + // 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); - try_reward_mixnode( - deps.as_mut(), - env, - info, - node_identity.clone(), - node_reward_params, - ) - .unwrap(); + let computed_del11_reward = + pre_rewarding_del11 / prior_delegates1 * delegates_reward1; - let mixnode = crate::mixnodes::storage::mixnodes() - .load(&deps.storage, &node_identity) - .unwrap(); + let pre_rewarding_del12 = del12.dec_amount() + + (prior_unit_reward - del12.cumulative_reward_ratio) * del12.dec_amount() + / (del12.cumulative_reward_ratio + unit_delegation_base); - assert_eq!( - test_helpers::read_mixnode_pledge_amount(&deps.storage, &node_identity) - .unwrap() - .u128() - + mixnode.accumulated_rewards().u128(), - pre_reward_bond - + mix1_operator_reward - + mix1_delegator1_reward - + mix1_delegator2_reward - ); + let computed_del12_reward = + pre_rewarding_del12 / prior_delegates1 * delegates_reward1; - assert_eq!( - storage::REWARD_POOL.load(&deps.storage).unwrap().u128(), - INITIAL_REWARD_POOL - - (mix1_operator_reward + mix1_delegator1_reward + mix1_delegator2_reward) - ); + // sanity check + compare_decimals( + computed_del11_reward + computed_del12_reward, + delegates_reward1, + None, + ); - // it's all correctly saved - match storage::REWARDING_STATUS - .load(deps.as_ref().storage, (0u32, node_identity)) - .unwrap() - { - RewardingStatus::Complete(result) => assert_eq!( - RewardingResult { - node_reward: Uint128::new(mix_1_reward_result.reward().checked_cast().unwrap()), - }, - result - ), - _ => unreachable!(), + 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 delegator_rewarding_tx { + mod withdrawing_delegator_reward { use super::*; + use crate::interval::pending_events; + use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup}; + use cosmwasm_std::{BankMsg, CosmosMsg, Decimal, Uint128}; + use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; #[test] - fn cannot_be_called_if_mixnode_is_fully_rewarded() { - // everything was done in a single reward call - let mut deps = test_helpers::init_contract(); - let mut env = mock_env(); - let current_state = mixnet_params_storage::CONTRACT_STATE - .load(deps.as_mut().storage) - .unwrap(); - let rewarding_validator_address = current_state.rewarding_validator_address; + fn can_only_be_done_if_delegation_exists() { + let mut test = TestSetup::new(); + // add relatively huge stake so that the reward would be high enough to offset operating costs + let mix_id1 = + test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let mix_id2 = + test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); - let node_owner: Addr = Addr::unchecked("alice"); + let delegator1 = "delegator1"; + let delegator2 = "delegator2"; - #[allow(clippy::inconsistent_digit_grouping)] - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - coins(10000_000_000, TEST_COIN_DENOM), - deps.as_mut(), + let sender1 = mock_info(delegator1, &[]); + let sender2 = mock_info(delegator2, &[]); + + // note that there's no delegation from delegator1 towards mix1 + test.add_immediate_delegation(delegator2, 100_000_000u128, mix_id1); + + test.add_immediate_delegation(delegator1, 100_000_000u128, mix_id2); + test.add_immediate_delegation(delegator2, 100_000_000u128, mix_id2); + + // perform some rewarding so that we'd have non-zero rewards + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id1, mix_id2]); + test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); + test.reward_with_distribution(mix_id2, test_helpers::performance(100.0)); + + let res = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id1); + assert_eq!( + res, + Err(MixnetContractError::NoMixnodeDelegationFound { + mix_id: mix_id1, + address: delegator1.to_string(), + proxy: None, + }) ); - env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING; + // sanity check for other ones + let res = try_withdraw_delegator_reward(test.deps_mut(), sender1, mix_id2); + assert!(res.is_ok()); - let epoch = Interval::init_epoch(env.clone()); - save_epoch(&mut deps.storage, &epoch).unwrap(); - save_epoch_reward_params(epoch.id(), &mut deps.storage).unwrap(); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), mix_id1); + assert!(res.is_ok()); - let info = mock_info(rewarding_validator_address.as_ref(), &[]); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender2, mix_id2); + assert!(res.is_ok()); + } - try_reward_mixnode( - deps.as_mut(), - env.clone(), - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), - ) - .unwrap(); + #[test] + fn tokens_are_only_sent_if_reward_is_nonzero() { + let mut test = TestSetup::new(); + // add relatively huge stake so that the reward would be high enough to offset operating costs + let mix_id1 = + test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let mix_id2 = + test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); - // there was another page of delegators, but they were already dealt with - let node_owner: Addr = Addr::unchecked("bob"); + // very low stake so operating cost would be higher than total reward + let low_stake_id = + test.add_dummy_mixnode("mix-owner3", Some(Uint128::new(100_000_000))); - #[allow(clippy::inconsistent_digit_grouping)] - let node_identity = test_helpers::add_mixnode( - node_owner.as_str(), - coins(10000_000_000, TEST_COIN_DENOM), - deps.as_mut(), - ); + let delegator = "delegator"; + let sender = mock_info(delegator, &[]); - for i in 0..50 + 1 { - try_delegate_to_mixnode( - deps.as_mut(), - env.clone(), - mock_info( - &*format!("delegator{:04}", i), - &[coin(2000_000000, TEST_COIN_DENOM)], - ), - node_identity.clone(), - ) - .unwrap(); + test.add_immediate_delegation(delegator, 100_000_000u128, mix_id1); + test.add_immediate_delegation(delegator, 100_000_000u128, mix_id2); + test.add_immediate_delegation(delegator, 1_000u128, low_stake_id); + + // reward mix1, but don't reward mix2 + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id1, low_stake_id]); + test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); + test.reward_with_distribution(low_stake_id, test_helpers::performance(100.0)); + + let res1 = + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id1).unwrap(); + assert!(matches!( + &res1.messages[0].msg, + CosmosMsg::Bank(BankMsg::Send { to_address, amount }) if to_address == delegator && !amount[0].amount.is_zero() + ),); + + let res2 = + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id2).unwrap(); + assert!(res2.messages.is_empty()); + + let res3 = + try_withdraw_delegator_reward(test.deps_mut(), sender, low_stake_id).unwrap(); + assert!(res3.messages.is_empty()); + } + + #[test] + fn can_only_be_done_for_fully_bonded_nodes() { + // note: if node has unbonded or is in the process of unbonding, the expected + // way of getting back the rewards is to completely undelegate + let mut test = TestSetup::new(); + let mix_id_unbonding = + test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let mix_id_unbonded_leftover = + test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + + let delegator = "delegator"; + let sender = mock_info(delegator, &[]); + + test.add_immediate_delegation(delegator, 100_000_000u128, mix_id_unbonding); + test.add_immediate_delegation(delegator, 100_000_000u128, mix_id_unbonded_leftover); + + let performance = test_helpers::performance(100.0); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]); + + // go through few rewarding cycles before unbonding nodes (partially or fully) + for _ in 0..10 { + test.reward_with_distribution(mix_id_unbonding, performance); + test.reward_with_distribution(mix_id_unbonded_leftover, performance); + + test.skip_to_next_epoch_end(); } - env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING; - test_helpers::update_env_and_progress_interval(&mut env, deps.as_mut().storage); + // start unbonding the first node and fully unbond the other + let mut bond = mixnodes_storage::mixnode_bonds() + .load(test.deps().storage, mix_id_unbonding) + .unwrap(); + bond.is_unbonding = true; + mixnodes_storage::mixnode_bonds() + .save(test.deps_mut().storage, mix_id_unbonding, &bond) + .unwrap(); - let info = mock_info(rewarding_validator_address.as_ref(), &[]); - try_reward_mixnode( - deps.as_mut(), - env, - info.clone(), - node_identity.clone(), - tests::fixtures::node_reward_params_fixture(100), + let env = test.env(); + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) + .unwrap(); + + let res = + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id_unbonding); + assert_eq!( + res, + Err(MixnetContractError::MixnodeIsUnbonding { + mix_id: mix_id_unbonding + }) + ); + + let res = + try_withdraw_delegator_reward(test.deps_mut(), sender, mix_id_unbonded_leftover); + assert_eq!( + res, + Err(MixnetContractError::MixnodeHasUnbonded { + mix_id: mix_id_unbonded_leftover + }) + ); + } + + #[test] + fn correctly_determines_earned_share_and_resets_reward_ratio() { + let mut test = TestSetup::new(); + let mix_id_single = + test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let mix_id_quad = + test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + + let delegator1 = "delegator1"; + let delegator2 = "delegator2"; + let delegator3 = "delegator3"; + let delegator4 = "delegator4"; + let sender1 = mock_info(delegator1, &[]); + let sender2 = mock_info(delegator2, &[]); + let sender3 = mock_info(delegator3, &[]); + let sender4 = mock_info(delegator4, &[]); + + let amount_single = 100_000_000u128; + + let amount_quad1 = 50_000_000u128; + let amount_quad2 = 200_000_000u128; + let amount_quad3 = 250_000_000u128; + let amount_quad4 = 500_000_000u128; + + test.add_immediate_delegation(delegator1, amount_single, mix_id_single); + + test.add_immediate_delegation(delegator1, amount_quad1, mix_id_quad); + test.add_immediate_delegation(delegator2, amount_quad2, mix_id_quad); + test.add_immediate_delegation(delegator3, amount_quad3, mix_id_quad); + test.add_immediate_delegation(delegator4, amount_quad4, mix_id_quad); + + let performance = test_helpers::performance(100.0); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_single, mix_id_quad]); + + // accumulate some rewards + let mut accumulated_single = Decimal::zero(); + let mut accumulated_quad = Decimal::zero(); + for _ in 0..10 { + let dist = test.reward_with_distribution(mix_id_single, performance); + // sanity check to make sure test is actually doing what it's supposed to be doing + assert!(!dist.delegates.is_zero()); + + accumulated_single += dist.delegates; + let dist = test.reward_with_distribution(mix_id_quad, performance); + accumulated_quad += dist.delegates; + + test.skip_to_next_epoch_end(); + } + + let before = test.read_delegation(mix_id_single, delegator1, None); + assert_eq!(before.cumulative_reward_ratio, Decimal::zero()); + let res1 = + try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id_single) + .unwrap(); + let (_, reward) = test_helpers::get_bank_send_msg(&res1).unwrap(); + assert_eq!(truncate_reward_amount(accumulated_single), reward[0].amount); + let after = test.read_delegation(mix_id_single, delegator1, None); + assert_ne!(after.cumulative_reward_ratio, Decimal::zero()); + assert_eq!( + after.cumulative_reward_ratio, + test.mix_rewarding(mix_id_single).total_unit_reward + ); + + // withdraw first two rewards. note that due to scaling we expect second reward to be 4x the first one + let before1 = test.read_delegation(mix_id_quad, delegator1, None); + let before2 = test.read_delegation(mix_id_quad, delegator2, None); + assert_eq!(before1.cumulative_reward_ratio, Decimal::zero()); + assert_eq!(before2.cumulative_reward_ratio, Decimal::zero()); + let res1 = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id_quad) + .unwrap(); + let (_, reward1) = test_helpers::get_bank_send_msg(&res1).unwrap(); + let res2 = try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), mix_id_quad) + .unwrap(); + let (_, reward2) = test_helpers::get_bank_send_msg(&res2).unwrap(); + // the seeming "error" comes from reward truncation, + // say "actual" reward1 was `100.9`, while reward2 was 4x that, i.e. `403.6 + // however, upon truncating and conversion to coins they'd be `100` and `403` respectively + // (clearly no longer holding the 4x ratio exactly), but this is NOT a bug, + // this is the expected behaviour. + // what we must assert here is that |a - b| <= ratio, rather than just a == b + // assert_eq!(reward1[0].amount * Uint128::new(4), reward2[0].amount); + assert_eq_with_leeway( + reward1[0].amount * Uint128::new(4), + reward2[0].amount, + Uint128::new(4), + ); + + let after1 = test.read_delegation(mix_id_quad, delegator1, None); + assert_ne!(after1.cumulative_reward_ratio, Decimal::zero()); + assert_eq!( + after1.cumulative_reward_ratio, + test.mix_rewarding(mix_id_quad).total_unit_reward + ); + let after2 = test.read_delegation(mix_id_quad, delegator2, None); + assert_ne!(after2.cumulative_reward_ratio, Decimal::zero()); + assert_eq!( + after2.cumulative_reward_ratio, + test.mix_rewarding(mix_id_quad).total_unit_reward + ); + + // accumulate some more + for _ in 0..10 { + let dist = test.reward_with_distribution(mix_id_quad, performance); + accumulated_quad += dist.delegates; + test.skip_to_next_epoch_end(); + } + + let before1_new = test.read_delegation(mix_id_quad, delegator1, None); + let before2_new = test.read_delegation(mix_id_quad, delegator2, None); + let before3 = test.read_delegation(mix_id_quad, delegator3, None); + let before4 = test.read_delegation(mix_id_quad, delegator4, None); + + assert_eq!( + before1_new.cumulative_reward_ratio, + after1.cumulative_reward_ratio + ); + assert_eq!( + before2_new.cumulative_reward_ratio, + after2.cumulative_reward_ratio + ); + assert_eq!(before3.cumulative_reward_ratio, Decimal::zero()); + assert_eq!(before4.cumulative_reward_ratio, Decimal::zero()); + + let res1 = + try_withdraw_delegator_reward(test.deps_mut(), sender1, mix_id_quad).unwrap(); + let (_, reward1_new) = test_helpers::get_bank_send_msg(&res1).unwrap(); + let res2 = + try_withdraw_delegator_reward(test.deps_mut(), sender2, mix_id_quad).unwrap(); + let (_, reward2_new) = test_helpers::get_bank_send_msg(&res2).unwrap(); + + // the ratio between first and second delegator is still the same + assert_eq_with_leeway( + reward1_new[0].amount * Uint128::new(4), + reward2_new[0].amount, + Uint128::new(4), + ); + + let res3 = + try_withdraw_delegator_reward(test.deps_mut(), sender3, mix_id_quad).unwrap(); + let (_, reward3) = test_helpers::get_bank_send_msg(&res3).unwrap(); + let res4 = + try_withdraw_delegator_reward(test.deps_mut(), sender4, mix_id_quad).unwrap(); + let (_, reward4) = test_helpers::get_bank_send_msg(&res4).unwrap(); + + // (and so is the ratio between 3rd and 4th) + assert_eq_with_leeway( + reward3[0].amount * Uint128::new(2), + reward4[0].amount, + Uint128::new(2), + ); + + // and finally the total distributed equals to total reward claimed + let total_claimed = reward1[0].amount + + reward2[0].amount + + reward1_new[0].amount + + reward2_new[0].amount + + reward3[0].amount + + reward4[0].amount; + + // we're adding 6 values together so our leeway can be at most 6 + let accumulated_actual = truncate_reward_amount(accumulated_quad); + assert_eq_with_leeway(total_claimed, accumulated_actual, Uint128::new(6)); + } + } + + #[cfg(test)] + mod withdrawing_operator_reward { + use super::*; + use crate::interval::pending_events; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::{BankMsg, CosmosMsg, Uint128}; + + #[test] + fn can_only_be_done_if_bond_exists() { + let mut test = TestSetup::new(); + + let owner = "mix-owner"; + let mix_id = test.add_dummy_mixnode(owner, Some(Uint128::new(1_000_000_000_000))); + let sender = mock_info("random-guy", &[]); + + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id]); + test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender.clone()); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: sender.sender + }) ) - .unwrap(); + } + + #[test] + fn tokens_are_only_sent_if_reward_is_nonzero() { + let mut test = TestSetup::new(); + + let owner1 = "mix-owner1"; + let owner2 = "mix-owner2"; + let mix_id1 = test.add_dummy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); + test.add_dummy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); + + let sender1 = mock_info(owner1, &[]); + let sender2 = mock_info(owner2, &[]); + + // reward mix1, but don't reward mix2 + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id1]); + test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); + + let res1 = try_withdraw_operator_reward(test.deps_mut(), sender1).unwrap(); + assert!(matches!( + &res1.messages[0].msg, + CosmosMsg::Bank(BankMsg::Send { to_address, amount }) if to_address == owner1 && !amount[0].amount.is_zero() + ),); + + let res2 = try_withdraw_operator_reward(test.deps_mut(), sender2).unwrap(); + assert!(res2.messages.is_empty()); + } + + #[test] + fn can_only_be_done_for_fully_bonded_nodes() { + // note: if node has unbonded or is in the process of unbonding, the expected + // way of getting back the rewards is finish the undelegation + let mut test = TestSetup::new(); + let owner1 = "mix-owner1"; + let owner2 = "mix-owner2"; + let sender1 = mock_info(owner1, &[]); + let sender2 = mock_info(owner2, &[]); + let mix_id_unbonding = + test.add_dummy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); + let mix_id_unbonded_leftover = + test.add_dummy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); + + // add some delegation to the second node so that it wouldn't be cleared upon unbonding + test.add_immediate_delegation("delegator", 100_000_000u128, mix_id_unbonded_leftover); + + let performance = test_helpers::performance(100.0); + test.skip_to_next_epoch_end(); + test.update_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]); + + // go through few rewarding cycles before unbonding nodes (partially or fully) + for _ in 0..10 { + test.reward_with_distribution(mix_id_unbonding, performance); + test.reward_with_distribution(mix_id_unbonded_leftover, performance); + + test.skip_to_next_epoch_end(); + } + + // start unbonding the first node and fully unbond the other + let mut bond = mixnodes_storage::mixnode_bonds() + .load(test.deps().storage, mix_id_unbonding) + .unwrap(); + bond.is_unbonding = true; + mixnodes_storage::mixnode_bonds() + .save(test.deps_mut().storage, mix_id_unbonding, &bond) + .unwrap(); + + let env = test.env(); + 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 { + mix_id: mix_id_unbonding + }) + ); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender2); + assert_eq!( + res, + Err(MixnetContractError::NoAssociatedMixNodeBond { + owner: Addr::unchecked(owner2) + }) + ); + } + } + + #[cfg(test)] + mod updating_active_set { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + + #[test] + fn can_only_be_done_by_contract_owner() { + let mut test = TestSetup::new(); + + let rewarding_validator = test.rewarding_validator(); + let owner = test.owner(); + let random = mock_info("random-guy", &[]); + + let env = test.env(); + let res = try_update_active_set_size( + test.deps_mut(), + env.clone(), + rewarding_validator, + 42, + false, + ); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + let res = try_update_active_set_size(test.deps_mut(), env.clone(), random, 42, false); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + assert!(res.is_ok()) + } + + #[test] + fn new_size_cant_be_bigger_than_rewarded_set() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let rewarded_set_size = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .rewarded_set_size; + + let env = test.env(); + let res = try_update_active_set_size( + test.deps_mut(), + env, + owner.clone(), + rewarded_set_size + 1, + false, + ); + assert_eq!(res, Err(MixnetContractError::InvalidActiveSetSize)); + + // if its equal, its fine + // (make sure we start with the fresh state) + let mut test = TestSetup::new(); + let env = test.env(); + let res = try_update_active_set_size( + test.deps_mut(), + env, + owner.clone(), + rewarded_set_size, + false, + ); + assert!(res.is_ok()); + + // as well as if its any value lower than that + let mut test = TestSetup::new(); + let env = test.env(); + let res = try_update_active_set_size( + test.deps_mut(), + env, + owner, + rewarded_set_size - 100, + false, + ); + assert!(res.is_ok()); + } + + #[test] + fn if_interval_is_finished_change_happens_immediately() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + test.skip_to_current_interval_end(); + let env = test.env(); + let res = try_update_active_set_size(test.deps_mut(), env, owner.clone(), 42, false); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + assert_ne!(old, new); + assert_eq!(new, 42); + + // sanity check for "normal" case + let mut test = TestSetup::new(); + let env = test.env(); + let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + assert_eq!(old, new); + } + + #[test] + fn if_update_is_forced_it_happens_immediately() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + let env = test.env(); + let res = try_update_active_set_size(test.deps_mut(), env, owner.clone(), 42, true); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + assert_ne!(old, new); + assert_eq!(new, 42); + } + + #[test] + fn without_forcing_it_change_happens_upon_clearing_epoch_events() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + let env = test.env(); + let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + assert_eq!(old, new); + + // make sure it's actually saved to pending events + let events = test.pending_epoch_events(); + assert!( + matches!(events[0].kind, PendingEpochEventKind::UpdateActiveSetSize { new_size } if new_size == 42) + ); + + test.execute_all_pending_events(); + let new = storage::REWARDING_PARAMS + .load(test.deps().storage) + .unwrap() + .active_set_size; + assert_ne!(old, new); + assert_eq!(new, 42); + } + } + + #[cfg(test)] + mod updating_rewarding_params { + use super::*; + use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; + use cosmwasm_std::Decimal; + + #[test] + fn can_only_be_done_by_contract_owner() { + let mut test = TestSetup::new(); + + let rewarding_validator = test.rewarding_validator(); + let owner = test.owner(); + let random = mock_info("random-guy", &[]); + + let update = IntervalRewardingParamsUpdate { + reward_pool: None, + staking_supply: None, + staking_supply_scale_factor: None, + sybil_resistance_percent: None, + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: Some(123), + }; + + let env = test.env(); + let res = try_update_rewarding_params( + test.deps_mut(), + env.clone(), + rewarding_validator, + update, + false, + ); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + let res = + try_update_rewarding_params(test.deps_mut(), env.clone(), random, update, false); + assert_eq!(res, Err(MixnetContractError::Unauthorized)); + + let res = try_update_rewarding_params(test.deps_mut(), env, owner, update, false); + assert!(res.is_ok()) + } + + #[test] + fn request_must_contain_at_least_single_update() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let empty_update = IntervalRewardingParamsUpdate { + reward_pool: None, + staking_supply: None, + staking_supply_scale_factor: None, + sybil_resistance_percent: None, + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: None, + }; + + let env = test.env(); + let res = try_update_rewarding_params(test.deps_mut(), env, owner, empty_update, false); + assert_eq!(res, Err(MixnetContractError::EmptyParamsChangeMsg)); + } + + #[test] + fn if_interval_is_finished_change_happens_immediately() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let update = IntervalRewardingParamsUpdate { + reward_pool: None, + staking_supply: None, + staking_supply_scale_factor: None, + sybil_resistance_percent: None, + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: Some(123), + }; + + let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + test.skip_to_current_interval_end(); + + let env = test.env(); + let res = + try_update_rewarding_params(test.deps_mut(), env, owner.clone(), update, false); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + assert_ne!(old, new); + assert_eq!(new.rewarded_set_size, 123); + + // sanity check for "normal" case + let mut test = TestSetup::new(); + let env = test.env(); + let res = try_update_rewarding_params(test.deps_mut(), env, owner, update, false); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + assert_eq!(old, new); + } + + #[test] + fn if_update_is_forced_it_happens_immediately() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let update = IntervalRewardingParamsUpdate { + reward_pool: None, + staking_supply: None, + staking_supply_scale_factor: None, + sybil_resistance_percent: None, + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: Some(123), + }; + + let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + let env = test.env(); + let res = + try_update_rewarding_params(test.deps_mut(), env, owner.clone(), update, true); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + assert_ne!(old, new); + assert_eq!(new.rewarded_set_size, 123); + } + + #[test] + fn without_forcing_it_change_happens_upon_clearing_interval_events() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let update = IntervalRewardingParamsUpdate { + reward_pool: None, + staking_supply: None, + staking_supply_scale_factor: None, + sybil_resistance_percent: None, + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: Some(123), + }; + + let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + let env = test.env(); + let res = try_update_rewarding_params(test.deps_mut(), env, owner, update, false); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + assert_eq!(old, new); + + // make sure it's actually saved to pending events + let events = test.pending_interval_events(); + assert!( + matches!(events[0].kind,PendingIntervalEventKind::UpdateRewardingParams { update } if update.rewarded_set_size == Some(123)) + ); + + test.execute_all_pending_events(); + let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + assert_ne!(old, new); + assert_eq!(new.rewarded_set_size, 123); + } + + #[test] + fn upon_update_fields_are_recomputed_accordingly() { + let mut test = TestSetup::new(); + let owner = test.owner(); + + let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + + let two = Decimal::from_atomics(2u32, 0).unwrap(); + let four = Decimal::from_atomics(4u32, 0).unwrap(); + + // TODO: be more fuzzy about it and try to vary other fields that can cause + // re-computation like pool emission or rewarded set size update + let update = IntervalRewardingParamsUpdate { + reward_pool: Some(old.interval.reward_pool / two), + staking_supply: Some(old.interval.staking_supply * four), + staking_supply_scale_factor: None, + sybil_resistance_percent: None, + active_set_work_factor: None, + interval_pool_emission: None, + rewarded_set_size: None, + }; + + let env = test.env(); + let res = try_update_rewarding_params(test.deps_mut(), env, owner, update, true); + assert!(res.is_ok()); + let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); + + // with half the reward pool, our reward budget is also halved + assert_decimals( + old.interval.epoch_reward_budget, + two * new.interval.epoch_reward_budget, + ); + + // and with 4x the staking supply, the saturation point is also increased 4-folds + assert_decimals( + four * old.interval.stake_saturation_point, + new.interval.stake_saturation_point, + ); } } } diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index 7776e5522b..b4ed7f3fd2 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -1,59 +1,162 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::{DEFAULT_OPERATOR_INTERVAL_COST, INTERVAL_SECONDS}; -use crate::interval::storage::{current_epoch, EPOCHS}; +use crate::gateways::storage as gateways_storage; use crate::mixnodes::storage as mixnodes_storage; -use crate::{constants, gateways::storage as gateways_storage}; +use cosmwasm_std::{Addr, BankMsg, Coin, CosmosMsg, Deps, Response, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::{IdentityKeyRef, MixNodeBond}; -use crate::error::ContractError; -use cosmwasm_std::{Addr, Deps, Storage}; -use mixnet_contract_common::{reward_params::EpochRewardParams, IdentityKeyRef}; +// helper trait to attach `Msg` to a response if it's provided +pub(crate) trait AttachOptionalMessage { + fn add_optional_message(self, msg: Option>>) -> Self; +} -pub(crate) fn is_authorized(sender: String, storage: &dyn Storage) -> Result<(), ContractError> { +impl AttachOptionalMessage for Response { + fn add_optional_message(self, msg: Option>>) -> Self { + if let Some(msg) = msg { + self.add_message(msg) + } else { + self + } + } +} + +// pub fn debug_with_visibility>(api: &dyn Api, msg: S) { +// api.debug(&*format!("\n\n\n=========================================\n{}\n=========================================\n\n\n", msg.into())); +// } + +/// Attempts to construct a `BankMsg` to send specified tokens to the provided +/// proxy address. If that's unavailable, the `BankMsg` will use the "owner" as the +/// "to_address". +pub(crate) fn send_to_proxy_or_owner( + proxy: &Option, + owner: &Addr, + amount: Vec, +) -> BankMsg { + BankMsg::Send { + to_address: proxy.as_ref().unwrap_or(owner).to_string(), + amount, + } +} + +pub(crate) fn validate_pledge( + mut pledge: Vec, + minimum_pledge: Coin, +) -> Result { + // check if anything was put as bond + if pledge.is_empty() { + return Err(MixnetContractError::NoBondFound); + } + + if pledge.len() > 1 { + return Err(MixnetContractError::MultipleDenoms); + } + + // check that the denomination is correct + if pledge[0].denom != minimum_pledge.denom { + return Err(MixnetContractError::WrongDenom { + received: pledge[0].denom.clone(), + expected: minimum_pledge.denom, + }); + } + + // check that the pledge contains the minimum amount of tokens + if pledge[0].amount < minimum_pledge.amount { + return Err(MixnetContractError::InsufficientPledge { + received: pledge[0].clone(), + minimum: minimum_pledge, + }); + } + + Ok(pledge.pop().unwrap()) +} + +pub(crate) fn validate_delegation_stake( + mut delegation: Vec, + minimum_delegation: Option, + expected_denom: String, +) -> Result { + // check if anything was put as delegation + if delegation.is_empty() { + return Err(MixnetContractError::EmptyDelegation); + } + + if delegation.len() > 1 { + return Err(MixnetContractError::MultipleDenoms); + } + + // check that the denomination is correct + if delegation[0].denom != expected_denom { + return Err(MixnetContractError::WrongDenom { + received: delegation[0].denom.clone(), + expected: expected_denom, + }); + } + + // if we have a minimum set, check if enough tokens were sent, otherwise just check if its non-zero + if let Some(minimum_delegation) = minimum_delegation { + if delegation[0].amount < minimum_delegation.amount { + return Err(MixnetContractError::InsufficientDelegation { + received: delegation[0].clone(), + minimum: minimum_delegation, + }); + } + } else if delegation[0].amount.is_zero() { + return Err(MixnetContractError::EmptyDelegation); + } + + Ok(delegation.pop().unwrap()) +} + +pub(crate) fn ensure_is_authorized( + sender: Addr, + storage: &dyn Storage, +) -> Result<(), MixnetContractError> { if sender != crate::mixnet_contract_settings::storage::rewarding_validator_address(storage)? { - return Err(ContractError::Unauthorized); + return Err(MixnetContractError::Unauthorized); } Ok(()) } -pub fn epochs_in_interval(storage: &dyn Storage) -> Result { - let epoch = current_epoch(storage)?; - Ok(INTERVAL_SECONDS / epoch.length_secs()) -} - -#[allow(dead_code)] -pub fn current_operator_epoch_cost(storage: &dyn Storage) -> Result { - Ok(DEFAULT_OPERATOR_INTERVAL_COST / epochs_in_interval(storage)?) -} - -pub fn operator_cost_at_epoch(storage: &dyn Storage, epoch_id: u32) -> Result { - let epoch = EPOCHS.load(storage, epoch_id)?; - // This is historical, so we can't use the function defined above - let epochs_in_interval = INTERVAL_SECONDS / epoch.length_secs(); - Ok(DEFAULT_OPERATOR_INTERVAL_COST / epochs_in_interval) -} - -pub(crate) fn epoch_reward_params( +pub(crate) fn ensure_is_owner( + sender: Addr, storage: &dyn Storage, -) -> Result { - let state = crate::mixnet_contract_settings::storage::CONTRACT_STATE - .load(storage) - .map(|settings| settings.params)?; - let reward_pool = crate::rewards::storage::REWARD_POOL.load(storage)?; - let interval_reward_percent = crate::constants::INTERVAL_REWARD_PERCENT; - let epochs_in_interval = epochs_in_interval(storage)?; +) -> Result<(), MixnetContractError> { + if sender + != crate::mixnet_contract_settings::storage::CONTRACT_STATE + .load(storage)? + .owner + { + return Err(MixnetContractError::Unauthorized); + } + Ok(()) +} - let epoch_reward_params = EpochRewardParams::new( - (reward_pool.u128() / 100 / epochs_in_interval as u128) * interval_reward_percent as u128, - state.mixnode_rewarded_set_size as u128, - state.mixnode_active_set_size as u128, - state.staking_supply.u128(), - constants::SYBIL_RESISTANCE_PERCENT, - constants::ACTIVE_SET_WORK_FACTOR, - ); +pub(crate) fn ensure_proxy_match( + actual: &Option, + expected: &Option, +) -> Result<(), MixnetContractError> { + if actual != expected { + return Err(MixnetContractError::ProxyMismatch { + existing: expected + .as_ref() + .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), + incoming: actual + .as_ref() + .map_or_else(|| "None".to_string(), |a| a.as_str().to_string()), + }); + } + Ok(()) +} - Ok(epoch_reward_params) +pub(crate) fn ensure_bonded(bond: &MixNodeBond) -> Result<(), MixnetContractError> { + if bond.is_unbonding { + return Err(MixnetContractError::MixnodeIsUnbonding { + mix_id: bond.mix_id, + }); + } + Ok(()) } // check if the target address has already bonded a mixnode or gateway, @@ -61,14 +164,14 @@ pub(crate) fn epoch_reward_params( pub(crate) fn ensure_no_existing_bond( storage: &dyn Storage, sender: &Addr, -) -> Result<(), ContractError> { - if mixnodes_storage::mixnodes() +) -> Result<(), MixnetContractError> { + if mixnodes_storage::mixnode_bonds() .idx .owner .item(storage, sender.clone())? .is_some() { - return Err(ContractError::AlreadyOwnsMixnode); + return Err(MixnetContractError::AlreadyOwnsMixnode); } if gateways_storage::gateways() @@ -77,7 +180,7 @@ pub(crate) fn ensure_no_existing_bond( .item(storage, sender.clone())? .is_some() { - return Err(ContractError::AlreadyOwnsGateway); + return Err(MixnetContractError::AlreadyOwnsGateway); } Ok(()) @@ -88,7 +191,7 @@ pub(crate) fn validate_node_identity_signature( owner: &Addr, signature: String, identity: IdentityKeyRef<'_>, -) -> Result<(), ContractError> { +) -> Result<(), MixnetContractError> { let owner_bytes = owner.as_bytes(); let mut identity_bytes = [0u8; 32]; @@ -96,20 +199,20 @@ pub(crate) fn validate_node_identity_signature( let identity_used_bytes = bs58::decode(identity) .into(&mut identity_bytes) - .map_err(|err| ContractError::MalformedEd25519IdentityKey(err.to_string()))?; + .map_err(|err| MixnetContractError::MalformedEd25519IdentityKey(err.to_string()))?; let signature_used_bytes = bs58::decode(signature) .into(&mut signature_bytes) - .map_err(|err| ContractError::MalformedEd25519Signature(err.to_string()))?; + .map_err(|err| MixnetContractError::MalformedEd25519Signature(err.to_string()))?; if identity_used_bytes != 32 { - return Err(ContractError::MalformedEd25519IdentityKey( - "Too few bytes provided".into(), + return Err(MixnetContractError::MalformedEd25519IdentityKey( + "Too few bytes provided for the public key".into(), )); } if signature_used_bytes != 64 { - return Err(ContractError::MalformedEd25519Signature( - "Too few bytes provided".into(), + return Err(MixnetContractError::MalformedEd25519Signature( + "Too few bytes provided for the signature".into(), )); } @@ -118,7 +221,7 @@ pub(crate) fn validate_node_identity_signature( .ed25519_verify(owner_bytes, &signature_bytes, &identity_bytes) .map_err(cosmwasm_std::StdError::verification_err)?; if !res { - Err(ContractError::InvalidEd25519Signature) + Err(MixnetContractError::InvalidEd25519Signature) } else { Ok(()) } @@ -162,7 +265,7 @@ mod tests { .to_base58_string(); assert_eq!( - Err(ContractError::MalformedEd25519IdentityKey( + Err(MixnetContractError::MalformedEd25519IdentityKey( "buffer provided to decode base58 encoded string into was too small".into() )), validate_node_identity_signature( @@ -174,7 +277,7 @@ mod tests { ); assert_eq!( - Err(ContractError::MalformedEd25519Signature( + Err(MixnetContractError::MalformedEd25519Signature( "buffer provided to decode base58 encoded string into was too small".into() )), validate_node_identity_signature( @@ -186,8 +289,8 @@ mod tests { ); assert_eq!( - Err(ContractError::MalformedEd25519IdentityKey( - "Too few bytes provided".into() + Err(MixnetContractError::MalformedEd25519IdentityKey( + "Too few bytes provided for the public key".into() )), validate_node_identity_signature( deps.as_ref(), @@ -198,8 +301,8 @@ mod tests { ); assert_eq!( - Err(ContractError::MalformedEd25519Signature( - "Too few bytes provided".into() + Err(MixnetContractError::MalformedEd25519Signature( + "Too few bytes provided for the signature".into() )), validate_node_identity_signature( deps.as_ref(), @@ -210,7 +313,7 @@ mod tests { ); assert_eq!( - Err(ContractError::InvalidEd25519Signature), + Err(MixnetContractError::InvalidEd25519Signature), validate_node_identity_signature( deps.as_ref(), &address1, @@ -220,7 +323,7 @@ mod tests { ); assert_eq!( - Err(ContractError::InvalidEd25519Signature), + Err(MixnetContractError::InvalidEd25519Signature), validate_node_identity_signature( deps.as_ref(), &address1, @@ -230,7 +333,7 @@ mod tests { ); assert_eq!( - Err(ContractError::InvalidEd25519Signature), + Err(MixnetContractError::InvalidEd25519Signature), validate_node_identity_signature( deps.as_ref(), &address2, @@ -240,7 +343,7 @@ mod tests { ); assert_eq!( - Err(ContractError::InvalidEd25519Signature), + Err(MixnetContractError::InvalidEd25519Signature), validate_node_identity_signature( deps.as_ref(), &address1, diff --git a/contracts/mixnet/src/support/tests/fixtures.rs b/contracts/mixnet/src/support/tests/fixtures.rs index 813a68e537..5a3d5e38a6 100644 --- a/contracts/mixnet/src/support/tests/fixtures.rs +++ b/contracts/mixnet/src/support/tests/fixtures.rs @@ -1,12 +1,9 @@ -use crate::contract::INITIAL_MIXNODE_PLEDGE; -use crate::mixnodes::storage as mixnodes_storage; -use crate::{mixnodes::storage::StoredMixnodeBond, support::tests}; -use cosmwasm_std::{coin, Addr, Coin}; -use mixnet_contract_common::reward_params::NodeRewardParams; -use mixnet_contract_common::{Gateway, GatewayBond, Layer, MixNode}; +use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; +use cosmwasm_std::{coin, Coin}; +use mixnet_contract_common::mixnode::MixNodeCostParams; +use mixnet_contract_common::{Gateway, MixNode, Percent}; pub const TEST_COIN_DENOM: &str = "unym"; -pub const TEST_REWARDING_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub fn mix_node_fixture() -> MixNode { MixNode { @@ -17,7 +14,13 @@ pub fn mix_node_fixture() -> MixNode { sphinx_key: "sphinx".to_string(), identity_key: "identity".to_string(), version: "0.10.0".to_string(), - profit_margin_percent: 10, + } +} + +pub fn mix_node_cost_params_fixture() -> MixNodeCostParams { + MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: coin(40_000_000, TEST_COIN_DENOM), } } @@ -33,51 +36,16 @@ pub fn gateway_fixture() -> Gateway { } } -pub fn gateway_bond_fixture(owner: &str) -> GatewayBond { - let gateway = Gateway { - identity_key: format!("id-{}", owner), - ..tests::fixtures::gateway_fixture() - }; - GatewayBond::new( - coin(50, TEST_COIN_DENOM), - Addr::unchecked(owner), - 12_345, - gateway, - None, - ) -} - -pub(crate) fn stored_mixnode_bond_fixture(owner: &str) -> mixnodes_storage::StoredMixnodeBond { - StoredMixnodeBond::new( - coin(50, TEST_COIN_DENOM), - Addr::unchecked(owner), - Layer::One, - 12_345, - MixNode { - identity_key: format!("id-{}", owner), - sphinx_key: format!("sphinx-{}", owner), - ..super::fixtures::mix_node_fixture() - }, - None, - None, - None, - ) -} - pub fn good_mixnode_pledge() -> Vec { vec![Coin { denom: TEST_COIN_DENOM.to_string(), - amount: INITIAL_MIXNODE_PLEDGE, + amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, }] } pub fn good_gateway_pledge() -> Vec { vec![Coin { denom: TEST_COIN_DENOM.to_string(), - amount: INITIAL_MIXNODE_PLEDGE, + amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, }] } - -pub fn node_reward_params_fixture(uptime: u128) -> NodeRewardParams { - NodeRewardParams::new(0, uptime, true) -} diff --git a/contracts/mixnet/src/support/tests/messages.rs b/contracts/mixnet/src/support/tests/messages.rs index 75cb210ba0..f3593b3aca 100644 --- a/contracts/mixnet/src/support/tests/messages.rs +++ b/contracts/mixnet/src/support/tests/messages.rs @@ -1,33 +1,13 @@ -use mixnet_contract_common::{ExecuteMsg, Gateway, IdentityKey, MixNode, SphinxKey}; -use rand::thread_rng; +use mixnet_contract_common::{ExecuteMsg, Gateway, IdentityKey}; +use rand_chacha::rand_core::{CryptoRng, RngCore}; use crate::support::tests; -pub(crate) fn valid_bond_mixnode_msg(sender: &str) -> (ExecuteMsg, (IdentityKey, SphinxKey)) { - let keypair = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); - let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut thread_rng()); - let owner_signature = keypair - .private_key() - .sign(sender.as_bytes()) - .to_base58_string(); - - let identity_key = keypair.public_key().to_base58_string(); - let sphinx_key = legit_sphinx_key.public_key().to_base58_string(); - ( - ExecuteMsg::BondMixnode { - mix_node: MixNode { - identity_key: identity_key.clone(), - sphinx_key: sphinx_key.clone(), - ..tests::fixtures::mix_node_fixture() - }, - owner_signature, - }, - (identity_key, sphinx_key), - ) -} - -pub(crate) fn valid_bond_gateway_msg(sender: &str) -> (ExecuteMsg, IdentityKey) { - let keypair = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); +pub(crate) fn valid_bond_gateway_msg( + mut rng: impl RngCore + CryptoRng, + sender: &str, +) -> (ExecuteMsg, IdentityKey) { + let keypair = crypto::asymmetric::identity::KeyPair::new(&mut rng); let owner_signature = keypair .private_key() .sign(sender.as_bytes()) diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index 19fe66dd35..a7cf4a3875 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -1,5 +1,6 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + #[cfg(test)] pub mod fixtures; #[cfg(test)] @@ -9,60 +10,736 @@ pub mod queries; #[cfg(test)] pub mod test_helpers { + use crate::constants; use crate::contract::instantiate; + use crate::delegations::queries::query_mixnode_delegations_paged; use crate::delegations::storage as delegations_storage; + use crate::delegations::transactions::try_delegate_to_mixnode; use crate::gateways::transactions::try_add_gateway; - use crate::interval; - use crate::interval::storage as interval_storage; + use crate::interval::transactions::{ + perform_pending_epoch_actions, perform_pending_interval_actions, + }; + use crate::interval::{pending_events, storage as interval_storage}; + use crate::mixnet_contract_settings::storage as mixnet_params_storage; + use crate::mixnet_contract_settings::storage::{ + minimum_mixnode_pledge, rewarding_denom, rewarding_validator_address, + }; use crate::mixnodes::storage as mixnodes_storage; - use crate::mixnodes::transactions::try_add_mixnode; + use crate::mixnodes::transactions::{ + try_add_mixnode, try_add_mixnode_on_behalf, try_remove_mixnode, + }; + use crate::rewards::storage as rewards_storage; + use crate::rewards::transactions::try_reward_mixnode; use crate::support::tests; + use crate::support::tests::fixtures::TEST_COIN_DENOM; use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::mock_info; use cosmwasm_std::testing::MockApi; use cosmwasm_std::testing::MockQuerier; - use cosmwasm_std::Coin; - use cosmwasm_std::DepsMut; - use cosmwasm_std::OwnedDeps; - use cosmwasm_std::{coin, Env, Timestamp}; - use cosmwasm_std::{Addr, StdResult, Storage}; - use cosmwasm_std::{Empty, MemoryStorage}; - use mixnet_contract_common::{Delegation, Gateway, IdentityKeyRef, InstantiateMsg, MixNode}; - use rand::thread_rng; + use cosmwasm_std::{Addr, BankMsg, CosmosMsg, Storage}; + use cosmwasm_std::{Coin, Order}; + use cosmwasm_std::{Decimal, Empty, MemoryStorage}; + use cosmwasm_std::{Deps, OwnedDeps}; + use cosmwasm_std::{DepsMut, MessageInfo}; + use cosmwasm_std::{Env, Response, Timestamp, Uint128}; + use mixnet_contract_common::events::{ + may_find_attribute, MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY, + }; + 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, MixId, MixNode, + Percent, RewardedSetNodeStatus, + }; + use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng}; + use rand_chacha::ChaCha20Rng; + use std::time::Duration; - use super::fixtures::TEST_COIN_DENOM; - use super::fixtures::TEST_REWARDING_VALIDATOR_ADDRESS; + pub fn assert_eq_with_leeway(a: Uint128, b: Uint128, leeway: Uint128) { + if a > b { + assert!(a - b <= leeway) + } else { + assert!(b - a <= leeway) + } + } - pub fn add_mixnode(sender: &str, stake: Vec, deps: DepsMut<'_>) -> String { - let keypair = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); + pub fn assert_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) + } + } + + pub struct TestSetup { + pub deps: OwnedDeps>, + pub env: Env, + pub rng: ChaCha20Rng, + + pub rewarding_validator: MessageInfo, + pub owner: MessageInfo, + } + + impl TestSetup { + pub fn new() -> Self { + let deps = init_contract(); + let rewarding_validator_address = + rewarding_validator_address(deps.as_ref().storage).unwrap(); + let owner = mixnet_params_storage::CONTRACT_STATE + .load(deps.as_ref().storage) + .unwrap() + .owner; + + TestSetup { + deps, + env: mock_env(), + rng: test_rng(), + rewarding_validator: mock_info(rewarding_validator_address.as_ref(), &[]), + owner: mock_info(owner.as_str(), &[]), + } + } + + pub fn deps(&self) -> Deps<'_> { + self.deps.as_ref() + } + + pub fn deps_mut(&mut self) -> DepsMut<'_> { + self.deps.as_mut() + } + + pub fn env(&self) -> Env { + self.env.clone() + } + + pub fn rewarding_validator(&self) -> MessageInfo { + self.rewarding_validator.clone() + } + + pub fn rewarding_params(&self) -> RewardingParams { + rewards_storage::REWARDING_PARAMS + .load(self.deps().storage) + .unwrap() + } + + pub fn owner(&self) -> MessageInfo { + self.owner.clone() + } + + pub fn vesting_contract(&self) -> Addr { + mixnet_params_storage::CONTRACT_STATE + .load(self.deps().storage) + .unwrap() + .vesting_contract_address + } + + pub fn current_interval(&self) -> Interval { + interval_storage::current_interval(self.deps().storage).unwrap() + } + + 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) -> MixId { + let stake = match stake { + Some(amount) => { + let denom = rewarding_denom(self.deps().storage).unwrap(); + Coin { denom, amount } + } + None => minimum_mixnode_pledge(self.deps.as_ref().storage).unwrap(), + }; + + let env = self.env(); + add_mixnode(&mut self.rng, self.deps.as_mut(), env, owner, vec![stake]) + } + + pub fn add_dummy_mixnode_with_proxy( + &mut self, + owner: &str, + stake: Option, + proxy: Addr, + ) -> MixId { + let stake = match stake { + Some(amount) => { + let denom = rewarding_denom(self.deps().storage).unwrap(); + Coin { denom, amount } + } + None => minimum_mixnode_pledge(self.deps.as_ref().storage).unwrap(), + }; + + let keypair = crypto::asymmetric::identity::KeyPair::new(&mut self.rng); + let owner_signature = keypair + .private_key() + .sign(owner.as_bytes()) + .to_base58_string(); + + let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); + + let info = mock_info(proxy.as_str(), &[stake]); + let key = keypair.public_key().to_base58_string(); + let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER + .may_load(self.deps().storage) + .unwrap() + .unwrap_or_default(); + + let env = self.env(); + try_add_mixnode_on_behalf( + self.deps_mut(), + env, + info, + MixNode { + identity_key: key, + sphinx_key: legit_sphinx_key.public_key().to_base58_string(), + ..tests::fixtures::mix_node_fixture() + }, + tests::fixtures::mix_node_cost_params_fixture(), + owner.to_string(), + owner_signature, + ) + .unwrap(); + + // newly added mixnode gets assigned the current counter + 1 + current_id_counter + 1 + } + + pub fn start_unbonding_mixnode(&mut self, mix_id: MixId) { + let bond_details = mixnodes_storage::mixnode_bonds() + .load(self.deps().storage, mix_id) + .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: MixId) { + let env = self.env(); + 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: MixId, + ) { + let denom = rewarding_denom(self.deps().storage).unwrap(); + let amount = Coin { + denom, + amount: amount.into(), + }; + let env = self.env(); + pending_events::delegate( + self.deps_mut(), + &env, + env.block.height, + Addr::unchecked(delegator), + target, + amount, + None, + ) + .unwrap(); + } + + pub fn add_immediate_delegation_with_proxy( + &mut self, + delegator: &str, + amount: impl Into, + target: MixId, + proxy: Addr, + ) { + let denom = rewarding_denom(self.deps().storage).unwrap(); + let amount = Coin { + denom, + amount: amount.into(), + }; + let env = self.env(); + pending_events::delegate( + self.deps_mut(), + &env, + env.block.height, + Addr::unchecked(delegator), + target, + amount, + Some(proxy), + ) + .unwrap(); + } + + #[allow(unused)] + pub fn add_delegation( + &mut self, + delegator: &str, + amount: impl Into, + target: MixId, + ) { + let denom = rewarding_denom(self.deps().storage).unwrap(); + let amount = Coin { + denom, + amount: amount.into(), + }; + let env = self.env(); + delegate(self.deps_mut(), env, delegator, vec![amount], target) + } + + 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) { + self.skip_to_next_epoch(); + self.skip_to_current_epoch_end(); + } + + pub fn skip_to_current_epoch_end(&mut self) { + let interval = interval_storage::current_interval(self.deps().storage).unwrap(); + let epoch_end = interval.current_epoch_end_unix_timestamp(); + // skip few blocks just in case + self.env.block.height += 10; + self.env.block.time = Timestamp::from_seconds(epoch_end as u64); + } + + pub fn skip_to_current_interval_end(&mut self) { + let interval = interval_storage::current_interval(self.deps().storage).unwrap(); + let interval_end = interval.current_interval_end_unix_timestamp(); + // skip few blocks just in case + self.env.block.height += 10; + self.env.block.time = Timestamp::from_seconds(interval_end as u64); + } + + pub fn skip_to_next_epoch(&mut self) { + let interval = interval_storage::current_interval(self.deps().storage).unwrap(); + let epoch_end = interval.current_epoch_end_unix_timestamp(); + // skip few blocks just in case + self.env.block.height += 10; + self.env.block.time = Timestamp::from_seconds(epoch_end as u64 + 1); + let advanced = interval.advance_epoch(); + + if interval.current_epoch_id() != interval.epochs_in_interval() { + assert_eq!(interval.current_epoch_id() + 1, advanced.current_epoch_id()) + } + + interval_storage::save_interval(self.deps_mut().storage, &advanced).unwrap() + } + + pub fn update_rewarded_set(&mut self, nodes: Vec) { + let active_set_size = rewards_storage::REWARDING_PARAMS + .load(self.deps().storage) + .unwrap() + .active_set_size; + interval_storage::update_rewarded_set(self.deps_mut().storage, active_set_size, nodes) + .unwrap(); + } + + pub fn instantiate_simulator(&self, node: MixId) -> Simulator { + simulator_from_single_node_state(self.deps(), node) + } + + pub fn execute_all_pending_events(&mut self) { + let env = self.env(); + execute_all_pending_events(self.deps_mut(), env) + } + + pub fn pending_interval_events(&self) -> Vec { + interval_storage::PENDING_INTERVAL_EVENTS + .range(self.deps().storage, None, None, Order::Ascending) + .map(|res| res.unwrap().1) + .collect::>() + } + + pub fn pending_epoch_events(&self) -> Vec { + interval_storage::PENDING_EPOCH_EVENTS + .range(self.deps().storage, None, None, Order::Ascending) + .map(|res| res.unwrap().1) + .collect::>() + } + + pub fn reward_with_distribution( + &mut self, + mix_id: MixId, + performance: Performance, + ) -> RewardDistribution { + let env = self.env(); + let sender = self.rewarding_validator(); + + let res = + try_reward_mixnode(self.deps_mut(), env, sender, mix_id, performance).unwrap(); + let operator: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + OPERATOR_REWARD_KEY, + &res, + ) + .parse() + .unwrap(); + let delegates: Decimal = find_attribute( + Some(MixnetEventType::MixnodeRewarding.to_string()), + DELEGATES_REWARD_KEY, + &res, + ) + .parse() + .unwrap(); + + RewardDistribution { + operator, + delegates, + } + } + + pub fn read_delegation( + &mut self, + mix: MixId, + owner: &str, + proxy: Option<&str>, + ) -> Delegation { + read_delegation( + self.deps().storage, + mix, + &Addr::unchecked(owner), + &proxy.map(Addr::unchecked), + ) + .unwrap() + } + + pub fn mix_rewarding(&self, node: MixId) -> MixNodeRewarding { + rewards_storage::MIXNODE_REWARDING + .load(self.deps().storage, node) + .unwrap() + } + + 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_single_node_state(deps: Deps<'_>, node: MixId) -> Simulator { + let mix_rewarding = rewards_storage::MIXNODE_REWARDING + .load(deps.storage, node) + .unwrap(); + let delegations = query_mixnode_delegations_paged(deps, node, None, None).unwrap(); + if delegations.delegations.len() as u32 + == constants::DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT + { + // can't be bothered to deal with paging for this test case since it's incredibly unlikely + // we'd ever need it + panic!("too many delegations") + } + let rewarding_params = rewards_storage::REWARDING_PARAMS + .load(deps.storage) + .unwrap(); + let interval = interval_storage::current_interval(deps.storage).unwrap(); + 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)> { + for msg in &response.messages { + if let CosmosMsg::Bank(BankMsg::Send { to_address, amount }) = &msg.msg { + return Some((to_address.clone(), amount.clone())); + } + } + None + } + + 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 { + continue; + } + } + if let Some(attr) = may_find_attribute(event, attribute) { + return attr; + } + } + // this is only used in tests so panic here is fine + panic!("did not find the attribute") + } + + // using floats in tests is fine + // (what it does is converting % value, like 12.34 into `Performance` (`Percent`) + // which internally is represented by decimal `0.1234` + pub fn performance(val: f32) -> Performance { + assert!(val <= 100.0); + assert!(val >= 0.0); + + // hehe, that's such a nasty conversion, but it works for test purposes + let str = (val / 100.0).to_string(); + let dec = str.parse().unwrap(); + Performance::new(dec).unwrap() + } + + // use rng with constant seed for all tests so that they would be deterministic + pub fn test_rng() -> ChaCha20Rng { + let dummy_seed = [42u8; 32]; + rand_chacha::ChaCha20Rng::from_seed(dummy_seed) + } + + pub fn execute_all_pending_events(mut deps: DepsMut<'_>, env: Env) { + perform_pending_epoch_actions(deps.branch(), &env, None).unwrap(); + perform_pending_interval_actions(deps.branch(), &env, None).unwrap(); + } + + pub fn mixnode_with_signature( + mut rng: impl RngCore + CryptoRng, + sender: &str, + ) -> (MixNode, String) { + let keypair = crypto::asymmetric::identity::KeyPair::new(&mut rng); + let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut rng); let owner_signature = keypair .private_key() .sign(sender.as_bytes()) .to_base58_string(); - let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut thread_rng()); + let identity_key = keypair.public_key().to_base58_string(); + let sphinx_key = legit_sphinx_key.public_key().to_base58_string(); - let info = mock_info(sender, &stake); - let key = keypair.public_key().to_base58_string(); - - try_add_mixnode( - deps, - mock_env(), - info, + ( MixNode { - identity_key: key.clone(), - sphinx_key: legit_sphinx_key.public_key().to_base58_string(), + identity_key, + sphinx_key, ..tests::fixtures::mix_node_fixture() }, owner_signature, ) - .unwrap(); - key } - pub fn add_gateway(sender: &str, stake: Vec, deps: DepsMut<'_>) -> String { - let keypair = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); + pub fn gateway_with_signature( + mut rng: impl RngCore + CryptoRng, + sender: &str, + ) -> (Gateway, String) { + let keypair = crypto::asymmetric::identity::KeyPair::new(&mut rng); + let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut rng); + let owner_signature = keypair + .private_key() + .sign(sender.as_bytes()) + .to_base58_string(); + + let identity_key = keypair.public_key().to_base58_string(); + let sphinx_key = legit_sphinx_key.public_key().to_base58_string(); + + ( + Gateway { + identity_key, + sphinx_key, + ..tests::fixtures::gateway_fixture() + }, + owner_signature, + ) + } + + 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(), + None, + ) + .unwrap(); + } + } + + pub fn add_dummy_mixnodes( + mut rng: impl RngCore + CryptoRng, + mut deps: DepsMut<'_>, + env: Env, + n: usize, + ) { + for i in 0..n { + add_mixnode( + &mut rng, + deps.branch(), + env.clone(), + &format!("owner{}", i), + tests::fixtures::good_mixnode_pledge(), + ); + } + } + + pub fn add_dummy_gateways( + mut rng: impl RngCore + CryptoRng, + mut deps: DepsMut<'_>, + env: Env, + n: usize, + ) { + for i in 0..n { + add_gateway( + &mut rng, + deps.branch(), + env.clone(), + &format!("owner{}", i), + tests::fixtures::good_mixnode_pledge(), + ); + } + } + + pub fn add_dummy_unbonded_mixnodes( + mut rng: impl RngCore + CryptoRng, + mut deps: DepsMut<'_>, + n: usize, + ) { + for i in 0..n { + add_unbonded_mixnode(&mut rng, deps.branch(), None, &format!("owner{}", i)); + } + } + + pub fn add_dummy_unbonded_mixnodes_with_owner( + mut rng: impl RngCore + CryptoRng, + mut deps: DepsMut<'_>, + owner: &str, + n: usize, + ) { + for _ in 0..n { + add_unbonded_mixnode(&mut rng, deps.branch(), None, owner); + } + } + + pub fn add_dummy_unbonded_mixnodes_with_identity( + mut rng: impl RngCore + CryptoRng, + mut deps: DepsMut<'_>, + identity: &str, + n: usize, + ) { + for i in 0..n { + add_unbonded_mixnode( + &mut rng, + deps.branch(), + Some(identity), + &format!("owner{}", i), + ); + } + } + + // same note as with `add_mixnode` + pub fn add_unbonded_mixnode( + mut rng: impl RngCore + CryptoRng, + deps: DepsMut<'_>, + identity_key: Option<&str>, + owner: &str, + ) -> MixId { + let id = loop { + let candidate = rng.next_u32(); + if !mixnodes_storage::unbonded_mixnodes().has(deps.storage, candidate) { + break candidate; + } + }; + + // we don't care about 'correctness' of the identity key here + mixnodes_storage::unbonded_mixnodes() + .save( + deps.storage, + id, + &UnbondedMixnode { + identity_key: identity_key + .unwrap_or(&*format!("identity{}", id)) + .to_string(), + owner: Addr::unchecked(owner), + proxy: None, + unbonding_height: 12345, + }, + ) + .unwrap(); + + id + } + + // note to whoever wants to refactor this function, you dont want to grab rng here directly + // via `let rng = test_rng()` + // because it's extremely likely you might end up calling `add_mixnode()` multiple times + // in the same test and thus you're going to get mixnodes with the same keys and that's + // not what you want (presumably) + pub fn add_mixnode( + mut rng: impl RngCore + CryptoRng, + deps: DepsMut<'_>, + env: Env, + sender: &str, + stake: Vec, + ) -> MixId { + let keypair = crypto::asymmetric::identity::KeyPair::new(&mut rng); + let owner_signature = keypair + .private_key() + .sign(sender.as_bytes()) + .to_base58_string(); + + let legit_sphinx_key = crypto::asymmetric::encryption::KeyPair::new(&mut rng); + + let info = mock_info(sender, &stake); + let key = keypair.public_key().to_base58_string(); + let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER + .may_load(deps.storage) + .unwrap() + .unwrap_or_default(); + + try_add_mixnode( + deps, + env, + info, + MixNode { + identity_key: key, + sphinx_key: legit_sphinx_key.public_key().to_base58_string(), + ..tests::fixtures::mix_node_fixture() + }, + tests::fixtures::mix_node_cost_params_fixture(), + owner_signature, + ) + .unwrap(); + + // newly added mixnode gets assigned the current counter + 1 + current_id_counter + 1 + } + + // same note as with `add_mixnode` + pub fn add_gateway( + mut rng: impl RngCore + CryptoRng, + deps: DepsMut<'_>, + env: Env, + sender: &str, + stake: Vec, + ) -> String { + let keypair = crypto::asymmetric::identity::KeyPair::new(&mut rng); let owner_signature = keypair .private_key() .sign(sender.as_bytes()) @@ -72,7 +749,7 @@ pub mod test_helpers { let key = keypair.public_key().to_base58_string(); try_add_gateway( deps, - mock_env(), + env, info, Gateway { identity_key: key.clone(), @@ -84,11 +761,31 @@ pub mod test_helpers { key } + fn initial_rewarding_params() -> InitialRewardingParams { + let reward_pool = 250_000_000_000_000u128; + let staking_supply = 100_000_000_000_000u128; + + InitialRewardingParams { + initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens) + initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M + staking_supply_scale_factor: Percent::hundred(), + sybil_resistance: Percent::from_percentage_value(30).unwrap(), + active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), + interval_pool_emission: Percent::from_percentage_value(2).unwrap(), + rewarded_set_size: 240, + active_set_size: 100, + } + } + pub fn init_contract() -> OwnedDeps> { let mut deps = mock_dependencies(); let msg = InstantiateMsg { - rewarding_validator_address: TEST_REWARDING_VALIDATOR_ADDRESS.to_string(), - mixnet_denom: TEST_COIN_DENOM.to_string(), + rewarding_validator_address: "rewarder".into(), + vesting_contract_address: "vesting-contract".to_string(), + rewarding_denom: TEST_COIN_DENOM.to_string(), + epochs_in_interval: 720, + epoch_duration: Duration::from_secs(60 * 60), + initial_rewarding_params: initial_rewarding_params(), }; let env = mock_env(); let info = mock_info("creator", &[]); @@ -96,58 +793,22 @@ pub mod test_helpers { deps } - // currently not used outside tests - pub(crate) fn read_mixnode_pledge_amount( - storage: &dyn Storage, - identity: IdentityKeyRef<'_>, - ) -> StdResult { - let node = mixnodes_storage::mixnodes().load(storage, identity)?; - Ok(node.pledge_amount.amount) - } - - pub(crate) fn save_dummy_delegation( - storage: &mut dyn Storage, - mix: impl Into, - owner: impl Into, - block_height: u64, - ) { - let delegation = Delegation { - owner: Addr::unchecked(owner.into()), - node_identity: mix.into(), - amount: coin(12345, TEST_COIN_DENOM), - block_height: block_height, - proxy: None, - }; - - delegations_storage::delegations() - .save(storage, delegation.storage_key(), &delegation) - .unwrap(); + 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, env, info, mix_id).unwrap(); } pub(crate) fn read_delegation( storage: &dyn Storage, - mix: impl Into, - owner: impl Into>, - block_height: u64, + mix: MixId, + owner: &Addr, + proxy: &Option, ) -> Option { delegations_storage::delegations() - .may_load(storage, (mix.into(), owner.into(), block_height)) + .may_load( + storage, + Delegation::generate_storage_key(mix, owner, proxy.as_ref()), + ) .unwrap() } - - pub(crate) fn update_env_and_progress_interval(env: &mut Env, storage: &mut dyn Storage) { - // make sure current block time is within the expected next interval - env.block.time = Timestamp::from_seconds( - (interval_storage::current_epoch(storage) - .unwrap() - .next() - .start_unix_timestamp() - + 123) as u64, - ); - - let sender = - crate::mixnet_contract_settings::storage::rewarding_validator_address(storage).unwrap(); - - interval::transactions::try_advance_epoch(env.clone(), storage, sender).unwrap(); - } } diff --git a/contracts/mixnet/src/support/tests/queries.rs b/contracts/mixnet/src/support/tests/queries.rs index e53caf148e..1955e8a85c 100644 --- a/contracts/mixnet/src/support/tests/queries.rs +++ b/contracts/mixnet/src/support/tests/queries.rs @@ -1,31 +1,13 @@ +use crate::contract::query; use cosmwasm_std::{ from_binary, testing::{mock_env, MockApi, MockQuerier, MockStorage}, - Addr, Coin, OwnedDeps, -}; -use mixnet_contract_common::{ - GatewayBond, MixNodeBond, PagedGatewayResponse, PagedMixnodeResponse, QueryMsg, + OwnedDeps, }; +use mixnet_contract_common::{GatewayBond, PagedGatewayResponse, QueryMsg}; -use crate::contract::query; - -use super::fixtures::TEST_COIN_DENOM; - -pub fn get_mix_nodes(deps: &mut OwnedDeps) -> Vec { - let result = query( - deps.as_ref(), - mock_env(), - QueryMsg::GetMixNodes { - start_after: None, - limit: Option::from(2), - }, - ) - .unwrap(); - - let page: PagedMixnodeResponse = from_binary(&result).unwrap(); - page.nodes -} - +// I honestly don't know why we're using this way of querying in tests, but I couldn't be bothered to change it +// since I haven't done anything to gateways pub fn get_gateways(deps: &mut OwnedDeps) -> Vec { let result = query( deps.as_ref(), @@ -38,13 +20,20 @@ pub fn get_gateways(deps: &mut OwnedDeps) -> .unwrap(); let page: PagedGatewayResponse = from_binary(&result).unwrap(); + if page.start_next_after.is_some() { + let next_page = query( + deps.as_ref(), + mock_env(), + QueryMsg::GetGateways { + start_after: page.start_next_after, + limit: None, + }, + ) + .unwrap(); + let next_page: PagedGatewayResponse = from_binary(&next_page).unwrap(); + if !next_page.nodes.is_empty() { + panic!("we didn't manage to get all gateways in a single query") + } + } page.nodes } - -pub fn query_contract_balance( - address: Addr, - deps: OwnedDeps, -) -> Vec { - let querier = deps.as_ref().querier; - vec![querier.query_balance(address, TEST_COIN_DENOM).unwrap()] -} 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/mixnet/v2-changes.md b/contracts/mixnet/v2-changes.md new file mode 100644 index 0000000000..ecb290703c --- /dev/null +++ b/contracts/mixnet/v2-changes.md @@ -0,0 +1,903 @@ + +# Mixnet contract changes + +This file shall describe (hopefully) all relevant changes made to the contract as the result of changing reward calculation. + +## Overview + +There are two main changes performed to the mixnet contract that have a cascading effect on the rest of the system. They are as follows: + +1. The delegator rewarding is modified so that in order to determine the correct reward, we no longer have to iterate through data from all the epochs to correctly compound the reward. Instead, we assume there's a theoretical "unit" delegation on each mixnode that we keep track of and scale all the actual delegation values accordingly. It's very similar to the idea presented in the [Cosmos' F1 paper](https://drops.dagstuhl.de/opus/volltexte/2020/11974/pdf/OASIcs-Tokenomics-2019-10.pdf). I've explained the entire algorithm on an example in more details on [our gitlab](https://gitlab.nymte.ch/jstuczyn/reward-testing/-/blob/main/README.md). + +2. Mixnodes are no longer stored and indexed by their identity keys. Instead, they get assigned a unique `NodeId` (just an increasing `u64` id). This is to resolve my favourite ~~bug~~ feature (I will explain this in slightly more details in the next sections) that causes rebonded mixnode to retain its delegations. With this change the following would happen: + - new mixnode bonds, gets assigned id `X` + - delegations are being made towards this mixnode + - mixnode decides to unbond + - the same mixnode rebonds, with the same identity key, same owner, same sphinx key, etc. but this time it gets assigned new id `Y` + - as a result the delegations are still pointing to `X` and thus are no longer accumulating any rewards + +While not as major as the above changes, the other notable changes include: +- introduction of `PendingEpochEvent` and `PendingIntervalEvent`. It means that whenever a relevant request is received, it's only going to get executed once the current epoch (or interval) finishes. This might include, for example, mixnode unbonding or changing mixnode cost parameters. +- rewarding parameters, such as the size of the reward pool are only updated at the end of the current **interval**. They should not get modified between epochs. +- node uptime/performance can now be a decimal value as opposed to integer between 0-100 +- node operators can now set their operating costs +- profit margin can now be a decimal value as opposed to integer between 0-100 +- delegation/undelegation is no longer instantaneous. They will happen at the end of corresponding epoch. + +## 'Benefits' to community: +- compounding happens automatically - you no longer have to keep track of it +- node operators can now set their operating costs +- profit margin can be more granular +- operators can update their basic configuration without rebonding +- if mixnode unbonds, it loses all of its delegations (permanently) + +## Instantiation + +The `InstantiateMsg` contains more fields to remove dependency on our "beloved" `NETWORK_DEFAULTS` and constants. Now we have to explicitly specify relevant parameters during instantiation: +```rust +pub struct InstantiateMsg { + pub rewarding_validator_address: String, + pub vesting_contract_address: String, + pub rewarding_denom: String, + pub epochs_in_interval: u32, + pub epoch_duration: Duration, + pub initial_rewarding_params: InitialRewardingParams, +} +``` + +We have to explicitly state, as before, address of the rewarding validator which is authorized to update rewarded sets and distribute rewards to mixnodes, but also vesting contract address (since we need to know if we should call `Track...` methods. Without it we could end up attempt to call a vesting contract method on a non-contract address), interval/epoch related parameters (so that we wouldn't accidentally try to set 10min epochs via migration :eyes:) and initial rewarding parameters that include things such as the size of the initial reward pool or the per interval emission. + +## Mixnodes + +### Overview + +The mixnode part (apart from the delegations themselves) is the one most heavily affected by the changes made. + +- All the associated structs were either modified or completely replaced, for example our top level `MixNodeBond` has been superseded by `MixNodeDetails` that _mostly_ contains what the original `MixNodeBond` had, but also holds additional information regarding rewarding parameters. +- Furthermore, as mentioned before, the indexing works differently now. We use `NodeId` to identify nodes as opposed to `IdentityKey`. +- However, our unique indices are still in place, i.e. it's impossible to bond multiple mixnodes with the same identities. +- "unbonding" is no longer instantaneous, instead it is executed when the current epoch finishes. +- at the time of writing this, "bonding" is still instant, though it might change. +- operators can now set their interval operating costs alongside their profit margins +- profit margin and operating cost changes will only happen at the end of current interval +- operators can now change basic information about their mixnodes without having to unbond, this includes things such as the version or host information +- ... + +### Types/Models + +#### Added + +##### MixNodeDetails + +Top level struct containing all information about particular mixnode, i.e. public keys, host information, cost function, rewarding parameters, etc. + +```rust +pub struct MixNodeDetails { + pub bond_information: MixNodeBond, + + pub rewarding_details: MixNodeRewarding, +} +``` + +##### MixNodeRewarding + +New struct containing information required to determine correct rewards for all delegators and the operator. Keeps track of the currently distributed rewards as well as the value of the "unit delegation". + +It is stored in separate `Map`, as after mixnode finishes the unbonding process, if there are any delegators who haven't undelegated yet, we need to know the below information in order to determine their rewards correctly. +```rust +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: FullEpochId, + + // 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, +} +``` + +##### MixNodeCostParams + +Contains all cost-function related information of a given mixnode, i.e. currently the profit margin and the operating cost (per interval). It is provided by the node operator at the time of bonding and can only be changed as an interval rolls over. + +```rust +pub struct MixNodeCostParams { + pub profit_margin_percent: Percent, + + /// Operating cost of the associated mixnode per the entire interval. + pub interval_operating_cost: Coin, +} + +``` + +##### UnbondedMixnode + +This struct is used to keep track very basic information about nodes that have already unbonded, as we would only know their `NodeId`. It is especially useful if your delegation is pointing to an unbonded node and you wanted to know the owner of the node that decided to unbond or its identity key. + +```rust +pub struct UnbondedMixnode { + pub identity: IdentityKey, + pub owner: Addr, + pub unbonding_height: u64, +} +``` + +##### MixNodeConfigUpdate + +Encapsulates information sent to the contract whenever operator wants to update basic configuration information about the bonded node. + +```rust +pub struct MixNodeConfigUpdate { + pub host: String, + pub mix_port: u16, + pub verloc_port: u16, + pub http_api_port: u16, + pub version: String, +} +``` + +#### Removed + +##### StoredMixnodeBond + +The initial idea behind `StoredMixnodeBond` was to store the total delegation separately to the operator pledge (and accumulated rewards). The same issue is now resolved with the `MixNodeBond` and `MixNodeRewarding`. + +#### Modified + +##### MixNodeBond + +```rust +// operator information + data assigned by the contract(s) +pub struct MixNodeBond { + // + // ... + // + // `id` field has been added that contains information about the assigned `NodeId` + // +++ pub id: NodeId, + // + // we no longer keep total pledge (alongside delegation) on `MixNodeBond` via the `pledge_amount`, instead we only hold the `original_pledge` which is **NEVER** modified + // --- pub pledge_amount: Coin + // +++ pub original_pledge: Coin, + // + // `block_height` has been renamed to `bonding_height` to be more explicit about the intent + // --- pub block_height: u64 + // +++ pub bonding_height: u64, + // + // `is_unbonding` field has been added to indicate when the mixnode has issue the request to unbond but the epoch hasn't rolled over yet (to prevent delegations on an unbonding node) + // +++ pub is_unbonding: bool, + // + // `accumulated_rewards` field is no longer required to keep track of all rewards for particular node + // --- pub accumulated_rewards: Option +} +``` + +##### MixNode + +```rust +// information provided by the operator +pub struct MixNode { + // + // ... + // + // `profit_margin_percent` has been removed and this information is now provided via `MixNodeCostParams` + // --- pub profit_margin_percent: u8, + // +++ +} +``` + +##### Layer + +```rust +pub enum Layer { + // `Gateway` layer has been removed from the `Layer` enum + //--- Gateway = 0, + //+++ + One = 1, + Two = 2, + Three = 3, +} +``` + +### Transactions + +As before, all transactions have their associated `OnBehalf` equivalent that allows them to be called from the vesting contract. + +#### Added + +##### UpdateMixnodeCostParams + +This one allows you to update the cost parameters of your mixnode, i.e. the profit margin and the interval operating costs. Execution of this transaction will result in the creation of a `PendingIntervalEvent` that will get resolved at the end of the current interval. + +#### Removed + +##### CheckpointMixnodes + +Due to the changes to the rewarding system, we no longer have to be checkpointing mixnodes every epoch in order to keep track of their stake/parameters at those blocks. + +#### Modified + +##### UpdateMixnodeConfig + +Updating mixnode config allowed you to update your profit margin. This functionality has been replaced with `UpdateMixnodeCostParams` and instead `UpdateMixnodeConfig` lets you to instantaneously update basic configuration such as the host information or the node version. + +##### UnbondMixnode + +In general sense `UnbondMixnode` works as before, i.e. it will eventually result in the mixnode getting removed from the directory. However, it's no longer instant. Whenever the transaction is executed, it will instead push a `PendingEpochEvent` that shall get resolved at the end of the current epoch. + +The only immediate effect is that `is_unbonding` field on the `MixNodeBond` is going to be set to `true` and as a result, no new delegations are going to be permitted on this node. + +##### BondMixnode + +The only difference made to the mixnode bonding process is that operators need to provide an additional argument, of type `MixNodeCostParams`, to specify the cost function arguments of the node. + +### Queries + +The most relevant thing here to note is that whenever old queries were using `mix_identity` as one of their arguments, they instead use `mix_id` of type `NodeId` (`u64`). + +#### Added + +##### GetUnbondedMixNodes + +New query allowing to grab the details of all mixnodes (paged) that have unbonded at some point in the past. + +##### GetUnbondedMixNodeInformation + +Same as above, but rather than getting information for all the mixnodes, it does it for the node specified by the provided `mix_id`. + +##### GetStakeSaturation + +Allows to directly obtain stake saturation (i.e. of the full bond (pledge + delegations)) of given mixnode. + +##### GetMixnodeRewardingDetails + +Allows obtaining `MixNodeRewarding` details of a particular node that, among other things, contain total delegation towards this node or the current value of the "unit" delegation. + +#### Removed + +Everything that was related to node snapshotting is removed, this includes the below to queries: + +##### GetMixnodeAtHeight + +No longer needed due to no snapshotting. + +##### GetCheckpointsForMixnode + +No longer needed due to no snapshotting. + +##### GetCurrentOperatorCost + +In the previous version the operator cost was a constant value shared by all operators. Now it is configurable and it can be queried by getting information associated via the particular node, for example with `GetMixnodeRewardingDetails` or `GetMixnodeDetails`. + +#### Modified + +##### GetMixNodes => GetMixNodeBonds, GetMixNodesDetailed + +Since the structure of `MixNodeBond` has changed, `GetMixNodes` is replaced by `GetMixNodeBonds` that returns all `MixNodeBond` (paged) while `GetMixNodesDetailed` returns `MixNodeDetails`, that apart from the bond also contains rewarding details. + +##### GetMixnodeBond + +Similarly to the above query for the bond information of given mixnode has been superseded by `GetMixnodeDetails`. + +##### OwnsMixnode + +`OwnsMixnode` has been renamed to `GetOwnedMixnode` to keep the naming consistent. + +### Storage + +#### Added + +- `MIXNODE_ID_COUNTER` - as mentioned before all mixnodes are indexed by an increasing `NodeId`. This counter keep track of the current value. +- `UNBONDED_MIXNODES` - `Map`storing basic information about the mixnodes that have unbonded. + +#### Removed + +- memoized `TOTAL_DELEGATION` was removed. Similar functionality is achieved via `rewards_storage::MIXNODE_REWARDING`. +- `LAST_PM_UPDATE_TIME` is also removed. We no longer have to keep track of that since the profit margin updates are enforced to be happening at the end of intervals. + +#### Modified + +- As mentioned multiple times before, mixnodes are no longer snapshot and we index them with `NodeId` and thus instead of using `IndexedSnapshotMap<'a, IdentityKeyRef<'a>, StoredMixnodeBond, MixnodeBondIndex<'a>>` we use `IndexedMap<'a, NodeId, MixNodeBond, MixnodeBondIndex<'a>>` +- To preserve uniqueness on identity keys, `MixnodeBondIndex` now also contains `UniqueIndex` on the `identity_key`. + +## Gateways + +### Overview + +Gateways remain mostly unchanged and unaffected by the fallout of other changes. We still keep the gateways indexed by their identity keys. This might change in the future, but for the time being, there's no reason to do anything about it. + +The only relevant change is that `OwnsGateway` query has been renamed to `GetOwnedGateway` to keep the naming consistent. + +## Delegation + +### Overview + +Apart from mixnodes, the delegations are the other part of the system most affected by the introduced changes. Structurally-wise, the differences are relatively minimal, but the logic behind them, especially concerning rewarding and reward estimation (which will be described in more details in the subsequent sections) has changed in a meaningful way. + +### Types/Models + +#### Added + +N/A + +#### Removed + +N/A + +#### Modified + +##### Delegation + +```rust +pub struct Delegation { + // + // ... + // + // `node_identity` of the associated mixnode has been replaced by its assigned `node_id` as mixnode indexing has been modified + // --- pub node_identity: IdentityKey, + // +++ pub node_id: NodeId, + + // Value of the "unit delegation" associated with the mixnode at the time of delegation. It's used purely for calculating rewards + // +++ pub cumulative_reward_ratio: Decimal, + + // `block_height` has been renamed to `height` + // --- pub block_height: u64, + // +++ pub height: u64, +} +``` + +### Transactions + +Apart from the changes to the mixnode indexing (i.e. `identity_key => node_id`) there's been no significant changes to the transactions involving delegations. Of course this excludes anything regarding rewards, but this part is going to have its own dedicated section below. + +### Queries + +The same holds true for queries. If we exclude reward estimation-related queries and changes due to the new indexing, there are hardly any changes. The only notable difference is the introduction of `GetAllDelegations` which allows one to query for all delegations in the system as opposed to being restricted to a single owner or a single mixnode. The responses are still, however, paged. + +### Storage + +#### Added + +N/A + +#### Removed + +- `PENDING_DELEGATION_EVENTS` `Map` has been removed as this concept is being superseded by the `PendingEpochEvent` queue. + +#### Modified + +The storage key structure of delegation has been slightly adjusted compared to the previous version: +- The composite storage key no longer includes the block height as we're now able to immediately work with the potentially changed values, +- For the simplicity sake, the composite subkey created for the purpose of querying by the owner/proxy combination has been changed from being a `Vec` to instead being a base58-encoded String (of the same data). This makes it slightly easier for the clients to use it, especially in paged queries. + +## Interval + +### Overview + +Generally we've been going back and forth with having explicit distinction between epochs and intervals and making this purely implicit. In this iteration of the contract both pieces of data are explicit. `Interval` has an associated id, etc. as well as it holds information about the current epoch, number of epochs in interval, etc. + +The other notable change to how interval behaves is that we expanded the concept of particular events being executed as given epoch (or interval) rolls over. Previously this was only applicable to `PendingDelegations`. + +Also, now advancing epoch happens in the same message as writing the new rewarded set, so it's impossible to perform one without the other. Speaking of updating the rewarded set, I was attempting to be smart and reduce number of storage read by not writing entries that hasn't changed (i.e. if node was `Active` and its updated status is still `Active`, don't do anything). We're about to see if this wasn't a stupid overkill... + +### Types/Models + +#### Added + +##### PendingEpochEvent + +New structure keeping track of events that shall get invoked at the end of the current **epoch** (after rewards have already been distributed). + +```rust +pub enum PendingEpochEvent { + Delegate { + owner: Addr, + mix_id: NodeId, + amount: Coin, + proxy: Option, + }, + Undelegate { + owner: Addr, + mix_id: NodeId, + proxy: Option, + }, + UnbondMixnode { + mix_id: NodeId, + }, + UpdateActiveSetSize { + new_size: u32, + }, +} +``` + +##### PendingIntervalEvent + +New structure keeping track of events that shall get invoked at the end of the current **interval** (after rewards have already been distributed). + +```rust +pub enum PendingIntervalEvent { + ChangeMixCostParams { + mix: NodeId, + new_costs: MixNodeCostParams, + }, + UpdateRewardingParams { + update: IntervalRewardingParamsUpdate, + }, + UpdateIntervalConfig { + epochs_in_interval: u32, + epoch_duration_secs: u64, + }, +} +``` + +#### Removed + +N/A + +#### Modified + +##### Interval + +```rust +pub struct Interval { + // + // ... + // + // we're now explicitly keeping track of the expected number of epochs in the stored interval (as opposed to making it implicit via constants) + // +++ epochs_in_interval: u32, + // + // we're just being explicit about what we're keeping track of + // --- start: OffsetDateTime, + // +++ current_epoch_start: OffsetDateTime, + // + // the same is true for this one + // --- length: Duration, + // +++ epoch_length: Duration, + // + // and we're also explicitly separating ids of interval and epochs. Do note that it's illegal for `current_epoch_id` to be equal or larger to `epochs_in_interval` (in that case it should roll over back to 0) + // --- id: u32, + // +++ id: IntervalId, + // +++ current_epoch_id: EpochId, +} +``` + +### Transactions + +#### Added + +There's a couple of newly added transaction that allow changing rewarding-related parameters, such as the active set size or pool emission, etc. But those changes should preferably only be executed at the end of the current epoch/interval (depending on a particular change requested) so that the current rewarding interval wouldn't be affected in an unexpected way. However, the transactions include the `force_immediately` field to make the change immediate if required. + +##### UpdateActiveSetSize + +Allows updating the active set size. Note that the new size **must** be equal to or smaller than the current rewarded set. If `force_immediately` is not set, the change will be applied at the end of the current epoch. + +##### UpdateRewardingParams + +Allows updating (almost) all the other rewarding-related global parameters: + +```rust +pub struct IntervalRewardingParamsUpdate { + pub reward_pool: Option, + pub staking_supply: Option, + + pub sybil_resistance_percent: Option, + pub active_set_work_factor: Option, + pub interval_pool_emission: Option, + pub rewarded_set_size: Option, +} +``` + +Note that at least a single change must be specified. If `force_immediately` is not set, the change will be applied at the end of the current interval. + +##### UpdateIntervalConfig + +Allows adjusting configuration of the interval, i.e. number of epochs it contains as well as the duration of the epochs themselves. Similarly to the above, if `force_immediately` is not set, the change will be applied at the end of the current interval. + +##### ReconcileEpochEvents + +Serves a very similar purpose to the removed `ReconcileDelegations`. But rather than being limited to just delegation creation/removal, this transaction would attempt to execute all pending epoch and interval events. + +Do note that if current epoch is in **NOT** in progress, nothing is going to happen. Furthermore, interval events will only get executed if apart from the current epoch being over, the interval itself is over. + +Anyone willing to pay the associated gas costs is can call this transaction. It's not limited to the `owner` account. + +#### Removed + +##### InitEpoch + +Since we're going to be creating a brand-new contract, we no longer have to separately initialise the epoch (interval). It's going to be performed implicitly during contract instantiation. + +##### ReconcileDelegations + +As explained before, superseded by `ReconcileEpochEvents`. + +##### CheckpointMixnodes + +As explained multiple times before, due to the changes to the reward calculation algorithm, we no longer have to keep track of the state of all the mixnodes at each epoch. + +##### WriteRewardedSet + +This functionality has been moved into `AdvanceCurrentEpoch` so that it would not be possible to roll over the epoch without explicitly updating the rewarded set. + +##### GetRewardedSetUpdateDetails + +Rewarded set is now always being updated whenever epoch rolls over + +##### GetRewardedSetRefreshBlocks + +Same as above + +##### GetCurrentRewardedSetHeight + +We no longer keep track of rewarded sets at given height thanks to the change to the reward calculation + +#### Modified + +##### AdvanceCurrentEpoch + +The main idea behind this transaction remains unchanged - if the current epoch/interval is over, this call rolls it over to the next one. However, it is now also responsible for additional functionalities: +- updating the rewarded set to the newly provided value, +- emptying the `PendingEpochEvent` and `PendingIntervalEvent` queues if there's anything left in there -> Do note that a separate explicit call in a different transaction is preferred, since there might be a significant amount of events to go through, +- if the interval has rolled over all the pending reward pool changes from `RewardPoolChange` (that will be elaborated in the rewards section) are applied + +### Queries + +#### Added + +##### GetCurrentIntervalDetails + +Allows querying for the information about the current `Interval` alongside data on the current blocktime and whether the current epoch and interval are already over. + +##### GetPendingEpochEvents + +Paged query for obtaining all currently pending `PendingEpochEvents` that shall get cleared at the end of the current epoch. + +##### GetPendingIntervalEvents + +Paged query for obtaining all currently pending `PendingIntervalEvents` that shall get cleared at the end of the current interval. + +#### Removed + +##### GetEpochsInInterval + +This was removed in favour of `GetCurrentIntervalDetails` that returns the same piece of data on top of additional content. + +#### Modified + +##### GetRewardedSet + +Querying for the rewarded set no longer lets you specify the block height. It always grabs the current one. + +### Storage + +#### Added + +Similarly to `MIXNODE_ID_COUNTER`, we keep an increasing id for epoch and interval events. Essentially we want to ensure that we'd execute them in the order they were created and we can't use block height as it's very possible multiple requests might be created in the same block height. Thus we introduce `EPOCH_EVENT_ID_COUNTER` and `INTERVAL_EVENT_ID_COUNTER` for that purpose. + +Furthermore, we keep track of the ID of the most recently executed event (in both categories), so we'd known more easily if there have been any new ones pushed without having to explicitly query for them. For that end we use `LAST_PROCESSED_EPOCH_EVENT` and `LAST_PROCESSED_INTERVAL_EVENT` + +Finally, rather obviously, we have to store the actual events and those are being help in `PENDING_EPOCH_EVENTS` and `PENDING_INTERVAL_EVENTS` `Map`s. + +#### Removed + +- `CURRENT_EPOCH_REWARD_PARAMS` - in a way superseded by `rewards_storage::REWARDING_PARAMS` that holds current rewarding parameters for the entire interval. +- `CURRENT_REWARDED_SET_HEIGHT` - we only hold a single rewarded set at a time +- `EPOCHS` - all epoch related information is included in the `Interval` data. + +#### Modified + +- `CURRENT_EPOCH` has been replaced by a differently named `CURRENT_INTERVAL` +- `REWARDED_SET` - the storage key no longer requires using the block height as there's only ever a single rewarded set at a time + +## Contract settings + +### Overview + +Contract state/settings now explicitly contain information that previously was implicit via the constants or network defaults (such as the `denom`). Also, parameters affecting rewarding, such as the active set size, were moved to more appropriate modules. + +### Types/Models + +#### Added + +N/A + +#### Removed + +N/A + +#### Modified + +##### ContractState + +```rust +pub struct ContractState { + // + // ... + // + // we're explicitly keeping track of what we think is the vesting contract (as specified during instantiation), so that we'd known if we should call `Track...` methods on the proxy address + // +++ pub vesting_contract_address: Addr, + // added information about the expected coin denomination that's used for rewarding + // +++ pub rewarding_denom: String, +} +``` + +##### ContractStateParams + +```rust +pub struct ContractStateParams { + // + // ... + // + // we're tracking all minimum pledges explicitly as `Coin` now + // --- pub minimum_mixnode_pledge: Uint128 + // +++ pub minimum_mixnode_pledge: Coin, + // + // --- pub minimum_gateway_pledge: Uint128, + // +++ pub minimum_gateway_pledge: Coin, + // + // optional functionality to set minimum delegation amount if required + // +++ pub minimum_mixnode_delegation: Option, + // + // Attributes directly affecting rewarding are moved to `rewards_storage` now + // --- pub mixnode_rewarded_set_size: u32, + // --- pub mixnode_active_set_size: u32, + // --- pub staking_supply: Uint128, +} +``` + +### Transactions + +The only transaction, i.e. updating state params, is no longer a unit enum. It was changed from +```rust +pub enum ExecuteMsg { + UpdateContractStateParams(ContractStateParams), +} +``` + +to +```rust +pub enum ExecuteMsg { + UpdateContractStateParams { + updated_parameters: ContractStateParams, + }, +} +``` + +### Queries + +#### Added + +##### GetState + +Introduced new query to get the entire `ContractState` struct, so we'd known about, for example, the rewarding denom or the rewarding validator address. + +#### Removed + +N/A + +#### Modified + +##### StateParams + +Was renamed to `GetStateParams` to keep naming consistent + +### Storage + +No relevant changes were performed to the storage structure of the contract settings. + +## Rewards + +### Overview + +Changing the logic behind the rewards was the main motivation behind this new contract version. The main things in this section, apart from what was already mentioned before, include but is not limited to: +- reward pool (and the staking supply) being only updated at the end of the given interval. However, the accounting is still happening as the rewards are distributed, so we'd known how much the pool should be adjusted by. +- we no longer keep any historical information regarding past epochs/parameters/etc for the purposes of rewarding. Whatever exists in the storage at the time is the thing that's going to be used for the next distribution. +- queries for reward estimation now require constant(ish) amount of gas as opposed to growing linearly with the number of epochs since last claim/compounding. +- + +### Types/Models + +#### Added + +##### MixNodeRewarding + +The most important struct created for the purposes of the changes described. All the data here allows us to correctly determine rewards for all the delegators by scaling the value of `total_unit_reward` based on the ratio the delegation to `unit_delegation` and scaled by the unit delegation reward at the time of delegation of the delegate. + +```rust +pub struct MixNodeRewarding { + /// Information provided by the operator that influence the cost function. + pub cost_params: MixNodeCostParams, + + /// Total pledge and compounded reward earned by the node operator. + pub operator: Decimal, + + /// Total delegation and compounded reward earned by all node delegators. + pub delegates: Decimal, + + /// Cumulative reward earned by the "unit delegation" since the block 0. + pub total_unit_reward: Decimal, + + /// Value of the theoretical "unit delegation" that has delegated to this mixnode at block 0. + 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. + pub last_rewarded_epoch: FullEpochId, + + // 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) + pub unique_delegations: u32, +} +``` + +##### RewardPoolChange + +Whenever we distribute rewards, we keep track of how much should get removed from the reward pool and moved into the staking supply when the interval finishes. + +```rust +pub(crate) struct RewardPoolChange { + /// Indicates amount that shall get moved from the reward pool to the staking supply + /// upon the current interval finishing. + pub removed: Decimal, + + // this will be used once coconut credentials are in use; + /// Indicates amount that shall get added to the both reward pool and not touch the staking supply + /// upon the current interval finishing. + #[allow(unused)] + pub added: Decimal, +} +``` + +##### RewardingParams and IntervalRewardParams + +Those are used for keeping track of parameters used for rewarding of all nodes during a particular interval. Unless there's an exceptionally good reason for it, they remain constants within an interval. + +```rust +pub struct RewardingParams { + /// Parameters that should remain unchanged throughout an interval. + pub interval: IntervalRewardParams, + + // while the active set size can change between epochs to accommodate for bandwidth demands, + // the active set size should be unchanged between epochs and should only be adjusted between + // intervals. However, it makes more sense to keep both of those values together as they're + // very strongly related to each other. + pub rewarded_set_size: u32, + pub active_set_size: u32, +} + +pub struct IntervalRewardParams { + /// Current value of the rewarding pool. + /// It is expected to be constant throughout the interval. + pub reward_pool: Decimal, + + /// Current value of the staking supply. + /// It is expected to be constant throughout the interval. + pub staking_supply: Decimal, + + // computed values + /// Current value of the computed reward budget per epoch, per node. + /// It is expected to be constant throughout the interval. + pub epoch_reward_budget: Decimal, + + /// Current value of the stake saturation point. + /// It is expected to be constant throughout the interval. + pub stake_saturation_point: Decimal, + + // constants(-ish) + // default: 30% + /// Current value of the sybil resistance percent (`alpha`). + /// It is not really expected to be changing very often. + /// As a matter of fact, unless there's a very specific reason, it should remain constant. + pub sybil_resistance: Percent, + + // default: 10 + /// Current active set work factor. + /// It is not really expected to be changing very often. + /// As a matter of fact, unless there's a very specific reason, it should remain constant. + pub active_set_work_factor: Decimal, + + // default: 2% + /// Current maximum interval pool emission. + /// Assuming all nodes in the rewarded set are fully saturated and have 100% performance, + /// this % of the reward pool would get distributed in rewards to all operators and its delegators. + /// It is not really expected to be changing very often. + /// As a matter of fact, unless there's a very specific reason, it should remain constant. + pub interval_pool_emission: Percent, +} +``` + +#### Removed + +##### DelegatorRewardParams, StoredNodeRewardResult, EpochRewardParams, NodeRewardResult + +All reward-related results and parameters got consolidated, mostly into `RewardingParams` + +### Transactions + +#### Added + +#### Removed + +##### CompoundOperatorReward, CompoundDelegatorReward, CompoundOperatorRewardOnBehalf, CompoundDelegatorRewardOnBehalf + +All compounding-related transactions have been removed as they're no longer required since the compounding is happening automatically now. + +#### Modified + +##### RewardMixnode + +As with previous changes, we're now rewarding given mixnode by its `NodeId` as opposed to `IdentityKey`. Furthermore, we no longer have to pass entire set of `NodeRewardParams`. Everything is implicit from the contract state, with the single exception of node `Performance`, which is now required. However, the strong typing ensures its always in the correct range. + +##### ClaimOperator/DelegatorReward + +All claim-related operations have been renamed to `Withdraw` for consistency with cosmos-sdk. It essentially zeroes your reward and moves this amount to your account address. + +### Queries + +#### Added + +##### GetPendingMixNodeOperatorReward + +Added variant of obtaining pending operator reward by the id of the bonded mixnode. + +#### Removed + +##### GetRewardPool, GetCirculatingSupply, GetStakingSupply, GetIntervalRewardPercent, GetSybilResistancePercent, GetActiveSetWorkFactor + +All the queries regarding individual rewarding parameters have been consolidated into a single `GetRewardingParams` + +#### Modified + +##### QueryOperatorReward + +Renamed to `GetPendingOperatorReward` for consistency' sake. + +##### QueryDelegatorReward + +Renamed to `GetPendingDelegatorReward` for consistency' sake. + +### Storage + +As with everything in this module, storage was also completely revamped. The changes here mostly follow on the changes to data structs. + +#### Added + +- `REWARDING_PARAMS` - all the rewarding parameters are consolidated in a single `Item` +- `PENDING_REWARD_POOL_CHANGE` - keeping track of the reward pool changes that shall get applied at the end of the interval +- `MIXNODE_REWARDING` - per mixnode, indexed by `NodeId`, parameters required to determine operator and delegates rewards + +#### Removed + +- `REWARD_POOL` - incorporated into `REWARDING_PARAMS` +- `REWARDING_STATUS` - it was already deprecated to begin with since we're no longer explicitly rewarding delegators, +- `DELEGATOR_REWARD_CLAIMED_HEIGHT`, `OPERATOR_REWARD_CLAIMED_HEIGHT` - due to auto-compounding, we don't have to keep track of heights of reward claiming +- `EPOCH_REWARD_PARAMS` - we no longer have to retroactively determine rewards for past epochs and thus we no longer have to keep track of rewarding params for past epochs + +## Final remarks + +As mentioned during multiple chats, I think the migration the rest of our codebase is going to be a huge undertaking mostly because of how many aspects of the system this change is affecting. From the top of my head, we'd need to definitely change our `nymd client` (and as a result `validator-api`, `clients`, etc.) and also the vesting contract. + +With the latter case (and with the current mixnet contract), it's going to be even trickier given that the current contract is already live. We will need to adjust how the values are stored, i.e. mixnodes are now indexed by `NodeId` as opposed to `IdentityKey`. My recommendation would be to create a migration such that it would "cancel" / "return" (you name it) all existing delegations and bonds so that the users would have to make new ones under the new contract. diff --git a/contracts/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index 3f29aa7e94..e13491a4f6 100644 --- a/contracts/vesting/Cargo.toml +++ b/contracts/vesting/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vesting-contract" -version = "1.0.1" +version = "1.1.0" authors = ["Drazen Urch "] edition = "2021" @@ -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 "} @@ -23,3 +24,6 @@ cw-storage-plus = { version = "0.13.4", features = ["iterator"] } schemars = "0.8" serde = { version = "1.0", default-features = false, features = ["derive"] } thiserror = { version = "1.0" } + +[build-dependencies] +vergen = { version = "5", default-features = false, features = ["build", "git", "rustc"] } diff --git a/contracts/vesting/build.rs b/contracts/vesting/build.rs new file mode 100644 index 0000000000..01b3a20dc6 --- /dev/null +++ b/contracts/vesting/build.rs @@ -0,0 +1,8 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use vergen::{vergen, Config}; + +fn main() { + vergen(Config::default()).expect("failed to extract build metadata") +} diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index b1dad8944c..da293349bf 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -1,18 +1,21 @@ use crate::errors::ContractError; -use crate::queued_migrations::migrate_config_from_env; +use crate::queued_migrations::migrate_to_v2_mixnet_contract; use crate::storage::{ - account_from_address, locked_pledge_cap, update_locked_pledge_cap, ADMIN, - MIXNET_CONTRACT_ADDRESS, MIX_DENOM, + account_from_address, BlockTimestampSecs, ADMIN, DELEGATIONS, MIXNET_CONTRACT_ADDRESS, + MIX_DENOM, }; use crate::traits::{ DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, VestingAccount, }; use crate::vesting::{populate_vesting_periods, Account}; +use contracts_common::ContractBuildInformation; use cosmwasm_std::{ - coin, entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, - Response, Timestamp, Uint128, + coin, entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, Order, + QueryResponse, Response, StdResult, Timestamp, Uint128, }; -use mixnet_contract_common::{Gateway, IdentityKey, MixNode}; +use cw_storage_plus::Bound; +use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; +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, @@ -22,10 +25,14 @@ use vesting_contract_common::events::{ use vesting_contract_common::messages::{ ExecuteMsg, InitMsg, MigrateMsg, QueryMsg, VestingSpecification, }; -use vesting_contract_common::{OriginalVestingResponse, Period, PledgeData}; +use vesting_contract_common::{ + AllDelegationsResponse, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeCap, + PledgeData, VestingDelegation, +}; pub const INITIAL_LOCKED_PLEDGE_CAP: Uint128 = Uint128::new(100_000_000_000); +/// Instantiate the contract #[entry_point] pub fn instantiate( deps: DepsMut<'_>, @@ -33,7 +40,7 @@ pub fn instantiate( info: MessageInfo, msg: InitMsg, ) -> Result { - // ADMIN is set to the address that instantiated the contract, TODO: make this updatable + //! ADMIN is set to the address that instantiated the contract ADMIN.save(deps.storage, &info.sender.to_string())?; MIXNET_CONTRACT_ADDRESS.save(deps.storage, &msg.mixnet_contract_address)?; MIX_DENOM.save(deps.storage, &msg.mix_denom)?; @@ -41,9 +48,8 @@ pub fn instantiate( } #[entry_point] -pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result { - migrate_config_from_env(_deps, _env, _msg)?; - Ok(Response::default()) +pub fn migrate(deps: DepsMut<'_>, _env: Env, msg: MigrateMsg) -> Result { + migrate_to_v2_mixnet_contract(deps, msg) } #[entry_point] @@ -54,41 +60,41 @@ 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) } ExecuteMsg::ClaimOperatorReward {} => try_claim_operator_reward(deps, info), - ExecuteMsg::ClaimDelegatorReward { mix_identity } => { - try_claim_delegator_reward(deps, info, mix_identity) + ExecuteMsg::ClaimDelegatorReward { mix_id } => { + try_claim_delegator_reward(deps, info, mix_id) } - ExecuteMsg::CompoundDelegatorReward { mix_identity } => { - try_compound_delegator_reward(mix_identity, info, deps) + ExecuteMsg::UpdateMixnodeConfig { new_config } => { + try_update_mixnode_config(new_config, info, deps) + } + ExecuteMsg::UpdateMixnodeCostParams { new_costs } => { + try_update_mixnode_cost_params(new_costs, info, deps) } - ExecuteMsg::CompoundOperatorReward {} => try_compound_operator_reward(info, deps), - ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - } => try_update_mixnode_config(profit_margin_percent, info, deps), ExecuteMsg::UpdateMixnetAddress { address } => { try_update_mixnet_address(address, info, deps) } - ExecuteMsg::DelegateToMixnode { - mix_identity, - amount, - } => try_delegate_to_mixnode(mix_identity, amount, info, env, deps), - ExecuteMsg::UndelegateFromMixnode { mix_identity } => { - try_undelegate_from_mixnode(mix_identity, info, deps) + ExecuteMsg::DelegateToMixnode { mix_id, amount } => { + try_delegate_to_mixnode(mix_id, amount, info, env, deps) + } + ExecuteMsg::UndelegateFromMixnode { mix_id } => { + try_undelegate_from_mixnode(mix_id, info, deps) } ExecuteMsg::CreateAccount { owner_address, staking_address, vesting_spec, + cap, } => try_create_periodic_vesting_account( &owner_address, staking_address, vesting_spec, + cap, info, env, deps, @@ -98,14 +104,23 @@ pub fn execute( } ExecuteMsg::TrackUndelegation { owner, - mix_identity, + mix_id, amount, - } => try_track_undelegation(&owner, mix_identity, amount, info, deps), + } => try_track_undelegation(&owner, mix_id, amount, info, deps), ExecuteMsg::BondMixnode { mix_node, + cost_params, owner_signature, amount, - } => try_bond_mixnode(mix_node, owner_signature, amount, info, env, deps), + } => try_bond_mixnode( + mix_node, + cost_params, + owner_signature, + amount, + info, + env, + deps, + ), ExecuteMsg::UnbondMixnode {} => try_unbond_mixnode(info, deps), ExecuteMsg::TrackUnbondMixnode { owner, amount } => { try_track_unbond_mixnode(&owner, amount, info, deps) @@ -128,28 +143,47 @@ pub fn execute( } } +/// Update locked_pledge_cap, the hard cap for staking/bonding with unvested tokens. +/// +/// 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()) } +/// Update config for a mixnode bonded with vesting account, sends [mixnet_contract_common::ExecuteMsg::UpdateMixnodeConfig] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_update_mixnode_config( - profit_margin_percent: u8, + new_config: MixNodeConfigUpdate, info: MessageInfo, deps: DepsMut, ) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_update_mixnode_config(profit_margin_percent, deps.storage) + account.try_update_mixnode_config(new_config, deps.storage) } -// Only contract admin, set at init +pub fn try_update_mixnode_cost_params( + new_costs: MixNodeCostParams, + info: MessageInfo, + deps: DepsMut, +) -> Result { + let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; + account.try_update_mixnode_cost_params(new_costs, deps.storage) +} + +/// Updates mixnet contract address, for cases when a new mixnet contract is deployed. +/// +/// Callable by ADMIN only, see [instantiate]. pub fn try_update_mixnet_address( address: String, info: MessageInfo, @@ -162,7 +196,7 @@ pub fn try_update_mixnet_address( Ok(Response::default()) } -// Only contract owner of vesting account +/// Withdraw already vested coins. pub fn try_withdraw_vested_coins( amount: Coin, env: Env, @@ -203,6 +237,7 @@ pub fn try_withdraw_vested_coins( } } +/// Transfer ownership of the entire vesting account. fn try_transfer_ownership( to_address: String, info: MessageInfo, @@ -219,6 +254,7 @@ fn try_transfer_ownership( } } +/// Set or update staking address for a vesting account. fn try_update_staking_address( to_address: Option, info: MessageInfo, @@ -236,7 +272,7 @@ fn try_update_staking_address( } } -// Owner or staking +/// Bond a gateway, sends [mixnet_contract_common::ExecuteMsg::BondGatewayOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_bond_gateway( gateway: Gateway, owner_signature: String, @@ -251,11 +287,13 @@ pub fn try_bond_gateway( account.try_bond_gateway(gateway, owner_signature, pledge, &env, deps.storage) } +/// Unbond a gateway, sends [mixnet_contract_common::ExecuteMsg::UnbondGatewayOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_unbond_gateway(info: MessageInfo, deps: DepsMut<'_>) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; account.try_unbond_gateway(deps.storage) } +/// Track gateway unbonding, invoked by the mixnet contract after succesful unbonding, message containes coins returned including any accrued rewards. pub fn try_track_unbond_gateway( owner: &str, amount: Coin, @@ -270,16 +308,10 @@ pub fn try_track_unbond_gateway( Ok(Response::new().add_event(new_track_gateway_unbond_event())) } -pub fn try_compound_operator_reward( - info: MessageInfo, - deps: DepsMut<'_>, -) -> Result { - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_compound_operator_reward(deps.storage) -} - +/// Bond a mixnode, sends [mixnet_contract_common::ExecuteMsg::BondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_bond_mixnode( mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, amount: Coin, info: MessageInfo, @@ -289,14 +321,23 @@ pub fn try_bond_mixnode( let mix_denom = MIX_DENOM.load(deps.storage)?; let pledge = validate_funds(&[amount], mix_denom)?; let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_bond_mixnode(mix_node, owner_signature, pledge, &env, deps.storage) + account.try_bond_mixnode( + mix_node, + cost_params, + owner_signature, + pledge, + &env, + deps.storage, + ) } +/// Unbond a mixnode, sends [mixnet_contract_common::ExecuteMsg::UnbondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_unbond_mixnode(info: MessageInfo, deps: DepsMut<'_>) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; account.try_unbond_mixnode(deps.storage) } +/// Track mixnode unbonding, invoked by the mixnet contract after succesful unbonding, message containes coins returned including any accrued rewards. pub fn try_track_unbond_mixnode( owner: &str, amount: Coin, @@ -311,6 +352,7 @@ pub fn try_track_unbond_mixnode( Ok(Response::new().add_event(new_track_mixnode_unbond_event())) } +/// Track reward collection, invoked by the mixnert contract after sucessful reward compounding or claiming fn try_track_reward( deps: DepsMut<'_>, info: MessageInfo, @@ -325,9 +367,10 @@ fn try_track_reward( Ok(Response::new().add_event(new_track_reward_event())) } +/// 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_identity: IdentityKey, + mix_id: MixId, amount: Coin, info: MessageInfo, deps: DepsMut<'_>, @@ -336,12 +379,14 @@ fn try_track_undelegation( return Err(ContractError::NotMixnetContract(info.sender)); } let account = account_from_address(address, deps.storage, deps.api)?; - account.track_undelegation(mix_identity, amount, deps.storage)?; + + account.track_undelegation(mix_id, amount, deps.storage)?; Ok(Response::new().add_event(new_track_undelegation_event())) } +/// Delegate to mixnode, sends [mixnet_contract_common::ExecuteMsg::DelegateToMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].. fn try_delegate_to_mixnode( - mix_identity: IdentityKey, + mix_id: MixId, amount: Coin, info: MessageInfo, env: Env, @@ -350,18 +395,11 @@ fn try_delegate_to_mixnode( let mix_denom = MIX_DENOM.load(deps.storage)?; let amount = validate_funds(&[amount], mix_denom)?; let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_delegate_to_mixnode(mix_identity, amount, &env, deps.storage) -} - -fn try_compound_delegator_reward( - mix_identity: IdentityKey, - info: MessageInfo, - deps: DepsMut<'_>, -) -> Result { - let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_compound_delegator_reward(mix_identity, deps.storage) + + account.try_delegate_to_mixnode(mix_id, amount, &env, deps.storage) } +/// Claims operator reward, sends [mixnet_contract_common::ExecuteMsg::ClaimOperatorRewardOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. fn try_claim_operator_reward( deps: DepsMut<'_>, info: MessageInfo, @@ -370,28 +408,36 @@ fn try_claim_operator_reward( account.try_claim_operator_reward(deps.storage) } +/// Claims delegator reward, sends [mixnet_contract_common::ExecuteMsg::ClaimDelegatorRewardOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. fn try_claim_delegator_reward( deps: DepsMut<'_>, info: MessageInfo, - mix_identity: String, + mix_id: MixId, ) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_claim_delegator_reward(mix_identity, deps.storage) + + account.try_claim_delegator_reward(mix_id, deps.storage) } +/// Undelegates from a mixnode, sends [mixnet_contract_common::ExecuteMsg::UndelegateFromMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. fn try_undelegate_from_mixnode( - mix_identity: IdentityKey, + mix_id: MixId, info: MessageInfo, deps: DepsMut<'_>, ) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; - account.try_undelegate_from_mixnode(mix_identity, deps.storage) + + account.try_undelegate_from_mixnode(mix_id, deps.storage) } +/// Creates a new periodic vesting account, and deposits funds to vest into the contract. +/// +/// Callable by ADMIN only, see [instantiate]. fn try_create_periodic_vesting_account( owner_address: &str, staking_address: Option, vesting_spec: Option, + cap: Option, info: MessageInfo, env: Env, deps: DepsMut<'_>, @@ -399,6 +445,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(); @@ -414,6 +461,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 @@ -434,6 +486,7 @@ fn try_create_periodic_vesting_account( coin.clone(), start_time, periods, + cap, deps.storage, )?; @@ -448,7 +501,7 @@ 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::GetContractVersion {} => to_binary(&get_contract_version()), QueryMsg::LockedCoins { vesting_account_address, block_time, @@ -518,15 +571,18 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result { to_binary(&try_get_current_vesting_period(&address, deps, env)?) } + QueryMsg::GetDelegationTimes { address, mix_id } => { + to_binary(&try_get_delegation_times(deps, &address, mix_id)?) + } + QueryMsg::GetAllDelegations { start_after, limit } => { + to_binary(&try_get_all_delegations(deps, start_after, limit)?) + } }; Ok(query_res?) } -pub fn get_locked_pledge_cap(deps: Deps<'_>) -> 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, deps: Deps<'_>, @@ -536,11 +592,13 @@ pub fn try_get_current_vesting_period( Ok(account.get_current_vesting_period(env.block.time)) } +/// Loads mixnode bond from vesting contract storage. pub fn try_get_mixnode(address: &str, deps: Deps<'_>) -> Result, ContractError> { let account = account_from_address(address, deps.storage, deps.api)?; account.load_mixnode_pledge(deps.storage) } +/// Loads gateway bond from vesting contract storage. pub fn try_get_gateway(address: &str, deps: Deps<'_>) -> Result, ContractError> { let account = account_from_address(address, deps.storage, deps.api)?; account.load_gateway_pledge(deps.storage) @@ -550,6 +608,22 @@ pub fn try_get_account(address: &str, deps: Deps<'_>) -> Result ContractBuildInformation { + // as per docs + // env! macro will expand to the value of the named environment variable at + // compile time, yielding an expression of type `&'static str` + ContractBuildInformation { + build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(), + build_version: env!("VERGEN_BUILD_SEMVER").to_string(), + commit_sha: env!("VERGEN_GIT_SHA").to_string(), + commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(), + commit_branch: env!("VERGEN_GIT_BRANCH").to_string(), + rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(), + } +} + +/// Gets currently locked coins, see [crate::traits::VestingAccount::locked_coins] pub fn try_get_locked_coins( vesting_account_address: &str, block_time: Option, @@ -560,6 +634,7 @@ pub fn try_get_locked_coins( account.locked_coins(block_time, &env, deps.storage) } +/// Returns currently locked coins, see [crate::traits::VestingAccount::spendable_coins] pub fn try_get_spendable_coins( vesting_account_address: &str, block_time: Option, @@ -570,6 +645,7 @@ pub fn try_get_spendable_coins( account.spendable_coins(block_time, &env, deps.storage) } +/// Returns coins that have vested, see [crate::traits::VestingAccount::get_vested_coins] pub fn try_get_vested_coins( vesting_account_address: &str, block_time: Option, @@ -580,6 +656,7 @@ pub fn try_get_vested_coins( account.get_vested_coins(block_time, &env, deps.storage) } +/// Returns coins that are vesting, see [crate::traits::VestingAccount::get_vesting_coins] pub fn try_get_vesting_coins( vesting_account_address: &str, block_time: Option, @@ -590,6 +667,7 @@ pub fn try_get_vesting_coins( account.get_vesting_coins(block_time, &env, deps.storage) } +/// See [crate::traits::VestingAccount::get_start_time] pub fn try_get_start_time( vesting_account_address: &str, deps: Deps<'_>, @@ -598,6 +676,7 @@ pub fn try_get_start_time( Ok(account.get_start_time()) } +/// See [crate::traits::VestingAccount::get_end_time] pub fn try_get_end_time( vesting_account_address: &str, deps: Deps<'_>, @@ -606,6 +685,7 @@ pub fn try_get_end_time( Ok(account.get_end_time()) } +/// See [crate::traits::VestingAccount::get_original_vesting] pub fn try_get_original_vesting( vesting_account_address: &str, deps: Deps<'_>, @@ -614,6 +694,7 @@ pub fn try_get_original_vesting( Ok(account.get_original_vesting()) } +/// See [crate::traits::VestingAccount::get_delegated_free] pub fn try_get_delegated_free( block_time: Option, vesting_account_address: &str, @@ -624,6 +705,7 @@ pub fn try_get_delegated_free( account.get_delegated_free(block_time, &env, deps.storage) } +/// See [crate::traits::VestingAccount::get_delegated_vesting] pub fn try_get_delegated_vesting( block_time: Option, vesting_account_address: &str, @@ -634,6 +716,64 @@ pub fn try_get_delegated_vesting( account.get_delegated_vesting(block_time, &env, deps.storage) } +/// Returns timestamps at which delegations were made +pub fn try_get_delegation_times( + deps: Deps<'_>, + vesting_account_address: &str, + 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)?; + + let delegation_timestamps = DELEGATIONS + .prefix((account.storage_key(), mix_id)) + .keys(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + + Ok(DelegationTimesResponse { + owner, + account_id: account.storage_key(), + mix_id, + delegation_timestamps, + }) +} + +pub fn try_get_all_delegations( + deps: Deps<'_>, + start_after: Option<(u32, MixId, BlockTimestampSecs)>, + limit: Option, +) -> Result { + let limit = limit.unwrap_or(100).min(200) as usize; + + let start = start_after.map(Bound::exclusive); + let delegations = DELEGATIONS + .range(deps.storage, start, None, Order::Ascending) + .map(|kv| { + kv.map( + |((account_id, mix_id, block_timestamp), amount)| VestingDelegation { + account_id, + mix_id, + block_timestamp, + amount, + }, + ) + }) + .collect::>>()?; + + let start_next_after = if delegations.len() < limit { + None + } else { + delegations + .last() + .map(|delegation| delegation.storage_key()) + }; + + Ok(AllDelegationsResponse { + delegations, + start_next_after, + }) +} + fn validate_funds(funds: &[Coin], mix_denom: String) -> Result { if funds.is_empty() || funds[0].amount.is_zero() { return Err(ContractError::EmptyFunds); diff --git a/contracts/vesting/src/errors.rs b/contracts/vesting/src/errors.rs index 867620ff8f..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::IdentityKey; +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, IdentityKey), + 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/lib.rs b/contracts/vesting/src/lib.rs index 4e44a69be8..8da47f12a6 100644 --- a/contracts/vesting/src/lib.rs +++ b/contracts/vesting/src/lib.rs @@ -1,3 +1,6 @@ +#![allow(rustdoc::private_intra_doc_links)] +//! Nym vesting contract, providing vesting accounts with ability to stake unvested tokens + pub mod contract; mod errors; mod queued_migrations; diff --git a/contracts/vesting/src/queued_migrations.rs b/contracts/vesting/src/queued_migrations.rs index 75d63ba648..9bb44331a1 100644 --- a/contracts/vesting/src/queued_migrations.rs +++ b/contracts/vesting/src/queued_migrations.rs @@ -1,17 +1,15 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{DepsMut, Env, Response}; +use crate::errors::ContractError; +use crate::storage::MIXNET_CONTRACT_ADDRESS; +use cosmwasm_std::{DepsMut, Response}; use vesting_contract_common::MigrateMsg; -use crate::{errors::ContractError, storage::MIX_DENOM}; - -pub fn migrate_config_from_env( +pub fn migrate_to_v2_mixnet_contract( deps: DepsMut<'_>, - _env: Env, msg: MigrateMsg, ) -> Result { - MIX_DENOM.save(deps.storage, &msg.mix_denom)?; - - Ok(Default::default()) + MIXNET_CONTRACT_ADDRESS.save(deps.storage, &msg.v2_mixnet_contract_address)?; + Ok(Response::new()) } diff --git a/contracts/vesting/src/storage.rs b/contracts/vesting/src/storage.rs index 400d90fc3e..a1216fa87e 100644 --- a/contracts/vesting/src/storage.rs +++ b/contracts/vesting/src/storage.rs @@ -1,11 +1,11 @@ +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; +use mixnet_contract_common::{IdentityKey, MixId}; use vesting_contract_common::PledgeData; -type BlockHeight = u64; +pub(crate) type BlockTimestampSecs = u64; pub const KEY: Item<'_, u32> = Item::new("key"); const ACCOUNTS: Map<'_, String, Account> = Map::new("acc"); @@ -14,28 +14,15 @@ const BALANCES: Map<'_, u32, Uint128> = Map::new("blc"); const WITHDRAWNS: Map<'_, u32, Uint128> = Map::new("wthd"); const BOND_PLEDGES: Map<'_, u32, PledgeData> = Map::new("bnd"); const GATEWAY_PLEDGES: Map<'_, u32, PledgeData> = Map::new("gtw"); -pub const DELEGATIONS: Map<'_, (u32, IdentityKey, BlockHeight), Uint128> = Map::new("dlg"); +pub const _OLD_DELEGATIONS: Map<'_, (u32, IdentityKey, BlockTimestampSecs), Uint128> = + Map::new("dlg"); +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, IdentityKey, BlockHeight), + key: (u32, MixId, BlockTimestampSecs), amount: Uint128, storage: &mut dyn Storage, ) -> Result<(), ContractError> { @@ -44,7 +31,7 @@ pub fn save_delegation( } pub fn remove_delegation( - key: (u32, IdentityKey, BlockHeight), + 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/bonding_account.rs b/contracts/vesting/src/traits/bonding_account.rs index 1ce2690df9..36c248e393 100644 --- a/contracts/vesting/src/traits/bonding_account.rs +++ b/contracts/vesting/src/traits/bonding_account.rs @@ -1,18 +1,17 @@ use crate::errors::ContractError; use cosmwasm_std::{Coin, Env, Response, Storage}; -use mixnet_contract_common::{Gateway, MixNode}; +use mixnet_contract_common::{ + mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, + Gateway, MixNode, +}; pub trait MixnodeBondingAccount { - fn try_compound_operator_reward( - &self, - storage: &dyn Storage, - ) -> Result; - fn try_claim_operator_reward(&self, storage: &dyn Storage) -> Result; fn try_bond_mixnode( &self, mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, pledge: Coin, env: &Env, @@ -29,7 +28,13 @@ pub trait MixnodeBondingAccount { fn try_update_mixnode_config( &self, - profit_margin_percent: u8, + new_config: MixNodeConfigUpdate, + storage: &mut dyn Storage, + ) -> Result; + + fn try_update_mixnode_cost_params( + &self, + new_costs: MixNodeCostParams, storage: &mut dyn Storage, ) -> Result; } diff --git a/contracts/vesting/src/traits/delegating_account.rs b/contracts/vesting/src/traits/delegating_account.rs index 174a9202ea..4beefabe28 100644 --- a/contracts/vesting/src/traits/delegating_account.rs +++ b/contracts/vesting/src/traits/delegating_account.rs @@ -1,23 +1,17 @@ use crate::errors::ContractError; use cosmwasm_std::{Coin, Env, Response, Storage, Uint128}; -use mixnet_contract_common::IdentityKey; +use mixnet_contract_common::MixId; pub trait DelegatingAccount { fn try_claim_delegator_reward( &self, - mix_identity: IdentityKey, - storage: &dyn Storage, - ) -> Result; - - fn try_compound_delegator_reward( - &self, - mix_identity: IdentityKey, + mix_id: MixId, storage: &dyn Storage, ) -> Result; fn try_delegate_to_mixnode( &self, - mix_identity: IdentityKey, + mix_id: MixId, amount: Coin, env: &Env, storage: &mut dyn Storage, @@ -25,7 +19,7 @@ pub trait DelegatingAccount { fn try_undelegate_from_mixnode( &self, - mix_identity: IdentityKey, + mix_id: MixId, storage: &dyn Storage, ) -> Result; @@ -36,7 +30,7 @@ pub trait DelegatingAccount { fn track_delegation( &self, block_height: u64, - mix_identity: IdentityKey, + mix_id: MixId, // Save some gas by passing it in current_balance: Uint128, delegation: Coin, @@ -46,7 +40,7 @@ pub trait DelegatingAccount { // vesting account performs an undelegation. fn track_undelegation( &self, - mix_identity: IdentityKey, + mix_id: MixId, amount: Coin, storage: &mut dyn Storage, ) -> Result<(), ContractError>; diff --git a/contracts/vesting/src/traits/vesting_account.rs b/contracts/vesting/src/traits/vesting_account.rs index 95f445114e..04173a56c6 100644 --- a/contracts/vesting/src/traits/vesting_account.rs +++ b/contracts/vesting/src/traits/vesting_account.rs @@ -9,12 +9,13 @@ pub trait VestingAccount { env: &Env, ) -> Result; - // locked_coins returns the set of coins that are not spendable (can still be delegated tough) (i.e. locked), - // defined as the vesting coins that are not delegated or pledged. - // - // To get spendable coins of a vesting account, first the total balance must - // be retrieved and the locked tokens can be subtracted from the total balance. - // Note, the spendable balance can be negative. + /// Returns the set of coins that are not spendable (can still be delegated tough) (i.e. locked), + /// defined as the vesting coins that are not delegated or pledged. + /// + /// To get spendable coins of a vesting account, first the total balance must + /// be retrieved and the locked tokens can be subtracted from the total balance. + /// Note, the spendable balance can be negative. + /// See [/vesting-contract/struct.Account.html/method.locked_coins] for impl fn locked_coins( &self, block_time: Option, @@ -22,7 +23,8 @@ pub trait VestingAccount { storage: &dyn Storage, ) -> Result; - // Calculates the total spendable balance that can be sent to other accounts. + /// Calculated as current_balance minus [crate::traits::VestingAccount::locked_coins] + /// See [/vesting-contract/struct.Account.html/method.spendable_coins] for impl fn spendable_coins( &self, block_time: Option, @@ -30,12 +32,15 @@ pub trait VestingAccount { storage: &dyn Storage, ) -> Result; + /// See [/vesting-contract/struct.Account.html/method.get_vested_coins] for impl fn get_vested_coins( &self, block_time: Option, env: &Env, storage: &dyn Storage, ) -> Result; + + /// See [/vesting-contract/struct.Account.html/method.get_vesting_coins] for impl fn get_vesting_coins( &self, block_time: Option, @@ -43,39 +48,52 @@ pub trait VestingAccount { storage: &dyn Storage, ) -> Result; + /// See [/vesting-contract/struct.Account.html/method.get_start_time] for impl fn get_start_time(&self) -> Timestamp; + /// See [/vesting-contract/struct.Account.html/method.get_end_time] for impl fn get_end_time(&self) -> Timestamp; + /// Returns amount of coins set at account creation + /// See [/vesting-contract/struct.Account.html/method.get_original_vesting] for impl fn get_original_vesting(&self) -> OriginalVestingResponse; + + /// See [/vesting-contract/struct.Account.html/method.get_delegated_free] for impl fn get_delegated_free( &self, block_time: Option, env: &Env, storage: &dyn Storage, ) -> Result; + + /// See [/vesting-contract/struct.Account.html/method.get_delegated_vesting] for impl fn get_delegated_vesting( &self, block_time: Option, env: &Env, storage: &dyn Storage, ) -> Result; + + /// See [/vesting-contract/struct.Account.html/method.get_pledged_free] for impl fn get_pledged_free( &self, block_time: Option, env: &Env, storage: &dyn Storage, ) -> Result; + /// See [/vesting-contract/struct.Account.html/method.get_pledged_vesting] for impl fn get_pledged_vesting( &self, block_time: Option, env: &Env, storage: &dyn Storage, ) -> Result; + /// See [/vesting-contract/struct.Account.html/method.transfer_ownership] for impl fn transfer_ownership( &mut self, to_address: &Addr, storage: &mut dyn Storage, ) -> Result<(), ContractError>; + /// See [/vesting-contract/struct.Account.html/method.update_staking_address] for impl fn update_staking_address( &mut self, to_address: Option, diff --git a/contracts/vesting/src/vesting/account/delegating_account.rs b/contracts/vesting/src/vesting/account/delegating_account.rs index 48f32207ac..62fa42b21e 100644 --- a/contracts/vesting/src/vesting/account/delegating_account.rs +++ b/contracts/vesting/src/vesting/account/delegating_account.rs @@ -1,68 +1,45 @@ use crate::errors::ContractError; -use crate::storage::locked_pledge_cap; use crate::storage::save_delegation; use crate::storage::MIXNET_CONTRACT_ADDRESS; -use crate::storage::MIX_DENOM; 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::IdentityKey; +use mixnet_contract_common::MixId; use vesting_contract_common::events::{ new_vesting_delegation_event, new_vesting_undelegation_event, }; -use vesting_contract_common::one_ucoin; use super::Account; impl DelegatingAccount for Account { fn try_claim_delegator_reward( &self, - mix_identity: IdentityKey, + mix_id: MixId, storage: &dyn Storage, ) -> Result { - let msg = MixnetExecuteMsg::ClaimDelegatorRewardOnBehalf { + let msg = MixnetExecuteMsg::WithdrawDelegatorRewardOnBehalf { owner: self.owner_address().to_string(), - mix_identity, + mix_id, }; - let compound_delegator_reward_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; - - Ok(Response::new().add_message(compound_delegator_reward_msg)) - } - - fn try_compound_delegator_reward( - &self, - mix_identity: IdentityKey, - storage: &dyn Storage, - ) -> Result { - let msg = MixnetExecuteMsg::CompoundDelegatorRewardOnBehalf { - owner: self.owner_address().to_string(), - mix_identity, - }; - let compound_delegator_reward_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; + let compound_delegator_reward_msg = + wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; Ok(Response::new().add_message(compound_delegator_reward_msg)) } fn try_delegate_to_mixnode( &self, - mix_identity: IdentityKey, + 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 +56,7 @@ impl DelegatingAccount for Account { } let msg = MixnetExecuteMsg::DelegateToMixnodeOnBehalf { - mix_identity: mix_identity.clone(), + mix_id, delegate: self.owner_address().into_string(), }; let delegate_to_mixnode = wasm_execute( @@ -88,8 +65,8 @@ impl DelegatingAccount for Account { vec![coin.clone()], )?; self.track_delegation( - env.block.height, - mix_identity, + env.block.time.seconds(), + mix_id, current_balance, coin, storage, @@ -102,25 +79,22 @@ impl DelegatingAccount for Account { fn try_undelegate_from_mixnode( &self, - mix_identity: IdentityKey, + mix_id: MixId, storage: &dyn Storage, ) -> Result { - if !self.any_delegation_for_mix(&mix_identity, storage) { + if !self.any_delegation_for_mix(mix_id, storage) { return Err(ContractError::NoSuchDelegation( self.owner_address(), - mix_identity, + mix_id, )); } let msg = MixnetExecuteMsg::UndelegateFromMixnodeOnBehalf { - mix_identity, + mix_id, delegate: self.owner_address().into_string(), }; - let undelegate_from_mixnode = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; + let undelegate_from_mixnode = + wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; Ok(Response::new() .add_message(undelegate_from_mixnode) @@ -129,14 +103,14 @@ impl DelegatingAccount for Account { fn track_delegation( &self, - block_height: u64, - mix_identity: IdentityKey, + block_timestamp_secs: u64, + mix_id: MixId, current_balance: Uint128, delegation: Coin, storage: &mut dyn Storage, ) -> Result<(), ContractError> { save_delegation( - (self.storage_key(), mix_identity, block_height), + (self.storage_key(), mix_id, block_timestamp_secs), delegation.amount, storage, )?; @@ -147,11 +121,11 @@ impl DelegatingAccount for Account { fn track_undelegation( &self, - mix_identity: IdentityKey, + mix_id: MixId, amount: Coin, storage: &mut dyn Storage, ) -> Result<(), ContractError> { - self.remove_delegations_for_mix(&mix_identity, storage)?; + self.remove_delegations_for_mix(mix_id, storage)?; let new_balance = Uint128::new(self.load_balance(storage)?.u128() + amount.amount.u128()); self.save_balance(new_balance, storage)?; Ok(()) diff --git a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs index 33af1d6506..91cdd41c68 100644 --- a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs @@ -1,8 +1,6 @@ use super::PledgeData; use crate::errors::ContractError; -use crate::storage::locked_pledge_cap; use crate::storage::MIXNET_CONTRACT_ADDRESS; -use crate::storage::MIX_DENOM; use crate::traits::GatewayBondingAccount; use crate::traits::VestingAccount; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; @@ -10,7 +8,6 @@ use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, Gateway}; use vesting_contract_common::events::{ new_vesting_gateway_bonding_event, new_vesting_gateway_unbonding_event, }; -use vesting_contract_common::one_ucoin; use super::Account; @@ -24,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 { @@ -74,11 +72,7 @@ impl GatewayBondingAccount for Account { }; if let Some(_bond) = self.load_gateway_pledge(storage)? { - let unbond_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; + let unbond_msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; Ok(Response::new() .add_message(unbond_msg) diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index 6312647a9a..e3aec82584 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -1,85 +1,46 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::Account; use crate::errors::ContractError; -use crate::storage::locked_pledge_cap; use crate::storage::MIXNET_CONTRACT_ADDRESS; -use crate::storage::MIX_DENOM; use crate::traits::MixnodeBondingAccount; use crate::traits::VestingAccount; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; +use mixnet_contract_common::mixnode::MixNodeConfigUpdate; +use mixnet_contract_common::mixnode::MixNodeCostParams; use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, MixNode}; use vesting_contract_common::events::{ new_vesting_mixnode_bonding_event, new_vesting_mixnode_unbonding_event, - new_vesting_update_mixnode_config_event, + new_vesting_update_mixnode_config_event, new_vesting_update_mixnode_cost_params_event, }; - -use vesting_contract_common::one_ucoin; use vesting_contract_common::PledgeData; -use super::Account; - impl MixnodeBondingAccount for Account { fn try_claim_operator_reward(&self, storage: &dyn Storage) -> Result { - let msg = MixnetExecuteMsg::ClaimOperatorRewardOnBehalf { + let msg = MixnetExecuteMsg::WithdrawOperatorRewardOnBehalf { owner: self.owner_address().into_string(), }; - let compound_operator_reward_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; + let compound_operator_reward_msg = + wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; Ok(Response::new().add_message(compound_operator_reward_msg)) } - fn try_compound_operator_reward( - &self, - storage: &dyn Storage, - ) -> Result { - let msg = MixnetExecuteMsg::CompoundOperatorRewardOnBehalf { - owner: self.owner_address().into_string(), - }; - - let compound_operator_reward_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; - - Ok(Response::new().add_message(compound_operator_reward_msg)) - } - - fn try_update_mixnode_config( - &self, - profit_margin_percent: u8, - storage: &mut dyn Storage, - ) -> Result { - let msg = MixnetExecuteMsg::UpdateMixnodeConfigOnBehalf { - profit_margin_percent, - owner: self.owner_address().into_string(), - }; - - let update_mixnode_config_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; - - Ok(Response::new() - .add_message(update_mixnode_config_msg) - .add_event(new_vesting_update_mixnode_config_event())) - } - fn try_bond_mixnode( &self, mix_node: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, pledge: 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)? + 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 { @@ -105,6 +66,7 @@ impl MixnodeBondingAccount for Account { let msg = MixnetExecuteMsg::BondMixnodeOnBehalf { mix_node, + cost_params, owner: self.owner_address().into_string(), owner_signature, }; @@ -128,11 +90,7 @@ impl MixnodeBondingAccount for Account { }; if self.load_mixnode_pledge(storage)?.is_some() { - let unbond_msg = wasm_execute( - MIXNET_CONTRACT_ADDRESS.load(storage)?, - &msg, - vec![one_ucoin(MIX_DENOM.load(storage)?)], - )?; + let unbond_msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; Ok(Response::new() .add_message(unbond_msg) @@ -155,4 +113,40 @@ impl MixnodeBondingAccount for Account { self.remove_mixnode_pledge(storage)?; Ok(()) } + + fn try_update_mixnode_config( + &self, + new_config: MixNodeConfigUpdate, + storage: &mut dyn Storage, + ) -> Result { + let msg = MixnetExecuteMsg::UpdateMixnodeConfigOnBehalf { + new_config, + owner: self.owner_address().into_string(), + }; + + let update_mixnode_config_msg = + wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; + + Ok(Response::new() + .add_message(update_mixnode_config_msg) + .add_event(new_vesting_update_mixnode_config_event())) + } + + fn try_update_mixnode_cost_params( + &self, + new_costs: MixNodeCostParams, + storage: &mut dyn Storage, + ) -> Result { + let msg = MixnetExecuteMsg::UpdateMixnodeCostParamsOnBehalf { + new_costs, + owner: self.owner_address().into_string(), + }; + + let update_mixnode_costs_msg = + wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; + + Ok(Response::new() + .add_message(update_mixnode_costs_msg) + .add_event(new_vesting_update_mixnode_cost_params_event())) + } } diff --git a/contracts/vesting/src/vesting/account/mod.rs b/contracts/vesting/src/vesting/account/mod.rs index 5418619cfe..8520dad85e 100644 --- a/contracts/vesting/src/vesting/account/mod.rs +++ b/contracts/vesting/src/vesting/account/mod.rs @@ -3,14 +3,15 @@ use crate::errors::ContractError; use crate::storage::{ load_balance, load_bond_pledge, load_gateway_pledge, load_withdrawn, remove_bond_pledge, remove_delegation, remove_gateway_pledge, save_account, save_balance, save_bond_pledge, - save_gateway_pledge, save_withdrawn, DELEGATIONS, KEY, + 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::IdentityKey; +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; @@ -31,6 +32,8 @@ pub struct Account { pub periods: Vec, pub coin: Coin, storage_key: u32, + #[serde(default)] + pub pledge_cap: Option, } impl Account { @@ -40,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)?; @@ -51,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() } @@ -101,10 +117,9 @@ impl Account { } } + /// Returns the index of the next vesting period. Unless the current time is somehow in the past or vesting has not started yet. + /// In case vesting is over it will always return NUM_VESTING_PERIODS. pub fn get_current_vesting_period(&self, block_time: Timestamp) -> Period { - // Returns the index of the next vesting period. Unless the current time is somehow in the past or vesting has not started yet. - // In case vesting is over it will always return NUM_VESTING_PERIODS. - if block_time.seconds() < self.periods.first().unwrap().start_time { Period::Before } else if self.periods.last().unwrap().end_time() < block_time { @@ -198,9 +213,9 @@ impl Account { remove_gateway_pledge(self.storage_key(), storage) } - pub fn any_delegation_for_mix(&self, mix: &str, storage: &dyn Storage) -> bool { + pub fn any_delegation_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> bool { DELEGATIONS - .prefix((self.storage_key(), mix.to_string())) + .prefix((self.storage_key(), mix_id)) .range(storage, None, None, Order::Ascending) .next() .is_some() @@ -208,7 +223,7 @@ impl Account { pub fn remove_delegations_for_mix( &self, - mix: &str, + mix_id: MixId, storage: &mut dyn Storage, ) -> Result<(), ContractError> { let limit = 50; @@ -219,7 +234,7 @@ impl Account { loop { block_heights.extend( DELEGATIONS - .prefix((self.storage_key(), mix.to_string())) + .prefix((self.storage_key(), mix_id)) .keys(storage, start_after, None, Order::Ascending) .take(limit) .filter_map(|key| key.ok()), @@ -238,18 +253,18 @@ impl Account { } for block_height in block_heights { - remove_delegation((self.storage_key(), mix.to_string(), block_height), storage)?; + remove_delegation((self.storage_key(), mix_id, block_height), storage)?; } Ok(()) } pub fn total_delegations_for_mix( &self, - mix: IdentityKey, + mix_id: MixId, storage: &dyn Storage, ) -> Result { Ok(DELEGATIONS - .prefix((self.storage_key(), mix)) + .prefix((self.storage_key(), mix_id)) .range(storage, None, None, Order::Ascending) .filter_map(|x| x.ok()) .fold(Uint128::zero(), |acc, (_key, val)| acc + val)) @@ -262,4 +277,19 @@ impl Account { .filter_map(|x| x.ok()) .fold(Uint128::zero(), |acc, (_key, val)| acc + val)) } + + pub fn total_delegations_at_timestamp( + &self, + storage: &dyn Storage, + start_time: BlockTimestampSecs, + ) -> Result { + Ok(DELEGATIONS + .sub_prefix(self.storage_key()) + .range(storage, None, None, Order::Ascending) + .filter_map(|x| x.ok()) + .filter(|((_mix, block_time), _amount)| *block_time <= start_time) + .fold(Uint128::zero(), |acc, ((_mix, _block_time), amount)| { + acc + amount + })) + } } diff --git a/contracts/vesting/src/vesting/account/vesting_account.rs b/contracts/vesting/src/vesting/account/vesting_account.rs index dc93627d1d..53f39e2891 100644 --- a/contracts/vesting/src/vesting/account/vesting_account.rs +++ b/contracts/vesting/src/vesting/account/vesting_account.rs @@ -1,7 +1,7 @@ use crate::errors::ContractError; -use crate::storage::{delete_account, save_account, DELEGATIONS, MIX_DENOM}; +use crate::storage::{delete_account, save_account, MIX_DENOM}; use crate::traits::VestingAccount; -use cosmwasm_std::{Addr, Coin, Env, Order, Storage, Timestamp, Uint128}; +use cosmwasm_std::{Addr, Coin, Env, Storage, Timestamp, Uint128}; use vesting_contract_common::{OriginalVestingResponse, Period}; use super::Account; @@ -16,20 +16,14 @@ impl VestingAccount for Account { + self.get_pledged_vesting(None, env, storage)?.amount) } - fn track_reward(&self, amount: Coin, storage: &mut dyn Storage) -> Result<(), ContractError> { - let current_balance = self.load_balance(storage)?; - let new_balance = current_balance + amount.amount; - self.save_balance(new_balance, storage)?; - Ok(()) - } - + /// See [VestingAccount::locked_coins] for documentation. + /// Returns 0 in case of underflow. Which is fine, as the amount of pledged and delegated tokens can be larger then vesting_coins due to rewards and vesting periods expiring fn locked_coins( &self, block_time: Option, env: &Env, storage: &dyn Storage, ) -> Result { - // Returns 0 in case of underflow. Which is fine, as the amount of pledged and delegated tokens can be larger then vesting_coins due to rewards and vesting periods expiring Ok(Coin { amount: Uint128::new( self.get_vesting_coins(block_time, env, storage)? @@ -129,26 +123,20 @@ 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, Period::In(idx) => self.periods[idx as usize].start_time, }; - let coin = DELEGATIONS - .sub_prefix(self.storage_key()) - .range(storage, None, None, Order::Ascending) - .filter_map(|x| x.ok()) - .filter(|((_mix, block_time), _amount)| *block_time < start_time) - .fold(Uint128::zero(), |acc, ((_mix, _block_time), amount)| { - acc + amount - }); + let coin = self.total_delegations_at_timestamp(storage, start_time)?; let amount = Uint128::new(coin.u128().min(max_available.u128())); @@ -158,6 +146,7 @@ impl VestingAccount for Account { }) } + // TODO: why do we allow querying for block times in the past? - just use env.block.time all the time fn get_delegated_vesting( &self, block_time: Option, @@ -166,9 +155,11 @@ impl VestingAccount for Account { ) -> Result { let block_time = block_time.unwrap_or(env.block.time); let delegated_free = self.get_delegated_free(Some(block_time), env, storage)?; - let total_delegations = self.total_delegations(storage)?; - let amount = total_delegations - delegated_free.amount; + let delegations_before_start_time = + self.total_delegations_at_timestamp(storage, block_time.seconds())?; + + let amount = delegations_before_start_time - delegated_free.amount; Ok(Coin { amount, @@ -261,4 +252,11 @@ impl VestingAccount for Account { save_account(self, storage)?; Ok(()) } + + fn track_reward(&self, amount: Coin, storage: &mut dyn Storage) -> Result<(), ContractError> { + let current_balance = self.load_balance(storage)?; + let new_balance = current_balance + amount.amount; + self.save_balance(new_balance, storage)?; + Ok(()) + } } diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index 9dc389864d..d678d2aab8 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -36,19 +36,23 @@ pub fn populate_vesting_periods( #[cfg(test)] mod tests { - use crate::contract::execute; - use crate::storage::load_account; + 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, }; use crate::traits::DelegatingAccount; use crate::traits::VestingAccount; use crate::traits::{GatewayBondingAccount, MixnodeBondingAccount}; + use crate::vesting::{populate_vesting_periods, Account}; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coins, Addr, Coin, Timestamp, Uint128}; - use mixnet_contract_common::{Gateway, MixNode}; - use vesting_contract_common::messages::ExecuteMsg; + use mixnet_contract_common::mixnode::MixNodeCostParams; + 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() { @@ -59,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()); @@ -271,19 +276,15 @@ mod tests { let withdrawn = account.load_withdrawn(&deps.storage).unwrap(); assert_eq!(withdrawn, Uint128::zero()); - let mix_identity = "alice".to_string(); + let mix_id = 1; let delegation = Coin { amount: Uint128::new(90_000_000_000), denom: TEST_COIN_DENOM.to_string(), }; - let ok = account.try_delegate_to_mixnode( - mix_identity.clone(), - delegation.clone(), - &env, - &mut deps.storage, - ); + let ok = + account.try_delegate_to_mixnode(mix_id, delegation.clone(), &env, &mut deps.storage); assert!(ok.is_ok()); let vested_coins = account.get_vested_coins(None, &env, &deps.storage).unwrap(); @@ -311,11 +312,11 @@ mod tests { .unwrap(); assert_eq!(spendable.amount, Uint128::new(160_000_000_000)); - let ok = account.try_undelegate_from_mixnode(mix_identity.clone(), &mut deps.storage); + let ok = account.try_undelegate_from_mixnode(mix_id, &mut deps.storage); assert!(ok.is_ok()); account - .track_undelegation(mix_identity.clone(), delegation.clone(), &mut deps.storage) + .track_undelegation(mix_id, delegation.clone(), &mut deps.storage) .unwrap(); let delegated_free = account @@ -361,12 +362,8 @@ mod tests { .unwrap(); assert_eq!(spendable.amount, Uint128::zero()); - let ok = account.try_delegate_to_mixnode( - mix_identity.clone(), - delegation.clone(), - &env, - &mut deps.storage, - ); + let ok = + account.try_delegate_to_mixnode(mix_id, delegation.clone(), &env, &mut deps.storage); assert!(ok.is_ok()); let vested_coins = account.get_vested_coins(None, &env, &deps.storage).unwrap(); @@ -395,16 +392,42 @@ 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( - "alice".to_string(), + 1, Coin { amount: Uint128::new(1_000_000_000_001), denom: TEST_COIN_DENOM.to_string(), @@ -415,7 +438,7 @@ mod tests { assert!(err.is_err()); let ok = account.try_delegate_to_mixnode( - "alice".to_string(), + 1, Coin { amount: Uint128::new(90_000_000_000), denom: TEST_COIN_DENOM.to_string(), @@ -427,7 +450,7 @@ mod tests { // Fails due to delegation locked delegation cap let ok = account.try_delegate_to_mixnode( - "alice".to_string(), + 1, Coin { amount: Uint128::new(20_000_000_000), denom: TEST_COIN_DENOM.to_string(), @@ -440,9 +463,9 @@ 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( - "alice".to_string(), + 1, Coin { amount: Uint128::new(500_000_000_001), denom: TEST_COIN_DENOM.to_string(), @@ -452,11 +475,13 @@ mod tests { ); assert!(err.is_err()); - let total_delegations = account - .total_delegations_for_mix("alice".to_string(), &deps.storage) - .unwrap(); + 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) @@ -533,11 +558,19 @@ mod tests { sphinx_key: "sphinx".to_string(), identity_key: "identity".to_string(), version: "0.10.0".to_string(), - profit_margin_percent: 10, + }; + + let cost_params = MixNodeCostParams { + profit_margin_percent: Percent::from_percentage_value(10).unwrap(), + interval_operating_cost: Coin { + denom: "NYM".to_string(), + amount: Uint128::new(40), + }, }; // Try delegating too much let err = account.try_bond_mixnode( mix_node.clone(), + cost_params.clone(), "alice".to_string(), Coin { amount: Uint128::new(1_000_000_000_001), @@ -550,6 +583,7 @@ mod tests { let ok = account.try_bond_mixnode( mix_node.clone(), + cost_params.clone(), "alice".to_string(), Coin { amount: Uint128::new(90_000_000_000), @@ -566,6 +600,7 @@ mod tests { // Try delegating too much again let err = account.try_bond_mixnode( mix_node, + cost_params, "alice".to_string(), Coin { amount: Uint128::new(10_000_000_001), @@ -757,4 +792,172 @@ mod tests { .unwrap(); assert_eq!(Uint128::zero(), bonded_vesting.amount); } + + #[test] + fn delegated_free() { + let mut deps = init_contract(); + let mut env = mock_env(); + + let vesting_period_length_secs = 3600; + + let account_creation_timestamp = 1650000000; + let account_creation_blockheight = 12345; + + // this value is completely arbitrary, I just wanted to keep consistent + // (and make sure that if block timestamp increases so does the block height) + let blocks_per_period = 100; + + env.block.height = account_creation_blockheight; + env.block.time = Timestamp::from_seconds(account_creation_timestamp); + + // lets define some helper timestamps + + // our account is set to be created after 2 vesting periods already passed + let vesting_start_blockheight = account_creation_blockheight - 2 * blocks_per_period; + let vesting_start_timestamp = account_creation_timestamp - 2 * vesting_period_length_secs; + + let vesting_period2_start_blockheight = vesting_start_blockheight + blocks_per_period; + let vesting_period2_start_timestamp = vesting_start_timestamp + vesting_period_length_secs; + + // this vesting period is currently in progress! + let vesting_period3_start_blockheight = + vesting_period2_start_blockheight + blocks_per_period; + let vesting_period3_start_timestamp = + vesting_period2_start_timestamp + vesting_period_length_secs; + + // and this one is in the future! (in relation to account creation) + let vesting_period4_start_blockheight = + vesting_period3_start_blockheight + blocks_per_period; + let vesting_period4_start_timestamp = + vesting_period3_start_timestamp + vesting_period_length_secs; + + // lets create our vesting account + let periods = populate_vesting_periods( + vesting_start_timestamp, + VestingSpecification::new(None, Some(vesting_period_length_secs), None), + ); + + let vesting_account = Account::new( + Addr::unchecked("owner"), + Some(Addr::unchecked("staking")), + Coin { + amount: Uint128::new(1_000_000_000_000), + denom: TEST_COIN_DENOM.to_string(), + }, + Timestamp::from_seconds(account_creation_timestamp), + periods, + Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))), + deps.as_mut().storage, + ) + .unwrap(); + + // time for some delegations + + let mix_id = 42; + + let delegation = Coin { + amount: Uint128::new(90_000_000_000), + denom: TEST_COIN_DENOM.to_string(), + }; + + // delegate explicitly at the time the account was created + // (i.e. after 2 vesting periods already elapsed) + env.block.height = account_creation_blockheight; + env.block.time = Timestamp::from_seconds(account_creation_timestamp); + let ok = vesting_account.try_delegate_to_mixnode( + mix_id, + delegation.clone(), + &env, + &mut deps.storage, + ); + assert!(ok.is_ok()); + + let vested_coins = vesting_account + .get_vested_coins(None, &env, &deps.storage) + .unwrap(); + let vesting_coins = vesting_account + .get_vesting_coins(None, &env, &deps.storage) + .unwrap(); + assert_eq!(vested_coins.amount, Uint128::new(250_000_000_000)); + assert_eq!(vesting_coins.amount, Uint128::new(750_000_000_000)); + let delegated_free = vesting_account + .get_delegated_free(None, &env, &deps.storage) + .unwrap(); + let delegated_vesting = vesting_account + .get_delegated_vesting(None, &env, &deps.storage) + .unwrap(); + + // all good so far + assert_eq!(delegated_free.amount, Uint128::new(90_000_000_000)); + assert_eq!(delegated_vesting.amount, Uint128::zero()); + + // some time passes, and we're now into the next vesting period, more of our coins got unlocked! + env.block.height = vesting_period4_start_blockheight; + env.block.time = Timestamp::from_seconds(vesting_period4_start_timestamp); + + let vested_coins = vesting_account + .get_vested_coins(None, &env, &deps.storage) + .unwrap(); + let vesting_coins = vesting_account + .get_vesting_coins(None, &env, &deps.storage) + .unwrap(); + assert_eq!(vested_coins.amount, Uint128::new(375_000_000_000)); + assert_eq!(vesting_coins.amount, Uint128::new(625_000_000_000)); + + // and nothing about our existing delegation changed + let delegated_free = vesting_account + .get_delegated_free(None, &env, &deps.storage) + .unwrap(); + let delegated_vesting = vesting_account + .get_delegated_vesting(None, &env, &deps.storage) + .unwrap(); + assert_eq!(delegated_free.amount, Uint128::new(90_000_000_000)); + assert_eq!(delegated_vesting.amount, Uint128::zero()); + + // however, create a new delegation now in this brand new vesting period + let delegation = Coin { + amount: Uint128::new(50_000_000_000), + denom: TEST_COIN_DENOM.to_string(), + }; + let ok = vesting_account.try_delegate_to_mixnode( + mix_id, + delegation.clone(), + &env, + &mut deps.storage, + ); + assert!(ok.is_ok()); + + // we're still good here, we have delegated in total 140M from our vested tokens! + let delegated_free = vesting_account + .get_delegated_free(None, &env, &deps.storage) + .unwrap(); + let delegated_vesting = vesting_account + .get_delegated_vesting(None, &env, &deps.storage) + .unwrap(); + assert_eq!(delegated_free.amount, Uint128::new(140_000_000_000)); + assert_eq!(delegated_vesting.amount, Uint128::zero()); + + // but let's ask now a different question: + // how many vested tokens have I had delegated during vesting period3? (i.e. after account creation) + let delegated_free = vesting_account + .get_delegated_free( + Some(Timestamp::from_seconds(vesting_period3_start_timestamp)), + &env, + &deps.storage, + ) + .unwrap(); + let delegated_vesting = vesting_account + .get_delegated_vesting( + Some(Timestamp::from_seconds(vesting_period3_start_timestamp)), + &env, + &deps.storage, + ) + .unwrap(); + + // returns 90M as the 50M delegation didn't exist at this point of time + assert_eq!(delegated_free.amount, Uint128::new(90_000_000_000)); + + // the 50M delegation wasn't a thing here for VESTING tokens either + assert_eq!(delegated_vesting.amount, Uint128::zero()); + } } 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/README.md b/examples/README.md new file mode 100644 index 0000000000..00ba21ad4a --- /dev/null +++ b/examples/README.md @@ -0,0 +1,9 @@ +# Examples + +This directory contains examples of using the libraries in this repo: + +### CLI + +#### CLI Commands + +- [Use an account public key to verify a signature](./cli/commands/verify-signature) \ No newline at end of file diff --git a/tools/Cargo.lock b/examples/cli/commands/verify-signature/Cargo.lock similarity index 79% rename from tools/Cargo.lock rename to examples/cli/commands/verify-signature/Cargo.lock index d32be85ea2..82e26f40d1 100644 --- a/tools/Cargo.lock +++ b/examples/cli/commands/verify-signature/Cargo.lock @@ -16,15 +16,15 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "anyhow" -version = "1.0.57" +version = "1.0.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +checksum = "1485d4d2cc45e7b201ee3767015c96faa5904387c9d87c6efdd0fb511f12d305" [[package]] name = "async-trait" -version = "0.1.53" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" dependencies = [ "proc-macro2", "quote", @@ -59,9 +59,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "az" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f771a5d1f5503f7f4279a30f3643d3421ba149848b89ecaaec0ea2acf04a5ac4" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" [[package]] name = "base16ct" @@ -77,9 +77,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64ct" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179" +checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" [[package]] name = "bip32" @@ -124,26 +124,14 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding 0.1.5", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "block-padding 0.2.1", - "generic-array 0.14.5", + "block-padding", + "generic-array", ] [[package]] @@ -152,16 +140,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ - "generic-array 0.14.5", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", + "generic-array", ] [[package]] @@ -195,21 +174,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.9.1" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" - -[[package]] -name = "byte-tools" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" [[package]] name = "bytemuck" -version = "1.9.1" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" [[package]] name = "byteorder" @@ -219,9 +192,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" [[package]] name = "cc" @@ -235,6 +208,45 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "clap" +version = "3.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13547f7012c01ab4a0e8f8967730ada8f9fdf419e8b6c792788f39cf4e46eefa" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -244,9 +256,21 @@ dependencies = [ "bitflags", ] +[[package]] +name = "coconut-bandwidth-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" +dependencies = [ + "cosmwasm-std", + "multisig-contract-common", + "schemars", + "serde", +] + [[package]] name = "coconut-interface" version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" dependencies = [ "bs58", "getset", @@ -266,9 +290,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "comfy-table" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85914173c2f558d61613bfbbf1911f14e630895087a7ed2fafc0f5319e1536e7" +dependencies = [ + "crossterm", + "strum", + "strum_macros", + "unicode-width", +] + [[package]] name = "config" version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" dependencies = [ "cfg-if", "handlebars", @@ -289,6 +326,7 @@ checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" [[package]] name = "contracts-common" version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" dependencies = [ "cosmwasm-std", ] @@ -311,8 +349,8 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "cosmos-sdk-proto" -version = "0.9.0" -source = "git+https://github.com/nymtech/cosmos-rust?branch=bugfix/account-id-length-validation#911fbe1236cfed591783ccef01018f7ccc97c496" +version = "0.12.3" +source = "git+https://github.com/neacsu/cosmos-rust?branch=neacsu/feegrant_support#f63ded63ec13e753ebe8bdafe9dc503df265d67d" dependencies = [ "prost", "prost-types", @@ -321,14 +359,14 @@ dependencies = [ [[package]] name = "cosmrs" -version = "0.4.1" -source = "git+https://github.com/nymtech/cosmos-rust?branch=bugfix/account-id-length-validation#911fbe1236cfed591783ccef01018f7ccc97c496" +version = "0.7.1" +source = "git+https://github.com/neacsu/cosmos-rust?branch=neacsu/feegrant_support#f63ded63ec13e753ebe8bdafe9dc503df265d67d" dependencies = [ "bip32", "cosmos-sdk-proto", "ecdsa", "eyre", - "getrandom 0.2.6", + "getrandom 0.2.7", "k256", "prost", "prost-types", @@ -382,9 +420,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "dc948ebb96241bb40ab73effeb80d9f93afaad49359d159a5e61be51619fe813" dependencies = [ "libc", ] @@ -398,6 +436,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.2" @@ -410,7 +473,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" dependencies = [ - "generic-array 0.14.5", + "generic-array", "rand_core 0.6.3", "subtle", "zeroize", @@ -418,11 +481,11 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.5", + "generic-array", "typenum", ] @@ -432,7 +495,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ - "generic-array 0.14.5", + "generic-array", "subtle", ] @@ -447,9 +510,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" dependencies = [ "byteorder", "digest 0.9.0", @@ -460,15 +523,51 @@ dependencies = [ [[package]] name = "cw-storage-plus" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9336ecef1e19d56cf6e3e932475fc6a3dee35eec5a386e07917a1d1ba6bb0e35" +checksum = "648b1507290bbc03a8d88463d7cd9b04b1fa0155e5eef366c4fa052b9caaac7a" dependencies = [ "cosmwasm-std", "schemars", "serde", ] +[[package]] +name = "cw-utils" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw3" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe19462a7f644ba60c19d3443cb90d00c50d9b6b3b0a3a7fca93df8261af979b" +dependencies = [ + "cosmwasm-std", + "cw-utils", + "schemars", + "serde", +] + +[[package]] +name = "cw4" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0acc3549d5ce11c6901b3a676f2e2628684722197054d97cd0101ea174ed5cbd" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + [[package]] name = "der" version = "0.5.1" @@ -478,22 +577,13 @@ dependencies = [ "const-oid", ] -[[package]] -name = "digest" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" -dependencies = [ - "generic-array 0.12.4", -] - [[package]] name = "digest" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.5", + "generic-array", ] [[package]] @@ -507,10 +597,16 @@ dependencies = [ ] [[package]] -name = "dyn-clone" -version = "1.0.5" +name = "dotenv" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e50f3adc76d6a43f5ed73b698a87d0760ca74617f60f7c3b879003536fdd28" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dyn-clone" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" [[package]] name = "ecdsa" @@ -562,9 +658,9 @@ dependencies = [ [[package]] name = "either" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "elliptic-curve" @@ -576,7 +672,7 @@ dependencies = [ "crypto-bigint", "der", "ff 0.11.1", - "generic-array 0.14.5", + "generic-array", "group 0.11.0", "rand_core 0.6.3", "sec1", @@ -593,6 +689,22 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "example-verify-signature" +version = "0.1.0" +dependencies = [ + "nym-cli-commands", +] + +[[package]] +name = "execute" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "eyre" version = "0.6.8" @@ -603,17 +715,11 @@ dependencies = [ "once_cell", ] -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "fastrand" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] @@ -640,9 +746,9 @@ dependencies = [ [[package]] name = "fixed" -version = "1.15.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36a65312835c1097a0c926ff3702df965285fadc33d948b87397ff8961bad881" +checksum = "ec8c4fbf8cd36f2a96740c31320902abbf0acbd733049b758707d842490c98c4" dependencies = [ "az", "bytemuck", @@ -653,13 +759,11 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39522e96686d38f4bc984b9198e3a0613264abaebaff2c5c918bfa6b6da09af" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ - "cfg-if", "crc32fast", - "libc", "miniz_oxide", ] @@ -718,9 +822,9 @@ checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" [[package]] name = "futures" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "ab30e97ab6aacfe635fad58f22c2bb06c8b685f7421eb1e064a729e2a5f481fa" dependencies = [ "futures-channel", "futures-core", @@ -733,9 +837,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "2bfc52cbddcfd745bf1740338492bb0bd83d76c67b445f91c5fb29fae29ecaa1" dependencies = [ "futures-core", "futures-sink", @@ -743,15 +847,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "d2acedae88d38235936c3922476b10fced7b2b68136f5e3c03c2d5be348a1115" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "1d11aa21b5b587a64682c0094c2bdd4df0076c5324961a40cc3abd7f37930528" dependencies = [ "futures-core", "futures-task", @@ -760,15 +864,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "93a66fc6d035a26a3ae255a6d2bca35eda63ae4c5512bef54449113f7a1228e5" [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "0db9cce532b0eae2ccf2766ab246f114b56b9cf6d445e00c2549fbc100ca045d" dependencies = [ "proc-macro2", "quote", @@ -777,21 +881,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "ca0bae1fe9752cf7fd9b0064c674ae63f97b37bc714d745cbde0afb7ec4e6765" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "842fc63b931f4056a24d59de13fb1272134ce261816e063e634ad0c15cdc5306" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "f0828a5471e340229c11c77ca80017937ce3c58cb788a17e5f1c2d5c485a9577" dependencies = [ "futures-channel", "futures-core", @@ -807,18 +911,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.12.4" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" -dependencies = [ - "typenum", -] - -[[package]] -name = "generic-array" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", @@ -837,14 +932,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -885,9 +980,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" dependencies = [ "bytes", "fnv", @@ -904,9 +999,12 @@ dependencies = [ [[package]] name = "half" -version = "1.8.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" +checksum = "ad6a9459c9c30b177b925162351f97e7d967c7ea8bab3b8352805327daf45554" +dependencies = [ + "crunchy", +] [[package]] name = "handlebars" @@ -924,9 +1022,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "headers" @@ -941,7 +1039,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1 0.10.0", + "sha-1", ] [[package]] @@ -955,12 +1053,9 @@ dependencies = [ [[package]] name = "heck" -version = "0.3.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" [[package]] name = "hermit-abi" @@ -995,9 +1090,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", @@ -1045,9 +1140,9 @@ dependencies = [ [[package]] name = "hyper" -version = "0.14.18" +version = "0.14.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" dependencies = [ "bytes", "futures-channel", @@ -1078,11 +1173,12 @@ dependencies = [ "headers", "http", "hyper", - "hyper-tls", - "native-tls", + "hyper-rustls", + "rustls-native-certs", "tokio", - "tokio-native-tls", + "tokio-rustls", "tower-service", + "webpki", ] [[package]] @@ -1135,9 +1231,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "1.8.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg 1.1.0", "hashbrown", @@ -1170,15 +1266,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "js-sys" -version = "0.3.57" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" dependencies = [ "wasm-bindgen", ] @@ -1211,9 +1307,19 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "lock_api" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" +dependencies = [ + "autocfg 1.1.0", + "scopeguard", +] [[package]] name = "log" @@ -1224,12 +1330,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - [[package]] name = "matches" version = "0.1.9" @@ -1256,18 +1356,18 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "miniz_oxide" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", @@ -1278,13 +1378,13 @@ dependencies = [ [[package]] name = "mixnet-contract-common" version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" dependencies = [ "az", "contracts-common", "cosmwasm-std", "fixed", "log", - "network-defaults", "schemars", "serde", "serde_repr", @@ -1293,6 +1393,19 @@ dependencies = [ "ts-rs", ] +[[package]] +name = "multisig-contract-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" +dependencies = [ + "cosmwasm-std", + "cw-utils", + "cw3", + "cw4", + "schemars", + "serde", +] + [[package]] name = "native-tls" version = "0.2.10" @@ -1314,8 +1427,10 @@ dependencies = [ [[package]] name = "network-defaults" version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" dependencies = [ "cfg-if", + "dotenv", "hex-literal", "once_cell", "serde", @@ -1363,57 +1478,45 @@ dependencies = [ ] [[package]] -name = "nym-types" +name = "nym-cli-commands" version = "1.0.0" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" dependencies = [ - "coconut-interface", - "config", + "base64", + "bip39", + "bs58", + "cfg-if", + "clap", + "comfy-table", "cosmrs", "cosmwasm-std", - "eyre", - "itertools", + "handlebars", + "humantime-serde", + "k256", "log", "mixnet-contract-common", - "reqwest", - "schemars", + "network-defaults", + "rand 0.6.5", "serde", "serde_json", - "strum", "thiserror", - "ts-rs", + "time", + "toml", "url", "validator-client", - "vesting-contract", - "vesting-contract-common", -] - -[[package]] -name = "nym-wallet-types" -version = "1.0.0" -dependencies = [ - "config", - "cosmrs", - "cosmwasm-std", - "mixnet-contract-common", - "nym-types", - "serde", - "serde_json", - "strum", - "ts-rs", - "validator-client", - "vesting-contract", "vesting-contract-common", ] [[package]] name = "nymcoconut" version = "0.5.0" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" dependencies = [ "bls12_381", "bs58", "digest 0.9.0", "ff 0.10.1", - "getrandom 0.2.6", + "getrandom 0.2.7", "group 0.10.0", "itertools", "rand 0.8.5", @@ -1425,15 +1528,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.12.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" - -[[package]] -name = "opaque-debug" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" [[package]] name = "opaque-debug" @@ -1443,9 +1540,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.40" +version = "0.10.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" +checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" dependencies = [ "bitflags", "cfg-if", @@ -1475,9 +1572,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.73" +version = "0.9.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0" +checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" dependencies = [ "autocfg 1.1.0", "cc", @@ -1486,6 +1583,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + [[package]] name = "pairing" version = "0.20.0" @@ -1496,10 +1599,33 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.7" +name = "parking_lot" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec 1.9.0", + "windows-sys", +] + +[[package]] +name = "paste" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9423e2b32f7a043629287a536f21951e8c6a82482d0acb1eeebfc90bc2225b22" [[package]] name = "pbkdf2" @@ -1545,18 +1671,19 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pest" -version = "2.1.3" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +checksum = "4b0560d531d1febc25a3c9398a62a71256c0178f2e3443baedd9ad4bb8c9deb4" dependencies = [ + "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.1.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +checksum = "905708f7f674518498c1f8d644481440f476d39ca6ecae83319bba7c6c12da91" dependencies = [ "pest", "pest_generator", @@ -1564,9 +1691,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.1.3" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +checksum = "5803d8284a629cc999094ecd630f55e91b561a1d1ba75e233b00ae13b91a69ad" dependencies = [ "pest", "pest_meta", @@ -1577,29 +1704,29 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.1.3" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +checksum = "1538eb784f07615c6d9a8ab061089c6c54a344c5b4301db51990ca1c241e8c04" dependencies = [ - "maplit", + "once_cell", "pest", - "sha-1 0.8.2", + "sha-1", ] [[package]] name = "pin-project" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.10" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", @@ -1667,18 +1794,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ "unicode-ident", ] [[package]] name = "prost" -version = "0.9.0" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e" dependencies = [ "bytes", "prost-derive", @@ -1686,9 +1813,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.9.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc" dependencies = [ "anyhow", "itertools", @@ -1699,9 +1826,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.9.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" +checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68" dependencies = [ "bytes", "prost", @@ -1715,9 +1842,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.18" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] @@ -1802,7 +1929,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", ] [[package]] @@ -1878,9 +2005,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] @@ -1896,9 +2023,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" dependencies = [ "base64", "bytes", @@ -1923,6 +2050,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-native-tls", + "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -1964,7 +2092,7 @@ checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" dependencies = [ "block-buffer 0.9.0", "digest 0.9.0", - "opaque-debug 0.3.0", + "opaque-debug", ] [[package]] @@ -1994,15 +2122,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "same-file" @@ -2048,6 +2176,12 @@ dependencies = [ "syn", ] +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + [[package]] name = "sct" version = "0.6.1" @@ -2065,7 +2199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" dependencies = [ "der", - "generic-array 0.14.5", + "generic-array", "pkcs8", "subtle", "zeroize", @@ -2073,9 +2207,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" dependencies = [ "bitflags", "core-foundation", @@ -2096,9 +2230,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.137" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" dependencies = [ "serde_derive", ] @@ -2114,18 +2248,18 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212e73464ebcde48d723aa02eb270ba62eff38a9b732df31f33f1b4e145f3a54" +checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" dependencies = [ "proc-macro2", "quote", @@ -2145,9 +2279,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.81" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa", "ryu", @@ -2156,9 +2290,9 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed" +checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" dependencies = [ "proc-macro2", "quote", @@ -2177,18 +2311,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "sha-1" version = "0.10.0" @@ -2210,7 +2332,7 @@ dependencies = [ "cfg-if", "cpufeatures", "digest 0.9.0", - "opaque-debug 0.3.0", + "opaque-debug", ] [[package]] @@ -2222,7 +2344,37 @@ dependencies = [ "block-buffer 0.9.0", "digest 0.9.0", "keccak", - "opaque-debug 0.3.0", + "opaque-debug", +] + +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", ] [[package]] @@ -2237,9 +2389,12 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg 1.1.0", +] [[package]] name = "smallvec" @@ -2250,6 +2405,12 @@ dependencies = [ "maybe-uninit", ] +[[package]] +name = "smallvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" + [[package]] name = "socket2" version = "0.4.4" @@ -2283,19 +2444,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "strum" -version = "0.23.0" +name = "strsim" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" -dependencies = [ - "strum_macros", -] +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" [[package]] name = "strum_macros" -version = "0.23.1" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck", "proc-macro2", @@ -2321,9 +2485,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.95" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" dependencies = [ "proc-macro2", "quote", @@ -2358,9 +2522,9 @@ dependencies = [ [[package]] name = "tendermint" -version = "0.23.3" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ef686b8ecd36550d0581f0989c9d8607090b23005df479d8e6ba348b5800b9" +checksum = "3ca881fa4dedd2b46334f13be7fbc8cc1549ba4be5a833fe4e73d1a1baaf7949" dependencies = [ "async-trait", "bytes", @@ -2389,9 +2553,9 @@ dependencies = [ [[package]] name = "tendermint-config" -version = "0.23.3" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc127f82e7a8c7337c1f293d65a821380d3407c4c44bc979ef4da0ebc3b31ed" +checksum = "f6c56ee93f4e9b7e7daba86d171f44572e91b741084384d0ae00df7991873dfd" dependencies = [ "flex-error", "serde", @@ -2403,9 +2567,9 @@ dependencies = [ [[package]] name = "tendermint-proto" -version = "0.23.3" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e0a0251fd81bed7420bea0f4d91c2a58f6c9fa34d5b2f70bed0ba8890de5aa" +checksum = "b71f925d74903f4abbdc4af0110635a307b3cb05b175fdff4a7247c14a4d0874" dependencies = [ "bytes", "flex-error", @@ -2421,15 +2585,15 @@ dependencies = [ [[package]] name = "tendermint-rpc" -version = "0.23.3" +version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626c493e9ced3a9de37583bbd929f4b6dbd49aa513ab9b4776aa44a9332ce9b5" +checksum = "a13e63f57ee05a1e927887191c76d1b139de9fa40c180b9f8727ee44377242a6" dependencies = [ "async-trait", "bytes", "flex-error", "futures", - "getrandom 0.2.6", + "getrandom 0.2.7", "http", "hyper", "hyper-proxy", @@ -2462,19 +2626,25 @@ dependencies = [ ] [[package]] -name = "thiserror" -version = "1.0.31" +name = "textwrap" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" dependencies = [ "proc-macro2", "quote", @@ -2483,9 +2653,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.9" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +checksum = "db76ff9fa4b1458b3c7f077f3ff9887394058460d21e634355b273aaf11eea45" dependencies = [ "itoa", "libc", @@ -2501,10 +2671,11 @@ checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" [[package]] name = "tokio" -version = "1.18.2" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" +checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" dependencies = [ + "autocfg 1.1.0", "bytes", "libc", "memchr", @@ -2519,9 +2690,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ "proc-macro2", "quote", @@ -2551,9 +2722,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" dependencies = [ "bytes", "futures-core", @@ -2574,40 +2745,28 @@ dependencies = [ [[package]] name = "tower-service" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ "cfg-if", "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tracing-core" -version = "0.1.26" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] @@ -2618,34 +2777,19 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "ts-rs" -version = "6.1.2" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d26cba30c9b3a2f537f765cf754126f11c983b7426d280ae0b4cef2374cab98" +checksum = "dc59f479df54269b400dd95bc3b7e81623b3e4b9c70c8ca7125ab8341eafa64e" dependencies = [ "thiserror", "ts-rs-macros", ] -[[package]] -name = "ts-rs-cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "mixnet-contract-common", - "nym-types", - "nym-wallet-types", - "ts-rs", - "validator-api-requests", - "validator-client", - "vesting-contract-common", - "walkdir", -] - [[package]] name = "ts-rs-macros" -version = "6.1.2" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd8e302fbcf5b60dfa1df443535967511442c5ce4eac8b4c9fa811f2274280a4" +checksum = "9f807fdb3151fee75df7485b901a89624358cd07a67a8fb1a5831bf5a07681ff" dependencies = [ "Inflector", "proc-macro2", @@ -2662,9 +2806,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" [[package]] name = "uint" @@ -2686,9 +2830,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" [[package]] name = "unicode-normalization" @@ -2696,14 +2840,14 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09c8070a9942f5e7cfccd93f490fdebd230ee3c3c9f107cb25bad5351ef671cf" dependencies = [ - "smallvec", + "smallvec 0.6.14", ] [[package]] -name = "unicode-segmentation" -version = "1.9.0" +name = "unicode-width" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" @@ -2739,7 +2883,12 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" [[package]] name = "validator-api-requests" version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" dependencies = [ + "bs58", + "coconut-interface", + "cosmrs", + "getset", "mixnet-contract-common", "schemars", "serde", @@ -2749,20 +2898,25 @@ dependencies = [ [[package]] name = "validator-client" version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" dependencies = [ "async-trait", "base64", "bip39", + "coconut-bandwidth-contract-common", "coconut-interface", "colored", "config", "cosmrs", "cosmwasm-std", + "cw3", + "execute", "flate2", "futures", "itertools", "log", "mixnet-contract-common", + "multisig-contract-common", "network-defaults", "prost", "reqwest", @@ -2771,7 +2925,6 @@ dependencies = [ "sha2", "thiserror", "tokio", - "ts-rs", "url", "validator-api-requests", "vesting-contract", @@ -2792,9 +2945,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" -version = "1.0.0" +version = "1.0.2" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" dependencies = [ - "config", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", @@ -2807,6 +2960,7 @@ dependencies = [ [[package]] name = "vesting-contract-common" version = "0.1.0" +source = "git+https://github.com/nymtech/nym?branch=develop#124103d51b8fdd66552c27575234d810e80ec6c8" dependencies = [ "config", "cosmwasm-std", @@ -2844,12 +2998,6 @@ version = "0.9.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2858,9 +3006,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2868,13 +3016,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -2883,9 +3031,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.30" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" dependencies = [ "cfg-if", "js-sys", @@ -2895,9 +3043,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2905,9 +3053,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" dependencies = [ "proc-macro2", "quote", @@ -2918,15 +3066,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" [[package]] name = "web-sys" -version = "0.3.57" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" dependencies = [ "js-sys", "wasm-bindgen", @@ -3036,9 +3184,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.5.5" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94693807d016b2f2d2e14420eb3bfcca689311ff775dcf113d74ea624b7cdf07" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" dependencies = [ "zeroize_derive", ] diff --git a/examples/cli/commands/verify-signature/Cargo.toml b/examples/cli/commands/verify-signature/Cargo.toml new file mode 100644 index 0000000000..5ad0673115 --- /dev/null +++ b/examples/cli/commands/verify-signature/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "example-verify-signature" +version = "0.1.0" +edition = "2021" + +[workspace] + +[dependencies] +nym-cli-commands = { git = "https://github.com/nymtech/nym", branch = "develop" } \ No newline at end of file diff --git a/examples/cli/commands/verify-signature/README.md b/examples/cli/commands/verify-signature/README.md new file mode 100644 index 0000000000..b626b7910d --- /dev/null +++ b/examples/cli/commands/verify-signature/README.md @@ -0,0 +1,43 @@ +# Example code to verify a signature + +This is an example app that shows how to verify a signature signed by an account key. + +Inputs to the app are: + +- **signature** - bytes represented as a hex string +- **public key** - in JSON format (you can query any Cosmos chain for account's public key as JSON, however it will need to have sent a signed transaction to the chain for this to be present) +- **message** - the string message to verify + +## Running locally + +Run the example by changning to this directory and running: + +``` +cargo run +``` + +And you should see the output: + +``` +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 the correct message with the wrong address: +FAILURE ❌ signature is not valid: account id does not match + +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 new file mode 100644 index 0000000000..d4bc1b5506 --- /dev/null +++ b/examples/cli/commands/verify-signature/src/main.rs @@ -0,0 +1,88 @@ +use nym_cli_commands::validator::signature::helpers::secp256k1_verify_with_public_key_json; + +fn main() { + println!("\nNym signature verification example\n\n"); + + // 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"; + + // the original message as a string to verify + 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:"); + 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!("\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( + 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..217433d4e5 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,13 @@ 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" } +logging = { path = "../common/logging"} 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/cache/mod.rs b/explorer-api/src/cache/mod.rs index 7096220486..74448f4417 100644 --- a/explorer-api/src/cache/mod.rs +++ b/explorer-api/src/cache/mod.rs @@ -1,15 +1,20 @@ +use std::borrow::Borrow; use std::collections::HashMap; +use std::hash::Hash; use std::time::{Duration, SystemTime}; const DEFAULT_CACHE_VALIDITY: Duration = Duration::from_secs(60); #[derive(Clone)] -pub(crate) struct Cache { - inner: HashMap>, +pub(crate) struct Cache { + inner: HashMap>, cache_validity_duration: Duration, } -impl Cache { +impl Cache +where + K: Eq + Hash, +{ pub(crate) fn new() -> Self { Cache { inner: HashMap::new(), @@ -29,23 +34,27 @@ impl Cache { self.inner.len() } - pub(crate) fn get_all(&self) -> Vec { + pub(crate) fn get_all(&self) -> Vec { self.inner .values() .map(|cache_item| cache_item.value.clone()) .collect() } - pub(crate) fn get(&self, key: &str) -> Option { + pub(crate) fn get(&self, key: &Q) -> Option + where + K: Borrow, + Q: Hash + Eq, + { self.inner .get(key) .filter(|cache_item| cache_item.valid_until >= SystemTime::now()) .map(|cache_item| cache_item.value.clone()) } - pub(crate) fn set(&mut self, key: &str, value: T) { + pub(crate) fn set(&mut self, key: K, value: V) { self.inner.insert( - key.to_string(), + key, CacheItem { valid_until: SystemTime::now() + self.cache_validity_duration, value, @@ -54,12 +63,20 @@ impl Cache { } #[allow(unused)] - pub(crate) fn remove(&mut self, key: &str) -> Option { + pub(crate) fn remove(&mut self, key: &Q) -> Option + where + K: Borrow, + Q: Hash + Eq, + { self.inner.remove(key).map(|item| item.value) } #[allow(unused)] - pub(crate) fn remove_if_expired(&mut self, key: &str) -> Option { + pub(crate) fn remove_if_expired(&mut self, key: &Q) -> Option + where + K: Borrow, + Q: Hash + Eq, + { if self.inner.get(key)?.has_expired() { self.remove(key) } else { diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index fd84a8c380..42fe9405be 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,111 +44,67 @@ 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 .inner .mixnodes - .is_location_valid(&cache_item.mix_node().identity_key) + .is_location_valid(cache_item.mix_id()) .await { // when the cached location is valid, don't locate and continue to next mix node 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, + Some(cache_item.mix_node().mix_port), + ) { + 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, ); - } - self.state - .inner - .mixnodes - .set_location(&cache_item.mix_node().identity_key, Some(location)) - .await; + if i > 0 && (i % 100) == 0 { + info!("Located {} mixnodes...", i + 1,); + } - // 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 - ); self.state .inner .mixnodes - .set_location(&cache_item.mix_node().identity_key, None) + .set_location(cache_item.mix_id(), Some(location)) .await; - }, - LocateError::RateLimited(e) => warn!( - "❌ Oh no, we've been rate limited! Location for {} failed. Response body: {}", - cache_item.mix_node().host, e - ), + + // one node has been located, so return out of the loop + return; + } + None => { + warn!("❌ Location for {} not found.", cache_item.mix_node().host); + self.state + .inner + .mixnodes + .set_location(cache_item.mix_id(), None) + .await; + } }, - } + 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/gateways/models.rs b/explorer-api/src/gateways/models.rs index 690aa7cb7e..a051d88e6b 100644 --- a/explorer-api/src/gateways/models.rs +++ b/explorer-api/src/gateways/models.rs @@ -1,17 +1,14 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::sync::Arc; - +use crate::cache::Cache; +use mixnet_contract_common::{GatewayBond, IdentityKey}; use serde::Serialize; +use std::sync::Arc; use tokio::sync::RwLock; -use mixnet_contract_common::GatewayBond; - -use crate::cache::Cache; - pub(crate) struct GatewayCache { - pub(crate) gateways: Cache, + pub(crate) gateways: Cache, } #[derive(Clone, Debug, Serialize, JsonSchema)] @@ -49,7 +46,7 @@ impl ThreadsafeGatewayCache { for gateway in gateways { guard .gateways - .set(gateway.clone().gateway.identity_key.as_str(), gateway) + .set(gateway.gateway.identity_key.clone(), gateway) } } } diff --git a/explorer-api/src/geo_ip/location.rs b/explorer-api/src/geo_ip/location.rs new file mode 100644 index 0000000000..a1afec55d6 --- /dev/null +++ b/explorer-api/src/geo_ip/location.rs @@ -0,0 +1,134 @@ +// 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, ToSocketAddrs}, + str::FromStr, + sync::Arc, +}; + +const DEFAULT_DATABASE_PATH: &str = "./geo_ip/GeoLite2-Country.mmdb"; +const FAKE_PORT: u16 = 1234; + +#[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, port: Option) -> Result, GeoIpError> { + let ip: IpAddr = FromStr::from_str(address).or_else(|_| { + debug!( + "Fail to create IpAddr from {}. Trying using internal lookup...", + &address + ); + let p = port.unwrap_or(FAKE_PORT); + let socket_addr = (address, p) + .to_socket_addrs() + .map_err(|e| { + error!("Fail to resolve IP address from {}:{}: {}", &address, p, e); + GeoIpError::NoValidIP + })? + .next() + .ok_or_else(|| { + error!("Fail to resolve IP address from {}:{}", &address, p); + GeoIpError::NoValidIP + })?; + let ip = socket_addr.ip(); + debug!("Internal lookup succeed, resolved ip: {}", ip); + Ok(ip) + })?; + 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..23bb92d941 --- /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, None) + .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/helpers.rs b/explorer-api/src/helpers.rs new file mode 100644 index 0000000000..6b3d019833 --- /dev/null +++ b/explorer-api/src/helpers.rs @@ -0,0 +1,10 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use mixnet_contract_common::{Decimal, Fraction}; + +pub(crate) fn best_effort_small_dec_to_f64(dec: Decimal) -> f64 { + let num = dec.numerator().u128() as f64; + let den = dec.denominator().u128() as f64; + num / den +} 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 7e6779794b..47dddf9f54 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -4,15 +4,21 @@ extern crate rocket; extern crate rocket_okapi; use clap::Parser; +use dotenv::dotenv; use log::info; +use logging::setup_logging; 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; pub(crate) mod mix_nodes; @@ -22,11 +28,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); @@ -110,18 +116,3 @@ async fn wait_for_signal() { }, } } - -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .init(); -} diff --git a/explorer-api/src/mix_node/delegations.rs b/explorer-api/src/mix_node/delegations.rs index adbc61e428..3d061c942a 100644 --- a/explorer-api/src/mix_node/delegations.rs +++ b/explorer-api/src/mix_node/delegations.rs @@ -1,25 +1,23 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use itertools::Itertools; - -use crate::client::ThreadsafeValidatorClient; -use mixnet_contract_common::Delegation; - use super::models::SummedDelegations; +use crate::client::ThreadsafeValidatorClient; +use itertools::Itertools; +use mixnet_contract_common::{Delegation, MixId}; pub(crate) async fn get_single_mixnode_delegations( client: &ThreadsafeValidatorClient, - pubkey: &str, + mix_id: MixId, ) -> Vec { match client .0 - .get_all_nymd_single_mixnode_delegations(pubkey.to_string()) + .get_all_nymd_single_mixnode_delegations(mix_id) .await { Ok(result) => result, Err(e) => { - error!("Could not get delegations for mix node {}: {:?}", pubkey, e); + error!("Could not get delegations for mix node {}: {:?}", mix_id, e); vec![] } } @@ -27,9 +25,9 @@ pub(crate) async fn get_single_mixnode_delegations( pub(crate) async fn get_single_mixnode_delegations_summed( client: &ThreadsafeValidatorClient, - pubkey: &str, + mix_id: MixId, ) -> Vec { - let delegations_by_owner = get_single_mixnode_delegations(client, pubkey) + let delegations_by_owner = get_single_mixnode_delegations(client, mix_id) .await .into_iter() .into_group_map_by(|delegation| delegation.owner.clone()); diff --git a/explorer-api/src/mix_node/econ_stats.rs b/explorer-api/src/mix_node/econ_stats.rs index 3c526097d4..22f106b1aa 100644 --- a/explorer-api/src/mix_node/econ_stats.rs +++ b/explorer-api/src/mix_node/econ_stats.rs @@ -2,40 +2,56 @@ // SPDX-License-Identifier: Apache-2.0 use crate::client::ThreadsafeValidatorClient; +use crate::helpers::best_effort_small_dec_to_f64; use crate::mix_node::models::EconomicDynamicsStats; +use contracts_common::truncate_decimal; +use mixnet_contract_common::MixId; pub(crate) async fn retrieve_mixnode_econ_stats( client: &ThreadsafeValidatorClient, - identity: &str, + mix_id: MixId, ) -> Option { let stake_saturation = client .0 .validator_api - .get_mixnode_stake_saturation(identity) + .get_mixnode_stake_saturation(mix_id) .await .ok()?; let inclusion_probability = client .0 .validator_api - .get_mixnode_inclusion_probability(identity) + .get_mixnode_inclusion_probability(mix_id) .await .ok()?; let reward_estimation = client .0 .validator_api - .get_mixnode_reward_estimation(identity) + .get_mixnode_reward_estimation(mix_id) + .await + .ok()?; + + let uptime_response = client + .0 + .validator_api + .get_mixnode_avg_uptime(mix_id) .await .ok()?; Some(EconomicDynamicsStats { - stake_saturation: stake_saturation.saturation, + stake_saturation: best_effort_small_dec_to_f64(stake_saturation.saturation) as f32, active_set_inclusion_probability: inclusion_probability.in_active, reserve_set_inclusion_probability: inclusion_probability.in_reserve, - estimated_total_node_reward: reward_estimation.estimated_total_node_reward, - estimated_operator_reward: reward_estimation.estimated_operator_reward, - estimated_delegators_reward: reward_estimation.estimated_delegators_reward, - current_interval_uptime: reward_estimation.reward_params.node.uptime().u128() as u8, + // drop precision for compatibility sake + estimated_total_node_reward: truncate_decimal( + reward_estimation.estimation.total_node_reward, + ) + .u128() as u64, + estimated_operator_reward: truncate_decimal(reward_estimation.estimation.operator).u128() + as u64, + estimated_delegators_reward: truncate_decimal(reward_estimation.estimation.delegates).u128() + as u64, + current_interval_uptime: uptime_response.avg_uptime, }) } diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index aa037c5651..00d9b9fd70 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -1,16 +1,6 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use reqwest::Error as ReqwestError; -use rocket::response::status::NotFound; -use rocket::serde::json::Json; -use rocket::{Route, State}; -use rocket_okapi::okapi::openapi3::OpenApi; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; - -use mixnet_contract_common::Delegation; - use crate::mix_node::delegations::{ get_single_mixnode_delegations, get_single_mixnode_delegations_summed, }; @@ -19,6 +9,14 @@ use crate::mix_node::models::{ EconomicDynamicsStats, NodeDescription, NodeStats, PrettyDetailedMixNodeBond, SummedDelegations, }; use crate::state::ExplorerApiStateContext; +use mixnet_contract_common::{Delegation, MixId}; +use reqwest::Error as ReqwestError; +use rocket::response::status::NotFound; +use rocket::serde::json::Json; +use rocket::{Route, State}; +use rocket_okapi::okapi::openapi3::OpenApi; +use rocket_okapi::openapi_get_routes_spec; +use rocket_okapi::settings::OpenApiSettings; pub fn mix_node_make_default_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { openapi_get_routes_spec![ @@ -31,163 +29,6 @@ pub fn mix_node_make_default_routes(settings: &OpenApiSettings) -> (Vec, ] } -#[openapi(tag = "mix_nodes")] -#[get("/")] -pub(crate) async fn get_by_id( - pubkey: &str, - state: &State, -) -> Result, NotFound> { - match state - .inner - .mixnodes - .get_detailed_mixnode_by_id(pubkey) - .await - { - Some(mixnode) => Ok(Json(mixnode)), - None => Err(NotFound("Mixnode not found".to_string())), - } -} - -#[openapi(tag = "mix_node")] -#[get("//delegations")] -pub(crate) async fn get_delegations( - pubkey: &str, - state: &State, -) -> Json> { - Json(get_single_mixnode_delegations(&state.inner.validator_client, pubkey).await) -} - -#[openapi(tag = "mix_node")] -#[get("//delegations/summed")] -pub(crate) async fn get_delegations_summed( - pubkey: &str, - state: &State, -) -> Json> { - Json(get_single_mixnode_delegations_summed(&state.inner.validator_client, pubkey).await) -} - -#[openapi(tag = "mix_node")] -#[get("//description")] -pub(crate) async fn get_description( - pubkey: &str, - state: &State, -) -> Option> { - match state.inner.mixnode.clone().get_description(pubkey).await { - Some(cache_value) => { - trace!("Returning cached value for {}", pubkey); - Some(Json(cache_value)) - } - None => { - trace!("No valid cache value for {}", pubkey); - match state.inner.get_mix_node(pubkey).await { - Some(bond) => { - match get_mix_node_description( - &bond.mix_node().host, - bond.mix_node().http_api_port, - ) - .await - { - Ok(response) => { - // cache the response and return as the HTTP response - state - .inner - .mixnode - .set_description(pubkey, response.clone()) - .await; - Some(Json(response)) - } - Err(e) => { - error!( - "Unable to get description for {} on {}:{} -> {}", - pubkey, - bond.mix_node().host, - bond.mix_node().http_api_port, - e - ); - Option::None - } - } - } - None => Option::None, - } - } - } -} - -#[openapi(tag = "mix_node")] -#[get("//stats")] -pub(crate) async fn get_stats( - pubkey: &str, - state: &State, -) -> Option> { - match state.inner.mixnode.get_node_stats(pubkey).await { - Some(cache_value) => { - trace!("Returning cached value for {}", pubkey); - Some(Json(cache_value)) - } - None => { - trace!("No valid cache value for {}", pubkey); - match state.inner.get_mix_node(pubkey).await { - Some(bond) => { - match get_mix_node_stats(&bond.mix_node().host, bond.mix_node().http_api_port) - .await - { - Ok(response) => { - // cache the response and return as the HTTP response - state - .inner - .mixnode - .set_node_stats(pubkey, response.clone()) - .await; - Some(Json(response)) - } - Err(e) => { - error!( - "Unable to get description for {} on {}:{} -> {}", - pubkey, - bond.mix_node().host, - bond.mix_node().http_api_port, - e - ); - Option::None - } - } - } - None => Option::None, - } - } - } -} - -#[openapi(tag = "mix_node")] -#[get("//economic-dynamics-stats")] -pub(crate) async fn get_economic_dynamics_stats( - pubkey: &str, - state: &State, -) -> Option> { - match state.inner.mixnode.get_econ_stats(pubkey).await { - Some(cache_value) => { - trace!("Returning cached value for {}", pubkey); - Some(Json(cache_value)) - } - None => { - trace!("No valid cache value for {}", pubkey); - - // get fresh value from the validator API - let econ_stats = - retrieve_mixnode_econ_stats(&state.inner.validator_client, pubkey).await?; - - // update cache - state - .inner - .mixnode - .set_econ_stats(pubkey, econ_stats.clone()) - .await; - Some(Json(econ_stats)) - } - } -} - async fn get_mix_node_description(host: &str, port: u16) -> Result { reqwest::get(format!("http://{}:{}/description", host, port)) .await? @@ -201,3 +42,155 @@ async fn get_mix_node_stats(host: &str, port: u16) -> Result() .await } + +#[openapi(tag = "mix_nodes")] +#[get("/")] +pub(crate) async fn get_by_id( + mix_id: MixId, + state: &State, +) -> Result, NotFound> { + match state.inner.mixnodes.get_detailed_mixnode(mix_id).await { + Some(mixnode) => Ok(Json(mixnode)), + None => Err(NotFound("Mixnode not found".to_string())), + } +} + +#[openapi(tag = "mix_node")] +#[get("//delegations")] +pub(crate) async fn get_delegations( + mix_id: MixId, + state: &State, +) -> Json> { + Json(get_single_mixnode_delegations(&state.inner.validator_client, mix_id).await) +} + +#[openapi(tag = "mix_node")] +#[get("//delegations/summed")] +pub(crate) async fn get_delegations_summed( + mix_id: MixId, + state: &State, +) -> Json> { + Json(get_single_mixnode_delegations_summed(&state.inner.validator_client, mix_id).await) +} + +#[openapi(tag = "mix_node")] +#[get("//description")] +pub(crate) async fn get_description( + mix_id: MixId, + state: &State, +) -> Option> { + match state.inner.mixnode.clone().get_description(mix_id).await { + Some(cache_value) => { + trace!("Returning cached value for {}", mix_id); + Some(Json(cache_value)) + } + None => { + trace!("No valid cache value for {}", mix_id); + match state.inner.get_mix_node(mix_id).await { + Some(bond) => { + match get_mix_node_description( + &bond.mix_node().host, + bond.mix_node().http_api_port, + ) + .await + { + Ok(response) => { + // cache the response and return as the HTTP response + state + .inner + .mixnode + .set_description(mix_id, response.clone()) + .await; + Some(Json(response)) + } + Err(e) => { + error!( + "Unable to get description for {} on {}:{} -> {}", + mix_id, + bond.mix_node().host, + bond.mix_node().http_api_port, + e + ); + Option::None + } + } + } + None => Option::None, + } + } + } +} + +#[openapi(tag = "mix_node")] +#[get("//stats")] +pub(crate) async fn get_stats( + mix_id: MixId, + state: &State, +) -> Option> { + match state.inner.mixnode.get_node_stats(mix_id).await { + Some(cache_value) => { + trace!("Returning cached value for {}", mix_id); + Some(Json(cache_value)) + } + None => { + trace!("No valid cache value for {}", mix_id); + match state.inner.get_mix_node(mix_id).await { + Some(bond) => { + match get_mix_node_stats(&bond.mix_node().host, bond.mix_node().http_api_port) + .await + { + Ok(response) => { + // cache the response and return as the HTTP response + state + .inner + .mixnode + .set_node_stats(mix_id, response.clone()) + .await; + Some(Json(response)) + } + Err(e) => { + error!( + "Unable to get description for {} on {}:{} -> {}", + mix_id, + bond.mix_node().host, + bond.mix_node().http_api_port, + e + ); + Option::None + } + } + } + None => Option::None, + } + } + } +} + +#[openapi(tag = "mix_node")] +#[get("//economic-dynamics-stats")] +pub(crate) async fn get_economic_dynamics_stats( + mix_id: MixId, + state: &State, +) -> Option> { + match state.inner.mixnode.get_econ_stats(mix_id).await { + Some(cache_value) => { + trace!("Returning cached value for {}", mix_id); + Some(Json(cache_value)) + } + None => { + trace!("No valid cache value for {}", mix_id); + + // get fresh value from the validator API + let econ_stats = + retrieve_mixnode_econ_stats(&state.inner.validator_client, mix_id).await?; + + // update cache + state + .inner + .mixnode + .set_econ_stats(mix_id, econ_stats.clone()) + .await; + Some(Json(econ_stats)) + } + } +} diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index 83789b85c5..1d709433b1 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -3,8 +3,9 @@ use crate::cache::Cache; use crate::mix_nodes::location::Location; -use mixnet_contract_common::{Addr, Coin, Layer, MixNode}; -use mixnet_contract_common::{Delegation, IdentityKey}; +use contracts_common::Percent; +use mixnet_contract_common::Delegation; +use mixnet_contract_common::{Addr, Coin, Layer, MixId, MixNode}; use serde::Deserialize; use serde::Serialize; use std::sync::Arc; @@ -22,6 +23,9 @@ pub(crate) enum MixnodeStatus { #[derive(Clone, Debug, Serialize, JsonSchema)] pub(crate) struct PrettyDetailedMixNodeBond { + // I leave this to @MS to refactor this type as a lot of things here are redundant thanks to + // the existence of `MixNodeDetails` + pub mix_id: MixId, pub location: Option, pub status: MixnodeStatus, pub pledge_amount: Coin, @@ -33,19 +37,21 @@ pub(crate) struct PrettyDetailedMixNodeBond { pub avg_uptime: u8, pub estimated_operator_apy: f64, pub estimated_delegators_apy: f64, + pub operating_cost: Coin, + pub profit_margin_percent: Percent, } #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct SummedDelegations { pub owner: Addr, - pub node_identity: IdentityKey, + pub mix_id: MixId, pub amount: Coin, } impl SummedDelegations { pub fn from(delegations: &[Delegation]) -> Option { let owner = get_common_owner(delegations)?; - let node_identity = get_common_node_identity(delegations)?; + let mix_id = get_common_mix_id(delegations)?; let denom = get_common_denom(delegations)?; let sum = delegations @@ -57,16 +63,16 @@ impl SummedDelegations { Some(SummedDelegations { owner, - node_identity, + mix_id, amount, }) } } 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)] @@ -85,44 +91,32 @@ impl ThreadsafeMixNodeCache { } } - pub(crate) async fn get_description(&self, identity_key: &str) -> Option { - self.inner.read().await.descriptions.get(identity_key) + 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, identity_key: &str) -> Option { - self.inner.read().await.node_stats.get(identity_key) + 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, identity_key: &str) -> Option { - self.inner.read().await.econ_stats.get(identity_key) + 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, identity_key: &str, description: NodeDescription) { + pub(crate) async fn set_description(&self, mix_id: MixId, description: NodeDescription) { self.inner .write() .await .descriptions - .set(identity_key, description); + .set(mix_id, description); } - pub(crate) async fn set_node_stats(&self, identity_key: &str, node_stats: NodeStats) { - self.inner - .write() - .await - .node_stats - .set(identity_key, node_stats); + 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, - identity_key: &str, - econ_stats: EconomicDynamicsStats, - ) { - self.inner - .write() - .await - .econ_stats - .set(identity_key, econ_stats); + 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); } } @@ -171,10 +165,10 @@ pub(crate) struct EconomicDynamicsStats { } fn get_common_owner(delegations: &[Delegation]) -> Option { - let owner = delegations.iter().next()?.owner(); + let owner = delegations.iter().next()?.owner.clone(); if delegations .iter() - .any(|delegation| delegation.owner() != owner) + .any(|delegation| delegation.owner != owner) { log::warn!("Unexpected different owners when summing delegations"); return None; @@ -182,16 +176,16 @@ fn get_common_owner(delegations: &[Delegation]) -> Option { Some(owner) } -fn get_common_node_identity(delegations: &[Delegation]) -> Option { - let node_identity = delegations.iter().next()?.node_identity(); +fn get_common_mix_id(delegations: &[Delegation]) -> Option { + let mix_id = delegations.iter().next()?.mix_id; if delegations .iter() - .any(|delegation| delegation.node_identity() != node_identity) + .any(|delegation| delegation.mix_id != mix_id) { log::warn!("Unexpected different node identities when summing delegations"); return None; } - Some(node_identity) + Some(mix_id) } fn get_common_denom(delegations: &[Delegation]) -> Option { diff --git a/explorer-api/src/mix_nodes/location.rs b/explorer-api/src/mix_nodes/location.rs index 80f24f6635..d14eaf771e 100644 --- a/explorer-api/src/mix_nodes/location.rs +++ b/explorer-api/src/mix_nodes/location.rs @@ -1,12 +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 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)] @@ -35,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 e2aced241a..82c2aedd1f 100644 --- a/explorer-api/src/mix_nodes/mod.rs +++ b/explorer-api/src/mix_nodes/mod.rs @@ -6,7 +6,6 @@ use std::time::Duration; 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 c07ef6ac17..07899742c9 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -5,9 +5,12 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::time::{Duration, SystemTime}; +use mixnet_contract_common::rewarding::helpers::truncate_reward; +use mixnet_contract_common::MixId; use serde::Serialize; -use tokio::sync::RwLock; +use tokio::sync::{RwLock, RwLockReadGuard}; +use crate::helpers::best_effort_small_dec_to_f64; use validator_client::models::MixNodeBondAnnotated; use crate::mix_node::models::{MixnodeStatus, PrettyDetailedMixNodeBond}; @@ -30,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 { @@ -45,10 +48,10 @@ impl MixNodesResult { } } - fn determine_node_status(&self, public_key: &str) -> MixnodeStatus { - if self.active_mixnodes.contains(public_key) { + fn determine_node_status(&self, mix_id: MixId) -> MixnodeStatus { + if self.active_mixnodes.contains(&mix_id) { MixnodeStatus::Active - } else if self.rewarded_mixnodes.contains(public_key) { + } else if self.rewarded_mixnodes.contains(&mix_id) { MixnodeStatus::Standby } else { MixnodeStatus::Inactive @@ -59,15 +62,15 @@ impl MixNodesResult { self.valid_until >= SystemTime::now() } - fn get_mixnode(&self, pubkey: &str) -> Option { + fn get_mixnode(&self, mix_id: MixId) -> Option { if self.is_valid() { - self.all_mixnodes.get(pubkey).cloned() + self.all_mixnodes.get(&mix_id).cloned() } else { None } } - fn get_mixnodes(&self) -> Option> { + fn get_mixnodes(&self) -> Option> { if self.is_valid() { Some(self.all_mixnodes.clone()) } else { @@ -97,11 +100,11 @@ impl ThreadsafeMixNodesCache { } } - pub(crate) async fn is_location_valid(&self, identity_key: &str) -> bool { + pub(crate) async fn is_location_valid(&self, mix_id: MixId) -> bool { self.locations .read() .await - .get(identity_key) + .get(&mix_id) .map_or(false, |cache_item| { cache_item.valid_until > SystemTime::now() }) @@ -111,48 +114,61 @@ impl ThreadsafeMixNodesCache { self.locations.read().await.clone() } - pub(crate) async fn set_location(&self, identity_key: &str, 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().await.insert( - identity_key.to_string(), - LocationCacheItem::new_from_location(location), - ); + self.locations + .write() + .await + .insert(mix_id, LocationCacheItem::new_from_location(location)); } - pub(crate) async fn get_mixnode(&self, pubkey: &str) -> Option { - self.mixnodes.read().await.get_mixnode(pubkey) + pub(crate) async fn get_mixnode(&self, mix_id: MixId) -> Option { + self.mixnodes.read().await.get_mixnode(mix_id) } - pub(crate) async fn get_mixnodes(&self) -> Option> { + pub(crate) async fn get_mixnodes(&self) -> Option> { self.mixnodes.read().await.get_mixnodes() } - pub(crate) async fn get_detailed_mixnode_by_id( + fn create_detailed_mixnode( &self, - identity_key: &str, + mix_id: MixId, + mixnodes_guard: &RwLockReadGuard<'_, MixNodesResult>, + location: Option<&LocationCacheItem>, + node: &MixNodeBondAnnotated, + ) -> PrettyDetailedMixNodeBond { + let denom = &node.mixnode_details.original_pledge().denom; + let rewarding_info = &node.mixnode_details.rewarding_details; + + PrettyDetailedMixNodeBond { + mix_id, + location: location.and_then(|l| l.location.clone()), + status: mixnodes_guard.determine_node_status(mix_id), + pledge_amount: truncate_reward(rewarding_info.operator, denom), + total_delegation: truncate_reward(rewarding_info.delegates, denom), + owner: node.mixnode_details.bond_information.owner.clone(), + layer: node.mixnode_details.bond_information.layer, + mix_node: node.mixnode_details.bond_information.mix_node.clone(), + avg_uptime: node.performance.round_to_integer(), + stake_saturation: best_effort_small_dec_to_f64(node.stake_saturation) as f32, + estimated_operator_apy: best_effort_small_dec_to_f64(node.estimated_operator_apy), + estimated_delegators_apy: best_effort_small_dec_to_f64(node.estimated_delegators_apy), + operating_cost: rewarding_info.cost_params.interval_operating_cost.clone(), + profit_margin_percent: rewarding_info.cost_params.profit_margin_percent, + } + } + + pub(crate) async fn get_detailed_mixnode( + &self, + mix_id: MixId, ) -> Option { let mixnodes_guard = self.mixnodes.read().await; let location_guard = self.locations.read().await; - let bond = mixnodes_guard.get_mixnode(identity_key); - let location = location_guard.get(identity_key); + let bond = mixnodes_guard.get_mixnode(mix_id); + let location = location_guard.get(&mix_id); - match bond { - Some(bond) => Some(PrettyDetailedMixNodeBond { - location: location.and_then(|l| l.location.clone()), - status: mixnodes_guard.determine_node_status(&bond.mix_node().identity_key), - pledge_amount: bond.mixnode_bond.pledge_amount, - total_delegation: bond.mixnode_bond.total_delegation, - owner: bond.mixnode_bond.owner, - layer: bond.mixnode_bond.layer, - mix_node: bond.mixnode_bond.mix_node, - avg_uptime: bond.uptime, - stake_saturation: bond.stake_saturation, - estimated_operator_apy: bond.estimated_operator_apy, - estimated_delegators_apy: bond.estimated_delegators_apy, - }), - None => None, - } + bond.map(|bond| self.create_detailed_mixnode(mix_id, &mixnodes_guard, location, &bond)) } pub(crate) async fn get_detailed_mixnodes(&self) -> Vec { @@ -163,21 +179,8 @@ impl ThreadsafeMixNodesCache { .all_mixnodes .values() .map(|bond| { - let location = location_guard.get(&bond.mix_node().identity_key); - let copy = bond.mixnode_bond.clone(); - PrettyDetailedMixNodeBond { - location: location.and_then(|l| l.location.clone()), - status: mixnodes_guard.determine_node_status(&bond.mix_node().identity_key), - pledge_amount: copy.pledge_amount, - total_delegation: copy.total_delegation, - owner: copy.owner, - layer: copy.layer, - mix_node: copy.mix_node, - avg_uptime: bond.uptime, - stake_saturation: bond.stake_saturation, - estimated_operator_apy: bond.estimated_operator_apy, - estimated_delegators_apy: bond.estimated_delegators_apy, - } + let location = location_guard.get(&bond.mix_id()); + self.create_detailed_mixnode(bond.mix_id(), &mixnodes_guard, location, bond) }) .collect() } @@ -185,13 +188,13 @@ 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 .into_iter() - .map(|bond| (bond.mix_node().identity_key.to_string(), bond)) + .map(|bond| (bond.mix_id(), bond)) .collect(); guard.rewarded_mixnodes = rewarded_nodes; guard.active_mixnodes = active_nodes; diff --git a/explorer-api/src/ping/http.rs b/explorer-api/src/ping/http.rs index 615bce29cb..7e7ebea06d 100644 --- a/explorer-api/src/ping/http.rs +++ b/explorer-api/src/ping/http.rs @@ -1,14 +1,15 @@ -use std::collections::HashMap; -use std::net::{SocketAddr, ToSocketAddrs}; -use std::time::Duration; +// Copyright 2021-2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 +use mixnet_contract_common::{MixId, MixNode}; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; - -use mixnet_contract_common::MixNodeBond; +use std::collections::HashMap; +use std::net::{SocketAddr, ToSocketAddrs}; +use std::time::Duration; use crate::ping::models::PingResponse; use crate::state::ExplorerApiStateContext; @@ -20,38 +21,38 @@ pub fn ping_make_default_routes(settings: &OpenApiSettings) -> (Vec, Open } #[openapi(tag = "ping")] -#[get("/")] +#[get("/")] pub(crate) async fn index( - pubkey: &str, + mix_id: MixId, state: &State, ) -> Option> { - match state.inner.ping.clone().get(pubkey).await { + match state.inner.ping.clone().get(mix_id).await { Some(cache_value) => { - trace!("Returning cached value for {}", pubkey); + trace!("Returning cached value for {}", mix_id); Some(Json(PingResponse { pending: cache_value.pending, ports: cache_value.ports, })) } None => { - trace!("No cache value for {}", pubkey); + trace!("No cache value for {}", mix_id); - match state.inner.get_mix_node(pubkey).await { - Some(bond) => { + match state.inner.get_mix_node(mix_id).await { + Some(node) => { // set status to pending, so that any HTTP requests are pending - state.inner.ping.set_pending(pubkey).await; + state.inner.ping.set_pending(mix_id).await; // do the check - let ports = Some(port_check(&bond.mixnode_bond).await); - trace!("Tested mix node {}: {:?}", pubkey, ports); + let ports = Some(port_check(node.mix_node()).await); + trace!("Tested mix node {}: {:?}", mix_id, ports); let response = PingResponse { ports, pending: false, }; // cache for 1 min - trace!("Caching value for {}", pubkey); - state.inner.ping.set(pubkey, response.clone()).await; + trace!("Caching value for {}", mix_id); + state.inner.ping.set(mix_id, response.clone()).await; // return response Some(Json(response)) @@ -62,23 +63,23 @@ pub(crate) async fn index( } } -async fn port_check(bond: &MixNodeBond) -> HashMap { +async fn port_check(mix_node: &MixNode) -> HashMap { let mut ports: HashMap = HashMap::new(); let ports_to_test = vec![ - bond.mix_node.http_api_port, - bond.mix_node.mix_port, - bond.mix_node.verloc_port, + mix_node.http_api_port, + mix_node.mix_port, + mix_node.verloc_port, ]; trace!( "Testing mix node {} on ports {:?}...", - bond.mix_node.identity_key, + mix_node.identity_key, ports_to_test ); for port in ports_to_test { - ports.insert(port, do_port_check(&bond.mix_node.host, port).await); + ports.insert(port, do_port_check(&mix_node.host, port).await); } ports diff --git a/explorer-api/src/ping/models.rs b/explorer-api/src/ping/models.rs index 28588d550f..9463e12bb6 100644 --- a/explorer-api/src/ping/models.rs +++ b/explorer-api/src/ping/models.rs @@ -2,11 +2,12 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, SystemTime}; +use mixnet_contract_common::MixId; use serde::Deserialize; use serde::Serialize; use tokio::sync::RwLock; -pub(crate) type PingCache = HashMap; +pub(crate) type PingCache = HashMap; const PING_TTL: Duration = Duration::from_secs(60 * 5); // 5 mins, before port check will be re-tried (only while pending) const CACHE_TTL: Duration = Duration::from_secs(60 * 60); // 1 hour, to cache result from port check @@ -23,11 +24,11 @@ impl ThreadsafePingCache { } } - pub(crate) async fn get(&self, identity_key: &str) -> Option { + pub(crate) async fn get(&self, mix_id: MixId) -> Option { self.inner .read() .await - .get(identity_key) + .get(&mix_id) .filter(|cache_item| cache_item.valid_until > SystemTime::now()) .map(|cache_item| { if cache_item.pending { @@ -43,9 +44,9 @@ impl ThreadsafePingCache { }) } - pub(crate) async fn set_pending(&self, identity_key: &str) { + pub(crate) async fn set_pending(&self, mix_id: MixId) { self.inner.write().await.insert( - identity_key.to_string(), + mix_id, PingCacheItem { pending: true, valid_until: SystemTime::now() + PING_TTL, @@ -54,9 +55,9 @@ impl ThreadsafePingCache { ); } - pub(crate) async fn set(&self, identity_key: &str, item: PingResponse) { + pub(crate) async fn set(&self, mix_id: MixId, item: PingResponse) { self.inner.write().await.insert( - identity_key.to_string(), + mix_id, PingCacheItem { pending: false, valid_until: SystemTime::now() + CACHE_TTL, diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index 695354799b..a511d5c0b8 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -3,9 +3,11 @@ use std::path::Path; use chrono::{DateTime, Utc}; use log::info; +use mixnet_contract_common::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::{ @@ -29,14 +31,15 @@ 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, pubkey: &str) -> Option { - self.mixnodes.get_mixnode(pubkey).await + pub(crate) async fn get_mix_node(&self, mix_id: MixId) -> Option { + self.mixnodes.get_mixnode(mix_id).await } } @@ -79,6 +82,7 @@ impl ExplorerApiStateContext { ping: ThreadsafePingCache::new(), validators: ThreadsafeValidatorCache::new(), validator_client: ThreadsafeValidatorClient::new(), + geo_ip: ThreadsafeGeoIp::new(), } } else { warn!( @@ -94,6 +98,7 @@ impl ExplorerApiStateContext { ping: ThreadsafePingCache::new(), validators: ThreadsafeValidatorCache::new(), validator_client: ThreadsafeValidatorClient::new(), + geo_ip: ThreadsafeGeoIp::new(), } } } diff --git a/explorer-api/src/tasks.rs b/explorer-api/src/tasks.rs index bac19b7ee9..08d43fb805 100644 --- a/explorer-api/src/tasks.rs +++ b/explorer-api/src/tasks.rs @@ -97,13 +97,13 @@ impl ExplorerApiTasks { .retrieve_rewarded_mixnodes() .await .into_iter() - .map(|bond| bond.mix_node().identity_key.clone()) + .map(|bond| bond.mix_id()) .collect(); let active_nodes = self .retrieve_active_mixnodes() .await .into_iter() - .map(|bond| bond.mix_node().identity_key.clone()) + .map(|bond| bond.mix_id()) .collect(); self.state .inner diff --git a/explorer-api/src/validators/models.rs b/explorer-api/src/validators/models.rs index 4f17cbe14a..cc68b58f40 100644 --- a/explorer-api/src/validators/models.rs +++ b/explorer-api/src/validators/models.rs @@ -19,7 +19,7 @@ pub(crate) struct PrettyValidatorInfo { } pub(crate) struct ValidatorCache { - pub(crate) validators: Cache, + pub(crate) validators: Cache, pub(crate) summary: ValidatorSummary, } @@ -61,7 +61,7 @@ impl ThreadsafeValidatorCache { for validator in validator_response.validators { let address = validator.address.to_string(); guard.validators.set( - address.clone().as_str(), + address.clone(), PrettyValidatorInfo { address, pub_key: validator.pub_key.to_hex(), diff --git a/explorer/package.json b/explorer/package.json index f391e05f79..b314878308 100644 --- a/explorer/package.json +++ b/explorer/package.json @@ -26,7 +26,8 @@ "react-router-dom": "6", "react-simple-maps": "^2.3.0", "react-tooltip": "^4.2.21", - "use-clipboard-copy": "^0.2.0" + "use-clipboard-copy": "^0.2.0", + "big.js": "^6.2.1" }, "devDependencies": { "@babel/core": "^7.15.0", diff --git a/explorer/src/api/constants.ts b/explorer/src/api/constants.ts index 109586c0fd..fb7216b64f 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.VALIDATOR_URL; +export const VALIDATOR_BASE_URL = process.env.VALIDATOR_URL; export const BIG_DIPPER = process.env.BIG_DIPPER_URL; // specific API routes @@ -10,10 +10,11 @@ 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. +export const UPTIME_STORY_API_GATEWAY = `${VALIDATOR_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this. // errors export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us."; diff --git a/explorer/src/api/index.ts b/explorer/src/api/index.ts index 5a9b40ef9a..37e68b8096 100644 --- a/explorer/src/api/index.ts +++ b/explorer/src/api/index.ts @@ -2,6 +2,7 @@ import { BLOCK_API, COUNTRY_DATA_API, GATEWAYS_API, + UPTIME_STORY_API_GATEWAY, MIXNODE_API, MIXNODE_PING, MIXNODES_API, @@ -15,6 +16,8 @@ import { DelegationsResponse, UniqDelegationsResponse, GatewayResponse, + GatewayReportResponse, + UptimeStoryResponse, MixNodeDescriptionResponse, MixNodeResponse, MixNodeResponseItem, @@ -23,7 +26,6 @@ import { StatsResponse, StatusResponse, SummaryOverviewResponse, - UptimeStoryResponse, ValidatorsResponse, } from '../typeDefs/explorer-api'; @@ -92,6 +94,12 @@ export class Api { return res.json(); }; + static fetchGatewayUptimeStoryById = async (id: string): Promise => + (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json(); + + static fetchGatewayReportById = async (id: string): Promise => + (await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json(); + static fetchValidators = async (): Promise => { const res = await fetch(VALIDATORS_API); const json = await res.json(); diff --git a/explorer/src/components/DetailTable.tsx b/explorer/src/components/DetailTable.tsx index 3957119cd3..1aab097a05 100644 --- a/explorer/src/components/DetailTable.tsx +++ b/explorer/src/components/DetailTable.tsx @@ -1,9 +1,12 @@ import * as React from 'react'; import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import { Tooltip } from '@nymproject/react/tooltip/Tooltip'; import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; import { Box } from '@mui/system'; import { cellStyles } from './Universal-DataGrid'; import { currencyToString } from '../utils/currency'; +import { GatewayEnrichedRowType } from './Gateways'; import { MixnodeRowType } from './MixNodes'; export type ColumnsType = { @@ -43,42 +46,60 @@ function formatCellValues(val: string | number, field: string) { export const DetailTable: React.FC<{ tableName: string; columnsData: ColumnsType[]; - rows: MixnodeRowType[]; -}> = ({ tableName, columnsData, rows }: UniversalTableProps) => ( - - - - - {columnsData?.map(({ field, title, flex }) => ( - - {title} - - ))} - - - - {rows.map((eachRow) => ( - - {columnsData?.map((_, index) => ( - - {formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)} + rows: MixnodeRowType[] | GatewayEnrichedRowType[]; +}> = ({ tableName, columnsData, rows }: UniversalTableProps) => { + const theme = useTheme(); + return ( + +
+ + + {columnsData?.map(({ field, title, flex, tooltipInfo }) => ( + + + {tooltipInfo && ( + + + + )} + {title} + ))} - ))} - -
-
-); + + + {rows.map((eachRow) => ( + + {columnsData?.map((data, index) => ( + + {formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)} + + ))} + + ))} + + + + ); +}; diff --git a/explorer/src/components/Filters/Filters.tsx b/explorer/src/components/Filters/Filters.tsx index 65ea7112dd..a01d64361e 100644 --- a/explorer/src/components/Filters/Filters.tsx +++ b/explorer/src/components/Filters/Filters.tsx @@ -47,6 +47,7 @@ const FilterItem = ({ scale={scale} min={min} max={max} + valueLabelFormat={(val: number) => (val === 100 && id === 'stakeSaturation' ? '>100' : val)} /> ); @@ -59,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(); @@ -66,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); @@ -78,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; }); }; @@ -141,11 +152,10 @@ export const Filters = () => { diff --git a/explorer/src/components/Filters/filterSchema.ts b/explorer/src/components/Filters/filterSchema.ts index b5e7fbdf89..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,33 +25,19 @@ export const generateFilterSchema = (upperSaturationValue?: number) => ({ stakeSaturation: { label: 'Stake saturation (%)', id: EnumFilterKey.stakeSaturation, - value: [0, upperSaturationValue || 100], - marks: [ - { label: '0', value: 0 }, - - { - label: '10', - value: 10, - }, - - { - label: '50', - value: 50, - }, - { label: '90', value: 90 }, - - { - label: upperSaturationValue ? `${upperSaturationValue}` : '100', - value: upperSaturationValue || 100, - }, - ], - max: upperSaturationValue, + value: [0, 100], + isSmooth: true, + marks: [0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100].map((value) => ({ + value: value < 100 ? value : 100, + label: value < 100 ? value : '>100', + })), tooltipInfo: "Select nodes with <100% saturation. Any additional stake above 100% saturation won't get rewards", }, routingScore: { label: 'Routing score (%)', id: EnumFilterKey.routingScore, value: [0, 100], + isSmooth: true, marks: [ { label: '0', value: 0 }, { label: '10', value: 10 }, diff --git a/explorer/src/components/Gateways.ts b/explorer/src/components/Gateways.ts index 9f05ff51b0..e75c9aeb05 100644 --- a/explorer/src/components/Gateways.ts +++ b/explorer/src/components/Gateways.ts @@ -1,23 +1,48 @@ -import { GatewayResponse } from '../typeDefs/explorer-api'; +import { GatewayResponse, GatewayResponseItem, GatewayReportResponse } from '../typeDefs/explorer-api'; export type GatewayRowType = { id: string; owner: string; - identity_key: string; + identityKey: string; bond: number; host: string; location: string; }; +export type GatewayEnrichedRowType = GatewayRowType & { + routingScore: string; + avgUptime: string; + clientsPort: number; + mixPort: number; +}; + export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] { return !arrayOfGateways ? [] : arrayOfGateways.map((gw) => ({ id: gw.owner, owner: gw.owner, - identity_key: gw.gateway.identity_key || '', + identityKey: gw.gateway.identity_key || '', location: gw?.gateway?.location || '', bond: gw.pledge_amount.amount || 0, host: gw.gateway.host || '', })); } + +export function gatewayEnrichedToGridRow( + gateway: GatewayResponseItem, + report: GatewayReportResponse, +): GatewayEnrichedRowType { + return { + id: gateway.owner, + owner: gateway.owner, + identityKey: gateway.gateway.identity_key || '', + location: gateway?.gateway?.location || '', + bond: gateway.pledge_amount.amount || 0, + host: gateway.gateway.host || '', + clientsPort: gateway.gateway.clients_port || 0, + mixPort: gateway.gateway.mix_port || 0, + routingScore: `${report.most_recent}%`, + avgUptime: `${report.last_day || report.last_hour}%`, + }; +} diff --git a/explorer/src/components/MixNodes/Economics/Columns.ts b/explorer/src/components/MixNodes/Economics/Columns.ts index c038b68bce..40fef70b68 100644 --- a/explorer/src/components/MixNodes/Economics/Columns.ts +++ b/explorer/src/components/MixNodes/Economics/Columns.ts @@ -39,6 +39,14 @@ export const EconomicsInfoColumns: ColumnsType[] = [ tooltipInfo: 'Percentage of the delegates rewards that the operator takes as fee before rewards are distributed to the delegates.', }, + { + field: 'operatingCost', + title: 'Operating Cost', + flex: 1, + headerAlign: 'left', + tooltipInfo: + 'Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators.', + }, { field: 'avgUptime', title: 'Routing Score', diff --git a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx b/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx index be9c4c94f0..aa3156ccf1 100644 --- a/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx +++ b/explorer/src/components/MixNodes/Economics/MixNodeEconomics.stories.tsx @@ -26,23 +26,19 @@ const row: EconomicsInfoRowWithIndex = { profitMargin: { value: '10 %', }, + operatingCost: { + value: '11121 NYM', + }, stakeSaturation: { value: '80 %', progressBarValue: 80, }, }; -const rowVeryHighProbabilitySelection: EconomicsInfoRowWithIndex = { +const rowGoodProbabilitySelection: EconomicsInfoRowWithIndex = { ...row, selectionChance: { - value: 'Very High', - }, -}; - -const rowModerateProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Moderate', + value: 'Good', }, }; @@ -53,13 +49,6 @@ const rowLowProbabilitySelection: EconomicsInfoRowWithIndex = { }, }; -const rowVeryLowProbabilitySelection: EconomicsInfoRowWithIndex = { - ...row, - selectionChance: { - value: 'Very Low', - }, -}; - const emptyRow: EconomicsInfoRowWithIndex = { id: 1, selectionChance: { @@ -78,6 +67,9 @@ const emptyRow: EconomicsInfoRowWithIndex = { profitMargin: { value: '-', }, + operatingCost: { + value: '-', + }, stakeSaturation: { value: '-', progressBarValue: 0, @@ -93,13 +85,6 @@ Empty.args = { tableName: 'storybook', }; -export const selectionChanceVeryHigh = Template.bind({}); -selectionChanceVeryHigh.args = { - rows: [rowVeryHighProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; - export const selectionChanceHigh = Template.bind({}); selectionChanceHigh.args = { rows: [row], @@ -107,9 +92,9 @@ selectionChanceHigh.args = { tableName: 'storybook', }; -export const selectionChanceModerate = Template.bind({}); -selectionChanceModerate.args = { - rows: [rowModerateProbabilitySelection], +export const selectionChanceGood = Template.bind({}); +selectionChanceGood.args = { + rows: [rowGoodProbabilitySelection], columnsData: EconomicsInfoColumns, tableName: 'storybook', }; @@ -120,10 +105,3 @@ selectionChanceLow.args = { columnsData: EconomicsInfoColumns, tableName: 'storybook', }; - -export const selectionChanceVeryLow = Template.bind({}); -selectionChanceVeryLow.args = { - rows: [rowVeryLowProbabilitySelection], - columnsData: EconomicsInfoColumns, - tableName: 'storybook', -}; diff --git a/explorer/src/components/MixNodes/Economics/Rows.ts b/explorer/src/components/MixNodes/Economics/Rows.ts index 0a620318d7..a4f998a1f0 100644 --- a/explorer/src/components/MixNodes/Economics/Rows.ts +++ b/explorer/src/components/MixNodes/Economics/Rows.ts @@ -1,19 +1,22 @@ -import { currencyToString } from '../../../utils/currency'; +import { currencyToString, unymToNym } from '../../../utils/currency'; import { useMixnodeContext } from '../../../context/mixnode'; import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '../../../typeDefs/explorer-api'; import { EconomicsInfoRowWithIndex } from './types'; +import { toPercentIntegerString } from '../../../utils'; 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 'Moderate': - case 'Low': - return inclusionProbability; - case 'VeryHigh': - return 'Very High'; case 'VeryLow': return 'Very Low'; + case 'VeryHigh': + return 'Very High'; + case 'High': + case 'Good': + case 'Low': + case 'Moderate': + return inclusionProbability; default: return '-'; } @@ -27,9 +30,13 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { const estimatedOperatorRewards = currencyToString((economicDynamicsStats?.data?.estimated_operator_reward || '').toString()) || '-'; const stakeSaturation = economicDynamicsStats?.data?.stake_saturation || '-'; - const profitMargin = mixNode?.data?.mix_node.profit_margin_percent || '-'; + const profitMargin = mixNode?.data?.profit_margin_percent + ? toPercentIntegerString(mixNode?.data?.profit_margin_percent) + : '-'; const avgUptime = economicDynamicsStats?.data?.current_interval_uptime; + const opCost = mixNode?.data?.operating_cost; + return { id: 1, estimatedTotalReward: { @@ -48,6 +55,9 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => { profitMargin: { value: profitMargin ? `${profitMargin} %` : '-', }, + operatingCost: { + value: opCost ? `${unymToNym(opCost.amount, 6)} NYM` : '-', + }, avgUptime: { value: avgUptime ? `${avgUptime} %` : '-', }, diff --git a/explorer/src/components/MixNodes/Economics/Table.tsx b/explorer/src/components/MixNodes/Economics/Table.tsx index d60a83da84..5977847436 100644 --- a/explorer/src/components/MixNodes/Economics/Table.tsx +++ b/explorer/src/components/MixNodes/Economics/Table.tsx @@ -19,14 +19,16 @@ const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => { return theme.palette.warning.main; } if (field === 'selectionChance') { + // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow switch (fieldValue) { case 'High': - case 'Very High': + case 'VeryHigh': return theme.palette.nym.networkExplorer.selectionChance.overModerate; + case 'Good': case 'Moderate': return theme.palette.nym.networkExplorer.selectionChance.moderate; case 'Low': - case 'Very Low': + case 'VeryLow': return theme.palette.nym.networkExplorer.selectionChance.underModerate; default: return theme.palette.nym.wallet.fee; diff --git a/explorer/src/components/MixNodes/Economics/types.ts b/explorer/src/components/MixNodes/Economics/types.ts index b82b35f139..7c4bc722db 100644 --- a/explorer/src/components/MixNodes/Economics/types.ts +++ b/explorer/src/components/MixNodes/Economics/types.ts @@ -10,6 +10,7 @@ export interface EconomicsInfoRow { stakeSaturation: EconomicsRowsType; profitMargin: EconomicsRowsType; avgUptime: EconomicsRowsType; + operatingCost: EconomicsRowsType; } export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number }; 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/MixNodes/index.ts b/explorer/src/components/MixNodes/index.ts index 55a6b7c080..3435c3bf3f 100644 --- a/explorer/src/components/MixNodes/index.ts +++ b/explorer/src/components/MixNodes/index.ts @@ -1,7 +1,10 @@ /* eslint-disable camelcase */ import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api'; +import { toPercentIntegerString } from '../../utils'; +import { unymToNym } from '../../utils/currency'; export type MixnodeRowType = { + mix_id: number; id: string; status: MixnodeStatus; owner: string; @@ -9,11 +12,13 @@ export type MixnodeRowType = { identity_key: string; bond: number; self_percentage: string; + pledge_amount: number; host: string; layer: string; profit_percentage: string; avg_uptime: string; stake_saturation: number; + operating_cost: string; }; export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] { @@ -25,9 +30,11 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): const delegations = Number(item.total_delegation.amount) || 0; const totalBond = pledge + delegations; const selfPercentage = ((pledge * 100) / totalBond).toFixed(2); - const profitPercentage = item.mix_node.profit_margin_percent || 0; + const profitPercentage = toPercentIntegerString(item.profit_margin_percent) || 0; const stakeSaturation = typeof item.stake_saturation === 'number' ? item.stake_saturation * 100 : 0; + return { + mix_id: item.mix_id, id: item.owner, status: item.status, owner: item.owner, @@ -35,10 +42,12 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): bond: totalBond || 0, location: item?.location?.country_name || '', self_percentage: selfPercentage, + pledge_amount: pledge, host: item?.mix_node?.host || '', layer: item?.layer || '', profit_percentage: `${profitPercentage}%`, avg_uptime: `${item.avg_uptime}%` || '-', stake_saturation: stakeSaturation, + operating_cost: `${unymToNym(item.operating_cost.amount, 6)} NYM`, }; } 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/Tooltip.tsx b/explorer/src/components/Tooltip.tsx new file mode 100644 index 0000000000..0febd9f891 --- /dev/null +++ b/explorer/src/components/Tooltip.tsx @@ -0,0 +1,36 @@ +import React, { ReactElement } from 'react'; +import { Tooltip as MUITooltip, TooltipComponentsPropsOverrides, TooltipProps } from '@mui/material'; + +type ValueType = T[keyof T]; + +type Props = { + text: string; + id: string; + placement?: ValueType>; + tooltipSx?: TooltipComponentsPropsOverrides; + children: React.ReactNode; +}; + +export const Tooltip = ({ text, id, placement, tooltipSx, children }: Props) => ( + t.palette.nym.networkExplorer.tooltip.background, + color: (t) => t.palette.nym.networkExplorer.tooltip.color, + '& .MuiTooltip-arrow': { + color: (t) => t.palette.nym.networkExplorer.tooltip.background, + }, + }, + ...tooltipSx, + }, + }} + arrow + > + {children as ReactElement} + +); 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/context/gateway.tsx b/explorer/src/context/gateway.tsx new file mode 100644 index 0000000000..dd96634518 --- /dev/null +++ b/explorer/src/context/gateway.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { ApiState, GatewayReportResponse, UptimeStoryResponse } from '../typeDefs/explorer-api'; +import { Api } from '../api'; +import { useApiState } from './hooks'; + +/** + * This context provides the state for a single gateway by identity key. + */ + +interface GatewayState { + uptimeReport?: ApiState; + uptimeStory?: ApiState; +} + +export const GatewayContext = React.createContext({}); + +export const useGatewayContext = (): React.ContextType => + React.useContext(GatewayContext); + +/** + * Provides a state context for a gateway by identity + * @param gatewayIdentityKey The identity key of the gateway + */ +export const GatewayContextProvider = ({ + gatewayIdentityKey, + children, +}: { + gatewayIdentityKey: string; + children: JSX.Element; +}) => { + const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = useApiState( + gatewayIdentityKey, + Api.fetchGatewayReportById, + 'Failed to fetch gateway uptime report by id', + ); + + const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState( + gatewayIdentityKey, + Api.fetchGatewayUptimeStoryById, + 'Failed to fetch gateway uptime history', + ); + + React.useEffect(() => { + // when the identity key changes, remove all previous data + clearUptimeReportById(); + clearUptimeHistory(); + Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]); + }, [gatewayIdentityKey]); + + const state = React.useMemo( + () => ({ + uptimeReport, + uptimeStory, + }), + [uptimeReport, uptimeStory], + ); + + return {children}; +}; diff --git a/explorer/src/context/hooks.ts b/explorer/src/context/hooks.ts index f891a31ca3..9c5579c899 100644 --- a/explorer/src/context/hooks.ts +++ b/explorer/src/context/hooks.ts @@ -1,30 +1,47 @@ import * as React from 'react'; import { ApiState } from '../typeDefs/explorer-api'; -type WrappedApiFn = () => Promise>; - +/** + * Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously + * @param id The id to fetch + * @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter) + * @param errorMessage A static error message, to use when no dynamic error message is returned + */ export const useApiState = ( - // eslint-disable-next-line @typescript-eslint/ban-types - fn: Function, + id: string, + fn: (argId: string) => Promise, errorMessage: string, -): [ApiState, WrappedApiFn] => { +): [ApiState | undefined, () => Promise>, () => void] => { + // stores the state const [value, setValue] = React.useState>(); - const wrappedFn = React.useCallback(async () => { + + // clear the value + const clearValueFn = () => setValue(undefined); + + // this provides a method to trigger the delegate to fetch data + const wrappedFetchFn = React.useCallback(async () => { + setValue({ isLoading: true }); try { + // keep previous state and set to loading setValue((prevState) => ({ ...prevState, isLoading: true })); - const data = await fn(); - setValue({ + + // delegate to user function to get data and set if successful + const data = await fn(id); + const newValue: ApiState = { isLoading: false, data, - }); - return data; + }; + setValue(newValue); + return newValue; } catch (error) { - setValue({ + // return the caught error or create a new error with the static error message + const newValue: ApiState = { error: error instanceof Error ? error : new Error(errorMessage), isLoading: false, - }); - return undefined; + }; + setValue(newValue); + return newValue; } - }, [setValue, fn]); - return [value || { isLoading: true }, wrappedFn]; + }, [setValue, fn, id, errorMessage]); + return [value, wrappedFetchFn, clearValueFn]; }; diff --git a/explorer/src/context/main.tsx b/explorer/src/context/main.tsx index 9255eb20e3..42e89b32ee 100644 --- a/explorer/src/context/main.tsx +++ b/explorer/src/context/main.tsx @@ -96,19 +96,22 @@ export const MainContextProvider: React.FC = ({ children }) => { const filterMixnodes = async (filters: { [key in EnumFilterKey]: number[] }, status?: MixnodeStatus) => { setMixnodes((d) => ({ ...d, isLoading: true })); const mxns = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes(); + const filtered = mxns?.filter( (m) => - m.mix_node.profit_margin_percent >= filters.profitMargin[0] && - m.mix_node.profit_margin_percent <= filters.profitMargin[1] && + +m.profit_margin_percent >= filters.profitMargin[0] / 100 && + +m.profit_margin_percent <= filters.profitMargin[1] / 100 && m.stake_saturation >= filters.stakeSaturation[0] && m.stake_saturation <= filters.stakeSaturation[1] && m.avg_uptime >= filters.routingScore[0] && m.avg_uptime <= filters.routingScore[1], ); + setMixnodes({ data: filtered, isLoading: false }); }; const fetchGateways = async () => { + setGateways((d) => ({ ...d, isLoading: true })); try { const data = await Api.fetchGateways(); setGateways({ data, isLoading: false }); diff --git a/explorer/src/context/mixnode.tsx b/explorer/src/context/mixnode.tsx index 6259b8c00d..a17366645c 100644 --- a/explorer/src/context/mixnode.tsx +++ b/explorer/src/context/mixnode.tsx @@ -11,6 +11,7 @@ import { UptimeStoryResponse, } from '../typeDefs/explorer-api'; import { Api } from '../api'; +import { useApiState } from './hooks'; import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../components/MixNodes'; /** @@ -153,47 +154,3 @@ export const MixnodeContextProvider: React.FC = ({ return {children}; }; - -/** - * Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously - * @param id The id to fetch - * @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter) - * @param errorMessage A static error message, to use when no dynamic error message is returned - */ -function useApiState( - id: string, - fn: (argId: string) => Promise, - errorMessage: string, -): [ApiState | undefined, () => Promise>, () => void] { - // stores the state - const [value, setValue] = React.useState | undefined>(); - - // clear the value - const clearValueFn = () => setValue(undefined); - - // this provides a method to trigger the delegate to fetch data - const wrappedFetchFn = React.useCallback(async () => { - try { - // keep previous state and set to loading - setValue((prevState) => ({ ...prevState, isLoading: true })); - - // delegate to user function to get data and set if successful - const data = await fn(id); - const newValue: ApiState = { - isLoading: false, - data, - }; - setValue(newValue); - return newValue; - } catch (error) { - // return the caught error or create a new error with the static error message - const newValue: ApiState = { - error: error instanceof Error ? error : new Error(errorMessage), - isLoading: false, - }; - setValue(newValue); - return newValue; - } - }, [setValue, fn]); - return [value || { isLoading: true }, wrappedFetchFn, clearValueFn]; -} diff --git a/explorer/src/pages/GatewayDetail/index.tsx b/explorer/src/pages/GatewayDetail/index.tsx new file mode 100644 index 0000000000..5fb7dd9c9f --- /dev/null +++ b/explorer/src/pages/GatewayDetail/index.tsx @@ -0,0 +1,179 @@ +import * as React from 'react'; +import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material'; +import { useParams } from 'react-router-dom'; +import { GatewayResponseItem } from '../../typeDefs/explorer-api'; +import { ColumnsType, DetailTable } from '../../components/DetailTable'; +import { gatewayEnrichedToGridRow, GatewayEnrichedRowType } from '../../components/Gateways'; +import { ComponentError } from '../../components/ComponentError'; +import { ContentCard } from '../../components/ContentCard'; +import { TwoColSmallTable } from '../../components/TwoColSmallTable'; +import { UptimeChart } from '../../components/UptimeChart'; +import { GatewayContextProvider, useGatewayContext } from '../../context/gateway'; +import { useMainContext } from '../../context/main'; +import { Title } from '../../components/Title'; + +const columns: ColumnsType[] = [ + { + field: 'identityKey', + title: 'Identity Key', + headerAlign: 'left', + width: 230, + }, + { + field: 'bond', + title: 'Bond', + flex: 1, + headerAlign: 'left', + }, + { + field: 'routingScore', + title: 'Routing Score', + flex: 1, + headerAlign: 'left', + tooltipInfo: + 'Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.', + }, + { + field: 'avgUptime', + title: 'Avg. Score', + flex: 1, + headerAlign: 'left', + tooltipInfo: 'Is the average routing score in the last 24 hours', + }, + { + field: 'host', + title: 'IP', + headerAlign: 'left', + width: 99, + }, + { + field: 'location', + title: 'Location', + headerAlign: 'left', + flex: 1, + }, + { + field: 'owner', + title: 'Owner', + headerAlign: 'left', + flex: 1, + }, +]; + +/** + * Shows gateway details + */ +const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: GatewayResponseItem | undefined }) => { + const [enrichGateway, setEnrichGateway] = React.useState(); + const [status, setStatus] = React.useState(); + const { uptimeReport, uptimeStory } = useGatewayContext(); + + React.useEffect(() => { + if (uptimeReport?.data && selectedGateway) { + setEnrichGateway(gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data)); + } + }, [uptimeReport, selectedGateway]); + + React.useEffect(() => { + if (enrichGateway) { + setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]); + } + }, [enrichGateway]); + + return ( + + + + <Grid container> + <Grid item xs={12}> + <DetailTable + columnsData={columns} + tableName="Gateway detail table" + rows={enrichGateway ? [enrichGateway] : []} + /> + </Grid> + </Grid> + + <Grid container spacing={2} mt={0}> + <Grid item xs={12} md={4}> + {status && ( + <ContentCard title="Gateway Status"> + <TwoColSmallTable + loading={false} + keys={['Mix port', 'Client WS API Port']} + values={status.map((each) => each)} + icons={status.map((elem) => !!elem)} + /> + </ContentCard> + )} + </Grid> + <Grid item xs={12} md={8}> + {uptimeStory && ( + <ContentCard title="Routing Score"> + {uptimeStory.error && <ComponentError text="There was a problem retrieving routing score." />} + <UptimeChart loading={uptimeStory.isLoading} xLabel="date" uptimeStory={uptimeStory} /> + </ContentCard> + )} + </Grid> + </Grid> + </Box> + ); +}; + +/** + * Guard component to handle loading and not found states + */ +const PageGatewayDetailGuard: React.FC = () => { + const [selectedGateway, setSelectedGateway] = React.useState<GatewayResponseItem | undefined>(); + const { gateways } = useMainContext(); + const { id } = useParams<{ id: string | undefined }>(); + + React.useEffect(() => { + if (gateways?.data) { + setSelectedGateway(gateways.data.find((gateway) => gateway.gateway.identity_key === id)); + } + }, [gateways, id]); + + if (gateways?.isLoading) { + return <CircularProgress />; + } + + if (gateways?.error) { + // eslint-disable-next-line no-console + console.error(gateways?.error); + return ( + <Alert severity="error"> + Oh no! Could not load mixnode <code>{id || ''}</code> + </Alert> + ); + } + + // loaded, but not found + if (gateways && !gateways.isLoading && !gateways.data) { + return ( + <Alert severity="warning"> + <AlertTitle>Gateway not found</AlertTitle> + Sorry, we could not find a mixnode with id <code>{id || ''}</code> + </Alert> + ); + } + + return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />; +}; + +/** + * Wrapper component that adds the mixnode content based on the `id` in the address URL + */ +export const PageGatewayDetail: React.FC = () => { + const { id } = useParams<{ id: string | undefined }>(); + + if (!id) { + return <Alert severity="error">Oh no! No mixnode identity key specified</Alert>; + } + + return ( + <GatewayContextProvider gatewayIdentityKey={id}> + <PageGatewayDetailGuard /> + </GatewayContextProvider> + ); +}; diff --git a/explorer/src/pages/Gateways/index.tsx b/explorer/src/pages/Gateways/index.tsx index 39f3619308..6b557d593f 100644 --- a/explorer/src/pages/Gateways/index.tsx +++ b/explorer/src/pages/Gateways/index.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { Button, Card, Grid, Typography } from '@mui/material'; +import { Link as RRDLink } from 'react-router-dom'; +import { Box, Button, Card, Grid, Typography, Link as MuiLink } from '@mui/material'; import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; import { SelectChangeEvent } from '@mui/material/Select'; import { useMainContext } from '../../context/main'; @@ -10,6 +11,7 @@ import { CustomColumnHeading } from '../../components/CustomColumnHeading'; import { Title } from '../../components/Title'; import { cellStyles, UniversalDataGrid } from '../../components/Universal-DataGrid'; import { currencyToString } from '../../utils/currency'; +import { Tooltip } from '../../components/Tooltip'; export const PageGateways: React.FC = () => { const { gateways } = useMainContext(); @@ -43,29 +45,20 @@ export const PageGateways: React.FC = () => { const columns: GridColDef[] = [ { - field: 'owner', - headerName: 'Owner', - renderHeader: () => <CustomColumnHeading headingTitle="Owner" />, - width: 380, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <Typography sx={cellStyles} data-testid="owner"> - {params.value} - </Typography> - ), - }, - { - field: 'identity_key', + field: 'identityKey', headerName: 'Identity Key', renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />, headerClassName: 'MuiDataGrid-header-override', width: 380, headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( - <Typography sx={cellStyles} data-testid="identity-key"> + <MuiLink + sx={{ ...cellStyles }} + component={RRDLink} + to={`/network-components/gateway/${params.row.identityKey}`} + > {params.value} - </Typography> + </MuiLink> ), }, { @@ -84,7 +77,7 @@ export const PageGateways: React.FC = () => { { field: 'host', renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />, - width: 110, + width: 180, headerAlign: 'left', headerClassName: 'MuiDataGrid-header-override', renderCell: (params: GridRenderCellParams) => ( @@ -96,7 +89,7 @@ export const PageGateways: React.FC = () => { { field: 'location', renderHeader: () => <CustomColumnHeading headingTitle="Location" />, - width: 150, + width: 180, headerAlign: 'left', headerClassName: 'MuiDataGrid-header-override', renderCell: (params: GridRenderCellParams) => ( @@ -105,10 +98,33 @@ export const PageGateways: React.FC = () => { sx={{ ...cellStyles, justifyContent: 'flex-start' }} data-testid="location-button" > - {params.value} + <Tooltip text={params.value} id="gateway-location-text"> + <Box + sx={{ + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }} + > + {params.value} + </Box> + </Tooltip> </Button> ), }, + { + field: 'owner', + headerName: 'Owner', + renderHeader: () => <CustomColumnHeading headingTitle="Owner" />, + width: 380, + headerAlign: 'left', + headerClassName: 'MuiDataGrid-header-override', + renderCell: (params: GridRenderCellParams) => ( + <Typography sx={cellStyles} data-testid="owner"> + {params.value} + </Typography> + ), + }, ]; const handlePageSize = (event: SelectChangeEvent<string>) => { @@ -133,7 +149,12 @@ export const PageGateways: React.FC = () => { pageSize={pageSize} searchTerm={searchTerm} /> - <UniversalDataGrid rows={gatewayToGridRow(filteredGateways)} columns={columns} pageSize={pageSize} /> + <UniversalDataGrid + pagination + rows={gatewayToGridRow(filteredGateways)} + columns={columns} + pageSize={pageSize} + /> </Card> </Grid> </Grid> 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 = () => { </Grid> <Grid item xs={12} md={8}> {uptimeStory && ( - <ContentCard title="Uptime story"> - {uptimeStory.error && <ComponentError text="There was a problem retrieving uptime history." />} - <UptimeChart loading={uptimeStory.isLoading} xLabel="date" yLabel="uptime" uptimeStory={uptimeStory} /> + <ContentCard title="Routing Score"> + {uptimeStory.error && <ComponentError text="There was a problem retrieving routing score." />} + <UptimeChart loading={uptimeStory.isLoading} xLabel="date" uptimeStory={uptimeStory} /> </ContentCard> )} </Grid> diff --git a/explorer/src/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx index 33ebb84993..f37707c134 100644 --- a/explorer/src/pages/Mixnodes/index.tsx +++ b/explorer/src/pages/Mixnodes/index.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid'; -import { Button, Card, Grid, Link as MuiLink } from '@mui/material'; +import { Button, Card, Grid, Link as MuiLink, Box } from '@mui/material'; import { Link as RRDLink, useParams, useNavigate } from 'react-router-dom'; import { SelectChangeEvent } from '@mui/material/Select'; import { SxProps } from '@mui/system'; @@ -18,6 +18,7 @@ import { currencyToString } from '../../utils/currency'; import { splice } from '../../utils'; import { getMixNodeStatusColor } from '../../components/MixNodes/Status'; import { MixNodeStatusDropdown } from '../../components/MixNodes/StatusDropdown'; +import { Tooltip } from '../../components/Tooltip'; const getCellFontStyle = (theme: Theme, row: MixnodeRowType, textColor?: string) => { const color = textColor || getMixNodeStatusColor(theme, row.status); @@ -82,20 +83,20 @@ export const PageMixnodes: React.FC = () => { const columns: GridColDef[] = [ { - field: 'owner', - headerName: 'Owner', - renderHeader: () => <CustomColumnHeading headingTitle="Owner" />, - width: 200, - headerAlign: 'left', + field: 'mix_id', + headerName: 'Mix ID', + renderHeader: () => <CustomColumnHeading headingTitle="Mix ID" />, headerClassName: 'MuiDataGrid-header-override', + width: 100, + headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( <MuiLink - href={`${BIG_DIPPER}/account/${params.value}`} - target="_blank" sx={getCellStyles(theme, params.row)} - data-testid="big-dipper-link" + component={RRDLink} + to={`/network-components/mixnode/${params.value}`} + data-testid="mix-id" > - {splice(7, 29, params.value)} + {params.value} </MuiLink> ), }, @@ -104,7 +105,7 @@ export const PageMixnodes: React.FC = () => { headerName: 'Identity Key', renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />, headerClassName: 'MuiDataGrid-header-override', - width: 180, + width: 170, headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( <> @@ -116,7 +117,7 @@ export const PageMixnodes: React.FC = () => { <MuiLink sx={getCellStyles(theme, params.row)} component={RRDLink} - to={`/network-components/mixnode/${params.value}`} + to={`/network-components/mixnode/${params.row.mix_id}`} data-testid="identity-link" > {splice(7, 29, params.value)} @@ -124,55 +125,19 @@ export const PageMixnodes: React.FC = () => { </> ), }, - { - field: 'location', - headerName: 'Location', - renderHeader: () => <CustomColumnHeading headingTitle="Location" />, - width: 150, - headerAlign: 'left', - headerClassName: 'MuiDataGrid-header-override', - renderCell: (params: GridRenderCellParams) => ( - <Button - onClick={() => handleSearch(params.value as string)} - sx={{ - ...getCellStyles(theme, params.row), - justifyContent: 'flex-start', - }} - > - {params.value} - </Button> - ), - }, - { - field: 'host', - headerName: 'Host', - renderHeader: () => <CustomColumnHeading headingTitle="Host" />, - headerClassName: 'MuiDataGrid-header-override', - width: 130, - headerAlign: 'left', - renderCell: (params: GridRenderCellParams) => ( - <MuiLink - sx={getCellStyles(theme, params.row)} - component={RRDLink} - to={`/network-components/mixnode/${params.row.identity_key}`} - > - {params.value} - </MuiLink> - ), - }, { field: 'bond', headerName: 'Stake', renderHeader: () => <CustomColumnHeading headingTitle="Stake" />, type: 'number', headerClassName: 'MuiDataGrid-header-override', - width: 200, + width: 170, headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( <MuiLink sx={getCellStyles(theme, params.row)} component={RRDLink} - to={`/network-components/mixnode/${params.row.identity_key}`} + to={`/network-components/mixnode/${params.row.mix_id}`} > {currencyToString(params.value)} </MuiLink> @@ -188,7 +153,7 @@ export const PageMixnodes: React.FC = () => { /> ), headerClassName: 'MuiDataGrid-header-override', - width: 190, + width: 185, headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( <MuiLink @@ -197,27 +162,27 @@ export const PageMixnodes: React.FC = () => { ...getCellStyles(theme, params.row, params.value > 100 ? 'theme.palette.warning.main' : undefined), }} component={RRDLink} - to={`/network-components/mixnode/${params.row.identity_key}`} + to={`/network-components/mixnode/${params.row.mix_id}`} > {`${params.value.toFixed(2)} %`} </MuiLink> ), }, { - field: 'self_percentage', - headerName: 'Self %', - width: 110, + field: 'pledge_amount', + headerName: 'Bond', + width: 175, headerClassName: 'MuiDataGrid-header-override', - renderHeader: () => <CustomColumnHeading headingTitle="Self %" />, + renderHeader: () => <CustomColumnHeading headingTitle="Bond" tooltipInfo="Node operator's share of stake." />, type: 'number', headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( <MuiLink sx={getCellStyles(theme, params.row)} component={RRDLink} - to={`/network-components/mixnode/${params.row.identity_key}`} + to={`/network-components/mixnode/${params.row.mix_id}`} > - {params.value}% + {currencyToString(params.value)} </MuiLink> ), }, @@ -231,13 +196,35 @@ export const PageMixnodes: React.FC = () => { /> ), headerClassName: 'MuiDataGrid-header-override', - width: 165, + width: 160, headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( <MuiLink sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }} component={RRDLink} - to={`/network-components/mixnode/${params.row.identity_key}`} + to={`/network-components/mixnode/${params.row.mix_id}`} + > + {params.value} + </MuiLink> + ), + }, + { + field: 'operating_cost', + headerName: 'Operating cost', + renderHeader: () => ( + <CustomColumnHeading + headingTitle="Operating cost" + tooltipInfo="Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators." + /> + ), + headerClassName: 'MuiDataGrid-header-override', + width: 170, + headerAlign: 'left', + renderCell: (params: GridRenderCellParams) => ( + <MuiLink + sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }} + component={RRDLink} + to={`/network-components/mixnode/${params.row.mix_id}`} > {params.value} </MuiLink> @@ -253,13 +240,77 @@ export const PageMixnodes: React.FC = () => { /> ), headerClassName: 'MuiDataGrid-header-override', - width: 160, + width: 165, headerAlign: 'left', renderCell: (params: GridRenderCellParams) => ( <MuiLink sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }} component={RRDLink} - to={`/network-components/mixnode/${params.row.identity_key}`} + to={`/network-components/mixnode/${params.row.mix_id}`} + > + {params.value} + </MuiLink> + ), + }, + { + field: 'owner', + headerName: 'Owner', + renderHeader: () => <CustomColumnHeading headingTitle="Owner" />, + width: 120, + headerAlign: 'left', + headerClassName: 'MuiDataGrid-header-override', + renderCell: (params: GridRenderCellParams) => ( + <MuiLink + href={`${BIG_DIPPER}/account/${params.value}`} + target="_blank" + sx={getCellStyles(theme, params.row)} + data-testid="big-dipper-link" + > + {splice(7, 29, params.value)} + </MuiLink> + ), + }, + { + field: 'location', + headerName: 'Location', + renderHeader: () => <CustomColumnHeading headingTitle="Location" />, + width: 120, + headerAlign: 'left', + headerClassName: 'MuiDataGrid-header-override', + renderCell: (params: GridRenderCellParams) => ( + <Button + onClick={() => handleSearch(params.value as string)} + sx={{ + ...getCellStyles(theme, params.row), + justifyContent: 'flex-start', + }} + > + <Tooltip text={params.value} id="mixnode-location-text"> + <Box + sx={{ + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis', + }} + > + {params.value} + </Box> + </Tooltip> + </Button> + ), + }, + { + field: 'host', + headerName: 'Host', + renderHeader: () => <CustomColumnHeading headingTitle="Host" />, + headerClassName: 'MuiDataGrid-header-override', + width: 130, + headerAlign: 'left', + renderCell: (params: GridRenderCellParams) => ( + <MuiLink + sx={getCellStyles(theme, params.row)} + component={RRDLink} + to={`/network-components/mixnode/${params.row.mix_id}`} > {params.value} </MuiLink> diff --git a/explorer/src/routes/network-components.tsx b/explorer/src/routes/network-components.tsx index 73b17d108a..0118641b2e 100644 --- a/explorer/src/routes/network-components.tsx +++ b/explorer/src/routes/network-components.tsx @@ -2,6 +2,7 @@ import * as React from 'react'; import { Routes as ReactRouterRoutes, Route, useNavigate } from 'react-router-dom'; import { BIG_DIPPER } from '../api/constants'; import { PageGateways } from '../pages/Gateways'; +import { PageGatewayDetail } from '../pages/GatewayDetail'; import { PageMixnodeDetail } from '../pages/MixnodeDetail'; import { PageMixnodes } from '../pages/Mixnodes'; @@ -18,6 +19,7 @@ export const NetworkComponentsRoutes: React.FC = () => ( <Route path="mixnodes" element={<PageMixnodes />} /> <Route path="mixnode/:id" element={<PageMixnodeDetail />} /> <Route path="gateways" element={<PageGateways />} /> + <Route path="gateway/:id" element={<PageGatewayDetail />} /> <Route path="validators" element={<ValidatorRoute />} /> <Route path="gateways/:id" element={<h1> Specific Gateways ID</h1>} /> </ReactRouterRoutes> diff --git a/explorer/src/styles.css b/explorer/src/styles.css index cd0370f64b..e06c314f6b 100644 --- a/explorer/src/styles.css +++ b/explorer/src/styles.css @@ -4,7 +4,7 @@ the theme declaration in index.tsx or the style prop in <DataGrid /> */ -.MuiDataGrid-sortIcon, .MuiDataGrid-menuIcon { +.MuiDataGrid-sortIcon, .MuiDataGrid-menuIcon, .MuiDataGrid-iconButtonContainer{ display: none !important; } diff --git a/explorer/src/typeDefs/explorer-api.ts b/explorer/src/typeDefs/explorer-api.ts index 3463f55d98..a01b87a1a9 100644 --- a/explorer/src/typeDefs/explorer-api.ts +++ b/explorer/src/typeDefs/explorer-api.ts @@ -30,7 +30,6 @@ export interface MixNode { sphinx_key: string; identity_key: string; version: string; - profit_margin_percent: number; location: string; } @@ -70,6 +69,7 @@ export const toMixnodeStatus = (status?: MixnodeStatusWithAll): MixnodeStatus | }; export interface MixNodeResponseItem { + mix_id: number; pledge_amount: Amount; total_delegation: Amount; owner: string; @@ -85,6 +85,8 @@ export interface MixNodeResponseItem { mix_node: MixNode; avg_uptime: number; stake_saturation: number; + operating_cost: Amount; + profit_margin_percent: string; } export type MixNodeResponse = MixNodeResponseItem[]; @@ -126,12 +128,9 @@ export type GatewayResponse = GatewayResponseItem[]; export interface GatewayReportResponse { identity: string; owner: string; - most_recent_ipv4: boolean; - most_recent_ipv6: boolean; - last_hour_ipv4: number; - last_hour_ipv6: number; - last_day_ipv4: number; - last_day_ipv6: number; + most_recent: number; + last_hour: number; + last_day: number; } export type GatewayHistoryResponse = StatsResponse; @@ -215,8 +214,9 @@ export type UptimeStoryResponse = { export type MixNodeEconomicDynamicsStatsResponse = { stake_saturation: number; - active_set_inclusion_probability: 'VeryHigh' | 'High' | 'Moderate' | 'Low' | 'VeryLow'; - reserve_set_inclusion_probability: 'VeryHigh' | 'High' | 'Moderate' | 'Low' | 'VeryLow'; + // TODO: when v2 will be deployed, remove cases: VeryHigh, Moderate and VeryLow + active_set_inclusion_probability: 'High' | 'Good' | 'Low' | 'VeryLow' | 'Moderate' | 'VeryHigh'; + reserve_set_inclusion_probability: 'High' | 'Good' | 'Low'; estimated_total_node_reward: number; estimated_operator_reward: number; estimated_delegators_reward: number; diff --git a/explorer/src/utils/currency.ts b/explorer/src/utils/currency.ts index 599ff5e69f..65cdde8952 100644 --- a/explorer/src/utils/currency.ts +++ b/explorer/src/utils/currency.ts @@ -1,4 +1,5 @@ import { printableCoin } from '@nymproject/nym-validator-client'; +import Big from 'big.js'; const DENOM = process.env.CURRENCY_DENOM || 'unym'; const DENOM_STAKING = process.env.CURRENCY_STAKING_DENOM || 'unyx'; @@ -14,3 +15,38 @@ export const stakingCurrencyToString = (amount: string, denom: string = DENOM_ST amount, denom, }); + +/** + * Converts a decimal number to a pretty representation + * with fixed decimal places. + * + * @param val - a decimal number of string form + * @param dp - number of decimal places (4 by default ie. 0.0000) + * @returns A prettyfied decimal number + */ +export const toDisplay = (val: string | number | Big, dp = 4) => { + let displayValue; + try { + displayValue = Big(val).toFixed(dp); + } catch (e: any) { + console.warn(`${displayValue} not a valid decimal number: ${e}`); + } + return displayValue; +}; + +/** + * Converts a decimal number of μNYM (micro NYM) to NYM. + * + * @param unym - a decimal number of μNYM + * @param dp - number of decimal places (4 by default ie. 0.0000) + * @returns The corresponding decimal number in NYM + */ +export const unymToNym = (unym: string | number | Big, dp = 4) => { + let nym; + try { + nym = Big(unym).div(1_000_000).toFixed(dp); + } catch (e: any) { + console.warn(`${unym} not a valid decimal number: ${e}`); + } + return nym; +}; diff --git a/explorer/src/utils/index.ts b/explorer/src/utils/index.ts index a44515b10c..1065c000ae 100644 --- a/explorer/src/utils/index.ts +++ b/explorer/src/utils/index.ts @@ -45,3 +45,11 @@ export const splice = (start: number, deleteCount: number, address?: string): st } return ''; }; + +/** + * Converts a stringified percentage float (0.0-1.0) to a stringified integer (0-100). + * + * @param value - the percentage to convert + * @returns A stringified integer + */ +export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString(); diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index fa000a3381..3cf8112ff9 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -4,7 +4,10 @@ [package] name = "nym-gateway" version = "1.0.2" -authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"] +authors = [ + "Dave Hrycyszyn <futurechimp@users.noreply.github.com>", + "Jędrzej Stuczyński <andrew@nymtech.net>", +] description = "Implementation of the Nym Mixnet Gateway" edition = "2021" rust-version = "1.56" @@ -16,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" @@ -28,24 +31,33 @@ once_cell = "1.7.2" pretty_env_logger = "0.4" rand = "0.7" serde = { version = "1.0.104", features = ["derive"] } -sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } -subtle-encoding = { version = "0.5", features = ["bech32-preview"]} +sqlx = { version = "0.5", features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } +subtle-encoding = { version = "0.5", features = ["bech32-preview"] } thiserror = "1" -tokio = { version = "1.19.1", features = [ "rt-multi-thread", "net", "signal", "fs" ] } -tokio-stream = { version = "0.1.9", features = [ "fs" ] } +tokio = { version = "1.21.2", features = [ + "rt-multi-thread", + "net", + "signal", + "fs", +] } +tokio-stream = { version = "0.1.9", features = ["fs"] } tokio-tungstenite = "0.14" -tokio-util = { version = "0.7.3", features = [ "codec" ] } -url = { version = "2.2", features = [ "serde" ] } -web3 = "0.17.0" +tokio-util = { version = "0.7.3", features = ["codec"] } +url = { version = "2.2", features = ["serde"] } # internal -coconut-interface = { path = "../common/coconut-interface" , optional = true} +coconut-interface = { path = "../common/coconut-interface", optional = true } credentials = { path = "../common/credentials" } config = { path = "../common/config" } crypto = { path = "../common/crypto" } -bandwidth-claim-contract = { path = "../common/bandwidth-claim-contract" } +completions = { path = "../common/completions" } +logging = { path = "../common/logging"} gateway-requests = { path = "gateway-requests" } -gateway-client = { path = "../common/client-libs/gateway-client" } mixnet-client = { path = "../common/client-libs/mixnet-client" } mixnode-common = { path = "../common/mixnode-common" } network-defaults = { path = "../common/network-defaults" } @@ -53,14 +65,30 @@ nymsphinx = { path = "../common/nymsphinx" } pemstore = { path = "../common/pemstore" } statistics-common = { path = "../common/statistics" } validator-api-requests = { path = "../validator-api/validator-api-requests" } -validator-client = { path = "../common/client-libs/validator-client", features = ["nymd-client"] } +validator-client = { path = "../common/client-libs/validator-client", features = [ + "nymd-client", +] } version-checker = { path = "../common/version-checker" } [features] -coconut = ["coconut-interface", "gateway-requests/coconut", "gateway-client/coconut", "credentials/coconut", "validator-api-requests/coconut"] -eth = [] +coconut = [ + "coconut-interface", + "gateway-requests/coconut", + "credentials/coconut", + "validator-api-requests/coconut", +] [build-dependencies] -tokio = { version = "1.19.1", features = ["rt-multi-thread", "macros"] } -sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } -vergen = { version = "5", default-features = false, features = ["build", "git", "rustc", "cargo"] } +tokio = { version = "1.21.2", features = ["rt-multi-thread", "macros"] } +sqlx = { version = "0.5", features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } +vergen = { version = "5", default-features = false, features = [ + "build", + "git", + "rustc", + "cargo", +] } diff --git a/gateway/build.rs b/gateway/build.rs index 3d75c39198..16bdf16335 100644 --- a/gateway/build.rs +++ b/gateway/build.rs @@ -7,7 +7,7 @@ async fn main() { let out_dir = env::var("OUT_DIR").unwrap(); let database_path = format!("{}/gateway-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/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index 5206f444a8..bfa20a9273 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -82,8 +82,10 @@ impl fmt::Display for GatewayRequestsError { RequestOfInvalidSize(actual) => write!( f, - "received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {} (EXTENDED))", - actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(), PacketSize::ExtendedPacket.size() + "received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))", + actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(), + PacketSize::ExtendedPacket8.size(), PacketSize::ExtendedPacket16.size(), + PacketSize::ExtendedPacket32.size() ), MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"), MalformedEncryption => write!(f, "the received encrypted data was malformed"), diff --git a/gateway/src/commands/init.rs b/gateway/src/commands/init.rs index eee994fbf2..5def9bf642 100644 --- a/gateway/src/commands/init.rs +++ b/gateway/src/commands/init.rs @@ -52,15 +52,10 @@ pub struct Init { mnemonic: Option<String>, /// Set this gateway to work in a enabled credentials mode that would disallow clients to bypass bandwidth credential requirement - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] #[clap(long)] enabled_credentials_mode: Option<bool>, - /// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap(long)] - eth_endpoint: String, - /// Enable/disable gateway anonymized statistics that get sent to a statistics aggregator server #[clap(long)] enabled_statistics: Option<bool>, @@ -83,12 +78,9 @@ impl From<Init> for OverrideConfig { validators: init_config.validators, mnemonic: init_config.mnemonic, - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] enabled_credentials_mode: init_config.enabled_credentials_mode, - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_endpoint: Some(init_config.eth_endpoint), - enabled_statistics: init_config.enabled_statistics, statistics_service_url: init_config.statistics_service_url, } @@ -177,10 +169,8 @@ mod tests { mnemonic: None, statistics_service_url: None, enabled_statistics: None, - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] enabled_credentials_mode: None, - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_endpoint: "".to_string(), }; std::env::set_var(BECH32_PREFIX, "n"); diff --git a/gateway/src/commands/mod.rs b/gateway/src/commands/mod.rs index 821c354733..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, }; @@ -18,8 +21,6 @@ pub(crate) mod run; pub(crate) mod sign; pub(crate) mod upgrade; -#[cfg(all(not(feature = "eth"), not(feature = "coconut")))] -const DEFAULT_ETH_ENDPOINT: &str = "https://rinkeby.infura.io/v3/00000000000000000000000000000000"; #[derive(Subcommand)] pub(crate) enum Commands { /// Initialise the gateway @@ -36,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. @@ -52,20 +59,21 @@ pub(crate) struct OverrideConfig { validators: Option<String>, mnemonic: Option<String>, - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] enabled_credentials_mode: Option<bool>, - - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_endpoint: Option<String>, } 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), } } @@ -102,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(raw_validators) = args.validators { - config = config.with_custom_validator_nymd(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 { @@ -141,29 +151,10 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi ); } - #[cfg(all(not(feature = "eth"), not(feature = "coconut")))] - { - config = config.with_eth_endpoint(String::from(DEFAULT_ETH_ENDPOINT)); - } - - // We set the disabled credentials mode flag if we either compile without 'eth', or if there is a flag we - // can read from, which is when we build with 'eth' (and without 'coconut'). - if cfg!(not(feature = "eth")) { - config = config.with_disabled_credentials_mode(true); - } - - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] { if let Some(enabled_credentials_mode) = args.enabled_credentials_mode { - config = config.with_disabled_credentials_mode(enabled_credentials_mode); - } - - if let Some(raw_validators) = args.validators { - config = config.with_custom_validator_nymd(parse_validators(&raw_validators)); - } - - if let Some(eth_endpoint) = args.eth_endpoint { - config = config.with_eth_endpoint(eth_endpoint); + config = config.with_disabled_credentials_mode(!enabled_credentials_mode); } } diff --git a/gateway/src/commands/run.rs b/gateway/src/commands/run.rs index 7f810b4f51..96a12278c3 100644 --- a/gateway/src/commands/run.rs +++ b/gateway/src/commands/run.rs @@ -52,15 +52,10 @@ pub struct Run { mnemonic: Option<String>, /// Set this gateway to work in a enabled credentials mode that would disallow clients to bypass bandwidth credential requirement - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] #[clap(long)] enabled_credentials_mode: Option<bool>, - /// URL of an Ethereum full node that we want to use for getting bandwidth tokens from ERC20 tokens - #[cfg(all(feature = "eth", not(feature = "coconut")))] - #[clap(long)] - eth_endpoint: Option<String>, - /// Enable/disable gateway anonymized statistics that get sent to a statistics aggregator server #[clap(long)] enabled_statistics: Option<bool>, @@ -83,12 +78,9 @@ impl From<Run> for OverrideConfig { validators: run_config.validators, mnemonic: run_config.mnemonic, - #[cfg(all(feature = "eth", not(feature = "coconut")))] + #[cfg(feature = "coconut")] enabled_credentials_mode: run_config.enabled_credentials_mode, - #[cfg(all(feature = "eth", not(feature = "coconut")))] - eth_endpoint: run_config.eth_endpoint, - enabled_statistics: run_config.enabled_statistics, statistics_service_url: run_config.statistics_service_url, } 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/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index d3acfacf52..30812e140f 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -66,6 +66,10 @@ impl NymConfig for Config { .join("gateways") } + fn try_default_root_directory() -> Option<PathBuf> { + dirs::home_dir().map(|path| path.join(".nym").join("gateways")) + } + fn root_directory(&self) -> PathBuf { self.gateway.nym_root_directory.clone() } @@ -123,6 +127,7 @@ impl Config { self } + #[cfg(feature = "coconut")] pub fn with_disabled_credentials_mode(mut self, disabled_credentials_mode: bool) -> Self { self.gateway.disabled_credentials_mode = disabled_credentials_mode; self @@ -153,12 +158,6 @@ impl Config { self } - #[cfg(not(feature = "coconut"))] - pub fn with_eth_endpoint(mut self, eth_endpoint: String) -> Self { - self.gateway.eth_endpoint = eth_endpoint; - self - } - pub fn with_listening_address<S: Into<String>>(mut self, listening_address: S) -> Self { let listening_address_string = listening_address.into(); if let Ok(ip_addr) = listening_address_string.parse() { @@ -232,11 +231,6 @@ impl Config { self.gateway.public_sphinx_key_file.clone() } - #[cfg(not(feature = "coconut"))] - pub fn get_eth_endpoint(&self) -> String { - self.gateway.eth_endpoint.clone() - } - pub fn get_enabled_statistics(&self) -> bool { self.gateway.enabled_statistics } @@ -249,10 +243,12 @@ impl Config { self.gateway.validator_api_urls.clone() } + #[cfg(feature = "coconut")] pub fn get_validator_nymd_endpoints(&self) -> Vec<Url> { self.gateway.validator_nymd_urls.clone() } + #[cfg(feature = "coconut")] pub fn get_cosmos_mnemonic(&self) -> bip39::Mnemonic { self.gateway.cosmos_mnemonic.clone() } @@ -293,6 +289,10 @@ impl Config { self.debug.maximum_connection_buffer_size } + pub fn get_use_legacy_sphinx_framing(&self) -> bool { + self.debug.use_legacy_framed_packet_version + } + pub fn get_message_retrieval_limit(&self) -> i64 { self.debug.message_retrieval_limit } @@ -352,10 +352,6 @@ pub struct Gateway { /// Path to file containing public sphinx key. public_sphinx_key_file: PathBuf, - /// Address to an Ethereum full node. - #[cfg(not(feature = "coconut"))] - eth_endpoint: String, - /// Wheather gateway collects and sends anonymized statistics enabled_statistics: bool, @@ -419,8 +415,6 @@ impl Default for Gateway { public_identity_key_file: Default::default(), private_sphinx_key_file: Default::default(), public_sphinx_key_file: Default::default(), - #[cfg(not(feature = "coconut"))] - eth_endpoint: "".to_string(), enabled_statistics: false, statistics_service_url: Url::from_str("http://127.0.0.1").unwrap(), validator_api_urls: vec![], @@ -466,6 +460,12 @@ struct Debug { /// Number of messages from offline client that can be pulled at once from the storage. message_retrieval_limit: i64, + + /// Specifies whether the mixnode should be using the legacy framing for the sphinx packets. + // it's set to true by default. The reason for that decision is to preserve compatibility with the + // existing nodes whilst everyone else is upgrading and getting the code for handling the new field. + // It shall be disabled in the subsequent releases. + use_legacy_framed_packet_version: bool, } impl Default for Debug { @@ -478,6 +478,8 @@ impl Default for Debug { maximum_connection_buffer_size: DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, stored_messages_filename_length: DEFAULT_STORED_MESSAGE_FILENAME_LENGTH, message_retrieval_limit: DEFAULT_MESSAGE_RETRIEVAL_LIMIT, + // TODO: remember to change it in one of future releases!! + use_legacy_framed_packet_version: true, } } } diff --git a/gateway/src/config/template.rs b/gateway/src/config/template.rs index 423f50fc7a..f7082fd629 100644 --- a/gateway/src/config/template.rs +++ b/gateway/src/config/template.rs @@ -38,9 +38,6 @@ private_sphinx_key_file = '{{ gateway.private_sphinx_key_file }}' # Path to file containing public sphinx key. public_sphinx_key_file = '{{ gateway.public_sphinx_key_file }}' -# Addess to an Ethereum full node. -eth_endpoint = '{{ gateway.eth_endpoint }}' - ##### additional gateway config options ##### # Optional address announced to the directory server for the clients to connect to. diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 0651b37237..3a850333b3 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use clap::{crate_version, Parser}; +use logging::setup_logging; use network_defaults::setup_env; use once_cell::sync::OnceCell; @@ -88,27 +89,6 @@ fn long_version() -> String { ) } -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .filter_module("sled", log::LevelFilter::Warn) - .filter_module("tungstenite", log::LevelFilter::Warn) - .filter_module("tokio_tungstenite", log::LevelFilter::Warn) - .init(); -} - #[cfg(test)] mod tests { use super::*; diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index cf11305bab..3f31f238cf 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -51,18 +51,6 @@ pub(crate) enum RequestHandlingError { #[error("Nymd Error - {0}")] NymdError(#[from] validator_client::nymd::error::NymdError), - #[cfg(not(feature = "coconut"))] - #[error("Ethereum web3 error")] - Web3Error(#[from] web3::Error), - - #[cfg(not(feature = "coconut"))] - #[error("Ethereum ABI error")] - EthAbiError(#[from] web3::ethabi::Error), - - #[cfg(not(feature = "coconut"))] - #[error("Ethereum contract error")] - EthContractError(#[from] web3::contract::Error), - #[cfg(feature = "coconut")] #[error("Validator API error - {0}")] APIError(#[from] validator_client::ValidatorClientError), @@ -287,18 +275,6 @@ where String::from("gateway"), )); } - debug!("Verifying Ethereum for token burn..."); - let gateway_owner = self - .inner - .erc20_bridge - .verify_eth_events(credential.verification_key()) - .await?; - self.inner - .erc20_bridge - .verify_gateway_owner(gateway_owner, &credential.gateway_identity()) - .await?; - debug!("Claim the token on Cosmos, to make sure it's not spent twice..."); - self.inner.erc20_bridge.claim_token(&credential).await?; let bandwidth = Bandwidth::from(credential); let bandwidth_value = bandwidth.value(); diff --git a/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs b/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs index ea5b721434..e60159c564 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/eth_events.rs @@ -6,129 +6,16 @@ use bandwidth_claim_contract::msg::ExecuteMsg; use bandwidth_claim_contract::payment::LinkPaymentData; use credentials::token::bandwidth::TokenCredential; use crypto::asymmetric::identity::{PublicKey, Signature, SIGNATURE_LENGTH}; -use gateway_client::bandwidth::eth_contract; use network_defaults::{ETH_EVENT_NAME, ETH_MIN_BLOCK_DEPTH}; use std::str::FromStr; +use validator_client::nymd::traits::MixnetQueryClient; use validator_client::nymd::{AccountId, NymdClient, SigningNymdClient}; use web3::contract::tokens::Detokenize; use web3::contract::{Contract, Error}; -use web3::ethabi::Token; use web3::transports::Http; use web3::types::{BlockNumber, FilterBuilder, H256}; use web3::Web3; -pub(crate) struct ERC20Bridge { - // This is needed because web3's Contract doesn't sufficiently expose it's eth interface - web3: Web3<Http>, - contract: Contract<Http>, - nymd_client: NymdClient<SigningNymdClient>, -} - -impl ERC20Bridge { - pub fn new(eth_endpoint: String, nymd_client: NymdClient<SigningNymdClient>) -> Self { - let transport = Http::new(ð_endpoint).expect("Invalid Ethereum endpoint"); - let web3 = Web3::new(transport); - - ERC20Bridge { - contract: eth_contract(web3.clone()), - web3, - nymd_client, - } - } - - pub(crate) async fn verify_eth_events( - &self, - verification_key: PublicKey, - ) -> Result<String, RequestHandlingError> { - // It's safe to unwrap here, as we are guarded by a unit test that checks the event - // name constant against the contract abi - let event = self.contract.abi().event(ETH_EVENT_NAME).unwrap(); - let latest_block = self.web3.eth().block_number().await?; - let check_until = if cfg!(debug_assertions) { - latest_block - } else { - latest_block - ETH_MIN_BLOCK_DEPTH - }; - let filter = FilterBuilder::default() - .address(vec![self.contract.address()]) - .topics( - Some(vec![event.signature()]), - Some(vec![H256::from(verification_key.to_bytes())]), - None, - None, - ) - .from_block(BlockNumber::Earliest) - .to_block(BlockNumber::Number(check_until)) - .build(); - // Get only the first event that checks out. If the client burns more tokens with the - // same verification key, those tokens would be lost - for l in self.web3.eth().logs(filter).await? { - let log = event.parse_log(web3::ethabi::RawLog { - topics: l.topics, - data: l.data.0, - })?; - let burned_event = - Burned::from_tokens(log.params.into_iter().map(|x| x.value).collect::<Vec<_>>())?; - if burned_event.verify(verification_key) { - return Ok(burned_event.cosmos_recipient); - } - } - - Err(RequestHandlingError::InvalidBandwidthCredential( - String::from("gateway"), - )) - } - - pub(crate) async fn verify_gateway_owner( - &self, - gateway_owner: String, - gateway_identity: &PublicKey, - ) -> Result<(), RequestHandlingError> { - let owner_address = AccountId::from_str(&gateway_owner).map_err(|_| { - RequestHandlingError::InvalidBandwidthCredential(String::from("gateway")) - })?; - let gateway_bond = self - .nymd_client - .owns_gateway(&owner_address) - .await? - .ok_or_else(|| { - RequestHandlingError::InvalidBandwidthCredential(String::from("gateway")) - })?; - if gateway_bond.gateway.identity_key == gateway_identity.to_base58_string() { - Ok(()) - } else { - Err(RequestHandlingError::InvalidBandwidthCredential( - String::from("gateway"), - )) - } - } - - pub(crate) async fn claim_token( - &self, - credential: &TokenCredential, - ) -> Result<(), RequestHandlingError> { - let bandwidth_claim_contract_address = self.nymd_client.bandwidth_claim_contract_address(); - let req = ExecuteMsg::LinkPayment { - data: LinkPaymentData::new( - credential.verification_key().to_bytes(), - credential.gateway_identity().to_bytes(), - credential.bandwidth(), - credential.signature_bytes(), - ), - }; - self.nymd_client - .execute( - bandwidth_claim_contract_address, - &req, - Default::default(), - "Linking payment", - vec![], - ) - .await?; - Ok(()) - } -} - #[derive(Debug)] pub struct Burned { /// The bandwidth bought by the client diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 8b33a807d2..abe2f30a87 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -29,9 +29,6 @@ use thiserror::Error; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError}; -#[cfg(not(feature = "coconut"))] -use crate::node::client_handling::websocket::connection_handler::eth_events::ERC20Bridge; - #[derive(Debug, Error)] enum InitialAuthenticationError { #[error("Internal gateway storage error")] @@ -76,8 +73,6 @@ pub(crate) struct FreshHandler<R, S, St> { pub(crate) socket_connection: SocketStream<S>, pub(crate) storage: St, - #[cfg(not(feature = "coconut"))] - pub(crate) erc20_bridge: Arc<ERC20Bridge>, #[cfg(feature = "coconut")] pub(crate) coconut_verifier: Arc<CoconutVerifier>, } @@ -101,7 +96,6 @@ where storage: St, active_clients_store: ActiveClientsStore, #[cfg(feature = "coconut")] coconut_verifier: Arc<CoconutVerifier>, - #[cfg(not(feature = "coconut"))] erc20_bridge: Arc<ERC20Bridge>, ) -> Self { FreshHandler { rng, @@ -113,8 +107,6 @@ where storage, #[cfg(feature = "coconut")] coconut_verifier, - #[cfg(not(feature = "coconut"))] - erc20_bridge, } } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs index 9172f9288f..27b8366563 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/mod.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/mod.rs @@ -17,8 +17,6 @@ pub(crate) use self::fresh::FreshHandler; mod authenticated; #[cfg(feature = "coconut")] pub(crate) mod coconut; -#[cfg(not(feature = "coconut"))] -pub(crate) mod eth_events; mod fresh; //// TODO: note for my future self to consider the following idea: diff --git a/gateway/src/node/client_handling/websocket/listener.rs b/gateway/src/node/client_handling/websocket/listener.rs index 08bf22267a..7e3fdbc5d9 100644 --- a/gateway/src/node/client_handling/websocket/listener.rs +++ b/gateway/src/node/client_handling/websocket/listener.rs @@ -16,9 +16,6 @@ use tokio::task::JoinHandle; #[cfg(feature = "coconut")] use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier; -#[cfg(not(feature = "coconut"))] -use crate::node::client_handling::websocket::connection_handler::eth_events::ERC20Bridge; - pub(crate) struct Listener { address: SocketAddr, local_identity: Arc<identity::KeyPair>, @@ -26,8 +23,6 @@ pub(crate) struct Listener { #[cfg(feature = "coconut")] pub(crate) coconut_verifier: Arc<CoconutVerifier>, - #[cfg(not(feature = "coconut"))] - erc20_bridge: Arc<ERC20Bridge>, } impl Listener { @@ -36,7 +31,6 @@ impl Listener { local_identity: Arc<identity::KeyPair>, disabled_credentials_mode: bool, #[cfg(feature = "coconut")] coconut_verifier: Arc<CoconutVerifier>, - #[cfg(not(feature = "coconut"))] erc20_bridge: ERC20Bridge, ) -> Self { Listener { address, @@ -44,8 +38,6 @@ impl Listener { disabled_credentials_mode, #[cfg(feature = "coconut")] coconut_verifier, - #[cfg(not(feature = "coconut"))] - erc20_bridge: Arc::new(erc20_bridge), } } @@ -84,8 +76,6 @@ impl Listener { active_clients_store.clone(), #[cfg(feature = "coconut")] Arc::clone(&self.coconut_verifier), - #[cfg(not(feature = "coconut"))] - Arc::clone(&self.erc20_bridge), ); tokio::spawn(async move { handle.start_handling().await }); } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 379aae5fe2..fd31af1af7 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -12,6 +12,7 @@ use crate::node::storage::Storage; use crypto::asymmetric::{encryption, identity}; use log::*; use mixnet_client::forwarder::{MixForwardingSender, PacketForwarder}; +#[cfg(feature = "coconut")] use network_defaults::NymNetworkDetails; use rand::seq::SliceRandom; use rand::thread_rng; @@ -23,10 +24,9 @@ use std::sync::Arc; use crate::config::persistence::pathfinder::GatewayPathfinder; #[cfg(feature = "coconut")] use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier; -#[cfg(not(feature = "coconut"))] -use crate::node::client_handling::websocket::connection_handler::eth_events::ERC20Bridge; #[cfg(feature = "coconut")] use credentials::obtain_aggregate_verification_key; +#[cfg(feature = "coconut")] use validator_client::nymd; use self::storage::PersistentStorage; @@ -181,7 +181,6 @@ where forwarding_channel: MixForwardingSender, active_clients_store: ActiveClientsStore, #[cfg(feature = "coconut")] coconut_verifier: Arc<CoconutVerifier>, - #[cfg(not(feature = "coconut"))] erc20_bridge: ERC20Bridge, ) { info!("Starting client [web]socket listener..."); @@ -196,8 +195,6 @@ where self.config.get_disabled_credentials_mode(), #[cfg(feature = "coconut")] coconut_verifier, - #[cfg(not(feature = "coconut"))] - erc20_bridge, ) .start( forwarding_channel, @@ -214,6 +211,7 @@ where self.config.get_packet_forwarding_maximum_backoff(), self.config.get_initial_connection_timeout(), self.config.get_maximum_connection_buffer_size(), + self.config.get_use_legacy_sphinx_framing(), ); tokio::spawn(async move { packet_forwarder.run().await }); @@ -241,6 +239,7 @@ where validator_client::ApiClient::new(validator_api.clone()) } + #[cfg(feature = "coconut")] fn random_nymd_client( &self, ) -> validator_client::nymd::NymdClient<validator_client::nymd::SigningNymdClient> { @@ -311,7 +310,7 @@ where obtain_aggregate_verification_key(&self.config.get_validator_api_endpoints()) .await .expect("failed to contact validators to obtain their verification keys"); - + #[cfg(feature = "coconut")] let nymd_client = self.random_nymd_client(); #[cfg(feature = "coconut")] let coconut_verifier = CoconutVerifier::new( @@ -322,9 +321,6 @@ where ) .expect("Could not create coconut verifier"); - #[cfg(not(feature = "coconut"))] - let erc20_bridge = ERC20Bridge::new(self.config.get_eth_endpoint(), nymd_client); - let mix_forwarding_channel = self.start_packet_forwarder(); let active_clients_store = ActiveClientsStore::new(); @@ -351,8 +347,6 @@ where active_clients_store, #[cfg(feature = "coconut")] Arc::new(coconut_verifier), - #[cfg(not(feature = "coconut"))] - erc20_bridge, ); info!("Finished nym gateway startup procedure - it should now be able to receive mix and client traffic!"); 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<OrderSignature, NymdError> { + 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..2fbe6bd5c6 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,8 @@ url = { version = "2.2", features = ["serde"] } ## internal config = { path="../common/config" } crypto = { path="../common/crypto" } +completions = { path="../common/completions" } +logging = { path="../common/logging" } mixnet-client = { path="../common/client-libs/mixnet-client" } mixnode-common = { path="../common/mixnode-common" } nonexhaustive-delayqueue = { path="../common/nonexhaustive-delayqueue" } @@ -51,7 +53,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/mixnode/src/config/mod.rs b/mixnode/src/config/mod.rs index 14279ccdc2..23a80a5127 100644 --- a/mixnode/src/config/mod.rs +++ b/mixnode/src/config/mod.rs @@ -96,6 +96,10 @@ impl NymConfig for Config { .join("mixnodes") } + fn try_default_root_directory() -> Option<PathBuf> { + dirs::home_dir().map(|path| path.join(".nym").join("mixnodes")) + } + fn root_directory(&self) -> PathBuf { self.mixnode.nym_root_directory.clone() } @@ -276,6 +280,10 @@ impl Config { self.debug.maximum_connection_buffer_size } + pub fn get_use_legacy_sphinx_framing(&self) -> bool { + self.debug.use_legacy_framed_packet_version + } + pub fn get_version(&self) -> &str { &self.mixnode.version } @@ -481,6 +489,12 @@ struct Debug { /// Maximum number of packets that can be stored waiting to get sent to a particular connection. maximum_connection_buffer_size: usize, + + /// Specifies whether the mixnode should be using the legacy framing for the sphinx packets. + // it's set to true by default. The reason for that decision is to preserve compatibility with the + // existing nodes whilst everyone else is upgrading and getting the code for handling the new field. + // It shall be disabled in the subsequent releases. + use_legacy_framed_packet_version: bool, } impl Default for Debug { @@ -492,6 +506,8 @@ impl Default for Debug { packet_forwarding_maximum_backoff: DEFAULT_PACKET_FORWARDING_MAXIMUM_BACKOFF, initial_connection_timeout: DEFAULT_INITIAL_CONNECTION_TIMEOUT, maximum_connection_buffer_size: DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE, + // TODO: remember to change it in one of future releases!! + use_legacy_framed_packet_version: true, } } } diff --git a/mixnode/src/main.rs b/mixnode/src/main.rs index 2e30e00976..d52be0c120 100644 --- a/mixnode/src/main.rs +++ b/mixnode/src/main.rs @@ -7,6 +7,7 @@ extern crate rocket; use ::config::defaults::setup_env; use clap::{crate_version, Parser}; use lazy_static::lazy_static; +use logging::setup_logging; mod commands; mod config; @@ -90,24 +91,6 @@ fn long_version() -> String { ) } -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .init(); -} - #[cfg(test)] mod tests { use super::*; diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index f2d3635139..0bca9b31b0 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -26,7 +26,7 @@ use rand::thread_rng; use std::net::SocketAddr; use std::process; use std::sync::Arc; -use task::{ShutdownListener, ShutdownNotifier}; +use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; use version_checker::parse_version; mod http; @@ -200,6 +200,7 @@ impl MixNode { self.config.get_packet_forwarding_maximum_backoff(), self.config.get_initial_connection_timeout(), self.config.get_maximum_connection_buffer_size(), + self.config.get_use_legacy_sphinx_framing(), ); let mut packet_forwarder = DelayForwarder::new( @@ -265,6 +266,8 @@ impl MixNode { // TODO: ask DH whether this function still makes sense in ^0.10 async fn check_if_same_ip_node_exists(&mut self) -> Option<String> { + // TODO: if anything, this should be getting data directly from the contract + // as opposed to the validator API let validator_client = self.random_api_client(); let existing_nodes = match validator_client.get_cached_mixnodes().await { Ok(nodes) => nodes, @@ -281,8 +284,8 @@ impl MixNode { existing_nodes .iter() - .find(|node| node.mix_node.host == our_host) - .map(|node| node.mix_node.identity_key.clone()) + .find(|node| node.bond_information.mix_node.host == our_host) + .map(|node| node.bond_information.mix_node.identity_key.clone()) } async fn wait_for_interrupt(&self, mut shutdown: ShutdownNotifier) { @@ -334,31 +337,3 @@ impl MixNode { self.wait_for_interrupt(shutdown).await } } - -#[cfg(unix)] -async fn wait_for_signal() { - use tokio::signal::unix::{signal, SignalKind}; - let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM channel"); - let mut sigquit = signal(SignalKind::quit()).expect("Failed to setup SIGQUIT channel"); - - tokio::select! { - _ = tokio::signal::ctrl_c() => { - log::info!("Received SIGINT"); - }, - _ = sigterm.recv() => { - log::info!("Received SIGTERM"); - } - _ = sigquit.recv() => { - log::info!("Received SIGQUIT"); - } - } -} - -#[cfg(not(unix))] -async fn wait_for_signal() { - tokio::select! { - _ = tokio::signal::ctrl_c() => { - log::info!("Received SIGINT"); - }, - } -} diff --git a/nym-connect/CHANGELOG.md b/nym-connect/CHANGELOG.md new file mode 100644 index 0000000000..4fc097fc77 --- /dev/null +++ b/nym-connect/CHANGELOG.md @@ -0,0 +1,25 @@ +## [nym-connect-v1.0.2](https://github.com/nymtech/nym/tree/nym-connect-v1.0.2) (2022-08-18) + +### Changed + +- nym-connect: "load balance" the service providers by picking a random Service Provider for each Service and storing in local storage so it remains sticky for the user ([#1540]) +- nym-connect: the ServiceProviderSelector only displays the available Services, and picks a random Service Provider for Services the user has never used before ([#1540]) +- nym-connect: add `local-forage` for storing user settings ([#1540]) + +[#1540]: https://github.com/nymtech/nym/pull/1540 + + +## [nym-connect-v1.0.1](https://github.com/nymtech/nym/tree/nym-connect-v1.0.1) (2022-07-22) + +### Added + +- nym-connect: initial proof-of-concept of a UI around the socks5 client was added +- nym-connect: add ability to select network requester and gateway ([#1427]) +- nym-connect: add ability to export gateway keys as JSON +- nym-connect: add auto updater + +### Changed + +- nym-connect: reuse config id instead of creating a new id on each connection + +[#1427]: https://github.com/nymtech/nym/pull/1427 \ No newline at end of file diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index a94e7ab621..2674eae1fa 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -57,28 +57,37 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" dependencies = [ "memchr", ] [[package]] name = "alloc-no-stdlib" -version = "2.0.3" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" [[package]] name = "alloc-stdlib" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -90,9 +99,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.58" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" [[package]] name = "arrayref" @@ -114,9 +123,9 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "async-trait" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" dependencies = [ "proc-macro2", "quote", @@ -158,20 +167,18 @@ dependencies = [ [[package]] name = "attohttpc" -version = "0.19.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262c3f7f5d61249d8c00e5546e2685cd15ebeeb1bc0f3cc5449350a1cb07319e" +checksum = "1fcf00bc6d5abb29b5f97e3c61a90b6d3caa12f3faf897d4a3e3607c050a35a7" dependencies = [ "flate2", "http", "log", "native-tls", - "openssl", "serde", "serde_json", "serde_urlencoded", "url", - "wildmatch", ] [[package]] @@ -200,12 +207,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "az" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f771a5d1f5503f7f4279a30f3643d3421ba149848b89ecaaec0ea2acf04a5ac4" - [[package]] name = "base16ct" version = "0.1.1" @@ -220,9 +221,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64ct" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdca834647821e0b13d9539a8634eb62d3501b6b6c2cec1722786ee6671b851" +checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" [[package]] name = "bip32" @@ -235,7 +236,7 @@ dependencies = [ "k256", "once_cell", "pbkdf2", - "rand_core 0.6.3", + "rand_core 0.6.4", "ripemd160", "sha2 0.9.9", "subtle 2.4.1", @@ -267,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" @@ -302,7 +291,7 @@ dependencies = [ "cc", "cfg-if", "constant_time_eq", - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -311,44 +300,23 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" -[[package]] -name = "block-buffer" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" -dependencies = [ - "block-padding 0.1.5", - "byte-tools", - "byteorder", - "generic-array 0.12.4", -] - [[package]] name = "block-buffer" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ - "block-padding 0.2.1", - "generic-array 0.14.5", + "block-padding", + "generic-array 0.14.6", ] [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ - "generic-array 0.14.5", -] - -[[package]] -name = "block-padding" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" -dependencies = [ - "byte-tools", + "generic-array 0.14.6", ] [[package]] @@ -367,7 +335,7 @@ dependencies = [ "ff 0.10.1", "group 0.10.0", "pairing", - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle 2.4.1", ] @@ -412,15 +380,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" - -[[package]] -name = "byte-slice-cast" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c5fdd0166095e1d463fc6cc01aa8ce547ad77a4e84d42eb6762b084e28067e" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" [[package]] name = "byte-tools" @@ -430,9 +392,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "bytemuck" -version = "1.10.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53dfa917ec274df8ed3c572698f381a24eef2efba9492d797301b72b6db408a" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" [[package]] name = "byteorder" @@ -442,9 +404,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" [[package]] name = "cairo-rs" @@ -472,9 +434,9 @@ dependencies = [ [[package]] name = "cargo_toml" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5809dd3e6444651fd1cdd3dbec71eca438c439a0fcc8081674a14da0afe50185" +checksum = "a4419e9adae9fd7e231b60d50467481bf8181ddeef6ed54683b23ae925c74c9c" dependencies = [ "serde", "serde_derive", @@ -542,14 +504,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" dependencies = [ - "libc", + "iana-time-zone", + "js-sys", "num-integer", "num-traits", "time 0.1.44", + "wasm-bindgen", "winapi", ] @@ -559,7 +523,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" dependencies = [ - "generic-array 0.14.5", + "generic-array 0.14.6", ] [[package]] @@ -574,9 +538,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.8" +version = "3.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" +checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" dependencies = [ "atty", "bitflags", @@ -590,10 +554,29 @@ dependencies = [ ] [[package]] -name = "clap_derive" -version = "3.2.7" +name = "clap_complete" +version = "3.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck 0.4.0", "proc-macro-error", @@ -621,6 +604,7 @@ dependencies = [ "futures", "gateway-client", "gateway-requests", + "gloo-timers", "humantime-serde", "log", "nonexhaustive-delayqueue", @@ -629,10 +613,16 @@ dependencies = [ "rand 0.7.3", "serde", "sled", + "tap", + "task", + "thiserror", "tokio", "topology", "url", "validator-client", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-timer", ] [[package]] @@ -715,21 +705,29 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.4" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" dependencies = [ "bytes", "memchr", ] +[[package]] +name = "completions" +version = "0.1.0" +dependencies = [ + "clap", + "clap_complete", + "clap_complete_fig", +] + [[package]] name = "config" version = "0.1.0" dependencies = [ "cfg-if", "handlebars", - "humantime-serde", "log", "network-defaults", "serde", @@ -754,6 +752,9 @@ name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", + "serde", + "thiserror", ] [[package]] @@ -826,7 +827,7 @@ dependencies = [ "k256", "prost", "prost-types", - "rand_core 0.6.3", + "rand_core 0.6.4", "serde", "serde_json", "subtle-encoding", @@ -844,15 +845,15 @@ dependencies = [ "digest 0.9.0", "ed25519-zebra", "k256", - "rand_core 0.6.3", + "rand_core 0.6.4", "thiserror", ] [[package]] name = "cosmwasm-derive" -version = "1.0.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b36e527620a2a3e00e46b6e731ab6c9b68d11069c986f7d7be8eba79ef081a4" +checksum = "0faf3a02389f78d6173b7e680751205015d5406f8abbaa9aa36fd216adc9f10d" dependencies = [ "syn", ] @@ -876,9 +877,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] @@ -913,7 +914,6 @@ version = "0.1.0" dependencies = [ "async-trait", "log", - "nymcoconut", "sqlx", "thiserror", "tokio", @@ -934,9 +934,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" dependencies = [ "cfg-if", "crossbeam-utils", @@ -944,9 +944,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" +checksum = "045ebe27666471bb549370b4b0b3e51b07f56325befa4284db65fc89c02511b1" dependencies = [ "autocfg 1.1.0", "cfg-if", @@ -958,9 +958,9 @@ dependencies = [ [[package]] name = "crossbeam-queue" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" +checksum = "1cd42583b04998a5363558e5f9291ee5a5ff6b49944332103f251e7479a82aa7" dependencies = [ "cfg-if", "crossbeam-utils", @@ -968,9 +968,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" dependencies = [ "cfg-if", "once_cell", @@ -990,11 +990,10 @@ dependencies = [ "blake3", "bs58", "cipher 0.4.3", - "config", "ctr 0.9.1", - "digest 0.10.3", + "digest 0.10.5", "ed25519-dalek", - "generic-array 0.14.5", + "generic-array 0.14.6", "hkdf 0.12.3", "hmac 0.12.1", "nymsphinx-types", @@ -1010,19 +1009,19 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" dependencies = [ - "generic-array 0.14.5", - "rand_core 0.6.3", + "generic-array 0.14.6", + "rand_core 0.6.4", "subtle 2.4.1", "zeroize", ] [[package]] name = "crypto-common" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5999502d32b9c48d492abe66392408144895020ec4709e549e840799f3bb74c0" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ - "generic-array 0.14.5", + "generic-array 0.14.6", "typenum", ] @@ -1042,7 +1041,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ - "generic-array 0.14.5", + "generic-array 0.14.6", "subtle 2.4.1", ] @@ -1084,9 +1083,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" +checksum = "cdffe87e1d521a10f9696f833fe502293ea446d7f256c06128293a4119bdf4cb" dependencies = [ "quote", "syn", @@ -1221,15 +1220,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "deflate" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c86f7e25f518f4b81808a2cf1c50996a61f5c2eb394b2393bd87f2a4780a432f" -dependencies = [ - "adler32", -] - [[package]] name = "der" version = "0.5.1" @@ -1267,16 +1257,16 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" dependencies = [ - "generic-array 0.14.5", + "generic-array 0.14.6", ] [[package]] name = "digest" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ - "block-buffer 0.10.2", + "block-buffer 0.10.3", "crypto-common", "subtle 2.4.1", ] @@ -1357,9 +1347,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140206b78fb2bc3edbcfc9b5ccbd0b30699cfe8d348b8b31b330e47df5291a5a" +checksum = "4f94fa09c2aeea5b8839e414b7b841bf429fd25b9c522116ac97ee87856d88b2" [[package]] name = "ecdsa" @@ -1404,7 +1394,7 @@ checksum = "403ef3e961ab98f0ba902771d29f842058578bb1ce7e3c59dad5a6a93e784c69" dependencies = [ "curve25519-dalek", "hex", - "rand_core 0.6.3", + "rand_core 0.6.4", "serde", "sha2 0.9.9", "thiserror", @@ -1413,9 +1403,9 @@ dependencies = [ [[package]] name = "either" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "elliptic-curve" @@ -1427,27 +1417,14 @@ dependencies = [ "crypto-bigint", "der", "ff 0.11.1", - "generic-array 0.14.5", + "generic-array 0.14.6", "group 0.11.0", - "rand_core 0.6.3", + "rand_core 0.6.4", "sec1", "subtle 2.4.1", "zeroize", ] -[[package]] -name = "embed-resource" -version = "1.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc24ff8d764818e9ab17963b0593c535f077a513f565e75e4352d758bc4d8c0" -dependencies = [ - "cc", - "rustc_version 0.4.0", - "toml", - "vswhom", - "winreg", -] - [[package]] name = "embed_plist" version = "1.2.2" @@ -1496,54 +1473,11 @@ 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.2" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" [[package]] name = "execute" @@ -1563,17 +1497,11 @@ dependencies = [ "once_cell", ] -[[package]] -name = "fake-simd" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" - [[package]] name = "fastrand" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] @@ -1584,7 +1512,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" dependencies = [ - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle 2.4.1", ] @@ -1594,7 +1522,7 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "131655483be284720a17d74ff97592b8e76576dc25563148601df2d7c9080924" dependencies = [ - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle 2.4.1", ] @@ -1629,31 +1557,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "fixed" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93db1ca4ef0496cc54f43660fcb423bc16ced939b8fdd9749e7abcd5674f1803" -dependencies = [ - "az", - "bytemuck", - "half", - "serde", - "typenum", -] - -[[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" @@ -1676,29 +1579,14 @@ dependencies = [ [[package]] name = "flume" -version = "0.10.13" +version = "0.10.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ceeb589a3157cac0ab8cc585feb749bd2cea5cb55a6ee802ad72d9fd38303da" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" dependencies = [ "futures-core", "futures-sink", "pin-project", - "spin 0.9.3", -] - -[[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", + "spin 0.9.4", ] [[package]] @@ -1724,11 +1612,10 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] @@ -1754,12 +1641,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" @@ -1772,9 +1653,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" dependencies = [ "futures-channel", "futures-core", @@ -1787,9 +1668,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" dependencies = [ "futures-core", "futures-sink", @@ -1797,15 +1678,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" dependencies = [ "futures-core", "futures-task", @@ -1825,30 +1706,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" - -[[package]] -name = "futures-lite" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" dependencies = [ "proc-macro2", "quote", @@ -1857,27 +1723,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" - -[[package]] -name = "futures-timer" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" dependencies = [ "futures-channel", "futures-core", @@ -1908,26 +1768,25 @@ dependencies = [ "credential-storage", "credentials", "crypto", - "fluvio-wasm-timer", "futures", "gateway-requests", "getrandom 0.2.7", - "json", "log", "network-defaults", "nymsphinx", "pemstore", "rand 0.7.3", - "secp256k1", + "task", "thiserror", "tokio", + "tokio-stream", "tokio-tungstenite", "tungstenite", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-timer", "wasm-utils", - "web3", ] [[package]] @@ -2022,15 +1881,15 @@ dependencies = [ [[package]] name = "generator" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1d9279ca822891c1a4dae06d185612cf8fc6acfe5dff37781b41297811b12ee" +checksum = "cc184cace1cea8335047a471cc1da80f18acf8a76f3bab2028d499e328948ec7" dependencies = [ "cc", "libc", "log", "rustversion", - "winapi", + "windows 0.32.0", ] [[package]] @@ -2044,9 +1903,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", @@ -2122,9 +1981,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.14.4" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c" +checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1" dependencies = [ "bitflags", "libc", @@ -2197,6 +2056,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" @@ -2216,7 +2087,7 @@ checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" dependencies = [ "byteorder", "ff 0.10.1", - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle 2.4.1", ] @@ -2227,7 +2098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" dependencies = [ "ff 0.11.1", - "rand_core 0.6.3", + "rand_core 0.6.4", "subtle 2.4.1", ] @@ -2288,9 +2159,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" dependencies = [ "bytes", "fnv", @@ -2301,16 +2172,10 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util 0.7.3", + "tokio-util", "tracing", ] -[[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - [[package]] name = "handlebars" version = "3.5.5" @@ -2336,9 +2201,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashlink" @@ -2351,9 +2216,9 @@ dependencies = [ [[package]] name = "headers" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ "base64", "bitflags", @@ -2362,7 +2227,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1 0.10.0", + "sha1", ] [[package]] @@ -2448,7 +2313,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -2473,7 +2338,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa 1.0.2", + "itoa 1.0.3", ] [[package]] @@ -2495,9 +2360,9 @@ checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" [[package]] name = "httparse" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -2545,7 +2410,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.2", + "itoa 1.0.3", "pin-project-lite", "socket2", "tokio", @@ -2604,6 +2469,20 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "237a0714f28b1ee39ccec0770ccb544eb02c9ef2c82bb096230eefcffa6468b0" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "once_cell", + "wasm-bindgen", + "winapi", +] + [[package]] name = "ico" version = "0.1.0" @@ -2651,56 +2530,17 @@ dependencies = [ [[package]] name = "image" -version = "0.24.2" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28edd9d7bc256be2502e325ac0628bde30b7001b9b52e0abe31a1a9dc2701212" +checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964" dependencies = [ "bytemuck", "byteorder", "color_quant", - "num-iter", "num-rational", "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" @@ -2714,7 +2554,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg 1.1.0", - "hashbrown 0.12.1", + "hashbrown 0.12.3", "serde", ] @@ -2742,7 +2582,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ - "generic-array 0.14.5", + "generic-array 0.14.6", ] [[package]] @@ -2774,9 +2614,9 @@ checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" [[package]] name = "itertools" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +checksum = "d8bf247779e67a9082a4790b45e71ac7cfd1321331a5c856a74a9faebdab78d0" dependencies = [ "either", ] @@ -2789,9 +2629,9 @@ checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "javascriptcore-rs" @@ -2816,20 +2656,6 @@ dependencies = [ "system-deps 5.0.0", ] -[[package]] -name = "jni" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", -] - [[package]] name = "jni" version = "0.19.0" @@ -2868,12 +2694,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" - [[package]] name = "json-patch" version = "0.2.6" @@ -2885,21 +2705,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" @@ -2970,15 +2775,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.126" +version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" [[package]] name = "libgit2-sys" -version = "0.13.4+1.4.2" +version = "0.14.0+1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fa6563431ede25f5cc7f6d803c6afbc1c5d3ad3d4925d12c882bf2b526f5d1" +checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b" dependencies = [ "cc", "libc", @@ -2998,9 +2803,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" +checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" [[package]] name = "libsqlite3-sys" @@ -3048,9 +2853,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" dependencies = [ "autocfg 1.1.0", "scopeguard", @@ -3065,6 +2870,14 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "logging" +version = "0.1.0" +dependencies = [ + "log", + "pretty_env_logger", +] + [[package]] name = "loom" version = "0.5.6" @@ -3095,12 +2908,6 @@ dependencies = [ "libc", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - [[package]] name = "markup5ever" version = "0.10.1" @@ -3171,9 +2978,9 @@ checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881" [[package]] name = "miniz_oxide" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] @@ -3194,17 +3001,17 @@ dependencies = [ name = "mixnet-contract-common" version = "0.1.0" dependencies = [ - "az", + "bs58", "contracts-common", "cosmwasm-std", - "fixed", + "humantime-serde", "log", "schemars", "serde", + "serde_json", "serde_repr", "thiserror", - "time 0.3.11", - "ts-rs", + "time 0.3.14", ] [[package]] @@ -3304,9 +3111,12 @@ dependencies = [ name = "nonexhaustive-delayqueue" version = "0.1.0" dependencies = [ + "futures-core", + "slab", "tokio", "tokio-stream", - "tokio-util 0.7.3", + "tokio-util", + "wasm-timer", ] [[package]] @@ -3404,7 +3214,7 @@ dependencies = [ [[package]] name = "nym-connect" -version = "1.0.1" +version = "1.0.2" dependencies = [ "bip39", "client-core", @@ -3414,6 +3224,7 @@ dependencies = [ "fix-path-env", "futures", "log", + "logging", "nym-socks5-client", "pretty_env_logger", "rand 0.8.5", @@ -3436,10 +3247,11 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.0.1" +version = "1.0.2" dependencies = [ "clap", "client-core", + "completions", "config", "credential-storage", "crypto", @@ -3448,6 +3260,7 @@ dependencies = [ "gateway-client", "gateway-requests", "log", + "logging", "network-defaults", "nymsphinx", "ordered-buffer", @@ -3459,6 +3272,7 @@ dependencies = [ "serde", "snafu", "socks5-requests", + "task", "tokio", "topology", "url", @@ -3583,7 +3397,7 @@ dependencies = [ "bytes", "nymsphinx-params", "nymsphinx-types", - "tokio-util 0.7.3", + "tokio-util", ] [[package]] @@ -3642,9 +3456,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" [[package]] name = "opaque-debug" @@ -3660,9 +3474,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "open" -version = "3.0.1" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360bcc8316bf6363aa3954c3ccc4de8add167b087e0259190a043c9514f910fe" +checksum = "b4a3100141f1733ea40b53381b0ae3117330735ef22309a190ac57b9576ea716" dependencies = [ "pathdiff", "windows-sys", @@ -3670,9 +3484,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.40" +version = "0.10.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" +checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" dependencies = [ "bitflags", "cfg-if", @@ -3702,9 +3516,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.74" +version = "0.9.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" +checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" dependencies = [ "autocfg 1.1.0", "cc", @@ -3722,9 +3536,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.1.0" +version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" [[package]] name = "pairing" @@ -3760,38 +3574,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" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" - [[package]] name = "parking_lot" version = "0.11.2" @@ -3842,9 +3624,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" [[package]] name = "pathdiff" @@ -3908,24 +3690,25 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pest" -version = "2.1.3" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +checksum = "cb779fcf4bb850fbbb0edc96ff6cf34fd90c4b1a112ce042653280d9a7364048" dependencies = [ + "thiserror", "ucd-trie", ] [[package]] name = "pest_derive" -version = "2.1.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +checksum = "502b62a6d0245378b04ffe0a7fb4f4419a4815fce813bd8a0ec89a56e07d67b1" dependencies = [ "pest", "pest_generator", @@ -3933,9 +3716,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.1.3" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +checksum = "451e629bf49b750254da26132f1a5a9d11fd8a95a3df51d15c4abd1ba154cb6c" dependencies = [ "pest", "pest_meta", @@ -3946,13 +3729,13 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.1.3" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +checksum = "bcec162c71c45e269dfc3fc2916eaeb97feab22993a21bcce4721d08cd7801a6" dependencies = [ - "maplit", + "once_cell", "pest", - "sha-1 0.8.2", + "sha1", ] [[package]] @@ -4055,18 +3838,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260" +checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" +checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ "proc-macro2", "quote", @@ -4112,7 +3895,7 @@ dependencies = [ "indexmap", "line-wrap", "serde", - "time 0.3.11", + "time 0.3.14", "xml-rs", ] @@ -4123,20 +3906,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0b0cabbbd20c2d7f06dbf015e06aad59b6ca3d9ed14848783e98af9aaf19925" dependencies = [ "bitflags", - "deflate 0.7.20", + "deflate", "inflate", "num-iter", ] [[package]] name = "png" -version = "0.17.5" +version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc38c0ad57efb786dd57b9864e5b18bae478c00c824dc55a38bbc9da95dde3ba" +checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c" dependencies = [ "bitflags", "crc32fast", - "deflate 1.0.0", + "flate2", "miniz_oxide", ] @@ -4162,25 +3945,13 @@ 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" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" dependencies = [ + "once_cell", "thiserror", "toml", ] @@ -4217,9 +3988,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" dependencies = [ "unicode-ident", ] @@ -4266,8 +4037,9 @@ dependencies = [ "log", "ordered-buffer", "socks5-requests", + "task", "tokio", - "tokio-util 0.7.3", + "tokio-util", ] [[package]] @@ -4284,19 +4056,13 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" 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" @@ -4338,7 +4104,7 @@ checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", "rand_chacha 0.3.1", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -4368,7 +4134,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -4397,9 +4163,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom 0.2.7", ] @@ -4496,9 +4262,9 @@ dependencies = [ [[package]] name = "raw-window-handle" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b800beb9b6e7d2df1fe337c9e3d04e3af22a124460fb4c30fcc22c9117cefb41" +checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a" dependencies = [ "cty", ] @@ -4514,9 +4280,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] @@ -4617,13 +4383,12 @@ dependencies = [ [[package]] name = "rfd" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f121348fd3b9035ed11be1f028e8944263c30641f8c5deacf57a4320782fb402" +checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" dependencies = [ "block", "dispatch", - "embed-resource", "glib-sys", "gobject-sys", "gtk-sys", @@ -4637,7 +4402,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows", + "windows 0.37.0", ] [[package]] @@ -4666,22 +4431,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" @@ -4697,7 +4446,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver 1.0.12", + "semver 1.0.14", ] [[package]] @@ -4727,15 +4476,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0a5f7c728f5d284929a1cccb5bc19884422bfe6ef4d6c409da2c41838983fcf" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "safemem" @@ -4816,35 +4565,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08da66b8b0965a5555b6bd6639e68ccba85e1e2506f5fbb089e93f8a04e1a2d1" dependencies = [ "der", - "generic-array 0.14.5", + "generic-array 0.14.6", "pkcs8", "subtle 2.4.1", "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" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" dependencies = [ "bitflags", "core-foundation", @@ -4894,9 +4625,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" dependencies = [ "serde", ] @@ -4912,9 +4643,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.138" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" dependencies = [ "serde_derive", ] @@ -4930,18 +4661,18 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.6" +version = "0.11.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212e73464ebcde48d723aa02eb270ba62eff38a9b732df31f33f1b4e145f3a54" +checksum = "cfc50e8183eeeb6178dcb167ae34a8051d63535023ae38b5d8d12beae193d37b" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.138" +version = "1.0.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" dependencies = [ "proc-macro2", "quote", @@ -4961,20 +4692,20 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.82" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ - "itoa 1.0.2", + "itoa 1.0.3", "ryu", "serde", ] [[package]] name = "serde_repr" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed" +checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca" dependencies = [ "proc-macro2", "quote", @@ -4988,7 +4719,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.2", + "itoa 1.0.3", "ryu", "serde", ] @@ -5047,18 +4778,6 @@ dependencies = [ "stable_deref_trait", ] -[[package]] -name = "sha-1" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" -dependencies = [ - "block-buffer 0.7.3", - "digest 0.8.1", - "fake-simd", - "opaque-debug 0.2.3", -] - [[package]] name = "sha-1" version = "0.9.8" @@ -5073,14 +4792,14 @@ dependencies = [ ] [[package]] -name = "sha-1" -version = "0.10.0" +name = "sha1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -5098,13 +4817,13 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.2" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" dependencies = [ "cfg-if", "cpufeatures", - "digest 0.10.3", + "digest 0.10.5", ] [[package]] @@ -5144,7 +4863,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02658e48d89f2bec991f9a78e69cfa4c316f8d6a6c4ec12fae1aeb263d486788" dependencies = [ "digest 0.9.0", - "rand_core 0.6.3", + "rand_core 0.6.4", ] [[package]] @@ -5155,9 +4874,12 @@ checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" [[package]] name = "slab" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg 1.1.0", +] [[package]] name = "sled" @@ -5213,9 +4935,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi", @@ -5226,22 +4948,7 @@ name = "socks5-requests" version = "0.1.0" dependencies = [ "nymsphinx-addressing", - "ordered-buffer", -] - -[[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", + "thiserror", ] [[package]] @@ -5303,9 +5010,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spin" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" +checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" dependencies = [ "lock_api", ] @@ -5365,7 +5072,7 @@ dependencies = [ "hashlink", "hex", "indexmap", - "itoa 1.0.2", + "itoa 1.0.3", "libc", "libsqlite3-sys", "log", @@ -5374,7 +5081,7 @@ dependencies = [ "paste", "percent-encoding", "rustls", - "sha2 0.10.2", + "sha2 0.10.6", "smallvec 1.9.0", "sqlformat", "sqlx-rt", @@ -5398,7 +5105,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "sha2 0.10.2", + "sha2 0.10.6", "sqlx-core", "sqlx-rt", "syn", @@ -5511,9 +5218,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" dependencies = [ "proc-macro2", "quote", @@ -5560,9 +5267,9 @@ dependencies = [ [[package]] name = "tao" -version = "0.12.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a71c32c2fa7bba46b01becf9cf470f6a781573af7e376c5e317a313ecce27545" +checksum = "43336f5d1793543ba96e2a1e75f3a5c7dcd592743be06a0ab3a190f4fcb4b934" dependencies = [ "bitflags", "cairo-rs", @@ -5583,7 +5290,7 @@ dependencies = [ "gtk", "image", "instant", - "jni 0.19.0", + "jni", "lazy_static", "libappindicator", "libc", @@ -5593,15 +5300,15 @@ dependencies = [ "ndk-sys", "objc", "once_cell", - "parking_lot 0.11.2", + "parking_lot 0.12.1", "paste", - "png 0.17.5", + "png 0.17.6", "raw-window-handle", "scopeguard", "serde", "unicode-segmentation", - "uuid 0.8.2", - "windows", + "uuid 1.1.2", + "windows 0.39.0", "windows-implement", "x11-dl", ] @@ -5623,11 +5330,19 @@ dependencies = [ "xattr", ] +[[package]] +name = "task" +version = "0.1.0" +dependencies = [ + "log", + "tokio", +] + [[package]] name = "tauri" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61fc211e0bd2c04c0aecd202d2cd72dd797a89da02989a39e1b9691462386d6" +checksum = "efbf22abd61d95ca9b2becd77f9db4c093892f73e8a07d21d8b0b2bf71a7bcea" dependencies = [ "anyhow", "attohttpc", @@ -5635,9 +5350,9 @@ dependencies = [ "cocoa", "dirs-next", "embed_plist", + "encoding_rs", "flate2", - "futures", - "futures-lite", + "futures-util", "glib", "glob", "gtk", @@ -5653,7 +5368,7 @@ dependencies = [ "raw-window-handle", "regex", "rfd", - "semver 1.0.12", + "semver 1.0.14", "serde", "serde_json", "serde_repr", @@ -5666,27 +5381,27 @@ dependencies = [ "tauri-utils", "tempfile", "thiserror", - "time 0.3.11", + "time 0.3.14", "tokio", "url", "uuid 1.1.2", "webkit2gtk", "webview2-com", - "windows", + "windows 0.39.0", "zip", ] [[package]] name = "tauri-build" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f2b32e551ec810ba4ab2ad735de5e3576e54bf0322ab0f4b7ce41244bc65ecf" +checksum = "0991fb306849897439dbd4a72e4cbed2413e2eb26cb4b3ba220b94edba8b4b88" dependencies = [ "anyhow", "cargo_toml", "heck 0.4.0", "json-patch", - "semver 1.0.12", + "semver 1.0.14", "serde_json", "tauri-utils", "winres", @@ -5694,35 +5409,35 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f1f7928dd040fc03c94207adfad506c0cf5b152982fd1dc0a621f7fd777e22" +checksum = "356fa253e40ae4d6ff02075011f2f2bb4066f5c9d8c1e16ca6912d7b75903ba6" dependencies = [ "base64", "brotli", "ico", "json-patch", "plist", - "png 0.17.5", + "png 0.17.6", "proc-macro2", "quote", "regex", - "semver 1.0.12", + "semver 1.0.14", "serde", "serde_json", - "sha2 0.10.2", + "sha2 0.10.6", "tauri-utils", "thiserror", - "time 0.3.11", + "time 0.3.14", "uuid 1.1.2", "walkdir", ] [[package]] name = "tauri-macros" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e50b9f52871c088857360319a37472d59f4644f1ed004489599d62831a1b6996" +checksum = "d6051fd6940ddb22af452340d03c66a3e2f5d72e0788d4081d91e31528ccdc4d" dependencies = [ "heck 0.4.0", "proc-macro2", @@ -5734,14 +5449,15 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "0.10.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4cff3b4d9469727fa2107c4b3d2eda110df1ba45103fb420178e536362fae4" +checksum = "d49439a5ea47f474572b854972f42eda2e02a470be5ca9609cc83bb66945abe2" dependencies = [ "gtk", "http", "http-range", "infer", + "rand 0.8.5", "raw-window-handle", "serde", "serde_json", @@ -5749,14 +5465,14 @@ dependencies = [ "thiserror", "uuid 1.1.2", "webview2-com", - "windows", + "windows 0.39.0", ] [[package]] name = "tauri-runtime-wry" -version = "0.10.2" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fa8c4edaf01d8b556e7172c844b1b4dd3399adcd1a606bd520fc3e65f698546" +checksum = "28dce920995fd49907aa9bea7249ed1771454f11f7611924c920a1f75fb614d4" dependencies = [ "cocoa", "gtk", @@ -5768,15 +5484,15 @@ dependencies = [ "uuid 1.1.2", "webkit2gtk", "webview2-com", - "windows", + "windows 0.39.0", "wry", ] [[package]] name = "tauri-utils" -version = "1.0.3" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ff4b68d9faeb57c9c727bf58c9c9768d2b67d8e84e62ce6146e7859a2e9c6b" +checksum = "1e8fdae6f29cef959809a3c3afef510c5b715a446a597ab8b791497585363f39" dependencies = [ "brotli", "ctor", @@ -5789,14 +5505,14 @@ dependencies = [ "phf 0.10.1", "proc-macro2", "quote", - "semver 1.0.12", + "semver 1.0.14", "serde", "serde_json", "serde_with", "thiserror", "url", "walkdir", - "windows", + "windows 0.39.0", ] [[package]] @@ -5840,7 +5556,7 @@ dependencies = [ "subtle 2.4.1", "subtle-encoding", "tendermint-proto", - "time 0.3.11", + "time 0.3.14", "zeroize", ] @@ -5873,7 +5589,7 @@ dependencies = [ "serde", "serde_bytes", "subtle-encoding", - "time 0.3.11", + "time 0.3.14", ] [[package]] @@ -5901,7 +5617,7 @@ dependencies = [ "tendermint-config", "tendermint-proto", "thiserror", - "time 0.3.11", + "time 0.3.14", "tokio", "tracing", "url", @@ -5931,9 +5647,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" [[package]] name = "thin-slice" @@ -5943,18 +5659,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" dependencies = [ "proc-macro2", "quote", @@ -5983,11 +5699,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.11" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" dependencies = [ - "itoa 1.0.2", + "itoa 1.0.3", "libc", "num_threads", "time-macros", @@ -5999,28 +5715,18 @@ 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.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", "socket2", @@ -6062,13 +5768,14 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" +checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af" dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -6086,24 +5793,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.10" +version = "0.7.4" 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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" dependencies = [ "bytes", "futures-core", @@ -6145,9 +5837,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" +checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ "cfg-if", "pin-project-lite", @@ -6168,9 +5860,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" +checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" dependencies = [ "once_cell", "valuable", @@ -6189,9 +5881,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.14" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a713421342a5a666b7577783721d3117f1b69a393df803ee17bb73b1e122a59" +checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" dependencies = [ "ansi_term", "matchers", @@ -6257,7 +5949,7 @@ dependencies = [ "input_buffer", "log", "rand 0.8.5", - "sha-1 0.9.8", + "sha-1", "thiserror", "url", "utf-8", @@ -6271,9 +5963,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" +checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" [[package]] name = "uint" @@ -6295,9 +5987,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "unicode-normalization" @@ -6310,15 +6002,15 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "unicode_categories" @@ -6334,13 +6026,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.2" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "22fe195a4f217c25b25cb5058ced57059824a678474874038dc88d211bf508d3" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", "serde", ] @@ -6362,9 +6053,6 @@ name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom 0.2.7", -] [[package]] name = "uuid" @@ -6382,11 +6070,11 @@ dependencies = [ "bs58", "coconut-interface", "cosmrs", + "cosmwasm-std", "getset", "mixnet-contract-common", "schemars", "serde", - "ts-rs", ] [[package]] @@ -6400,6 +6088,7 @@ dependencies = [ "coconut-interface", "colored", "config", + "contracts-common", "cosmrs", "cosmwasm-std", "cw3", @@ -6480,14 +6169,16 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" -version = "1.0.1" +version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", "schemars", "serde", "thiserror", + "vergen", "vesting-contract-common", ] @@ -6495,33 +6186,12 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ - "config", + "contracts-common", "cosmwasm-std", - "cw-storage-plus", + "log", "mixnet-contract-common", "schemars", "serde", - "ts-rs", -] - -[[package]] -name = "vswhom" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" -dependencies = [ - "libc", - "vswhom-sys", -] - -[[package]] -name = "vswhom-sys" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39" -dependencies = [ - "cc", - "libc", ] [[package]] @@ -6545,12 +6215,6 @@ dependencies = [ "quote", ] -[[package]] -name = "waker-fn" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" - [[package]] name = "walkdir" version = "2.3.2" @@ -6592,9 +6256,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", @@ -6602,13 +6266,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", @@ -6629,9 +6293,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", @@ -6639,9 +6303,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", @@ -6652,9 +6316,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" @@ -6678,52 +6356,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" @@ -6792,13 +6424,13 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.16.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a489a9420acabb3c2ed0434b6f71f6b56b9485ec32665a28dec1ee186d716e0f" +checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.39.0", "windows-implement", ] @@ -6815,24 +6447,19 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.16.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0258c53ee9adc0a4f8ba1c8c317588f7a58c7048a55b621d469ba75ab3709ca1" +checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7" dependencies = [ "regex", "serde", "serde_json", "thiserror", - "windows", + "windows 0.39.0", "windows-bindgen", + "windows-metadata", ] -[[package]] -name = "wildmatch" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee583bdc5ff1cf9db20e9db5bb3ff4c3089a8f6b8b31aff265c9aba85812db86" - [[package]] name = "winapi" version = "0.3.9" @@ -6864,13 +6491,25 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec" +dependencies = [ + "windows_aarch64_msvc 0.32.0", + "windows_i686_gnu 0.32.0", + "windows_i686_msvc 0.32.0", + "windows_x86_64_gnu 0.32.0", + "windows_x86_64_msvc 0.32.0", +] + [[package]] name = "windows" version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" dependencies = [ - "windows-implement", "windows_aarch64_msvc 0.37.0", "windows_i686_gnu 0.37.0", "windows_i686_msvc 0.37.0", @@ -6879,10 +6518,24 @@ dependencies = [ ] [[package]] -name = "windows-bindgen" -version = "0.37.0" +name = "windows" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bed7be31ade0af08fec9b5343e9edcc005d22b1f11859b8a59b24797f5858e8" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows-implement", + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + +[[package]] +name = "windows-bindgen" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41" dependencies = [ "windows-metadata", "windows-tokens", @@ -6890,9 +6543,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.37.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a1062e555f7d9d66fd1130ed4f7c6ec41a47529ee0850cd0e926d95b26bb14" +checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" dependencies = [ "syn", "windows-tokens", @@ -6900,9 +6553,9 @@ dependencies = [ [[package]] name = "windows-metadata" -version = "0.37.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f33f2b90a6664e369c41ab5ff262d06f048fc9685d9bf8a0e99a47750bb0463" +checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" [[package]] name = "windows-sys" @@ -6919,9 +6572,15 @@ dependencies = [ [[package]] name = "windows-tokens" -version = "0.37.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3263d25f1170419995b78ff10c06b949e8a986c35c208dc24333c64753a87169" +checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" [[package]] name = "windows_aarch64_msvc" @@ -6935,6 +6594,18 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + [[package]] name = "windows_i686_gnu" version = "0.36.1" @@ -6947,6 +6618,18 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + [[package]] name = "windows_i686_msvc" version = "0.36.1" @@ -6959,6 +6642,18 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" @@ -6971,6 +6666,18 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" @@ -6983,6 +6690,12 @@ version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" + [[package]] name = "winreg" version = "0.10.1" @@ -7003,19 +6716,22 @@ dependencies = [ [[package]] name = "wry" -version = "0.19.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce19dddbd3ce01dc8f14eb6d4c8f914123bf8379aaa838f6da4f981ff7104a3f" +checksum = "ff5c1352b4266fdf92c63479d2f58ab4cd29dc4e78fbc1b62011ed1227926945" dependencies = [ + "base64", "block", "cocoa", "core-graphics", + "crossbeam-channel", "gdk", "gio", "glib", "gtk", + "html5ever", "http", - "jni 0.18.0", + "kuchiki", "libc", "log", "objc", @@ -7023,27 +6739,22 @@ dependencies = [ "once_cell", "serde", "serde_json", + "sha2 0.10.6", "tao", "thiserror", "url", "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.39.0", "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.19.1" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dd0565fa8bfba8c5efe02725b14dff114c866724eff2cfd44d76cea74bcd87a" +checksum = "f7ae97874a928d821b061fce3d1fc52f08071dd53c89a6102bc06efcac3b2908" dependencies = [ "libc", "pkg-config", @@ -7051,9 +6762,9 @@ dependencies = [ [[package]] name = "x11-dl" -version = "2.19.1" +version = "2.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea26926b4ce81a6f5d9d0f3a0bc401e5a37c6ae14a1bfaa8ff6099ca80038c59" +checksum = "0c83627bc137605acc00bb399c7b908ef460b621fc37c953db2b09f88c449ea6" dependencies = [ "lazy_static", "libc", diff --git a/nym-connect/package.json b/nym-connect/package.json index e4593a6605..3eb24030d1 100644 --- a/nym-connect/package.json +++ b/nym-connect/package.json @@ -30,7 +30,8 @@ "@mui/material": "^5.2.2", "@mui/styles": "^5.2.2", "@nymproject/react": "^1.0.0", - "@tauri-apps/api": "^1.0.2", + "@tauri-apps/api": "^1.1.0", + "@tauri-apps/tauri-forage": "^1.0.0-beta.2", "clsx": "^1.1.1", "luxon": "^2.3.0", "pretty-bytes": "^6.0.0", @@ -52,7 +53,7 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", "@storybook/react": "^6.5.8", "@svgr/webpack": "^6.1.1", - "@tauri-apps/cli": "^1.0.2", + "@tauri-apps/cli": "^1.1.0", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^12.0.0", "@types/jest": "^27.0.1", diff --git a/nym-connect/src-tauri/Cargo.toml b/nym-connect/src-tauri/Cargo.toml index a529005681..b7ee99f14c 100644 --- a/nym-connect/src-tauri/Cargo.toml +++ b/nym-connect/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-connect" -version = "1.0.1" +version = "1.0.2" description = "nym-connect" authors = ["Nym Technologies SA"] license = "" @@ -13,10 +13,10 @@ rust-version = "1.58" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] -tauri-build = { version = "^1.0.2", features = [] } +tauri-build = { version = "^1.1.1", features = [] } -tauri-codegen = "^1.0.2" -tauri-macros = "^1.0.2" +tauri-codegen = "^1.1.1" +tauri-macros = "^1.1.1" [dependencies] @@ -32,14 +32,15 @@ reqwest = { version = "0.11", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tap = "1.0.1" -tauri = { version = "^1.0.2", features = ["clipboard-write-text", "shell-open", "system-tray", "updater"] } +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" } config-common = { path = "../../common/config", package = "config" } +logging = { path = "../../common/logging"} nym-socks5-client = { path = "../../clients/socks5" } topology = { path = "../../common/topology" } diff --git a/nym-connect/src-tauri/src/config/mod.rs b/nym-connect/src-tauri/src/config/mod.rs index 532f861f9e..f4d260ea57 100644 --- a/nym-connect/src-tauri/src/config/mod.rs +++ b/nym-connect/src-tauri/src/config/mod.rs @@ -16,10 +16,6 @@ use crate::{ static SOCKS5_CONFIG_ID: &str = "nym-connect"; -const DEFAULT_ETH_ENDPOINT: &str = "https://rinkeby.infura.io/v3/00000000000000000000000000000000"; -const DEFAULT_ETH_PRIVATE_KEY: &str = - "0000000000000000000000000000000000000000000000000000000000000001"; - pub fn socks5_config_id_appended_with(gateway_id: &str) -> Result<String> { use std::fmt::Write as _; let mut id = SOCKS5_CONFIG_ID.to_string(); @@ -37,9 +33,7 @@ pub async fn get_config_file_location( state: tauri::State<'_, Arc<RwLock<State>>>, ) -> Result<String> { let id = get_config_id(state).await?; - Ok(Config::config_file_location(&id) - .to_string_lossy() - .to_string()) + Config::config_file_location(&id).map(|d| d.to_string_lossy().to_string()) } #[derive(Debug)] @@ -94,8 +88,9 @@ impl Config { Ok(()) } - pub fn config_file_location(id: &str) -> PathBuf { - Socks5Config::default_config_file_path(Some(id)) + pub fn config_file_location(id: &str) -> Result<PathBuf> { + Socks5Config::try_default_config_file_path(Some(id)) + .ok_or(BackendError::CouldNotGetConfigFilename) } } @@ -107,9 +102,9 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str log::debug!( "Attempting to use config file location: {}", - Config::config_file_location(&id).to_string_lossy(), + Config::config_file_location(&id)?.to_string_lossy(), ); - let already_init = Config::config_file_location(&id).exists(); + let already_init = Config::config_file_location(&id)?.exists(); if already_init { log::info!( "SOCKS5 client \"{}\" was already initialised before! \ @@ -126,15 +121,6 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str log::trace!("Creating config for id: {}", id); let mut config = Config::new(id.as_str(), &provider_address); - // As far as I'm aware, these two are not used, they are only set because the socks5 init code - // requires them for initialising the bandwidth controller. - config - .get_base_mut() - .with_eth_endpoint(DEFAULT_ETH_ENDPOINT); - config - .get_base_mut() - .with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY); - if let Ok(raw_validators) = std::env::var(config_common::defaults::var_names::API_VALIDATOR) { config .get_base_mut() @@ -147,12 +133,12 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str Some(&chosen_gateway_id), config.get_socks5(), ) - .await; + .await?; config.get_base_mut().with_gateway_endpoint(gateway); let config_save_location = config.get_socks5().get_config_file_save_location(); config.get_socks5().save_to_file(None).tap_err(|_| { - log::warn!("Failed to save the config file"); + log::error!("Failed to save the config file"); })?; log::info!("Saved configuration file to {:?}", config_save_location); @@ -173,7 +159,7 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str ); log::info!("Client configuration completed."); - client_core::init::show_address(config.get_base()); + client_core::init::show_address(config.get_base())?; Ok(()) } @@ -183,7 +169,7 @@ async fn setup_gateway( register: bool, user_chosen_gateway_id: Option<&str>, config: &Socks5Config, -) -> GatewayEndpoint { +) -> Result<GatewayEndpoint> { if register { // Get the gateway details by querying the validator-api. Either pick one at random or use // the chosen one if it's among the available ones. @@ -192,16 +178,16 @@ async fn setup_gateway( config.get_base().get_validator_api_endpoints(), user_chosen_gateway_id, ) - .await; + .await?; log::debug!("Querying gateway gives: {}", gateway); // Registering with gateway by setting up and writing shared keys to disk log::trace!("Registering gateway"); client_core::init::register_with_gateway_and_store_keys(gateway.clone(), config.get_base()) - .await; + .await?; println!("Saved all generated keys"); - gateway.into() + Ok(gateway.into()) } else if user_chosen_gateway_id.is_some() { // Just set the config, don't register or create any keys // This assumes that the user knows what they are doing, and that the existing keys are @@ -211,22 +197,21 @@ async fn setup_gateway( config.get_base().get_validator_api_endpoints(), user_chosen_gateway_id, ) - .await; + .await?; log::debug!("Querying gateway gives: {}", gateway); - gateway.into() + Ok(gateway.into()) } else { println!("Not registering gateway, will reuse existing config and keys"); - match Socks5Config::load_from_file(Some(id)) { - Ok(existing_config) => existing_config.get_base().get_gateway_endpoint().clone(), - Err(err) => { - panic!( - "Unable to configure gateway: {err}. \n - Seems like the client was already initialized but it was not possible to read \ - the existing configuration file. \n - CAUTION: Consider backing up your gateway keys and try force gateway registration, or \ - removing the existing configuration and starting over." - ) - } - } + let existing_config = Socks5Config::load_from_file(Some(id)).map_err(|err| { + log::error!( + "Unable to configure gateway: {err}. \n + Seems like the client was already initialized but it was not possible to read \ + the existing configuration file. \n + CAUTION: Consider backing up your gateway keys and try force gateway registration, or \ + removing the existing configuration and starting over." + ); + BackendError::CouldNotLoadExistingGatewayConfiguration(err) + })?; + Ok(existing_config.get_base().get_gateway_endpoint().clone()) } } diff --git a/nym-connect/src-tauri/src/error.rs b/nym-connect/src-tauri/src/error.rs index df37001274..b2b44ee345 100644 --- a/nym-connect/src-tauri/src/error.rs +++ b/nym-connect/src-tauri/src/error.rs @@ -1,3 +1,4 @@ +use client_core::error::ClientCoreError; use serde::{Serialize, Serializer}; use thiserror::Error; @@ -29,13 +30,12 @@ pub enum BackendError { #[from] source: serde_json::Error, }, + #[error("{source}")] + ClientCoreError { + #[from] + source: ClientCoreError, + }, - #[error("State error")] - StateError, - #[error("Could not connect")] - CouldNotConnect, - #[error("Could not disconnect")] - CouldNotDisconnect, #[error("Could not send disconnect signal to the SOCKS5 client")] CoundNotSendDisconnectSignal, #[error("No service provider set")] @@ -52,6 +52,10 @@ pub enum BackendError { CouldNotInitWithoutServiceProvider, #[error("Could not get file name")] CouldNotGetFilename, + #[error("Could not get config file location")] + CouldNotGetConfigFilename, + #[error("Could not load existing gateway configuration")] + CouldNotLoadExistingGatewayConfiguration(std::io::Error), } impl Serialize for BackendError { diff --git a/nym-connect/src-tauri/src/main.rs b/nym-connect/src-tauri/src/main.rs index e1117c7516..4f4e8bb5fb 100644 --- a/nym-connect/src-tauri/src/main.rs +++ b/nym-connect/src-tauri/src/main.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use config_common::defaults::setup_env; +use logging::setup_logging; use tauri::Menu; use tokio::sync::RwLock; @@ -55,22 +56,3 @@ fn main() { .run(tauri::generate_context!()) .expect("error while running tauri application"); } - -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder - .filter_module("handlebars", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("sled", log::LevelFilter::Warn) - .filter_module("tokio_tungstenite", log::LevelFilter::Warn) - .filter_module("tungstenite", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .init(); -} diff --git a/nym-connect/src-tauri/src/state.rs b/nym-connect/src-tauri/src/state.rs index 2fce640487..6d0f34bc8f 100644 --- a/nym-connect/src-tauri/src/state.rs +++ b/nym-connect/src-tauri/src/state.rs @@ -101,7 +101,7 @@ impl State { // Setup configuration by writing to file if let Err(err) = self.init_config().await { - log::warn!("Failed to initialize: {}", err); + log::error!("Failed to initialize: {}", err); // Wait a little to give the user some rudimentary feedback that the click actually // registered. diff --git a/nym-connect/src-tauri/tauri.conf.json b/nym-connect/src-tauri/tauri.conf.json index 97ea3f1af8..3a4be90432 100644 --- a/nym-connect/src-tauri/tauri.conf.json +++ b/nym-connect/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "nym-connect", - "version": "1.0.1" + "version": "1.0.2" }, "build": { "distDir": "../dist", @@ -65,9 +65,9 @@ }, "windows": [ { - "title": "Nym Connect", + "title": "NymConnect", "width": 240, - "height": 480, + "height": 500, "resizable": false } ], diff --git a/nym-connect/src/components/ConnectionButton.tsx b/nym-connect/src/components/ConnectionButton.tsx index 7fe96c6da8..4fd093a569 100644 --- a/nym-connect/src/components/ConnectionButton.tsx +++ b/nym-connect/src/components/ConnectionButton.tsx @@ -1,6 +1,56 @@ import React from 'react'; import { ConnectionStatusKind } from '../types'; +const getBusyFillColor = (color: string): string => { + if (color === '#60D6EF') { + return '#21D072'; + } + return '#60D6EF'; +}; + +const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isError: boolean): string => { + if (isError && hover) { + return '#21D072'; + } + if (isError) { + return '#40475C'; + } + + switch (status) { + case ConnectionStatusKind.disconnected: + if (hover) { + return '#21D072'; + } + return '#60D6EF'; + case ConnectionStatusKind.connecting: + case ConnectionStatusKind.disconnecting: + return '#60D6EF'; + default: + // connected + if (hover) { + return '#DA465B'; + } + return '#21D072'; + } +}; + +const getStatusText = (status: ConnectionStatusKind, hover: boolean): string => { + switch (status) { + case ConnectionStatusKind.disconnected: + return 'Connect'; + case ConnectionStatusKind.connecting: + return 'Connecting'; + case ConnectionStatusKind.disconnecting: + return 'Connected'; + default: + // connected + if (hover) { + return 'Disconnect'; + } + return 'Connected'; + } +}; + export const ConnectionButton: React.FC<{ status: ConnectionStatusKind; disabled?: boolean; @@ -130,53 +180,3 @@ export const ConnectionButton: React.FC<{ </svg> ); }; - -const getBusyFillColor = (color: string): string => { - if (color === '#60D6EF') { - return '#21D072'; - } - return '#60D6EF'; -}; - -const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isError: boolean): string => { - if (isError && hover) { - return '#21D072'; - } - if (isError) { - return '#40475C'; - } - - switch (status) { - case ConnectionStatusKind.disconnected: - if (hover) { - return '#21D072'; - } - return '#60D6EF'; - case ConnectionStatusKind.connecting: - case ConnectionStatusKind.disconnecting: - return '#60D6EF'; - default: - // connected - if (hover) { - return '#DA465B'; - } - return '#21D072'; - } -}; - -const getStatusText = (status: ConnectionStatusKind, hover: boolean): string => { - switch (status) { - case ConnectionStatusKind.disconnected: - return 'Connect'; - case ConnectionStatusKind.connecting: - return 'Connecting'; - case ConnectionStatusKind.disconnecting: - return 'Connected'; - default: - // connected - if (hover) { - return 'Disconnect'; - } - return 'Connected'; - } -}; diff --git a/nym-connect/src/components/ServiceProviderSelector.tsx b/nym-connect/src/components/ServiceProviderSelector.tsx index 8e46cd5b03..b2fd29f42b 100644 --- a/nym-connect/src/components/ServiceProviderSelector.tsx +++ b/nym-connect/src/components/ServiceProviderSelector.tsx @@ -1,24 +1,53 @@ -import React from 'react'; +import React, { useEffect, useMemo } from 'react'; import IconButton from '@mui/material/IconButton'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; import ArrowDropDownCircleIcon from '@mui/icons-material/ArrowDropDownCircle'; import { Box, CircularProgress, Stack, Tooltip, Typography } from '@mui/material'; -import { ServiceProvider, Services } from '../types/directory'; +import { ServiceProvider, Service, Services } from '../types/directory'; + +type ServiceWithRandomSp = { + id: string; + description: string; + sp: ServiceProvider; +}; export const ServiceProviderSelector: React.FC<{ onChange?: (serviceProvider: ServiceProvider) => void; services?: Services; -}> = ({ services, onChange }) => { - const [serviceProvider, setServiceProvider] = React.useState<ServiceProvider | undefined>(); + currentSp?: ServiceProvider; +}> = ({ services, currentSp, onChange }) => { + const [service, setService] = React.useState<Service>(); + const [serviceProvider, setServiceProvider] = React.useState<ServiceProvider | undefined>(currentSp); const textEl = React.useRef<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null); const open = Boolean(anchorEl); + + useEffect(() => { + if (!serviceProvider && currentSp) { + setServiceProvider(currentSp); + } + }, [currentSp]); + + useEffect(() => { + if (services && serviceProvider) { + // retrieve the service corresponding to this service provider + setService( + services.find((s) => + s.items.some( + ({ id, address, gateway }) => + id === serviceProvider.id && address === serviceProvider.address && gateway === serviceProvider.gateway, + ), + ), + ); + } + }, [serviceProvider, services]); + const handleClick = () => { setAnchorEl(textEl.current); }; const handleClose = (newServiceProvider?: ServiceProvider) => { - if (newServiceProvider) { + if (newServiceProvider && newServiceProvider !== currentSp) { setServiceProvider(newServiceProvider); onChange?.(newServiceProvider); } @@ -39,6 +68,16 @@ export const ServiceProviderSelector: React.FC<{ ); } + const servicesWithRandomSp: ServiceWithRandomSp[] = useMemo( + () => + services.map(({ id, items, description }) => ({ + id, + description, + sp: items[Math.floor(Math.random() * items.length)], + })), + [services], + ); + return ( <> <Box display="flex" alignItems="center" justifyContent="space-between" sx={{ mt: 3 }}> @@ -48,7 +87,7 @@ export const ServiceProviderSelector: React.FC<{ fontWeight={700} color={(theme) => (serviceProvider ? undefined : theme.palette.primary.main)} > - {serviceProvider ? serviceProvider.description : 'Select a service'} + {service ? service.description : 'Select a service'} </Typography> <IconButton id="service-provider-button" @@ -65,44 +104,46 @@ export const ServiceProviderSelector: React.FC<{ anchorEl={anchorEl} open={open} onClose={() => handleClose()} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} MenuListProps={{ 'aria-labelledby': 'service-provider-button', + sx: { + minWidth: 160, + }, }} > - {services.map((service) => ( - <> - <MenuItem disabled dense sx={{ fontSize: 'small', fontWeight: 'bold', mb: -1 }}> - {service.description} - </MenuItem> - {service.items.map((sp) => ( - <MenuItem dense sx={{ fontSize: 'small', ml: 2, height: 'auto' }} onClick={() => handleClose(sp)}> - <Tooltip - title={ - <Stack direction="column"> - <Typography fontSize="inherit"> - <code>{sp.id}</code> - </Typography> - <Typography fontSize="inherit" fontWeight={700}> - {sp.description} - </Typography> - <Typography fontSize="inherit"> - Gateway <code>{sp.gateway.slice(0, 10)}...</code> - </Typography> - <Typography fontSize="inherit"> - Provider <code>{sp.address.slice(0, 10)}...</code> - </Typography> - </Stack> - } - arrow - placement="top" - > - <Typography fontSize="inherit" noWrap> + {servicesWithRandomSp.map(({ id, description, sp }) => ( + <MenuItem dense key={id} sx={{ fontSize: 'small', fontWeight: 'bold' }} onClick={() => handleClose(sp)}> + <Tooltip + title={ + <Stack direction="column"> + <Typography fontSize="inherit"> + <code>{sp.id}</code> + </Typography> + <Typography fontSize="inherit" fontWeight={700}> {sp.description} </Typography> - </Tooltip> - </MenuItem> - ))} - </> + <Typography fontSize="inherit"> + Gateway <code>{sp.gateway.slice(0, 10)}...</code> + </Typography> + <Typography fontSize="inherit"> + Provider <code>{sp.address.slice(0, 10)}...</code> + </Typography> + </Stack> + } + arrow + placement="top" + > + <Typography>{description}</Typography> + </Tooltip> + </MenuItem> ))} </Menu> </> diff --git a/nym-connect/src/context/main.tsx b/nym-connect/src/context/main.tsx index 53a53adbc2..42bfed638e 100644 --- a/nym-connect/src/context/main.tsx +++ b/nym-connect/src/context/main.tsx @@ -1,8 +1,9 @@ -import React, { createContext, useCallback, useContext, useEffect, useState } from 'react'; +import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { DateTime } from 'luxon'; import { invoke } from '@tauri-apps/api'; import type { UnlistenFn } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event'; +import { forage } from '@tauri-apps/tauri-forage'; import { ConnectionStatusKind } from '../types'; import { ConnectionStatsItem } from '../components/ConnectionStats'; import { ServiceProvider, Services } from '../types/directory'; @@ -36,7 +37,7 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode const [connectionStatus, setConnectionStatus] = useState<ConnectionStatusKind>(ConnectionStatusKind.disconnected); const [connectionStats, setConnectionStats] = useState<ConnectionStatsItem[]>(); const [connectedSince, setConnectedSince] = useState<DateTime>(); - const [services, setServices] = React.useState<Services>(); + const [services, setServices] = React.useState<Services>([]); const [serviceProvider, setRawServiceProvider] = React.useState<ServiceProvider>(); useEffect(() => { @@ -72,33 +73,71 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode await invoke('start_disconnecting'); }, []); + const setSpInStorage = async (sp: ServiceProvider) => { + await forage.setItem({ + key: 'nym-connect-sp', + value: sp, + } as any)(); + }; + const setServiceProvider = useCallback(async (newServiceProvider: ServiceProvider) => { await invoke('set_gateway', { gateway: newServiceProvider.gateway }); await invoke('set_service_provider', { serviceProvider: newServiceProvider.address }); + await setSpInStorage(newServiceProvider); setRawServiceProvider(newServiceProvider); }, []); - return ( - <ClientContext.Provider - value={{ - mode, - setMode, - connectionStatus, - setConnectionStatus, - connectionStats, - setConnectionStats, - connectedSince, - setConnectedSince, - startConnecting, - startDisconnecting, - services, - serviceProvider, - setServiceProvider, - }} - > - {children} - </ClientContext.Provider> + const getSpFromStorage = async () => { + try { + const spFromStorage = await forage.getItem({ key: 'nym-connect-sp' })(); + if (spFromStorage) { + setRawServiceProvider(spFromStorage); + } + } catch (e) { + console.warn(e); + } + }; + + useEffect(() => { + const validityCheck = async () => { + if (services.length > 0 && serviceProvider) { + const isValid = services.some(({ items }) => items.some(({ id }) => id === serviceProvider.id)); + if (!isValid) { + console.warn('invalid SP, cleaning local storage'); + await forage.removeItem({ + key: 'nym-connect-sp', + })(); + setRawServiceProvider(undefined); + } + } + }; + validityCheck(); + }, [services, serviceProvider]); + + useEffect(() => { + getSpFromStorage(); + }, []); + + const contextValue = useMemo( + () => ({ + mode, + setMode, + connectionStatus, + setConnectionStatus, + connectionStats, + setConnectionStats, + connectedSince, + setConnectedSince, + startConnecting, + startDisconnecting, + services, + serviceProvider, + setServiceProvider, + }), + [mode, connectedSince, connectionStatus, connectionStats, connectedSince, services, serviceProvider], ); + + return <ClientContext.Provider value={contextValue}>{children}</ClientContext.Provider>; }; export const useClientContext = () => useContext(ClientContext); diff --git a/nym-connect/src/layouts/DefaultLayout.tsx b/nym-connect/src/layouts/DefaultLayout.tsx index 9c329206d3..ba59b7f95d 100644 --- a/nym-connect/src/layouts/DefaultLayout.tsx +++ b/nym-connect/src/layouts/DefaultLayout.tsx @@ -6,6 +6,7 @@ import { ConnectionStatusKind } from '../types'; import { NeedHelp } from '../components/NeedHelp'; import { ServiceProviderSelector } from '../components/ServiceProviderSelector'; import { ServiceProvider, Services } from '../types/directory'; +import { useClientContext } from '../context/main'; export const DefaultLayout: React.FC<{ status: ConnectionStatusKind; @@ -20,6 +21,8 @@ export const DefaultLayout: React.FC<{ setServiceProvider(newServiceProvider); onServiceProviderChange?.(newServiceProvider); }; + const { serviceProvider: currentSp } = useClientContext(); + return ( <AppWindowFrame> <Typography fontWeight="400" fontSize="12px" textAlign="center" sx={{ opacity: 0.6 }}> @@ -31,10 +34,10 @@ export const DefaultLayout: React.FC<{ <br /> Nym mixnet for privacy. </Typography> - <ServiceProviderSelector services={services} onChange={handleServiceProviderChange} /> + <ServiceProviderSelector services={services} onChange={handleServiceProviderChange} currentSp={currentSp} /> <ConnectionButton status={status} - disabled={serviceProvider === undefined} + disabled={serviceProvider === undefined && currentSp === undefined} busy={busy} isError={isError} onClick={onConnectClick} diff --git a/nym-wallet/CHANGELOG.md b/nym-wallet/CHANGELOG.md new file mode 100644 index 0000000000..d3bd1b2f14 --- /dev/null +++ b/nym-wallet/CHANGELOG.md @@ -0,0 +1,223 @@ +# Changelog + +## Unreleased + +- Add window for showing logs for when the terminal is not available. + +## [nym-wallet-v1.0.9](https://github.com/nymtech/nym/releases/tag/nym-wallet-v1.0.8) (2022-09-08) + +- wallet: change default `nymd` URL to https://rpc.nymtech.net + +## [nym-wallet-v1.0.8](https://github.com/nymtech/nym/releases/tag/nym-wallet-v1.0.8) (2022-08-11) + +- wallet: new bonding flow and screen for bonded node +- wallet: compound and redeem functionalities for operator rewards +- wallet: a few minor touch ups and bug fixes + + +## [nym-wallet-v1.0.7](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.7) (2022-07-11) + +- wallet: dark mode +- wallet: when simulating gas costs, an automatic adjustment is being used ([#1388]). + +[#1388]: https://github.com/nymtech/nym/pull/1388 + + +## [nym-wallet-v1.0.6](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.6) (2022-06-21) + +- wallet: undelegating now uses either the mixnet or vesting contract, or both, depending on how delegations were made +- wallet: redeeming and compounding now uses both the mixnet and vesting contract +- wallet: the wallet backend learned how to archive wallet files +- wallet: add ENABLE_QA_MODE environment variable to enable QA mode on built wallet + +## [nym-wallet-v1.0.5](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.5) (2022-06-14) + +- wallet: add simple CLI tool for decrypting and recovering the wallet file. +- wallet: added support for multiple accounts ([#1265]) +- wallet: compound and claim reward endpoints for operators and delegators ([#1302]) +- wallet: require password to switch accounts +- wallet: the wallet backend learned how to keep track of validator name, either hardcoded or by querying the status endpoint. +- wallet: new delegation and rewards UI +- wallet: show version in nav bar +- wallet: contract admin route put back +- wallet: staking_supply field to StateParams +- wallet: show transaction hash for redeeming or compounding rewards + +[#1265]: https://github.com/nymtech/nym/pull/1265 +[#1302]: https://github.com/nymtech/nym/pull/1302 + +## [nym-wallet-v1.0.4](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.4) (2022-05-04) + +### Changed + +- all: the default behaviour of validator client is changed to use `broadcast_sync` and poll for transaction inclusion instead of using `broadcast_commit` to deal with timeouts ([#1246]) + +## [nym-wallet-v1.0.3](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.3) (2022-04-25) + +[Full Changelog](https://github.com/nymtech/nym/compare/nym-binaries-1.0.0-rc.2...nym-wallet-v1.0.3) + +**Fixed bugs:** + +- \[Issue\] Wallet 1.0.2 cannot send NYM tokens from a DelayedVestingAccount [\#1215](https://github.com/nymtech/nym/issues/1215) +- Main README not showing properly with GitHub dark mode [\#1211](https://github.com/nymtech/nym/issues/1211) + +**Merged pull requests:** + +- Bugfix - wallet undelegation for vesting accounts [\#1220](https://github.com/nymtech/nym/pull/1220) ([mmsinclair](https://github.com/mmsinclair)) +- Bugfix/delegation reconcile [\#1219](https://github.com/nymtech/nym/pull/1219) ([jstuczyn](https://github.com/jstuczyn)) +- Bugfix/query proxied pending delegations [\#1218](https://github.com/nymtech/nym/pull/1218) ([jstuczyn](https://github.com/jstuczyn)) +- Using custom gas multiplier in the wallet [\#1217](https://github.com/nymtech/nym/pull/1217) ([jstuczyn](https://github.com/jstuczyn)) +- Feature/vesting accounts support [\#1216](https://github.com/nymtech/nym/pull/1216) ([jstuczyn](https://github.com/jstuczyn)) +- Release/1.0.0 rc.2 [\#1214](https://github.com/nymtech/nym/pull/1214) ([jstuczyn](https://github.com/jstuczyn)) +- chore: fix dark mode rendering [\#1212](https://github.com/nymtech/nym/pull/1212) ([pwnfoo](https://github.com/pwnfoo)) +- Feature/spend coconut [\#1210](https://github.com/nymtech/nym/pull/1210) ([neacsu](https://github.com/neacsu)) +- Bugfix/unique sphinx key [\#1207](https://github.com/nymtech/nym/pull/1207) ([jstuczyn](https://github.com/jstuczyn)) +- Add cache read and write timeouts [\#1206](https://github.com/nymtech/nym/pull/1206) ([durch](https://github.com/durch)) +- Additional, more informative routes [\#1204](https://github.com/nymtech/nym/pull/1204) ([durch](https://github.com/durch)) +- Feature/aggregated econ dynamics explorer endpoint [\#1203](https://github.com/nymtech/nym/pull/1203) ([jstuczyn](https://github.com/jstuczyn)) +- Debugging validator [\#1198](https://github.com/nymtech/nym/pull/1198) ([durch](https://github.com/durch)) +- wallet: expose additional validator configuration functionality to the frontend [\#1195](https://github.com/nymtech/nym/pull/1195) ([octol](https://github.com/octol)) +- Update rewarding validator address [\#1193](https://github.com/nymtech/nym/pull/1193) ([durch](https://github.com/durch)) +- Crypto part of the Groth's NIDKG [\#1182](https://github.com/nymtech/nym/pull/1182) ([jstuczyn](https://github.com/jstuczyn)) +- fix unbond page [\#1180](https://github.com/nymtech/nym/pull/1180) ([tommyv1987](https://github.com/tommyv1987)) +- Type safe bounds [\#1179](https://github.com/nymtech/nym/pull/1179) ([durch](https://github.com/durch)) +- Fix delegation paging [\#1174](https://github.com/nymtech/nym/pull/1174) ([durch](https://github.com/durch)) +- Update binaries to rc version [\#1172](https://github.com/nymtech/nym/pull/1172) ([tommyv1987](https://github.com/tommyv1987)) +- Bump ansi-regex from 4.1.0 to 4.1.1 in /docker/typescript\_client/upload\_contract [\#1171](https://github.com/nymtech/nym/pull/1171) ([dependabot[bot]](https://github.com/apps/dependabot)) + +## [nym-wallet-v1.0.2](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.2) (2022-04-05) + +[Full Changelog](https://github.com/nymtech/nym/compare/nym-wallet-v1.0.1...nym-wallet-v1.0.2) + +**Merged pull requests:** + +- Wallet 1.0.2 visual tweaks [\#1197](https://github.com/nymtech/nym/pull/1197) ([mmsinclair](https://github.com/mmsinclair)) +- Password for wallet with routes [\#1196](https://github.com/nymtech/nym/pull/1196) ([fmtabbara](https://github.com/fmtabbara)) +- Add auto-updater to Nym Wallet [\#1194](https://github.com/nymtech/nym/pull/1194) ([mmsinclair](https://github.com/mmsinclair)) +- Fix clippy warnings for beta toolchain [\#1191](https://github.com/nymtech/nym/pull/1191) ([octol](https://github.com/octol)) +- wallet: expose validator urls to the frontend [\#1190](https://github.com/nymtech/nym/pull/1190) ([octol](https://github.com/octol)) +- wallet: add test for decrypting stored wallet file [\#1189](https://github.com/nymtech/nym/pull/1189) ([octol](https://github.com/octol)) +- Fix clippy warnings [\#1188](https://github.com/nymtech/nym/pull/1188) ([octol](https://github.com/octol)) +- Password for wallet with routes [\#1187](https://github.com/nymtech/nym/pull/1187) ([mmsinclair](https://github.com/mmsinclair)) +- wallet: add validate\_mnemonic [\#1186](https://github.com/nymtech/nym/pull/1186) ([octol](https://github.com/octol)) +- wallet: support removing accounts from the wallet file [\#1185](https://github.com/nymtech/nym/pull/1185) ([octol](https://github.com/octol)) +- Feature/adding discord [\#1184](https://github.com/nymtech/nym/pull/1184) ([gala1234](https://github.com/gala1234)) +- wallet: config backend for validator selection [\#1183](https://github.com/nymtech/nym/pull/1183) ([octol](https://github.com/octol)) +- Add storybook to wallet [\#1178](https://github.com/nymtech/nym/pull/1178) ([mmsinclair](https://github.com/mmsinclair)) +- wallet: connection test nymd and api urls independently [\#1170](https://github.com/nymtech/nym/pull/1170) ([octol](https://github.com/octol)) +- wallet: wire up account storage [\#1153](https://github.com/nymtech/nym/pull/1153) ([octol](https://github.com/octol)) +- Feature/signature on deposit [\#1151](https://github.com/nymtech/nym/pull/1151) ([neacsu](https://github.com/neacsu)) + +## [nym-wallet-v1.0.1](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.1) (2022-04-05) + +[Full Changelog](https://github.com/nymtech/nym/compare/nym-binaries-1.0.0-rc.1...nym-wallet-v1.0.1) + +**Closed issues:** + +- Check enabling bbbc simultaneously with open access. Estimate what it would take to make this the default compilation target. [\#1175](https://github.com/nymtech/nym/issues/1175) +- Get coconut credential for deposited tokens [\#1138](https://github.com/nymtech/nym/issues/1138) +- Make payments lazy [\#1135](https://github.com/nymtech/nym/issues/1135) +- Uptime on node selection for sets [\#1049](https://github.com/nymtech/nym/issues/1049) + +## [nym-wallet-v1.0.0](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.0) (2022-02-03) + +[Full Changelog](https://github.com/nymtech/nym/compare/v0.12.1...nym-wallet-v1.0.0) + +**Implemented enhancements:** + +- \[Feature Request\] Please enable registration without need for Telegram account [\#1016](https://github.com/nymtech/nym/issues/1016) +- Fast mixnode launch with a pre-built ISO + VM software [\#1001](https://github.com/nymtech/nym/issues/1001) + +**Fixed bugs:** + +- \[Issue\] [\#1000](https://github.com/nymtech/nym/issues/1000) +- \[Issue\] `nym-client` requires multiple attempts to run a server [\#869](https://github.com/nymtech/nym/issues/869) +- De-'float'-ing `Interval` \(`Display` impl + `serde`\) [\#1065](https://github.com/nymtech/nym/pull/1065) ([jstuczyn](https://github.com/jstuczyn)) +- display client address on wallet creation [\#1058](https://github.com/nymtech/nym/pull/1058) ([fmtabbara](https://github.com/fmtabbara)) + +**Closed issues:** + +- Rewarded set inclusion probability API endpoint [\#1037](https://github.com/nymtech/nym/issues/1037) +- Update cw-storage-plus to 0.11 [\#1032](https://github.com/nymtech/nym/issues/1032) +- Change `u128` fields in `RewardEstimationResponse` to `u64` [\#1029](https://github.com/nymtech/nym/issues/1029) +- Test out the mainnet Gravity Bridge [\#1006](https://github.com/nymtech/nym/issues/1006) +- Add vesting contract interface to nym-wallet [\#959](https://github.com/nymtech/nym/issues/959) +- Mixnode crash [\#486](https://github.com/nymtech/nym/issues/486) + +**Merged pull requests:** + +- create custom urls for mainnet [\#1095](https://github.com/nymtech/nym/pull/1095) ([fmtabbara](https://github.com/fmtabbara)) +- Wallet signing on MacOS [\#1093](https://github.com/nymtech/nym/pull/1093) ([mmsinclair](https://github.com/mmsinclair)) +- Fix rust 2018 idioms warnings [\#1092](https://github.com/nymtech/nym/pull/1092) ([octol](https://github.com/octol)) +- Prevent contract overwriting [\#1090](https://github.com/nymtech/nym/pull/1090) ([durch](https://github.com/durch)) +- Logout operation [\#1087](https://github.com/nymtech/nym/pull/1087) ([jstuczyn](https://github.com/jstuczyn)) +- Update to rust edition 2021 everywhere [\#1086](https://github.com/nymtech/nym/pull/1086) ([octol](https://github.com/octol)) +- Tag contract errors, and print out lines for easier QA [\#1084](https://github.com/nymtech/nym/pull/1084) ([durch](https://github.com/durch)) +- Feature/flexible vesting + utility queries [\#1083](https://github.com/nymtech/nym/pull/1083) ([durch](https://github.com/durch)) +- Bump @openzeppelin/contracts from 4.3.1 to 4.4.2 in /contracts/basic-bandwidth-generation [\#1082](https://github.com/nymtech/nym/pull/1082) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump nth-check from 2.0.0 to 2.0.1 in /clients/native/examples/js-examples/websocket [\#1081](https://github.com/nymtech/nym/pull/1081) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump url-parse from 1.5.1 to 1.5.4 in /clients/native/examples/js-examples/websocket [\#1080](https://github.com/nymtech/nym/pull/1080) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump follow-redirects from 1.14.1 to 1.14.7 in /clients/native/examples/js-examples/websocket [\#1079](https://github.com/nymtech/nym/pull/1079) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump nanoid from 3.1.23 to 3.2.0 in /clients/native/examples/js-examples/websocket [\#1078](https://github.com/nymtech/nym/pull/1078) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Setup basic test for mixnode stats reporting [\#1077](https://github.com/nymtech/nym/pull/1077) ([octol](https://github.com/octol)) +- Make wallet\_address mandatory for mixnode init [\#1076](https://github.com/nymtech/nym/pull/1076) ([octol](https://github.com/octol)) +- Tidy nym-mixnode module visibility [\#1075](https://github.com/nymtech/nym/pull/1075) ([octol](https://github.com/octol)) +- Feature/wallet login with password [\#1074](https://github.com/nymtech/nym/pull/1074) ([fmtabbara](https://github.com/fmtabbara)) +- Add trait to mock client dependency in DelayForwarder [\#1073](https://github.com/nymtech/nym/pull/1073) ([octol](https://github.com/octol)) +- Bump rust-version to latest stable for nym-mixnode [\#1072](https://github.com/nymtech/nym/pull/1072) ([octol](https://github.com/octol)) +- Fixes CI for our wasm build [\#1069](https://github.com/nymtech/nym/pull/1069) ([jstuczyn](https://github.com/jstuczyn)) +- Add @octol as codeowner [\#1068](https://github.com/nymtech/nym/pull/1068) ([octol](https://github.com/octol)) +- set-up inclusion probability [\#1067](https://github.com/nymtech/nym/pull/1067) ([fmtabbara](https://github.com/fmtabbara)) +- Feature/wasm client [\#1066](https://github.com/nymtech/nym/pull/1066) ([neacsu](https://github.com/neacsu)) +- Changed bech32\_prefix from punk to nymt [\#1064](https://github.com/nymtech/nym/pull/1064) ([jstuczyn](https://github.com/jstuczyn)) +- Bump nanoid from 3.1.30 to 3.2.0 in /testnet-faucet [\#1063](https://github.com/nymtech/nym/pull/1063) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump nanoid from 3.1.30 to 3.2.0 in /nym-wallet [\#1062](https://github.com/nymtech/nym/pull/1062) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Rework vesting contract storage [\#1061](https://github.com/nymtech/nym/pull/1061) ([durch](https://github.com/durch)) +- Mixnet Contract constants extraction [\#1060](https://github.com/nymtech/nym/pull/1060) ([jstuczyn](https://github.com/jstuczyn)) +- fix: make explorer footer year dynamic [\#1059](https://github.com/nymtech/nym/pull/1059) ([martinyung](https://github.com/martinyung)) +- Add mnemonic just on creation, to display it [\#1057](https://github.com/nymtech/nym/pull/1057) ([neacsu](https://github.com/neacsu)) +- Network Explorer: updates to API and UI to show the active set [\#1056](https://github.com/nymtech/nym/pull/1056) ([mmsinclair](https://github.com/mmsinclair)) +- Made contract addresses for query NymdClient construction optional [\#1055](https://github.com/nymtech/nym/pull/1055) ([jstuczyn](https://github.com/jstuczyn)) +- Introduced RPC query for total token supply [\#1053](https://github.com/nymtech/nym/pull/1053) ([jstuczyn](https://github.com/jstuczyn)) +- Feature/tokio console [\#1052](https://github.com/nymtech/nym/pull/1052) ([durch](https://github.com/durch)) +- Implemented beta clippy lint recommendations [\#1051](https://github.com/nymtech/nym/pull/1051) ([jstuczyn](https://github.com/jstuczyn)) +- add new function to update profit percentage [\#1050](https://github.com/nymtech/nym/pull/1050) ([fmtabbara](https://github.com/fmtabbara)) +- Upgrade Clap and use declarative argument parsing for nym-mixnode [\#1047](https://github.com/nymtech/nym/pull/1047) ([octol](https://github.com/octol)) +- Feature/additional bond validation [\#1046](https://github.com/nymtech/nym/pull/1046) ([fmtabbara](https://github.com/fmtabbara)) +- Fix clippy on relevant lints [\#1044](https://github.com/nymtech/nym/pull/1044) ([neacsu](https://github.com/neacsu)) +- Bump shelljs from 0.8.4 to 0.8.5 in /contracts/basic-bandwidth-generation [\#1043](https://github.com/nymtech/nym/pull/1043) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Endpoint for rewarded set inclusion probabilities [\#1042](https://github.com/nymtech/nym/pull/1042) ([durch](https://github.com/durch)) +- Bump follow-redirects from 1.14.4 to 1.14.7 in /contracts/basic-bandwidth-generation [\#1041](https://github.com/nymtech/nym/pull/1041) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Bump follow-redirects from 1.14.5 to 1.14.7 in /testnet-faucet [\#1040](https://github.com/nymtech/nym/pull/1040) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Feature/node settings update [\#1036](https://github.com/nymtech/nym/pull/1036) ([fmtabbara](https://github.com/fmtabbara)) +- Migrate to cw-storage-plus 0.11.1 [\#1035](https://github.com/nymtech/nym/pull/1035) ([durch](https://github.com/durch)) +- Bump @openzeppelin/contracts from 4.4.1 to 4.4.2 in /contracts/basic-bandwidth-generation [\#1034](https://github.com/nymtech/nym/pull/1034) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Feature/configurable wallet [\#1033](https://github.com/nymtech/nym/pull/1033) ([neacsu](https://github.com/neacsu)) +- Feature/downcast reward estimation [\#1031](https://github.com/nymtech/nym/pull/1031) ([durch](https://github.com/durch)) +- Wallet UI updates [\#1028](https://github.com/nymtech/nym/pull/1028) ([fmtabbara](https://github.com/fmtabbara)) +- Remove migration code [\#1027](https://github.com/nymtech/nym/pull/1027) ([neacsu](https://github.com/neacsu)) +- Chore/stricter dependency requirements [\#1025](https://github.com/nymtech/nym/pull/1025) ([jstuczyn](https://github.com/jstuczyn)) +- Feature/validator api client endpoints [\#1024](https://github.com/nymtech/nym/pull/1024) ([jstuczyn](https://github.com/jstuczyn)) +- Updated cosmrs to 0.4.1 [\#1023](https://github.com/nymtech/nym/pull/1023) ([jstuczyn](https://github.com/jstuczyn)) +- Feature/testnet deploy scripts [\#1022](https://github.com/nymtech/nym/pull/1022) ([mfahampshire](https://github.com/mfahampshire)) +- Changed wallet's client to a full validator client [\#1021](https://github.com/nymtech/nym/pull/1021) ([jstuczyn](https://github.com/jstuczyn)) +- Fix 404 link [\#1020](https://github.com/nymtech/nym/pull/1020) ([RiccardoMasutti](https://github.com/RiccardoMasutti)) +- Feature/additional mixnode endpoints [\#1019](https://github.com/nymtech/nym/pull/1019) ([jstuczyn](https://github.com/jstuczyn)) +- Introduced denom check when trying to withdraw vested coins [\#1018](https://github.com/nymtech/nym/pull/1018) ([jstuczyn](https://github.com/jstuczyn)) +- Add network defaults for qa [\#1017](https://github.com/nymtech/nym/pull/1017) ([neacsu](https://github.com/neacsu)) +- Feature/expanded events [\#1015](https://github.com/nymtech/nym/pull/1015) ([jstuczyn](https://github.com/jstuczyn)) +- update frontend to use new profit update api [\#1014](https://github.com/nymtech/nym/pull/1014) ([fmtabbara](https://github.com/fmtabbara)) +- Feature/node state endpoint [\#1013](https://github.com/nymtech/nym/pull/1013) ([jstuczyn](https://github.com/jstuczyn)) +- Feature/hourly set updates [\#1012](https://github.com/nymtech/nym/pull/1012) ([durch](https://github.com/durch)) +- Feature/remove unused profit margin [\#1011](https://github.com/nymtech/nym/pull/1011) ([neacsu](https://github.com/neacsu)) +- Feature/explorer node status [\#1010](https://github.com/nymtech/nym/pull/1010) ([jstuczyn](https://github.com/jstuczyn)) +- Use serial integer instead of random [\#1009](https://github.com/nymtech/nym/pull/1009) ([durch](https://github.com/durch)) +- Feature/configure profit [\#1008](https://github.com/nymtech/nym/pull/1008) ([neacsu](https://github.com/neacsu)) +- Feature/fix gateway sign [\#1004](https://github.com/nymtech/nym/pull/1004) ([neacsu](https://github.com/neacsu)) +- Fix clippy [\#1003](https://github.com/nymtech/nym/pull/1003) ([neacsu](https://github.com/neacsu)) +- Update wallet version [\#998](https://github.com/nymtech/nym/pull/998) ([tommyv1987](https://github.com/tommyv1987)) +- Fix wallet build instructions [\#997](https://github.com/nymtech/nym/pull/997) ([tommyv1987](https://github.com/tommyv1987)) +- Make the separation between testnet-mode and erc20 bandwidth mode clearer [\#994](https://github.com/nymtech/nym/pull/994) ([neacsu](https://github.com/neacsu)) +- Bump @openzeppelin/contracts from 3.4.0 to 4.4.1 in /contracts/basic-bandwidth-generation [\#983](https://github.com/nymtech/nym/pull/983) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Feature/implicit runtime [\#973](https://github.com/nymtech/nym/pull/973) ([jstuczyn](https://github.com/jstuczyn)) +- Differentiate staking and ownership [\#961](https://github.com/nymtech/nym/pull/961) ([durch](https://github.com/durch)) diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 2561ca57b6..dfbfe63531 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -64,6 +64,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -101,113 +116,6 @@ dependencies = [ "password-hash 0.4.1", ] -[[package]] -name = "arrayref" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" - -[[package]] -name = "arrayvec" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" - -[[package]] -name = "ashpd" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eea0a7a98b3bd2832eb087e1078f6f58db5a54447574d3007cdac6309c1e9f1" -dependencies = [ - "enumflags2", - "futures", - "rand 0.8.5", - "serde", - "serde_repr", - "zbus", -] - -[[package]] -name = "async-broadcast" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b" -dependencies = [ - "easy-parallel", - "event-listener", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" -dependencies = [ - "concurrent-queue", - "event-listener", - "futures-core", -] - -[[package]] -name = "async-executor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "once_cell", - "slab", -] - -[[package]] -name = "async-io" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" -dependencies = [ - "concurrent-queue", - "futures-lite", - "libc", - "log", - "once_cell", - "parking", - "polling", - "slab", - "socket2", - "waker-fn", - "winapi", -] - -[[package]] -name = "async-lock" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" -dependencies = [ - "event-listener", -] - -[[package]] -name = "async-recursion" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-task" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" - [[package]] name = "async-trait" version = "0.1.52" @@ -237,17 +145,17 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" dependencies = [ - "glib-sys 0.15.6", - "gobject-sys 0.15.5", + "glib-sys", + "gobject-sys", "libc", "system-deps 6.0.2", ] [[package]] name = "attohttpc" -version = "0.18.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e69e13a99a7e6e070bb114f7ff381e58c7ccc188630121fc4c2fe4bcf24cd072" +checksum = "262c3f7f5d61249d8c00e5546e2685cd15ebeeb1bc0f3cc5449350a1cb07319e" dependencies = [ "flate2", "http", @@ -287,12 +195,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "az" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f771a5d1f5503f7f4279a30f3643d3421ba149848b89ecaaec0ea2acf04a5ac4" - [[package]] name = "base16ct" version = "0.1.1" @@ -311,15 +213,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bip32" version = "0.3.0" @@ -372,21 +265,6 @@ dependencies = [ "digest 0.10.3", ] -[[package]] -name = "blake3" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "digest 0.10.3", - "rayon", -] - [[package]] name = "block" version = "0.1.6" @@ -453,6 +331,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bs58" version = "0.4.0" @@ -501,33 +400,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" -[[package]] -name = "bzip2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6afcd980b5f3a45017c57e57a2fcccbb351cc43a356ce117ef760ef8052b89b0" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.11+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - -[[package]] -name = "cache-padded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" - [[package]] name = "cairo-rs" version = "0.15.6" @@ -547,7 +419,7 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" dependencies = [ - "glib-sys 0.15.6", + "glib-sys", "libc", "system-deps 6.0.2", ] @@ -573,22 +445,19 @@ dependencies = [ ] [[package]] -name = "cfb" -version = "0.4.0" +name = "cesu8" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca453e8624711b2f0f4eb47076a318feda166252a827ee25d067b43de83dcba0" -dependencies = [ - "byteorder", - "uuid", -] +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] -name = "cfg-expr" -version = "0.8.1" +name = "cfb" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b412e83326147c2bb881f8b40edfbf9905b9b8abaebd0e47ca190ba62fda8f0e" +checksum = "74f89d248799e3f15f91b70917f65381062a01bb8e222700ea0e5a7ff9785f9c" dependencies = [ - "smallvec 1.8.0", + "byteorder", + "uuid 0.8.2", ] [[package]] @@ -615,12 +484,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "chrono" version = "0.4.19" @@ -645,26 +508,26 @@ dependencies = [ [[package]] name = "clap" -version = "3.1.18" +version = "3.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", "indexmap", - "lazy_static", - "strsim 0.10.0", + "once_cell", + "strsim", "termcolor", "textwrap", ] [[package]] name = "clap_derive" -version = "3.1.18" +version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck 0.4.0", "proc-macro-error", @@ -675,9 +538,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.0" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" dependencies = [ "os_str_bytes", ] @@ -743,6 +606,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colored" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + [[package]] name = "colored" version = "2.0.0" @@ -755,12 +635,13 @@ dependencies = [ ] [[package]] -name = "concurrent-queue" -version = "1.2.2" +name = "combine" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" dependencies = [ - "cache-padded", + "bytes", + "memchr", ] [[package]] @@ -769,7 +650,6 @@ version = "0.1.0" dependencies = [ "cfg-if", "handlebars", - "humantime-serde", "log", "network-defaults", "serde", @@ -783,17 +663,14 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "contracts-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "schemars", + "serde", + "thiserror", ] [[package]] @@ -942,38 +819,14 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "lazy_static", - "memoffset", - "scopeguard", -] - [[package]] name = "crossbeam-utils" -version = "0.8.7" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" +checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc" dependencies = [ "cfg-if", - "lazy_static", + "once_cell", ] [[package]] @@ -1135,38 +988,14 @@ dependencies = [ "serde", ] -[[package]] -name = "darling" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" -dependencies = [ - "darling_core 0.10.2", - "darling_macro 0.10.2", -] - [[package]] name = "darling" version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0d720b8683f8dd83c65155f0530560cba68cd2bf395f6513a483caee57ff7f4" dependencies = [ - "darling_core 0.13.1", - "darling_macro 0.13.1", -] - -[[package]] -name = "darling_core" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.9.3", - "syn", + "darling_core", + "darling_macro", ] [[package]] @@ -1179,18 +1008,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim 0.10.0", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" -dependencies = [ - "darling_core 0.10.2", - "quote", + "strsim", "syn", ] @@ -1200,7 +1018,7 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72c41b3b7352feb3211a0d743dc5700a4e3b60f51bd2b368892d1e0f9a95f44b" dependencies = [ - "darling_core 0.13.1", + "darling_core", "quote", "syn", ] @@ -1215,16 +1033,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "deflate" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174" -dependencies = [ - "adler32", - "byteorder", -] - [[package]] name = "der" version = "0.5.1" @@ -1234,17 +1042,6 @@ dependencies = [ "const-oid", ] -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "derive_more" version = "0.99.17" @@ -1361,12 +1158,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" -[[package]] -name = "easy-parallel" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946" - [[package]] name = "ecdsa" version = "0.13.4" @@ -1439,6 +1230,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embed-resource" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936c1354206a875581696369aef920e12396e93bbd251c43a7a3f3fa85023a7d" +dependencies = [ + "cc", + "rustc_version 0.4.0", + "toml", + "vswhom", + "winreg 0.10.1", +] + [[package]] name = "embed_plist" version = "1.2.2" @@ -1455,20 +1259,19 @@ dependencies = [ ] [[package]] -name = "enumflags2" -version = "0.7.4" +name = "enum-iterator" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b3ab37dc79652c9d85f1f7b6070d77d321d2467f5fe7b00d6b7a86c57b092ae" +checksum = "4eeac5c5edb79e4e39fe8439ef35207780a11f69c52cbe424ce3dfad4cb78de6" dependencies = [ - "enumflags2_derive", - "serde", + "enum-iterator-derive", ] [[package]] -name = "enumflags2_derive" -version = "0.7.4" +name = "enum-iterator-derive" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +checksum = "c134c37760b27a871ba422106eedbb8247da973a09e82558bf26d619c882b159" dependencies = [ "proc-macro2", "quote", @@ -1488,12 +1291,6 @@ dependencies = [ "termcolor", ] -[[package]] -name = "event-listener" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" - [[package]] name = "execute" version = "0.1.0" @@ -1527,6 +1324,16 @@ dependencies = [ "instant", ] +[[package]] +name = "fern" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bdd7b0849075e79ee9a1836df22c717d1eba30451796fdc631b04565dd11e2a" +dependencies = [ + "colored 1.9.3", + "log", +] + [[package]] name = "ff" version = "0.10.1" @@ -1569,19 +1376,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "fixed" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d38004fb072c44da88a322343c7c784bd1ff6f2e3f5b332050de60c5eea3e0e" -dependencies = [ - "az", - "bytemuck", - "half", - "serde", - "typenum", -] - [[package]] name = "flate2" version = "1.0.22" @@ -1805,9 +1599,9 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "413424d9818621fa3cfc8a3a915cdb89a7c3c507d56761b4ec83a9a98e587171" dependencies = [ - "gio-sys 0.15.6", - "glib-sys 0.15.6", - "gobject-sys 0.15.5", + "gio-sys", + "glib-sys", + "gobject-sys", "libc", "system-deps 6.0.2", ] @@ -1820,9 +1614,9 @@ checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", - "gio-sys 0.15.6", - "glib-sys 0.15.6", - "gobject-sys 0.15.5", + "gio-sys", + "glib-sys", + "gobject-sys", "libc", "pango-sys", "pkg-config", @@ -1836,7 +1630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" dependencies = [ "gdk-sys", - "glib-sys 0.15.6", + "glib-sys", "libc", "system-deps 6.0.2", "x11", @@ -1930,39 +1724,39 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", - "gio-sys 0.15.6", + "gio-sys", "glib", "libc", "once_cell", "thiserror", ] -[[package]] -name = "gio-sys" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0a41df66e57fcc287c4bcf74fc26b884f31901ea9792ec75607289b456f48fa" -dependencies = [ - "glib-sys 0.14.0", - "gobject-sys 0.14.0", - "libc", - "system-deps 3.2.0", - "winapi", -] - [[package]] name = "gio-sys" version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d0fa5052773f5a56b8ae47dab09d040f5d9ce1311f4f99006e16e9a08269296" dependencies = [ - "glib-sys 0.15.6", - "gobject-sys 0.15.5", + "glib-sys", + "gobject-sys", "libc", "system-deps 6.0.2", "winapi", ] +[[package]] +name = "git2" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "url", +] + [[package]] name = "glib" version = "0.15.6" @@ -1975,8 +1769,8 @@ dependencies = [ "futures-executor", "futures-task", "glib-macros", - "glib-sys 0.15.6", - "gobject-sys 0.15.5", + "glib-sys", + "gobject-sys", "libc", "once_cell", "smallvec 1.8.0", @@ -1991,23 +1785,13 @@ checksum = "41bfd8d227dead0829ac142454e97531b93f576d0805d779c42bfd799c65c572" dependencies = [ "anyhow", "heck 0.4.0", - "proc-macro-crate 1.1.3", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", "syn", ] -[[package]] -name = "glib-sys" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c1d60554a212445e2a858e42a0e48cece1bd57b311a19a9468f70376cf554ae" -dependencies = [ - "libc", - "system-deps 3.2.0", -] - [[package]] name = "glib-sys" version = "0.15.6" @@ -2037,24 +1821,13 @@ dependencies = [ "regex", ] -[[package]] -name = "gobject-sys" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa92cae29759dae34ab5921d73fff5ad54b3d794ab842c117e36cafc7994c3f5" -dependencies = [ - "glib-sys 0.14.0", - "libc", - "system-deps 3.2.0", -] - [[package]] name = "gobject-sys" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df6859463843c20cf3837e3a9069b6ab2051aeeadf4c899d33344f4aea83189a" dependencies = [ - "glib-sys 0.15.6", + "glib-sys", "libc", "system-deps 6.0.2", ] @@ -2115,9 +1888,9 @@ dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", - "gio-sys 0.15.6", - "glib-sys 0.15.6", - "gobject-sys 0.15.5", + "gio-sys", + "glib-sys", + "gobject-sys", "libc", "pango-sys", "system-deps 6.0.2", @@ -2130,7 +1903,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24f518afe90c23fba585b2d7697856f9e6a7bbc62f65588035e66f6afb01a2e9" dependencies = [ "anyhow", - "proc-macro-crate 1.1.3", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", @@ -2156,12 +1929,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - [[package]] name = "handlebars" version = "3.5.5" @@ -2269,9 +2036,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.6" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", @@ -2324,9 +2091,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "humantime-serde" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac34a56cfd4acddb469cc7fff187ed5ac36f498ba085caf8bbc725e3ff474058" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" dependencies = [ "humantime 2.1.0", "serde", @@ -2451,6 +2218,19 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "image" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e30ca2ecf7666107ff827a8e481de6a132a9b687ed3bb20bb1c144a36c00964" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", +] + [[package]] name = "indenter" version = "0.3.3" @@ -2470,9 +2250,9 @@ dependencies = [ [[package]] name = "infer" -version = "0.4.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f92b41dab759f9e8427c03f519c344a14655490b8db548dac1e57a75b3258391" +checksum = "20b2b533137b9cad970793453d4f921c2e91312a6d88b1085c07bc15fc51bb3b" dependencies = [ "cfb", ] @@ -2539,12 +2319,40 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c" dependencies = [ - "glib-sys 0.15.6", - "gobject-sys 0.15.5", + "glib-sys", + "gobject-sys", "libc", "system-deps 5.0.0", ] +[[package]] +name = "jni" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -2553,9 +2361,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" dependencies = [ "libc", ] @@ -2620,9 +2428,42 @@ 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 = "libgit2-sys" +version = "0.14.0+1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a00859c70c8a4f7218e6d1cc32875c4b55f6799445b842b0d8ed5e4c3d959b" +dependencies = [ + "cc", + "libc", + "libz-sys", + "pkg-config", +] + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "line-wrap" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" +dependencies = [ + "safemem", +] [[package]] name = "lock_api" @@ -2640,6 +2481,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ "cfg-if", + "serde", +] + +[[package]] +name = "logging" +version = "0.1.0" +dependencies = [ + "log", + "pretty_env_logger", ] [[package]] @@ -2740,15 +2590,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881" -[[package]] -name = "miniz_oxide" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435" -dependencies = [ - "adler32", -] - [[package]] name = "miniz_oxide" version = "0.4.4" @@ -2760,39 +2601,38 @@ dependencies = [ ] [[package]] -name = "mio" -version = "0.8.2" +name = "miniz_oxide" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ - "libc", - "log", - "miow", - "ntapi", - "wasi 0.11.0+wasi-snapshot-preview1", - "winapi", + "adler", ] [[package]] -name = "miow" -version = "0.3.7" +name = "mio" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ - "winapi", + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", ] [[package]] name = "mixnet-contract-common" version = "0.1.0" dependencies = [ - "az", + "bs58", "contracts-common", "cosmwasm-std", - "fixed", + "humantime-serde", "log", "schemars", "serde", + "serde_json", "serde_repr", "thiserror", "time 0.3.7", @@ -2831,9 +2671,9 @@ dependencies = [ [[package]] name = "ndk" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64d6af06fde0e527b1ba5c7b79a6cc89cfc46325b0b2887dffe8f70197e0c3c" +checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" dependencies = [ "bitflags", "jni-sys", @@ -2848,39 +2688,14 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e3c5cc68637e21fe8f077f6a1c9e0b9ca495bb74895226b476310f613325884" -[[package]] -name = "ndk-glue" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1454575120e3265d2442222299c711ace58ba417532ee4f0fc71b860016b93" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-macro", - "ndk-sys", -] - -[[package]] -name = "ndk-macro" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" -dependencies = [ - "darling 0.10.2", - "proc-macro-crate 0.1.5", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "ndk-sys" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] [[package]] name = "network-defaults" @@ -2901,34 +2716,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" -[[package]] -name = "nix" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" -dependencies = [ - "bitflags", - "cc", - "cfg-if", - "libc", - "memoffset", -] - [[package]] name = "nodrop" 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" @@ -2961,6 +2754,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg 1.1.0", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -2995,7 +2799,7 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d992b768490d7fe0d8586d9b5745f6c49f557da6d81dc982b1d167ad4edbb21" dependencies = [ - "proc-macro-crate 1.1.3", + "proc-macro-crate", "proc-macro2", "quote", "syn", @@ -3026,7 +2830,7 @@ dependencies = [ "schemars", "serde", "serde_json", - "strum 0.23.0", + "strum", "thiserror", "ts-rs", "url", @@ -3035,6 +2839,22 @@ dependencies = [ "vesting-contract-common", ] +[[package]] +name = "nym-wallet-recovery-cli" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "anyhow", + "argon2 0.4.0", + "base64", + "bip39", + "clap", + "log", + "logging", + "pretty_env_logger", + "serde_json", +] + [[package]] name = "nym-wallet-types" version = "1.0.0" @@ -3042,11 +2862,13 @@ dependencies = [ "config", "cosmrs", "cosmwasm-std", + "hex-literal", "mixnet-contract-common", + "network-defaults", "nym-types", "serde", "serde_json", - "strum 0.23.0", + "strum", "ts-rs", "validator-client", "vesting-contract", @@ -3055,7 +2877,7 @@ dependencies = [ [[package]] name = "nym_wallet" -version = "1.0.7" +version = "1.0.9" dependencies = [ "aes-gcm", "argon2 0.3.4", @@ -3064,15 +2886,17 @@ dependencies = [ "cfg-if", "chrono", "coconut-interface", - "colored", + "colored 2.0.0", "config", "cosmrs", "cosmwasm-std", "dirs", "dotenv", "eyre", + "fern", "futures", "itertools", + "k256", "log", "mixnet-contract-common", "nym-types", @@ -3083,7 +2907,8 @@ dependencies = [ "reqwest", "serde", "serde_json", - "strum 0.23.0", + "serde_repr", + "strum", "tauri", "tauri-build", "tauri-codegen", @@ -3126,6 +2951,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", + "objc_exception", ] [[package]] @@ -3139,6 +2965,15 @@ dependencies = [ "objc_id", ] +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + [[package]] name = "objc_id" version = "0.1.1" @@ -3150,9 +2985,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.9.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" [[package]] name = "opaque-debug" @@ -3168,12 +3003,12 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "open" -version = "2.1.0" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a82915836ef43159bb6a3c64d884c42329ccd0b8afdca737cf1e3dd701709dc" +checksum = "f23a407004a1033f53e93f9b45580d14de23928faad187384f891507c9b0c045" dependencies = [ "pathdiff", - "winapi", + "windows-sys", ] [[package]] @@ -3209,16 +3044,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "ordered-stream" -version = "0.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1" -dependencies = [ - "futures-core", - "pin-project-lite", -] - [[package]] name = "os_str_bytes" version = "6.0.0" @@ -3253,8 +3078,8 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7022c2fb88cd2d9d55e1a708a8c53a3ae8678234c4a54bf623400aeb7f31fac2" dependencies = [ - "glib-sys 0.15.6", - "gobject-sys 0.15.5", + "glib-sys", + "gobject-sys", "libc", "system-deps 6.0.2", ] @@ -3579,6 +3404,20 @@ version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" +[[package]] +name = "plist" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225" +dependencies = [ + "base64", + "indexmap", + "line-wrap", + "serde", + "time 0.3.7", + "xml-rs", +] + [[package]] name = "png" version = "0.11.0" @@ -3586,42 +3425,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0b0cabbbd20c2d7f06dbf015e06aad59b6ca3d9ed14848783e98af9aaf19925" dependencies = [ "bitflags", - "deflate 0.7.20", + "deflate", "inflate", "num-iter", ] [[package]] name = "png" -version = "0.16.8" +version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6" +checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c" dependencies = [ "bitflags", "crc32fast", - "deflate 0.8.6", - "miniz_oxide 0.3.7", + "flate2", + "miniz_oxide 0.5.4", ] -[[package]] -name = "polling" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" -dependencies = [ - "cfg-if", - "libc", - "log", - "wepoll-ffi", - "winapi", -] - -[[package]] -name = "pollster" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" - [[package]] name = "polyval" version = "0.5.3" @@ -3656,15 +3476,6 @@ dependencies = [ "log", ] -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - [[package]] name = "proc-macro-crate" version = "1.1.3" @@ -3957,38 +3768,13 @@ dependencies = [ [[package]] name = "raw-window-handle" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fba75eee94a9d5273a68c9e1e105d9cffe1ef700532325788389e5a83e2522b7" +checksum = "b800beb9b6e7d2df1fe337c9e3d04e3af22a124460fb4c30fcc22c9117cefb41" dependencies = [ "cty", ] -[[package]] -name = "rayon" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" -dependencies = [ - "autocfg 1.1.0", - "crossbeam-deque", - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "lazy_static", - "num_cpus", -] - [[package]] name = "rdrand" version = "0.4.0" @@ -4085,7 +3871,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "winreg 0.7.0", ] [[package]] @@ -4101,15 +3887,15 @@ dependencies = [ [[package]] name = "rfd" -version = "0.7.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aaf1d71ccd44689f7c2c72da1117fd8db71f72a76fe9b5c5dbb17ab903007e0" +checksum = "f121348fd3b9035ed11be1f028e8944263c30641f8c5deacf57a4320782fb402" dependencies = [ - "ashpd", "block", "dispatch", - "glib-sys 0.15.6", - "gobject-sys 0.15.5", + "embed-resource", + "glib-sys", + "gobject-sys", "gtk-sys", "js-sys", "lazy_static", @@ -4117,7 +3903,6 @@ dependencies = [ "objc", "objc-foundation", "objc_id", - "pollster", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", @@ -4206,6 +3991,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "same-file" version = "1.0.6" @@ -4342,6 +4133,9 @@ name = "semver" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d" +dependencies = [ + "serde", +] [[package]] name = "semver-parser" @@ -4452,7 +4246,7 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12e47be9471c72889ebafb5e14d5ff930d89ae7a67bbdb5f8abb564f845a927e" dependencies = [ - "darling 0.13.1", + "darling", "proc-macro2", "quote", "syn", @@ -4513,21 +4307,6 @@ dependencies = [ "digest 0.10.3", ] -[[package]] -name = "sha1" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" -dependencies = [ - "sha1_smol", -] - -[[package]] -name = "sha1_smol" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" - [[package]] name = "sha2" version = "0.9.9" @@ -4630,15 +4409,29 @@ dependencies = [ ] [[package]] -name = "soup2-sys" -version = "0.1.0" +name = "soup2" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f056675eda9a7417163e5f742bb119e8e1d385edd2ada8f7031a7230a3ec10a" +checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" dependencies = [ "bitflags", - "gio-sys 0.14.0", - "glib-sys 0.14.0", - "gobject-sys 0.14.0", + "gio", + "glib", + "libc", + "once_cell", + "soup2-sys", +] + +[[package]] +name = "soup2-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" +dependencies = [ + "bitflags", + "gio-sys", + "glib-sys", + "gobject-sys", "libc", "system-deps 5.0.0", ] @@ -4706,43 +4499,19 @@ dependencies = [ "quote", ] -[[package]] -name = "strsim" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" - [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "strum" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf86bbcfd1fa9670b7a129f64fc0c9fcbbfe4f1bc4210e9e98fe71ffc12cde2" - [[package]] name = "strum" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" dependencies = [ - "strum_macros 0.23.1", -] - -[[package]] -name = "strum_macros" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" -dependencies = [ - "heck 0.3.3", - "proc-macro2", - "quote", - "syn", + "strum_macros", ] [[package]] @@ -4796,24 +4565,6 @@ dependencies = [ "unicode-xid", ] -[[package]] -name = "system-deps" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "480c269f870722b3b08d2f13053ce0c2ab722839f472863c3e2d61ff3a1c2fa6" -dependencies = [ - "anyhow", - "cfg-expr 0.8.1", - "heck 0.3.3", - "itertools", - "pkg-config", - "strum 0.21.0", - "strum_macros 0.21.1", - "thiserror", - "toml", - "version-compare 0.0.11", -] - [[package]] name = "system-deps" version = "5.0.0" @@ -4842,9 +4593,9 @@ dependencies = [ [[package]] name = "tao" -version = "0.6.3" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c4c62fc16d698750c5c918088c8134c63e8661e65efc608fe614236c59462f4" +checksum = "f6fd7725dc1e593e9ecabd9fe49c112a204c8c8694db4182e78b2a5af490b1ae" dependencies = [ "bitflags", "cairo-rs", @@ -4860,39 +4611,32 @@ dependencies = [ "gdkx11-sys", "gio", "glib", - "glib-sys 0.15.6", + "glib-sys", "gtk", + "image", "instant", + "jni 0.19.0", "lazy_static", "libc", "log", "ndk", - "ndk-glue", + "ndk-context", "ndk-sys", "objc", - "parking_lot 0.11.2", + "once_cell", + "parking_lot 0.12.1", + "paste", + "png 0.17.6", "raw-window-handle", "scopeguard", "serde", - "tao-core-video-sys", "unicode-segmentation", + "uuid 1.1.2", "windows", - "windows_macros", + "windows-implement", "x11-dl", ] -[[package]] -name = "tao-core-video-sys" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271450eb289cb4d8d0720c6ce70c72c8c858c93dd61fc625881616752e6b98f6" -dependencies = [ - "cfg-if", - "core-foundation-sys", - "libc", - "objc", -] - [[package]] name = "tar" version = "0.4.38" @@ -4906,16 +4650,15 @@ dependencies = [ [[package]] name = "tauri" -version = "1.0.0-rc.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3571de0bcfd1f4f7599cbb3fe42f28ea9f52c3e89914ff04eaba68b5ee36bb51" +checksum = "e1a56a8b125069c2682bd31610109b4436c050c74447bee1078217a0325c1add" dependencies = [ + "anyhow", "attohttpc", "base64", - "bincode", - "cfg_aliases", + "cocoa", "dirs-next", - "either", "embed_plist", "flate2", "futures", @@ -4923,9 +4666,11 @@ dependencies = [ "glib", "glob", "gtk", + "heck 0.4.0", "http", "ignore", "minisign-verify", + "objc", "once_cell", "open", "percent-encoding", @@ -4946,20 +4691,27 @@ dependencies = [ "tauri-utils", "tempfile", "thiserror", + "time 0.3.7", "tokio", "url", - "uuid", + "uuid 1.1.2", + "webkit2gtk", + "webview2-com", + "windows", "zip", ] [[package]] name = "tauri-build" -version = "1.0.0-rc.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4dab79d01b8e429cb5bd98d9da90cbc1bedbd87d536d16adcf269c584bf6ae0" +checksum = "acafb1c515c5d14234a294461bd43c723639a84891a45f6a250fd3441ad2e8ed" dependencies = [ "anyhow", "cargo_toml", + "heck 0.4.0", + "json-patch", + "semver 1.0.6", "serde_json", "tauri-utils", "winres", @@ -4967,30 +4719,35 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "1.0.0-rc.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d06ecdbd6beaf7348259f82885742f013c0c96ac139445ec7c4b8050cd18349" +checksum = "16d62a3c8790d6cba686cea6e3f7f569d12c662c3274c2d165a4fd33e3871b72" dependencies = [ "base64", - "blake3", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.6", "proc-macro2", "quote", "regex", + "semver 1.0.6", "serde", "serde_json", "sha2 0.10.2", "tauri-utils", "thiserror", - "uuid", + "time 0.3.7", + "uuid 1.1.2", "walkdir", - "zstd", ] [[package]] name = "tauri-macros" -version = "1.0.0-rc.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba9ab86a4e81b31b227800c120b452dae137754e06d75b75c41c06cd06d301e2" +checksum = "7296fa17996629f43081e1c66d554703900187ed900c5bf46f97f0bcfb069278" dependencies = [ "heck 0.4.0", "proc-macro2", @@ -5002,36 +4759,39 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "0.3.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "851cd65e2c9df4dd70a4f9e115fb2441ac89f1c80df79de0c15a448b4acd7768" +checksum = "4e4cff3b4d9469727fa2107c4b3d2eda110df1ba45103fb420178e536362fae4" dependencies = [ "gtk", "http", "http-range", "infer", + "raw-window-handle", "serde", "serde_json", "tauri-utils", "thiserror", - "uuid", + "uuid 1.1.2", "webview2-com", "windows", ] [[package]] name = "tauri-runtime-wry" -version = "0.3.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0184f95e065fb80afadf53f0a5f87a75af2da774d0524fe8cb2976fbec4a0bf2" +checksum = "3fa8c4edaf01d8b556e7172c844b1b4dd3399adcd1a606bd520fc3e65f698546" dependencies = [ + "cocoa", "gtk", - "ico", - "infer", - "png 0.16.8", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", "tauri-runtime", "tauri-utils", - "uuid", + "uuid 1.1.2", + "webkit2gtk", "webview2-com", "windows", "wry", @@ -5039,27 +4799,29 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "1.0.0-rc.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad38ad698838886bf22ecb91c4b3d1ce178fdff7901ac7bff370103a4f51e59" +checksum = "12ff4b68d9faeb57c9c727bf58c9c9768d2b67d8e84e62ce6146e7859a2e9c6b" dependencies = [ + "brotli", "ctor", "glob", "heck 0.4.0", "html5ever", "json-patch", "kuchiki", + "memchr", "phf 0.10.1", "proc-macro2", "quote", + "semver 1.0.6", "serde", "serde_json", "serde_with", - "serialize-to-javascript", "thiserror", "url", "walkdir", - "zstd", + "windows", ] [[package]] @@ -5168,7 +4930,7 @@ dependencies = [ "tokio", "tracing", "url", - "uuid", + "uuid 0.8.2", "walkdir", ] @@ -5263,16 +5025,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", @@ -5533,6 +5295,12 @@ name = "uuid" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" + +[[package]] +name = "uuid" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" dependencies = [ "getrandom 0.2.5", ] @@ -5544,11 +5312,11 @@ dependencies = [ "bs58", "coconut-interface", "cosmrs", + "cosmwasm-std", "getset", "mixnet-contract-common", "schemars", "serde", - "ts-rs", ] [[package]] @@ -5560,8 +5328,9 @@ dependencies = [ "bip39", "coconut-bandwidth-contract-common", "coconut-interface", - "colored", + "colored 2.0.0", "config", + "contracts-common", "cosmrs", "cosmwasm-std", "cw3", @@ -5598,6 +5367,23 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vergen" +version = "5.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cf88d94e969e7956d924ba70741316796177fa0c79a2c9f4ab04998d96e966e" +dependencies = [ + "anyhow", + "cfg-if", + "chrono", + "enum-iterator", + "getset", + "git2", + "rustc_version 0.4.0", + "rustversion", + "thiserror", +] + [[package]] name = "version-compare" version = "0.0.11" @@ -5618,14 +5404,16 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" -version = "1.0.1" +version = "1.1.0" dependencies = [ + "contracts-common", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", "schemars", "serde", "thiserror", + "vergen", "vesting-contract-common", ] @@ -5633,13 +5421,32 @@ dependencies = [ name = "vesting-contract-common" version = "0.1.0" dependencies = [ - "config", + "contracts-common", "cosmwasm-std", - "cw-storage-plus", + "log", "mixnet-contract-common", "schemars", "serde", - "ts-rs", +] + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39" +dependencies = [ + "cc", + "libc", ] [[package]] @@ -5659,21 +5466,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wallet-recovery-cli" -version = "0.1.0" -dependencies = [ - "aes-gcm", - "anyhow", - "argon2 0.4.0", - "base64", - "bip39", - "clap", - "log", - "pretty_env_logger", - "serde_json", -] - [[package]] name = "want" version = "0.3.0" @@ -5780,48 +5572,49 @@ dependencies = [ [[package]] name = "webkit2gtk" -version = "0.17.1" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cbd39499e917de9dad36eb11c09f665eb984d432638ae7971feed98eb96df88" +checksum = "29952969fb5e10fe834a52eb29ad0814ccdfd8387159b0933edf1344a1c9cdcc" dependencies = [ "bitflags", "cairo-rs", "gdk", "gdk-sys", "gio", - "gio-sys 0.15.6", + "gio-sys", "glib", - "glib-sys 0.15.6", - "gobject-sys 0.15.5", + "glib-sys", + "gobject-sys", "gtk", "gtk-sys", "javascriptcore-rs", "libc", "once_cell", + "soup2", "webkit2gtk-sys", ] [[package]] name = "webkit2gtk-sys" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcce6f1e0fc7715d651dba29875741509f5fc12f4e2976907272a74405f2b01" +checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" dependencies = [ "atk-sys", "bitflags", "cairo-sys-rs", "gdk-pixbuf-sys", "gdk-sys", - "gio-sys 0.15.6", - "glib-sys 0.15.6", - "gobject-sys 0.15.5", + "gio-sys", + "glib-sys", + "gobject-sys", "gtk-sys", "javascriptcore-rs-sys", "libc", "pango-sys", "pkg-config", "soup2-sys", - "system-deps 5.0.0", + "system-deps 6.0.2", ] [[package]] @@ -5845,21 +5638,21 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.13.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb8e90ac2d9ce39cdb70017aaec641be09fbdd702b7b332b9896d053eb469524" +checksum = "a489a9420acabb3c2ed0434b6f71f6b56b9485ec32665a28dec1ee186d716e0f" dependencies = [ "webview2-com-macros", "webview2-com-sys", "windows", - "windows_macros", + "windows-implement", ] [[package]] name = "webview2-com-macros" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515c6c82fcee93f6edaacc72c8e233dbe4ff3ca569dce1901dfc36c404a3e99" +checksum = "eaebe196c01691db62e9e4ca52c5ef1e4fd837dcae27dae3ada599b5a8fd05ac" dependencies = [ "proc-macro2", "quote", @@ -5868,9 +5661,9 @@ dependencies = [ [[package]] name = "webview2-com-sys" -version = "0.13.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92160310b3322397e4ff8a8285a7429d73a07a68fda44ee80879605b93e53f76" +checksum = "0258c53ee9adc0a4f8ba1c8c317588f7a58c7048a55b621d469ba75ab3709ca1" dependencies = [ "regex", "serde", @@ -5880,15 +5673,6 @@ dependencies = [ "windows-bindgen", ] -[[package]] -name = "wepoll-ffi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" -dependencies = [ - "cc", -] - [[package]] name = "wildmatch" version = "2.1.0" @@ -5928,27 +5712,44 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.30.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b749ebd2304aa012c5992d11a25d07b406bdbe5f79d371cb7a918ce501a19eb0" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" dependencies = [ - "windows_aarch64_msvc 0.30.0", - "windows_i686_gnu 0.30.0", - "windows_i686_msvc 0.30.0", - "windows_x86_64_gnu 0.30.0", - "windows_x86_64_msvc 0.30.0", + "windows-implement", + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", ] [[package]] name = "windows-bindgen" -version = "0.30.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "944c545fcae9dd66488308f8b69aa3ba34f53714416ecfcdcbbfa4b6821e27c6" +checksum = "0bed7be31ade0af08fec9b5343e9edcc005d22b1f11859b8a59b24797f5858e8" dependencies = [ - "windows_quote", - "windows_reader", + "windows-metadata", + "windows-tokens", ] +[[package]] +name = "windows-implement" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a1062e555f7d9d66fd1130ed4f7c6ec41a47529ee0850cd0e926d95b26bb14" +dependencies = [ + "syn", + "windows-tokens", +] + +[[package]] +name = "windows-metadata" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f33f2b90a6664e369c41ab5ff262d06f048fc9685d9bf8a0e99a47750bb0463" + [[package]] name = "windows-sys" version = "0.36.1" @@ -5963,10 +5764,10 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_msvc" -version = "0.30.0" +name = "windows-tokens" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca" +checksum = "3263d25f1170419995b78ff10c06b949e8a986c35c208dc24333c64753a87169" [[package]] name = "windows_aarch64_msvc" @@ -5975,20 +5776,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" [[package]] -name = "windows_gen" -version = "0.30.0" +name = "windows_aarch64_msvc" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30dff4d91d22520628bb94b66f2bb313cb16a09a515a32320a84a1b449bc94c0" -dependencies = [ - "windows_quote", - "windows_reader", -] - -[[package]] -name = "windows_i686_gnu" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" [[package]] name = "windows_i686_gnu" @@ -5997,10 +5788,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" [[package]] -name = "windows_i686_msvc" -version = "0.30.0" +name = "windows_i686_gnu" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" [[package]] name = "windows_i686_msvc" @@ -6009,34 +5800,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" [[package]] -name = "windows_macros" -version = "0.30.0" +name = "windows_i686_msvc" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62ae44ab917e9005fe710d99d52d227ca0164b10a09be90649142cc3fab825d3" -dependencies = [ - "syn", - "windows_gen", - "windows_quote", - "windows_reader", -] - -[[package]] -name = "windows_quote" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71f02c51a77e6248c1206aaa920802c32d50a05205e229b118d7f3afd3036667" - -[[package]] -name = "windows_reader" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e44e6df0da993cda589c5ac852272fbb2a0ead67a031a017dd3eac11528a2d72" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" [[package]] name = "windows_x86_64_gnu" @@ -6045,10 +5812,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" [[package]] -name = "windows_x86_64_msvc" -version = "0.30.0" +name = "windows_x86_64_gnu" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" [[package]] name = "windows_x86_64_msvc" @@ -6056,6 +5823,12 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" + [[package]] name = "winreg" version = "0.7.0" @@ -6065,6 +5838,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winres" version = "0.1.12" @@ -6076,10 +5858,11 @@ dependencies = [ [[package]] name = "wry" -version = "0.13.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9514586e5c964d30cc7123c9aea9880ff7b3cb1c43f6a1dc8703941eb72ac89f" +checksum = "ce19dddbd3ce01dc8f14eb6d4c8f914123bf8379aaa838f6da4f981ff7104a3f" dependencies = [ + "block", "cocoa", "core-graphics", "gdk", @@ -6087,6 +5870,7 @@ dependencies = [ "glib", "gtk", "http", + "jni 0.18.0", "libc", "log", "objc", @@ -6101,7 +5885,7 @@ dependencies = [ "webkit2gtk-sys", "webview2-com", "windows", - "windows_macros", + "windows-implement", ] [[package]] @@ -6135,65 +5919,10 @@ dependencies = [ ] [[package]] -name = "zbus" -version = "2.1.1" +name = "xml-rs" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb86f3d4592e26a48b2719742aec94f8ae6238ebde20d98183ee185d1275e9a" -dependencies = [ - "async-broadcast", - "async-channel", - "async-executor", - "async-io", - "async-lock", - "async-recursion", - "async-task", - "async-trait", - "byteorder", - "derivative", - "enumflags2", - "event-listener", - "futures-core", - "futures-sink", - "futures-util", - "hex", - "lazy_static", - "nix", - "once_cell", - "ordered-stream", - "rand 0.8.5", - "serde", - "serde_repr", - "sha1", - "static_assertions", - "winapi", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36823cc10fddc3c6b19f048903262dacaf8274170e9a255784bdd8b4570a8040" -dependencies = [ - "proc-macro-crate 1.1.3", - "proc-macro2", - "quote", - "regex", - "syn", -] - -[[package]] -name = "zbus_names" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45dfcdcf87b71dad505d30cc27b1b7b88a64b6d1c435648f48f9dbc1fdc4b7e1" -dependencies = [ - "serde", - "static_assertions", - "zvariant", -] +checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" [[package]] name = "zeroize" @@ -6218,69 +5947,11 @@ dependencies = [ [[package]] name = "zip" -version = "0.5.13" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +checksum = "bf225bcf73bb52cbb496e70475c7bd7a3f769df699c0020f6c7bd9a96dcf0b8d" dependencies = [ "byteorder", - "bzip2", "crc32fast", - "flate2", - "thiserror", - "time 0.1.43", -] - -[[package]] -name = "zstd" -version = "0.10.0+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b1365becbe415f3f0fcd024e2f7b45bacfb5bdd055f0dc113571394114e7bdd" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "4.1.4+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f7cd17c9af1a4d6c24beb1cc54b17e2ef7b593dc92f19e9d9acad8b182bbaee" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "1.6.3+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "zvariant" -version = "3.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ea5dc38b2058fae6a5b79009388143dadce1e91c26a67f984a0fc0381c8033" -dependencies = [ - "byteorder", - "enumflags2", - "libc", - "serde", - "static_assertions", - "zvariant_derive", -] - -[[package]] -name = "zvariant_derive" -version = "3.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2cecc5a61c2a053f7f653a24cd15b3b0195d7f7ddb5042c837fb32e161fb7a" -dependencies = [ - "proc-macro-crate 1.1.3", - "proc-macro2", - "quote", - "syn", + "crossbeam-utils", ] diff --git a/nym-wallet/Cargo.toml b/nym-wallet/Cargo.toml index bb834e3d59..80e269fba7 100644 --- a/nym-wallet/Cargo.toml +++ b/nym-wallet/Cargo.toml @@ -3,6 +3,6 @@ resolver = "2" members = [ "src-tauri", - "wallet-recovery-cli", + "nym-wallet-recovery-cli", "nym-wallet-types" ] diff --git a/nym-wallet/wallet-recovery-cli/Cargo.toml b/nym-wallet/nym-wallet-recovery-cli/Cargo.toml similarity index 69% rename from nym-wallet/wallet-recovery-cli/Cargo.toml rename to nym-wallet/nym-wallet-recovery-cli/Cargo.toml index c56f9a6068..909467a6e0 100644 --- a/nym-wallet/wallet-recovery-cli/Cargo.toml +++ b/nym-wallet/nym-wallet-recovery-cli/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "wallet-recovery-cli" +name = "nym-wallet-recovery-cli" version = "0.1.0" edition = "2021" @@ -11,7 +11,9 @@ anyhow = "1.0" argon2 = "0.4" base64 = "0.13" bip39 = "1.0" -clap = { version = "3.0", features = ["derive"] } +clap = { version = "3.2", features = ["derive"] } log = "0.4" pretty_env_logger = "0.4" serde_json = "1.0.0" + +logging = { path = "../../common/logging" } \ No newline at end of file diff --git a/nym-wallet/wallet-recovery-cli/src/main.rs b/nym-wallet/nym-wallet-recovery-cli/src/main.rs similarity index 95% rename from nym-wallet/wallet-recovery-cli/src/main.rs rename to nym-wallet/nym-wallet-recovery-cli/src/main.rs index e60253ecf9..7cb2c3e0d7 100644 --- a/nym-wallet/wallet-recovery-cli/src/main.rs +++ b/nym-wallet/nym-wallet-recovery-cli/src/main.rs @@ -12,6 +12,7 @@ use aes_gcm::{aead::Aead, Aes256Gcm, Key, NewAead, Nonce}; use anyhow::{anyhow, Result}; use argon2::{Algorithm, Argon2, Params, Version}; use clap::Parser; +use logging::setup_logging; use serde_json::Value; // Mostly defaults @@ -32,15 +33,15 @@ enum DecryptedAccount { struct Args { /// Password used to attempt to decrypt the logins found in the file. The option can be /// provided multiple times for files that require multiple passwords. - #[clap(short, long, min_values(1), required = true)] + #[clap(short, long, value_parser, required = true)] password: Vec<String>, /// Path to the wallet file that will be decrypted. - #[clap(short, long)] + #[clap(short, long, value_parser)] file: String, /// Raw mode. Skips trying to parse the decrypted content. - #[clap(short, long)] + #[clap(short, long, action)] raw: bool, } @@ -61,18 +62,6 @@ fn main() -> Result<()> { decrypt_file(file, &args.password, &parse) } -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - log_builder.init(); -} - fn decrypt_file(file: File, passwords: &[String], parse: &ParseMode) -> Result<()> { let json_file: Value = serde_json::from_reader(file)?; @@ -170,7 +159,7 @@ fn get_login_entry(login: &Value) -> Result<(&str, &str, &str)> { } fn base64_decode(ciphertext: &str, iv: &str, salt: &str) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>)> { - 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 2a6fb8836b..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", @@ -26,7 +28,7 @@ nym-types = { path = "../../common/types" } [features] default = [] -generate-ts = [] +generate-ts = ["mixnet-contract-common/generate-ts", "nym-types/generate-ts"] diff --git a/nym-wallet/nym-wallet-types/src/admin.rs b/nym-wallet/nym-wallet-types/src/admin.rs index 9e2c30aefd..183ecb7a8b 100644 --- a/nym-wallet/nym-wallet-types/src/admin.rs +++ b/nym-wallet/nym-wallet-types/src/admin.rs @@ -1,10 +1,10 @@ -use std::convert::TryFrom; - -use cosmwasm_std::Uint128; -use serde::{Deserialize, Serialize}; +// Copyright 2022 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 use mixnet_contract_common::ContractStateParams; +use nym_types::currency::{DecCoin, RegisteredCoins}; use nym_types::error::TypesError; +use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( @@ -13,35 +13,44 @@ use nym_types::error::TypesError; )] #[derive(Serialize, Deserialize, Debug)] pub struct TauriContractStateParams { - minimum_mixnode_pledge: String, - minimum_gateway_pledge: String, - mixnode_rewarded_set_size: u32, - mixnode_active_set_size: u32, - staking_supply: String, + minimum_mixnode_pledge: DecCoin, + minimum_gateway_pledge: DecCoin, + minimum_mixnode_delegation: Option<DecCoin>, } -impl From<ContractStateParams> for TauriContractStateParams { - fn from(p: ContractStateParams) -> TauriContractStateParams { - TauriContractStateParams { - minimum_mixnode_pledge: p.minimum_mixnode_pledge.to_string(), - minimum_gateway_pledge: p.minimum_gateway_pledge.to_string(), - mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, - mixnode_active_set_size: p.mixnode_active_set_size, - staking_supply: p.staking_supply.to_string(), - } +impl TauriContractStateParams { + pub fn from_mixnet_contract_contract_state_params( + state_params: ContractStateParams, + reg: &RegisteredCoins, + ) -> Result<Self, TypesError> { + Ok(TauriContractStateParams { + minimum_mixnode_pledge: reg + .attempt_convert_to_display_dec_coin(state_params.minimum_mixnode_pledge.into())?, + minimum_gateway_pledge: reg + .attempt_convert_to_display_dec_coin(state_params.minimum_gateway_pledge.into())?, + minimum_mixnode_delegation: state_params + .minimum_mixnode_delegation + .map(|min_del| reg.attempt_convert_to_display_dec_coin(min_del.into())) + .transpose()?, + }) } -} -impl TryFrom<TauriContractStateParams> for ContractStateParams { - type Error = TypesError; - - fn try_from(p: TauriContractStateParams) -> Result<ContractStateParams, Self::Error> { + pub fn try_convert_to_mixnet_contract_params( + self, + reg: &RegisteredCoins, + ) -> Result<ContractStateParams, TypesError> { Ok(ContractStateParams { - minimum_mixnode_pledge: Uint128::try_from(p.minimum_mixnode_pledge.as_str())?, - minimum_gateway_pledge: Uint128::try_from(p.minimum_gateway_pledge.as_str())?, - mixnode_rewarded_set_size: p.mixnode_rewarded_set_size, - mixnode_active_set_size: p.mixnode_active_set_size, - staking_supply: Uint128::try_from(p.staking_supply.as_str())?, + minimum_mixnode_delegation: self + .minimum_mixnode_delegation + .map(|min_del| reg.attempt_convert_to_base_coin(min_del)) + .transpose()? + .map(Into::into), + minimum_mixnode_pledge: reg + .attempt_convert_to_base_coin(self.minimum_mixnode_pledge)? + .into(), + minimum_gateway_pledge: reg + .attempt_convert_to_base_coin(self.minimum_gateway_pledge)? + .into(), }) } } diff --git a/nym-wallet/nym-wallet-types/src/epoch.rs b/nym-wallet/nym-wallet-types/src/epoch.rs deleted file mode 100644 index 8e4968df07..0000000000 --- a/nym-wallet/nym-wallet-types/src/epoch.rs +++ /dev/null @@ -1,26 +0,0 @@ -use mixnet_contract_common::Interval; -use serde::{Deserialize, Serialize}; - -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/Epoch.ts") -)] -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize)] -pub struct Epoch { - id: u32, - start: i64, - end: i64, - duration_seconds: u64, -} - -impl From<Interval> for Epoch { - fn from(interval: Interval) -> Self { - Self { - id: interval.id(), - start: interval.start_unix_timestamp(), - end: interval.end_unix_timestamp(), - duration_seconds: interval.length().as_secs(), - } - } -} diff --git a/nym-wallet/nym-wallet-types/src/interval.rs b/nym-wallet/nym-wallet-types/src/interval.rs new file mode 100644 index 0000000000..c4937b4922 --- /dev/null +++ b/nym-wallet/nym-wallet-types/src/interval.rs @@ -0,0 +1,35 @@ +// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net> +// SPDX-License-Identifier: Apache-2.0 + +use mixnet_contract_common::{EpochId, Interval as ContractInterval, IntervalId}; +use serde::{Deserialize, Serialize}; + +// TODO: ask @MS why we can't just use ContractInterval directly +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export_to = "nym-wallet/src/types/rust/Interval.ts") +)] +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize)] +pub struct Interval { + id: IntervalId, + epochs_in_interval: u32, + + current_epoch_start_unix: i64, + current_epoch_id: EpochId, + epoch_length_seconds: u64, + total_elapsed_epochs: EpochId, +} + +impl From<ContractInterval> for Interval { + fn from(contract_interval: ContractInterval) -> Self { + Interval { + id: contract_interval.current_interval_id(), + epochs_in_interval: contract_interval.epochs_in_interval(), + current_epoch_start_unix: contract_interval.current_epoch_start_unix_timestamp(), + current_epoch_id: contract_interval.current_epoch_id(), + epoch_length_seconds: contract_interval.epoch_length_secs(), + total_elapsed_epochs: contract_interval.total_elapsed_epochs(), + } + } +} diff --git a/nym-wallet/nym-wallet-types/src/lib.rs b/nym-wallet/nym-wallet-types/src/lib.rs index d6e6600a65..91a50e5691 100644 --- a/nym-wallet/nym-wallet-types/src/lib.rs +++ b/nym-wallet/nym-wallet-types/src/lib.rs @@ -1,5 +1,5 @@ pub mod admin; pub mod app; -pub mod epoch; +pub mod interval; pub mod network; pub mod network_config; diff --git a/nym-wallet/nym-wallet-types/src/network.rs b/nym-wallet/nym-wallet-types/src/network.rs index 19bf580b47..1be3c80732 100644 --- a/nym-wallet/nym-wallet-types/src/network.rs +++ b/nym-wallet/nym-wallet-types/src/network.rs @@ -1,10 +1,10 @@ // Copyright 2021 - Nym Technologies SA <contact@nymtech.net> // SPDX-License-Identifier: Apache-2.0 -use config::defaults::all::Network as ConfigNetwork; -use config::defaults::{mainnet, qa, sandbox, DenomDetails}; +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)] @@ -48,6 +48,10 @@ impl Network { Network::MAINNET => mainnet::MIX_DENOM.display, } } + + pub fn default_zero_mix_display_coin(&self) -> DecCoin { + DecCoin::zero(self.display_mix_denom()) + } } impl Default for Network { @@ -62,23 +66,141 @@ impl fmt::Display for Network { } } -impl From<ConfigNetwork> for Network { - fn from(network: ConfigNetwork) -> Self { +impl From<Network> 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<Network> 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<Self, Self::Err> { + 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<String> { + 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<ValidatorDetails> { + 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<ValidatorDetails> { + 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 526a1385b4..2f8830b620 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -34,9 +34,11 @@ "@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", + "lodash": "^4.17.21", "notistack": "^2.0.3", "npm-run-all": "^4.1.5", "qrcode.react": "^1.0.1", @@ -45,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", @@ -61,9 +64,10 @@ "@pmmmwh/react-refresh-webpack-plugin": "^0.5.4", "@storybook/react": "^6.5.8", "@svgr/webpack": "^6.1.1", - "@tauri-apps/cli": "^1.0.0-rc.5", + "@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/public/log.html b/nym-wallet/public/log.html new file mode 100644 index 0000000000..5ced2243f1 --- /dev/null +++ b/nym-wallet/public/log.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>Nym Wallet Logs + + +
+ + diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 1ecea8e852..44bde03d68 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym_wallet" -version = "1.0.8" +version = "1.0.9" description = "Nym Native Wallet" authors = ["Nym Technologies SA"] license = "" @@ -13,10 +13,10 @@ rust-version = "1.58" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] -tauri-build = { version = "=1.0.0-rc.2", features = [] } +tauri-build = { version = "=1.0.4", features = [] } -tauri-codegen = "=1.0.0-rc.1" -tauri-macros = "=1.0.0-rc.1" +tauri-codegen = "=1.0.4" +tauri-macros = "=1.0.4" [dependencies] bip39 = "1.0" @@ -26,22 +26,25 @@ colored = "2.0" dirs = "4.0" dotenv = "0.15.0" eyre = "0.6.5" +fern = { version = "0.6.1", features = ["colored"] } futures = "0.3.15" itertools = "0.10" -log = "0.4" +log = { version = "0.4", features = ["serde"] } once_cell = "1.7.2" pretty_env_logger = "0.4" rand = "0.6.5" reqwest = {version = "0.11.9", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +serde_repr = "0.1" strum = { version = "0.23", features = ["derive"] } -tauri = { version = "=1.0.0-rc.2", features = ["clipboard-all", "shell-open", "updater", "window-maximize"] } +tauri = { version = "=1.0.5", features = ["clipboard-all", "shell-open", "updater", "window-maximize"] } tendermint-rpc = "0.23.0" thiserror = "1.0" tokio = { version = "1.10", features = ["full"] } toml = "0.5.8" url = "2.2" +k256 = { version = "0.10", features = ["ecdsa", "sha256"] } aes-gcm = "0.9.4" argon2 = { version = "0.3.2", features = ["std"] } diff --git a/nym-wallet/src-tauri/src/config/mod.rs b/nym-wallet/src-tauri/src/config/mod.rs index 6ac65f3b45..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::*; @@ -552,7 +635,7 @@ api_url = 'https://baz/api' .next() .map(|v| v.nymd_url) .unwrap(); - assert_eq!(nymd_url.as_ref(), "https://rpc.nyx.nodes.guru/"); + assert_eq!(nymd_url.as_ref(), "https://rpc.nymtech.net/"); let api_url = config .get_base_validators(WalletNetwork::MAINNET) diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 64a9bd7fe1..46d8840947 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -69,6 +69,11 @@ pub enum BackendError { #[from] source: reqwest::Error, }, + #[error("{source}")] + K256Error { + #[from] + source: k256::ecdsa::Error, + }, #[error("failed to encrypt the given data with the provided password")] EncryptionError, #[error("failed to decrypt the given data with the provided password")] @@ -111,6 +116,10 @@ pub enum BackendError { UnknownCoinDenom(String), #[error("Network {network} doesn't have any associated registered coin denoms")] NoCoinsRegistered { network: Network }, + #[error("Signature error {0}")] + SignatureError(String), + #[error("Unable to open a new window")] + NewWindowError, } impl Serialize for BackendError { diff --git a/nym-wallet/src-tauri/src/log.rs b/nym-wallet/src-tauri/src/log.rs new file mode 100644 index 0000000000..b673d023ae --- /dev/null +++ b/nym-wallet/src-tauri/src/log.rs @@ -0,0 +1,113 @@ +use std::str::FromStr; + +use fern::colors::ColoredLevelConfig; +use serde::Serialize; +use serde_repr::{Deserialize_repr, Serialize_repr}; +use tauri::Manager; + +pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerError> { + let colors = ColoredLevelConfig::new(); + let base_config = fern::Dispatch::new() + .level(global_level()) + .filter_lowlevel_external_components() + .show_operations(); + + let stdout_config = fern::Dispatch::new() + .format(move |out, message, record| { + out.finish(format_args!( + "{}[{}][{}] {}", + chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), + record.target(), + colors.color(record.level()), + message, + )) + }) + .chain(std::io::stdout()); + + let tauri_event_config = fern::Dispatch::new() + .format(move |out, message, record| { + out.finish(format_args!( + "{}[{}] {}", + chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"), + record.target(), + message, + )) + }) + .chain(fern::Output::call(move |record| { + let msg = LogMessage { + message: record.args().to_string(), + level: record.level().into(), + }; + app_handle.emit_all("log://log", msg).unwrap(); + })); + + base_config + .chain(stdout_config) + .chain(tauri_event_config) + .apply() +} + +trait FernExt { + fn show_operations(self) -> Self; + fn filter_lowlevel_external_components(self) -> Self; +} + +impl FernExt for fern::Dispatch { + fn show_operations(self) -> Self { + if ::std::env::var("RUST_TRACE_OPERATIONS").is_ok() { + self.level_for("nym_wallet::operations", log::LevelFilter::Trace) + } else { + self + } + } + + fn filter_lowlevel_external_components(self) -> Self { + self.level_for("hyper", log::LevelFilter::Warn) + .level_for("tokio_reactor", log::LevelFilter::Warn) + .level_for("reqwest", log::LevelFilter::Warn) + .level_for("mio", log::LevelFilter::Warn) + .level_for("want", log::LevelFilter::Warn) + .level_for("sled", log::LevelFilter::Warn) + .level_for("tungstenite", log::LevelFilter::Warn) + .level_for("tokio_tungstenite", log::LevelFilter::Warn) + .level_for("rustls", log::LevelFilter::Warn) + .level_for("tokio_util", log::LevelFilter::Warn) + } +} + +fn global_level() -> log::LevelFilter { + if let Ok(s) = ::std::env::var("RUST_LOG") { + log::LevelFilter::from_str(&s).unwrap_or(log::LevelFilter::Info) + } else { + log::LevelFilter::Info + } +} + +#[derive(Debug, Serialize, Clone)] +struct LogMessage { + message: String, + level: LogLevel, +} + +// Serialize to u16 instead of strings. +#[derive(Debug, Clone, Deserialize_repr, Serialize_repr)] +#[repr(u16)] +enum LogLevel { + Trace = 1, + Debug, + Info, + Warn, + Error, +} + +impl From for LogLevel { + fn from(level: log::Level) -> Self { + match level { + log::Level::Trace => LogLevel::Trace, + log::Level::Debug => LogLevel::Debug, + log::Level::Info => LogLevel::Info, + log::Level::Warn => LogLevel::Warn, + log::Level::Error => LogLevel::Error, + } + } +} diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 488b26e7a9..9bd44028c2 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -3,11 +3,22 @@ windows_subsystem = "windows" )] +use tauri::{Manager, Menu}; + use mixnet_contract_common::{Gateway, MixNode}; -use tauri::Menu; + +use crate::menu::AddDefaultSubmenus; +use crate::operations::help; +use crate::operations::mixnet; +use crate::operations::signatures; +use crate::operations::simulate; +use crate::operations::validator_api; +use crate::operations::vesting; +use crate::state::WalletState; mod config; mod error; +mod log; mod menu; mod network_config; mod operations; @@ -16,19 +27,11 @@ mod state; mod utils; mod wallet_storage; -use crate::menu::AddDefaultSubmenus; -use crate::operations::mixnet; -use crate::operations::simulate; -use crate::operations::validator_api; -use crate::operations::vesting; - -use crate::state::WalletState; - #[allow(clippy::too_many_lines)] fn main() { dotenv::dotenv().ok(); - setup_logging(); + let context = tauri::generate_context!(); tauri::Builder::default() .manage(WalletState::default()) .invoke_handler(tauri::generate_handler![ @@ -53,28 +56,28 @@ fn main() { mixnet::bond::bond_gateway, mixnet::bond::bond_mixnode, mixnet::bond::gateway_bond_details, - mixnet::bond::get_operator_rewards, + mixnet::bond::get_pending_operator_rewards, mixnet::bond::mixnode_bond_details, mixnet::bond::unbond_gateway, mixnet::bond::unbond_mixnode, - mixnet::bond::update_mixnode, + mixnet::bond::update_mixnode_cost_params, + 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_delegator_rewards, + mixnet::delegate::get_pending_delegator_rewards, mixnet::delegate::get_pending_delegation_events, mixnet::delegate::get_delegation_summary, - mixnet::delegate::get_all_pending_delegation_events, mixnet::delegate::get_all_mix_delegations, mixnet::delegate::undelegate_from_mixnode, mixnet::delegate::undelegate_all_from_mixnode, - mixnet::epoch::get_current_epoch, + mixnet::interval::get_current_interval, + mixnet::interval::get_pending_epoch_events, + mixnet::interval::get_pending_interval_events, mixnet::rewards::claim_delegator_reward, mixnet::rewards::claim_operator_reward, - mixnet::rewards::compound_operator_reward, - mixnet::rewards::compound_delegator_reward, mixnet::rewards::claim_locked_and_unlocked_delegator_reward, - mixnet::rewards::compound_locked_and_unlocked_delegator_reward, mixnet::send::send, network_config::add_validator, network_config::get_validator_api_urls, @@ -89,23 +92,24 @@ fn main() { utils::owns_mixnode, utils::get_env, utils::get_old_and_incorrect_hardcoded_fee, + utils::try_convert_pubkey_to_mix_id, + utils::default_mixnode_cost_params, validator_api::status::gateway_core_node_status, validator_api::status::mixnode_core_node_status, validator_api::status::mixnode_inclusion_probability, validator_api::status::mixnode_reward_estimation, validator_api::status::mixnode_stake_saturation, validator_api::status::mixnode_status, + validator_api::status::gateway_report, vesting::rewards::vesting_claim_delegator_reward, vesting::rewards::vesting_claim_operator_reward, - vesting::rewards::vesting_compound_operator_reward, - vesting::rewards::vesting_compound_delegator_reward, vesting::bond::vesting_bond_gateway, vesting::bond::vesting_bond_mixnode, vesting::bond::vesting_unbond_gateway, vesting::bond::vesting_unbond_mixnode, - vesting::bond::vesting_update_mixnode, + vesting::bond::vesting_update_mixnode_cost_params, + vesting::bond::vesting_update_mixnode_config, vesting::bond::withdraw_vested_coins, - vesting::delegate::get_pending_vesting_delegation_events, vesting::delegate::vesting_delegate_to_mixnode, vesting::delegate::vesting_undelegate_from_mixnode, vesting::queries::delegated_free, @@ -127,7 +131,8 @@ fn main() { simulate::mixnet::simulate_unbond_gateway, simulate::mixnet::simulate_bond_mixnode, simulate::mixnet::simulate_unbond_mixnode, - simulate::mixnet::simulate_update_mixnode, + simulate::mixnet::simulate_update_mixnode_config, + simulate::mixnet::simulate_update_mixnode_cost_params, simulate::mixnet::simulate_delegate_to_mixnode, simulate::mixnet::simulate_undelegate_from_mixnode, simulate::vesting::simulate_vesting_delegate_to_mixnode, @@ -136,45 +141,24 @@ fn main() { simulate::vesting::simulate_vesting_unbond_gateway, simulate::vesting::simulate_vesting_bond_mixnode, simulate::vesting::simulate_vesting_unbond_mixnode, - simulate::vesting::simulate_vesting_update_mixnode, + simulate::vesting::simulate_vesting_update_mixnode_config, + simulate::vesting::simulate_vesting_update_mixnode_cost_params, simulate::vesting::simulate_withdraw_vested_coins, simulate::vesting::simulate_vesting_claim_delegator_reward, simulate::vesting::simulate_vesting_claim_operator_reward, - simulate::vesting::simulate_vesting_compound_operator_reward, - simulate::vesting::simulate_vesting_compound_delegator_reward, simulate::mixnet::simulate_claim_delegator_reward, simulate::mixnet::simulate_claim_operator_reward, - simulate::mixnet::simulate_compound_operator_reward, - simulate::mixnet::simulate_compound_delegator_reward, + signatures::sign::sign, + signatures::sign::verify, + help::log::help_log_toggle_window, ]) - .menu(Menu::new().add_default_app_submenu_if_macos()) - .run(tauri::generate_context!()) + .menu(Menu::os_default(&context.package_info().name).add_default_app_submenus()) + .on_menu_event(|event| { + if event.menu_item_id() == menu::SHOW_LOG_WINDOW { + let _r = help::log::help_log_toggle_window(event.window().app_handle()); + } + }) + .setup(|app| Ok(log::setup_logging(app.app_handle())?)) + .run(context) .expect("error while running tauri application"); } - -fn setup_logging() { - let mut log_builder = pretty_env_logger::formatted_timed_builder(); - if let Ok(s) = ::std::env::var("RUST_LOG") { - log_builder.parse_filters(&s); - } else { - // default to 'Info' - log_builder.filter(None, log::LevelFilter::Info); - } - - if ::std::env::var("RUST_TRACE_OPERATIONS").is_ok() { - log_builder.filter_module("nym_wallet::operations", log::LevelFilter::Trace); - } - - log_builder - .filter_module("hyper", log::LevelFilter::Warn) - .filter_module("tokio_reactor", log::LevelFilter::Warn) - .filter_module("reqwest", log::LevelFilter::Warn) - .filter_module("mio", log::LevelFilter::Warn) - .filter_module("want", log::LevelFilter::Warn) - .filter_module("sled", log::LevelFilter::Warn) - .filter_module("tungstenite", log::LevelFilter::Warn) - .filter_module("tokio_tungstenite", log::LevelFilter::Warn) - .filter_module("rustls", log::LevelFilter::Warn) - .filter_module("tokio_util", log::LevelFilter::Warn) - .init(); -} diff --git a/nym-wallet/src-tauri/src/menu.rs b/nym-wallet/src-tauri/src/menu.rs index 6c7a8d4bd7..1aaa78c7fe 100644 --- a/nym-wallet/src-tauri/src/menu.rs +++ b/nym-wallet/src-tauri/src/menu.rs @@ -1,27 +1,21 @@ use tauri::Menu; -#[cfg(target_os = "macos")] -use tauri::{MenuItem, Submenu}; +use tauri::{CustomMenuItem, Submenu}; + +pub const SHOW_LOG_WINDOW: &str = "show_log_window"; pub trait AddDefaultSubmenus { - fn add_default_app_submenu_if_macos(self) -> Self; + fn add_default_app_submenus(self) -> Self; } impl AddDefaultSubmenus for Menu { - fn add_default_app_submenu_if_macos(self) -> Menu { - #[cfg(target_os = "macos")] - return self.add_submenu(Submenu::new( - "Menu", - Menu::new() - .add_native_item(MenuItem::Copy) - .add_native_item(MenuItem::Cut) - .add_native_item(MenuItem::Paste) - .add_native_item(MenuItem::Hide) - .add_native_item(MenuItem::HideOthers) - .add_native_item(MenuItem::SelectAll) - .add_native_item(MenuItem::ShowAll) - .add_native_item(MenuItem::Quit), - )); - #[cfg(not(target_os = "macos"))] - return self; + fn add_default_app_submenus(self) -> Self { + if ::std::env::var("NYM_WALLET_ENABLE_LOG").is_ok() { + let submenu = Submenu::new( + "Help", + Menu::new().add_item(CustomMenuItem::new(SHOW_LOG_WINDOW, "Show logs")), + ); + return self.add_submenu(submenu); + } + self } } diff --git a/nym-wallet/src-tauri/src/operations/help/log.rs b/nym-wallet/src-tauri/src/operations/help/log.rs new file mode 100644 index 0000000000..c8ff873f6a --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/help/log.rs @@ -0,0 +1,30 @@ +use crate::error::BackendError; +use tauri::Manager; + +#[tauri::command] +pub fn help_log_toggle_window(app_handle: tauri::AppHandle) -> Result<(), BackendError> { + if let Some(current_log_window) = app_handle.windows().get("log") { + log::info!("Closing log window..."); + if let Err(e) = current_log_window.close() { + log::error!("Unable to close log window: {:?}", e); + } + return Ok(()); + } + + log::info!("Creating log window..."); + match tauri::WindowBuilder::new(&app_handle, "log", tauri::WindowUrl::App("log.html".into())) + .title("Nym Wallet Logs") + .build() + { + Ok(window) => { + if let Err(e) = window.set_focus() { + log::error!("Unable to focus log window: {:?}", e); + } + Ok(()) + } + Err(e) => { + log::error!("Unable to create log window: {:?}", e); + Err(BackendError::NewWindowError) + } + } +} diff --git a/nym-wallet/src-tauri/src/operations/help/mod.rs b/nym-wallet/src-tauri/src/operations/help/mod.rs new file mode 100644 index 0000000000..f4ee9bc8d0 --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/help/mod.rs @@ -0,0 +1 @@ +pub mod log; diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index bb83b8a7fe..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}; @@ -104,10 +103,12 @@ async fn _connect_with_mnemonic( // Take the oppertunity to list all the known validators while we have the state. for network in WalletNetwork::iter() { - log::debug!( - "List of validators for {network}: [\n{}\n]", + // fern really wants us to not evaluate this inside the debug macro argument + let f = format!( + "{}", state.get_config_validator_entries(network).format(",\n") ); + log::debug!("List of validators for {network}: [\n{}\n]", f,); } state.config().clone() @@ -173,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))) @@ -197,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, @@ -237,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( @@ -258,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| { @@ -271,7 +272,7 @@ fn select_random_responding_url( } fn select_first_responding_url( - urls: &HashMap>, + urls: &HashMap>, network: WalletNetwork, //config: &Config, ) -> Option { @@ -424,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 @@ -464,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(); @@ -560,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/admin.rs b/nym-wallet/src-tauri/src/operations/mixnet/admin.rs index faced61bce..5074c5500e 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/admin.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/admin.rs @@ -1,9 +1,12 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::error::BackendError; -use crate::nymd_client; use crate::state::WalletState; use mixnet_contract_common::ContractStateParams; +use nym_types::transaction::TransactionExecuteResult; use nym_wallet_types::admin::TauriContractStateParams; -use std::convert::TryInto; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; use validator_client::nymd::Fee; #[tauri::command] @@ -11,9 +14,15 @@ pub async fn get_contract_settings( state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Getting contract settings"); - let res = nymd_client!(state).get_contract_settings().await?.into(); - log::trace!("<<< {:?}", res); - Ok(res) + + let guard = state.read().await; + let reg = guard.registered_coins()?; + let client = &guard.current_client()?.nymd; + + let res = client.get_mixnet_contract_settings().await?; + let converted = TauriContractStateParams::from_mixnet_contract_contract_state_params(res, reg)?; + log::trace!("<<< {:?}", converted); + Ok(converted) } #[tauri::command] @@ -21,16 +30,25 @@ pub async fn update_contract_settings( params: TauriContractStateParams, fee: Option, state: tauri::State<'_, WalletState>, -) -> Result { - let mixnet_contract_settings_params: ContractStateParams = params.try_into()?; +) -> Result { + let guard = state.read().await; + let reg = guard.registered_coins()?; + let client = &guard.current_client()?.nymd; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + + let mixnet_contract_settings_params: ContractStateParams = + params.try_convert_to_mixnet_contract_params(reg)?; log::info!( ">>> Updating contract settings: {:?}", mixnet_contract_settings_params ); - nymd_client!(state) - .update_contract_settings(mixnet_contract_settings_params.clone(), fee) + let res = client + .update_contract_state_params(mixnet_contract_settings_params, fee) .await?; - let res = mixnet_contract_settings_params.into(); + + log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); - Ok(res) + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index e350bd49d9..7098577545 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -1,15 +1,18 @@ -use std::time::Duration; +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 use crate::error::BackendError; use crate::state::WalletState; -use crate::{Gateway, MixNode}; +use crate::{nymd_client, Gateway, MixNode}; +use mixnet_contract_common::{MixId, MixNodeConfigUpdate}; use nym_types::currency::DecCoin; use nym_types::gateway::GatewayBond; -use nym_types::mixnode::MixNodeBond; +use nym_types::mixnode::{MixNodeCostParams, MixNodeDetails}; use nym_types::transaction::TransactionExecuteResult; -use reqwest::Error as ReqwestError; use serde::{Deserialize, Serialize}; -use validator_client::nymd::{Coin, Fee}; +use std::time::Duration; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; +use validator_client::nymd::Fee; #[derive(Debug, Serialize, Deserialize)] pub struct NodeDescription { @@ -69,15 +72,17 @@ pub async fn unbond_gateway( #[tauri::command] pub async fn bond_mixnode( mixnode: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, pledge: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; + let reg = guard.registered_coins()?; let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); - + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; log::info!( ">>> Bond mixnode: identity_key = {}, pledge_display = {}, pledge_base = {}, fee = {:?}", mixnode.identity_key, @@ -88,7 +93,7 @@ pub async fn bond_mixnode( let res = guard .current_client()? .nymd - .bond_mixnode(mixnode, owner_signature, pledge_base, fee) + .bond_mixnode(mixnode, cost_params, owner_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -114,22 +119,24 @@ pub async fn unbond_mixnode( } #[tauri::command] -pub async fn update_mixnode( - profit_margin_percent: u8, +pub async fn update_mixnode_cost_params( + new_costs: MixNodeCostParams, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; + let reg = guard.registered_coins()?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); + let cost_params = new_costs.try_convert_to_mixnet_contract_cost_params(reg)?; log::info!( - ">>> Update mixnode: profit_margin_percent = {}, fee {:?}", - profit_margin_percent, + ">>> Update mixnode cost parameters: new parameters = {}, fee {:?}", + cost_params.to_inline_json(), fee, ); let res = guard .current_client()? .nymd - .update_mixnode_config(profit_margin_percent, fee) + .update_mixnode_cost_params(cost_params, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -139,27 +146,82 @@ pub async fn update_mixnode( } #[tauri::command] -pub async fn mixnode_bond_details( +pub async fn update_mixnode_config( + update: MixNodeConfigUpdate, + fee: Option, state: tauri::State<'_, WalletState>, -) -> Result, BackendError> { +) -> Result { + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( + ">>> Update mixnode config: update = {}, fee {:?}", + update.to_inline_json(), + fee, + ); + let res = guard + .current_client()? + .nymd + .update_mixnode_config(update, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[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 bond = client.nymd.owns_mixnode(client.nymd.address()).await?; - let res = bond - .map(|bond| { + 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>, +) -> 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?; + let details = res + .mixnode_details + .map(|details| { guard .registered_coins() - .map(|reg| MixNodeBond::from_mixnet_contract_mixnode_bond(bond, reg)) + .map(|reg| MixNodeDetails::from_mixnet_contract_mixnode_details(details, reg)) }) .transpose()? .transpose()?; log::info!( - "<<< identity_key = {:?}", - res.as_ref().map(|r| r.mix_node.identity_key.to_string()) + "<<< mix_id/identity_key = {:?}", + details.as_ref().map(|r| ( + r.bond_information.mix_id, + &r.bond_information.mix_node.identity_key + )) ); - log::trace!("<<< {:?}", res); - Ok(res) + log::trace!("<<< {:?}", details); + Ok(details) } #[tauri::command] @@ -169,8 +231,9 @@ pub async fn gateway_bond_details( log::info!(">>> Get gateway bond details"); let guard = state.read().await; let client = guard.current_client()?; - let bond = client.nymd.owns_gateway(client.nymd.address()).await?; + let bond = client.nymd.get_owned_gateway(client.nymd.address()).await?; let res = bond + .gateway .map(|bond| { guard .registered_coins() @@ -188,23 +251,36 @@ pub async fn gateway_bond_details( } #[tauri::command] -pub async fn get_operator_rewards( +pub async fn get_pending_operator_rewards( address: String, state: tauri::State<'_, WalletState>, ) -> Result { - log::info!(">>> Get operator rewards for {}", address); + log::info!(">>> Get pending operator rewards for {}", address); let guard = state.read().await; - let network = guard.current_network(); - let denom = network.base_mix_denom(); - let reward_amount = guard + let res = guard .current_client()? .nymd - .get_operator_rewards(address) + .get_pending_operator_reward(&address.parse()?) .await?; - let base_coin = Coin::new(reward_amount.u128(), denom); - let display_coin: DecCoin = guard.attempt_convert_to_display_dec_coin(base_coin.clone())?; + + // note to @MS: now we're able to obtain more information than just the pending reward + // the entire returned struct contains the following: + /* + pub amount_staked: Option, + pub amount_earned: Option, + pub amount_earned_detailed: Option, + pub mixnode_still_fully_bonded: bool, + */ + + let base_coin = res.amount_earned; + let display_coin = base_coin + .as_ref() + .map(|c| guard.attempt_convert_to_display_dec_coin(c.clone().into())) + .transpose()? + .unwrap_or_else(|| guard.default_zero_mix_display_coin()); + log::info!( - "<<< rewards_base = {}, rewards_display = {}", + "<<< rewards_base = {:?}, rewards_display = {}", base_coin, display_coin ); @@ -213,40 +289,15 @@ pub async fn get_operator_rewards( #[tauri::command] pub async fn get_number_of_mixnode_delegators( - identity: String, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - let paged_delegations = client - .nymd - .get_mix_delegations_paged(identity, None, Some(20)) - .await?; - - Ok(paged_delegations.delegations.len()) -} - -async fn fetch_mix_node_description( - host: &str, - port: u16, -) -> Result { - let milli_second = Duration::from_millis(1000); - let client = reqwest::Client::builder().timeout(milli_second).build()?; - let response = client - .get(format!("http://{}:{}/description", host, port)) - .send() - .await; - - match response { - Ok(res) => { - let json = res.json::().await; - match json { - Ok(json) => Ok(json), - Err(e) => Err(e), - } - } - Err(e) => Err(e), - } + Ok(nymd_client!(state) + .get_mixnode_details(mix_id) + .await? + .mixnode_details + .map(|details| details.rewarding_details.unique_delegations) + .unwrap_or_default() as usize) } #[tauri::command] @@ -254,7 +305,12 @@ pub async fn get_mix_node_description( host: &str, port: u16, ) -> Result { - return fetch_mix_node_description(host, port) - .await - .map_err(|e| BackendError::ReqwestError { source: e }); + Ok(reqwest::Client::builder() + .timeout(Duration::from_millis(1000)) + .build()? + .get(format!("http://{}:{}/description", host, port)) + .send() + .await? + .json() + .await?) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 7e6f27ae8f..dfbf46389b 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -1,61 +1,89 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::error::BackendError; use crate::state::WalletState; -use crate::vesting::delegate::{ - get_pending_vesting_delegation_events, vesting_undelegate_from_mixnode, -}; -use crate::{api_client, nymd_client}; -use mixnet_contract_common::IdentityKey; +use crate::vesting::delegate::vesting_undelegate_from_mixnode; +use mixnet_contract_common::MixId; use nym_types::currency::DecCoin; -use nym_types::delegation::{ - Delegation, DelegationEvent, DelegationRecord, DelegationWithEverything, - DelegationsSummaryResponse, +use nym_types::delegation::{Delegation, DelegationWithEverything, DelegationsSummaryResponse}; +use nym_types::deprecated::{ + convert_to_delegation_events, DelegationEvent, WrappedDelegationEvent, }; +use nym_types::mixnode::MixNodeCostParams; +use nym_types::pending_events::PendingEpochEvent; use nym_types::transaction::TransactionExecuteResult; -use std::collections::HashMap; -use validator_client::nymd::{Coin, Fee}; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; +use validator_client::nymd::Fee; #[tauri::command] pub async fn get_pending_delegation_events( state: tauri::State<'_, WalletState>, -) -> Result, BackendError> { - log::info!(">>> Get pending delegation events"); +) -> Result, BackendError> { + log::info!(">>> [DEPRECATED] Get all pending delegation events"); let guard = state.read().await; let reg = guard.registered_coins()?; let client = guard.current_client()?; - let events = client - .nymd - .get_pending_delegation_events(client.nymd.address().to_string(), None) - .await?; - log::info!("<<< {} pending delegation events", events.len()); - log::trace!("<<< pending delegation events = {:?}", events); - - Ok(events + let events = client.get_all_nymd_pending_epoch_events().await?; + let converted = events .into_iter() - .map(|event| DelegationEvent::from_mixnet_contract(event, reg)) - .collect::>()?) + .map(|e| PendingEpochEvent::try_from_mixnet_contract(e, reg)) + .collect::, _>>()?; + + let delegation_events = convert_to_delegation_events(converted); + + // we only care about events concerning THIS client + let mut client_specific_events = Vec::new(); + for delegation_event in delegation_events { + if delegation_event.address_matches(client.nymd.address().as_ref()) { + let node_identity = client + .nymd + .get_mixnode_details(delegation_event.mix_id) + .await? + .mixnode_details + .map(|d| d.bond_information.mix_node.identity_key) + .unwrap_or_default(); + + client_specific_events + .push(WrappedDelegationEvent::new(delegation_event, node_identity)); + } + } + + log::info!( + "<<< {} pending delegation events", + client_specific_events.len() + ); + log::trace!( + "<<< pending delegation events = {:?}", + client_specific_events + ); + + Ok(client_specific_events) } #[tauri::command] pub async fn delegate_to_mixnode( - identity: &str, + mix_id: MixId, amount: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; + let client = guard.current_client()?; let delegation_base = guard.attempt_convert_to_base_coin(amount.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Delegate to mixnode: identity_key = {}, display_amount = {}, base_amount = {}, fee = {:?}", - identity, + ">>> Delegate to mixnode: mix_id = {}, display_amount = {}, base_amount = {}, fee = {:?}", + mix_id, amount, delegation_base, fee, ); - let res = nymd_client!(state) - .delegate_to_mixnode(identity, delegation_base, fee) + let res = client + .nymd + .delegate_to_mixnode(mix_id, delegation_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -66,7 +94,7 @@ pub async fn delegate_to_mixnode( #[tauri::command] pub async fn undelegate_from_mixnode( - identity: &str, + mix_id: MixId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -74,14 +102,14 @@ pub async fn undelegate_from_mixnode( let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Undelegate from mixnode: identity_key = {}, fee = {:?}", - identity, + ">>> Undelegate from mixnode: mix_id = {}, fee = {:?}", + mix_id, fee ); let res = guard .current_client()? .nymd - .remove_mixnode_delegation(identity, fee) + .undelegate_from_mixnode(mix_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -92,35 +120,29 @@ pub async fn undelegate_from_mixnode( #[tauri::command] pub async fn undelegate_all_from_mixnode( - identity: &str, + mix_id: MixId, uses_vesting_contract_tokens: bool, - fee: Option, - fee2: Option, + fee_liquid: Option, + fee_vesting: Option, state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::info!( - ">>> Undelegate all from mixnode: identity_key = {}, uses_vesting_contract_tokens = {}, fee = {:?}", - identity, + ">>> Undelegate all from mixnode: mix_id = {}, uses_vesting_contract_tokens = {}, fee_liquid = {:?}, fee_vesting = {:?}", + mix_id, uses_vesting_contract_tokens, - fee + fee_liquid, + fee_vesting, ); let mut res: Vec = - vec![undelegate_from_mixnode(identity, fee, state.clone()).await?]; + vec![undelegate_from_mixnode(mix_id, fee_liquid, state.clone()).await?]; if uses_vesting_contract_tokens { - res.push(vesting_undelegate_from_mixnode(identity, fee2, state.clone()).await?); + res.push(vesting_undelegate_from_mixnode(mix_id, fee_vesting, state).await?); } Ok(res) } -struct DelegationWithHistory { - pub delegation: Delegation, - pub amount_sum: DecCoin, - pub history: Vec, - pub uses_vesting_contract_tokens: bool, -} - #[tauri::command] pub async fn get_all_mix_delegations( state: tauri::State<'_, WalletState>, @@ -131,168 +153,93 @@ pub async fn get_all_mix_delegations( let client = guard.current_client()?; let reg = guard.registered_coins()?; - // TODO: add endpoint to validator API to get a single mix node bond - let mixnodes = client.validator_api.get_mixnodes().await?; - let address = client.nymd.address(); let network = guard.current_network(); - let display_mix_denom = network.display_mix_denom(); - let base_mix_denom = network.base_mix_denom(); + let base_mix_denom = network.base_mix_denom().to_string(); + let vesting_contract = client.nymd.vesting_contract_address(); log::info!(" >>> Get delegations"); let delegations = client.get_all_delegator_delegations(address).await?; log::info!(" <<< {} delegations", delegations.len()); - // first get pending events from the mixnet contract (operations made with unlocked tokens) - let mut pending_events_for_account = get_pending_delegation_events(state.clone()).await?; - - // then get pending events from the vesting contract (operations made with locked tokens) - let pending_vesting_events = get_pending_vesting_delegation_events(state.clone()).await?; - for event in pending_vesting_events { - pending_events_for_account.push(event); - } + let pending_events_for_account = get_pending_delegation_events(state.clone()).await?; log::info!( " <<< {} pending delegation events for account", pending_events_for_account.len() ); - let mut map: HashMap = HashMap::new(); + let mut with_everything: Vec = Vec::with_capacity(delegations.len()); - for pending_event in &pending_events_for_account { - if delegations - .iter() - .any(|d| d.node_identity == pending_event.node_identity) - { - let amount = pending_event - .amount - .clone() - .unwrap_or_else(|| DecCoin::zero(display_mix_denom)); - let delegation = DelegationWithHistory { - delegation: Delegation { - amount: amount.clone(), - node_identity: pending_event.node_identity.clone(), - proxy: pending_event.proxy.clone(), // TODO: ask @MS about delegations via vesting contract => surely we'd have proxy there? - owner: pending_event.address.clone(), - block_height: pending_event.block_height, - }, - amount_sum: amount, - uses_vesting_contract_tokens: false, - history: vec![], - }; - map.insert(delegation.delegation.node_identity.clone(), delegation); - } - } - - for d in delegations { - // create record of delegation - let delegated_on_iso_datetime = client - .nymd - .get_block_timestamp(Some(d.block_height as u32)) - .await? - .to_rfc3339(); - let amount = guard.attempt_convert_to_display_dec_coin(d.amount.clone().into())?; - - let record = DelegationRecord { - amount: amount.clone(), - block_height: d.block_height, - delegated_on_iso_datetime, - uses_vesting_contract_tokens: d.proxy.is_some(), - }; - - let entry = map - .entry(d.node_identity.clone()) - .or_insert(DelegationWithHistory { - delegation: Delegation::from_mixnet_contract(d, reg)?, - history: vec![], - amount_sum: DecCoin::zero(display_mix_denom), - uses_vesting_contract_tokens: false, - }); - - debug_assert_eq!(entry.amount_sum.denom, amount.denom); - - entry.history.push(record); - entry.amount_sum.amount += amount.amount; - entry.uses_vesting_contract_tokens = - entry.uses_vesting_contract_tokens || entry.delegation.proxy.is_some(); - } - - let mut with_everything: Vec = vec![]; - - for item in map { - let d = item.1.delegation; - let history = item.1.history; - let Delegation { - owner, - node_identity, - amount, - block_height, - proxy, - } = d; - let uses_vesting_contract_tokens = item.1.uses_vesting_contract_tokens; + for delegation in delegations { + let d = Delegation::from_mixnet_contract(delegation, reg)?; + let uses_vesting_contract_tokens = d + .proxy + .as_ref() + .map(|p| p.as_str() == vesting_contract.as_ref()) + .unwrap_or_default(); log::trace!( - " --- Delegation: node_identity = {}, amount = {}", - node_identity, - amount + " --- Delegation: mix_id = {}, amount = {}", + d.mix_id, + d.amount ); - let mixnode = mixnodes - .iter() - .find(|m| m.mix_node.identity_key == node_identity); + let mixnode = client.get_mixnode_details(d.mix_id).await?.mixnode_details; - let pledge_amount = mixnode - .map(|m| guard.attempt_convert_to_display_dec_coin(m.pledge_amount.clone().into())) + let accumulated_by_operator = mixnode + .as_ref() + .map(|m| { + guard.display_coin_from_base_decimal(&base_mix_denom, m.rewarding_details.operator) + }) .transpose()?; - let total_delegation = mixnode - .map(|m| guard.attempt_convert_to_display_dec_coin(m.total_delegation.clone().into())) + let accumulated_by_delegates = mixnode + .as_ref() + .map(|m| { + guard.display_coin_from_base_decimal(&base_mix_denom, m.rewarding_details.delegates) + }) .transpose()?; - let profit_margin_percent: Option = mixnode.map(|m| m.mix_node.profit_margin_percent); + let cost_params = mixnode + .as_ref() + .map(|m| { + MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + m.rewarding_details.cost_params.clone(), + reg, + ) + }) + .transpose()?; log::trace!(" >>> Get accumulated rewards: address = {}", address); - let accumulated_rewards = match client + let pending_reward = client .nymd - .get_delegator_rewards(address.to_string(), node_identity.clone(), proxy.clone()) - .await - { - Ok(rewards) => { - let reward = Coin::new(rewards.u128(), base_mix_denom); - let amount = guard.attempt_convert_to_display_dec_coin(reward)?; - log::trace!(" <<< rewards = {}, amount = {}", rewards, amount); + .get_pending_delegator_reward(address, d.mix_id, d.proxy.clone()) + .await?; + + let accumulated_rewards = match &pending_reward.amount_earned { + Some(reward) => { + let amount = guard.attempt_convert_to_display_dec_coin(reward.clone().into())?; + log::trace!(" <<< rewards = {:?}, amount = {}", pending_reward, amount); Some(amount) } - Err(_) => { + None => { log::trace!(" <<< no rewards waiting"); None } }; - let pending_events = - filter_pending_events(&node_identity, pending_events_for_account.clone()); - log::trace!( - " --- pending events for mixnode = {}", - pending_events.len() - ); - - log::trace!( - " >>> Get stake saturation: node_identity = {}", - node_identity - ); - let stake_saturation = api_client!(state) - .get_mixnode_stake_saturation(&node_identity) - .await - .ok() - .map(|r| r.saturation); + log::trace!(" >>> Get stake saturation: mix_id = {}", d.mix_id); + let stake_saturation = client.nymd.get_mixnode_stake_saturation(d.mix_id).await?; log::trace!(" <<< {:?}", stake_saturation); log::trace!( - " >>> Get average uptime percentage: node_identity = {}", - node_identity + " >>> Get average uptime percentage: mix_iid = {}", + d.mix_id ); - let avg_uptime_percent = api_client!(state) - .get_mixnode_avg_uptime(&node_identity) + let avg_uptime_percent = client + .validator_api + .get_mixnode_avg_uptime(d.mix_id) .await .ok() .map(|r| r.avg_uptime); @@ -300,10 +247,11 @@ pub async fn get_all_mix_delegations( log::trace!( " >>> Convert delegated on block height to timestamp: block_height = {}", - d.block_height + d.height ); - let timestamp = nymd_client!(state) - .get_block_timestamp(Some(d.block_height as u32)) + let timestamp = client + .nymd + .get_block_timestamp(Some(d.height as u32)) .await?; let delegated_on_iso_datetime = timestamp.to_rfc3339(); log::trace!( @@ -312,21 +260,37 @@ pub async fn get_all_mix_delegations( delegated_on_iso_datetime ); + let pending_events = filter_pending_events(d.mix_id, &pending_events_for_account); + log::trace!( + " --- pending events for mixnode = {}", + pending_events.len() + ); + + let mixnode_is_unbonding = mixnode.as_ref().map(|m| m.is_unbonding()); + log::trace!( + " >>> mixnode with mix_id: {} is unbonding: {:?}", + d.mix_id, + mixnode_is_unbonding + ); + with_everything.push(DelegationWithEverything { - owner: owner.to_string(), - node_identity: node_identity.to_string(), - amount: item.1.amount_sum, - block_height, + owner: d.owner, + mix_id: d.mix_id, + node_identity: mixnode + .map(|m| m.bond_information.mix_node.identity_key) + .unwrap_or_default(), + amount: d.amount, + block_height: d.height, uses_vesting_contract_tokens, delegated_on_iso_datetime, - stake_saturation, - accumulated_rewards, - profit_margin_percent, - pledge_amount, + stake_saturation: stake_saturation.uncapped_saturation, + accumulated_by_operator, avg_uptime_percent, - total_delegation, + accumulated_by_delegates, + cost_params, + unclaimed_rewards: accumulated_rewards, pending_events, - history, + mixnode_is_unbonding, }) } log::trace!("<<< {:?}", with_everything); @@ -335,41 +299,54 @@ pub async fn get_all_mix_delegations( } fn filter_pending_events( - node_identity: &str, - pending_events: Vec, + mix_id: MixId, + pending_events: &[WrappedDelegationEvent], ) -> Vec { pending_events .iter() - .filter(|e| e.node_identity == node_identity) + .filter(|e| e.event.mix_id == mix_id) .cloned() - .collect::>() + .map(|e| e.event) + .collect() } #[tauri::command] -pub async fn get_delegator_rewards( +pub async fn get_pending_delegator_rewards( address: String, - mix_identity: IdentityKey, + mix_id: MixId, proxy: Option, state: tauri::State<'_, WalletState>, ) -> Result { log::info!( - ">>> Get delegator rewards: mix_identity = {}, proxy = {:?}", - mix_identity, + ">>> Get pending delegator rewards: mix_id = {}, proxy = {:?}", + mix_id, proxy ); let guard = state.read().await; - let network = guard.current_network(); - let denom = network.base_mix_denom(); - let reward_amount = guard + let res = guard .current_client()? .nymd - .get_delegator_rewards(address, mix_identity, proxy) + .get_pending_delegator_reward(&address.parse()?, mix_id, proxy) .await?; - let base_coin = Coin::new(reward_amount.u128(), denom); - let display_coin: DecCoin = guard.attempt_convert_to_display_dec_coin(base_coin.clone())?; + + // note to @MS: now we're able to obtain more information than just the pending reward + // the entire returned struct contains the following: + /* + pub amount_staked: Option, + pub amount_earned: Option, + pub amount_earned_detailed: Option, + pub mixnode_still_fully_bonded: bool, + */ + + let base_coin = res.amount_earned; + let display_coin = base_coin + .as_ref() + .map(|c| guard.attempt_convert_to_display_dec_coin(c.clone().into())) + .transpose()? + .unwrap_or_else(|| guard.default_zero_mix_display_coin()); log::info!( - "<<< rewards_base = {}, rewards_display = {}", + "<<< rewards_base = {:?}, rewards_display = {}", base_coin, display_coin ); @@ -393,7 +370,7 @@ pub async fn get_delegation_summary( for d in &delegations { debug_assert_eq!(d.amount.denom, display_mix_denom); total_delegations.amount += d.amount.amount; - if let Some(rewards) = &d.accumulated_rewards { + if let Some(rewards) = &d.unclaimed_rewards { debug_assert_eq!(rewards.denom, display_mix_denom); total_rewards.amount += rewards.amount; } @@ -412,21 +389,3 @@ pub async fn get_delegation_summary( total_rewards, }) } - -#[tauri::command] -pub async fn get_all_pending_delegation_events( - state: tauri::State<'_, WalletState>, -) -> Result, BackendError> { - log::info!(">>> Get all pending delegation events"); - - // get pending events from mixnet and vesting contract - let mut pending_events_for_account = get_pending_delegation_events(state.clone()).await?; - let pending_vesting_events = get_pending_vesting_delegation_events(state.clone()).await?; - - // combine them - for event in pending_vesting_events { - pending_events_for_account.push(event); - } - - Ok(pending_events_for_account) -} diff --git a/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs b/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs deleted file mode 100644 index 2ca8fe0c05..0000000000 --- a/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::error::BackendError; -use crate::nymd_client; -use crate::state::WalletState; -use nym_wallet_types::epoch::Epoch; - -#[tauri::command] -pub async fn get_current_epoch( - state: tauri::State<'_, WalletState>, -) -> Result { - log::info!(">>> Get curren epoch"); - let interval = nymd_client!(state).get_current_epoch().await?; - log::info!("<<< curren epoch = {}", interval); - Ok(interval.into()) -} diff --git a/nym-wallet/src-tauri/src/operations/mixnet/interval.rs b/nym-wallet/src-tauri/src/operations/mixnet/interval.rs new file mode 100644 index 0000000000..ce88d840b4 --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/mixnet/interval.rs @@ -0,0 +1,58 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::BackendError; +use crate::nymd_client; +use crate::state::WalletState; +use nym_types::pending_events::{PendingEpochEvent, PendingIntervalEvent}; +use nym_wallet_types::interval::Interval; +use validator_client::nymd::traits::MixnetQueryClient; + +#[tauri::command] +pub async fn get_current_interval( + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> Get current interval"); + let res = nymd_client!(state).get_current_interval_details().await?; + log::info!("<<< current interval = {:?}", res); + Ok(res.interval.into()) +} + +#[tauri::command] +pub async fn get_pending_epoch_events( + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::info!(">>> Get pending epoch events"); + let guard = state.read().await; + let reg = guard.registered_coins()?; + let client = guard.current_client()?; + let res = client.get_all_nymd_pending_epoch_events().await?; + + log::info!("<<< got = {:?} events", res.len()); + + let converted = res + .into_iter() + .map(|e| PendingEpochEvent::try_from_mixnet_contract(e, reg)) + .collect::, _>>()?; + Ok(converted) +} + +#[tauri::command] +pub async fn get_pending_interval_events( + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::info!(">>> Get pending interval events"); + + let guard = state.read().await; + let reg = guard.registered_coins()?; + let client = guard.current_client()?; + let res = client.get_all_nymd_pending_interval_events().await?; + + log::info!("<<< got = {:?} events", res.len()); + + let converted = res + .into_iter() + .map(|e| PendingIntervalEvent::try_from_mixnet_contract(e, reg)) + .collect::, _>>()?; + Ok(converted) +} diff --git a/nym-wallet/src-tauri/src/operations/mixnet/mod.rs b/nym-wallet/src-tauri/src/operations/mixnet/mod.rs index 40ebc405d8..cb94278ee2 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/mod.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/mod.rs @@ -2,6 +2,6 @@ pub mod account; pub mod admin; pub mod bond; pub mod delegate; -pub mod epoch; +pub mod interval; pub mod rewards; pub mod send; diff --git a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs index 983c870c61..6fb8893ee9 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs @@ -1,9 +1,9 @@ use crate::error::BackendError; -use crate::nymd_client; use crate::state::WalletState; -use crate::vesting::rewards::{vesting_claim_delegator_reward, vesting_compound_delegator_reward}; -use mixnet_contract_common::IdentityKey; +use crate::vesting::rewards::vesting_claim_delegator_reward; +use mixnet_contract_common::MixId; use nym_types::transaction::TransactionExecuteResult; +use validator_client::nymd::traits::{MixnetQueryClient, MixnetSigningClient}; use validator_client::nymd::Fee; #[tauri::command] @@ -11,35 +11,14 @@ pub async fn claim_operator_reward( fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { - // TODO: handle operator bonding with vesting contract - log::info!(">>> Claim operator reward"); + // // TODO: handle operator bonding with vesting contract + log::info!(">>> Withdraw operator reward"); let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); let res = guard .current_client()? .nymd - .execute_claim_operator_reward(fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, fee_amount, - )?) -} - -#[tauri::command] -pub async fn compound_operator_reward( - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result { - // TODO: handle operator bonding with vesting contract - log::info!(">>> Compound operator reward"); - let guard = state.read().await; - let fee_amount = guard.convert_tx_fee(fee.as_ref()); - let res = guard - .current_client()? - .nymd - .execute_compound_operator_reward(fee) + .withdraw_operator_reward(fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -50,44 +29,17 @@ pub async fn compound_operator_reward( #[tauri::command] pub async fn claim_delegator_reward( - mix_identity: IdentityKey, + mix_id: MixId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { - log::info!( - ">>> Claim delegator reward: identity_key = {}", - mix_identity - ); + log::info!(">>> Withdraw delegator reward: mix_id = {}", mix_id); let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); let res = guard .current_client()? .nymd - .execute_claim_delegator_reward(mix_identity, fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, fee_amount, - )?) -} - -#[tauri::command] -pub async fn compound_delegator_reward( - mix_identity: IdentityKey, - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result { - log::info!( - ">>> Compound delegator reward: identity_key = {}", - mix_identity - ); - let guard = state.read().await; - let fee_amount = guard.convert_tx_fee(fee.as_ref()); - let res = guard - .current_client()? - .nymd - .execute_compound_delegator_reward(mix_identity, fee) + .withdraw_delegator_reward(mix_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -98,32 +50,34 @@ pub async fn compound_delegator_reward( #[tauri::command] pub async fn claim_locked_and_unlocked_delegator_reward( - mix_identity: IdentityKey, + mix_id: MixId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::info!( - ">>> Claim delegator reward (locked and unlocked): identity_key = {}", - mix_identity + ">>> Claim delegator reward (locked and unlocked): mix_id = {}", + mix_id ); - log::trace!(">>> Get delegations: identity_key = {}", mix_identity); - let address = nymd_client!(state).address().to_string(); - let delegations = nymd_client!(state) - .get_delegator_delegations_paged(address.clone(), None, None) // get all delegations, ignoring paging - .await? - .delegations; + let guard = state.read().await; + let client = guard.current_client()?; + + log::trace!(">>> Get delegations: mix_id = {}", mix_id); + let address = client.nymd.address(); + let delegations = client.get_all_delegator_delegations(address).await?; log::trace!("<<< {} delegations", delegations.len()); + let vesting_contract = client.nymd.vesting_contract_address().to_string(); + let liquid_delegation = client.get_delegation_details(mix_id, address, None).await?; + let vesting_delegation = client + .get_delegation_details(mix_id, address, Some(vesting_contract)) + .await?; + + drop(guard); + // decide which contracts to use, could be neither - let did_delegate_with_mixnet_contract = delegations - .iter() - .filter(|f| f.node_identity == mix_identity) - .any(|f| f.proxy.is_none()); - let did_delegate_with_vesting_contract = delegations - .iter() - .filter(|f| f.node_identity == mix_identity) - .any(|f| f.proxy.is_some()); + let did_delegate_with_mixnet_contract = liquid_delegation.delegation.is_some(); + let did_delegate_with_vesting_contract = vesting_delegation.delegation.is_some(); log::trace!( "<<< Delegations done with: mixnet contract = {}, vesting contract = {}", @@ -133,58 +87,10 @@ pub async fn claim_locked_and_unlocked_delegator_reward( let mut res: Vec = vec![]; if did_delegate_with_mixnet_contract { - res.push(claim_delegator_reward(mix_identity.clone(), fee.clone(), state.clone()).await?); + res.push(claim_delegator_reward(mix_id, fee.clone(), state.clone()).await?); } if did_delegate_with_vesting_contract { - res.push(vesting_claim_delegator_reward(mix_identity, fee, state).await?); - } - log::trace!("<<< {:?}", res); - Ok(res) -} - -#[tauri::command] -pub async fn compound_locked_and_unlocked_delegator_reward( - mix_identity: IdentityKey, - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result, BackendError> { - log::info!( - ">>> Compound delegator reward (locked and unlocked): identity_key = {}", - mix_identity - ); - - log::trace!(">>> Get delegations: identity_key = {}", mix_identity); - let address = nymd_client!(state).address().to_string(); - let delegations = nymd_client!(state) - .get_delegator_delegations_paged(address.clone(), None, None) // get all delegations, ignoring paging - .await? - .delegations; - log::trace!("<<< {} delegations", delegations.len()); - - // decide which contracts to use, could be neither - let did_delegate_with_mixnet_contract = delegations - .iter() - .filter(|f| f.node_identity == mix_identity) - .any(|f| f.proxy.is_none()); - let did_delegate_with_vesting_contract = delegations - .iter() - .filter(|f| f.node_identity == mix_identity) - .any(|f| f.proxy.is_some()); - - log::trace!( - "<<< Delegations done with: mixnet contract = {}, vesting contract = {}", - did_delegate_with_mixnet_contract, - did_delegate_with_vesting_contract - ); - - let mut res: Vec = vec![]; - if did_delegate_with_mixnet_contract { - res.push( - compound_delegator_reward(mix_identity.clone(), fee.clone(), state.clone()).await?, - ); - } - if did_delegate_with_vesting_contract { - res.push(vesting_compound_delegator_reward(mix_identity, fee, state).await?); + res.push(vesting_claim_delegator_reward(mix_id, fee, state).await?); } log::trace!("<<< {:?}", res); Ok(res) diff --git a/nym-wallet/src-tauri/src/operations/mod.rs b/nym-wallet/src-tauri/src/operations/mod.rs index a0720b85b0..457971d8c8 100644 --- a/nym-wallet/src-tauri/src/operations/mod.rs +++ b/nym-wallet/src-tauri/src/operations/mod.rs @@ -1,4 +1,6 @@ +pub mod help; pub mod mixnet; +pub mod signatures; pub mod simulate; pub mod validator_api; pub mod vesting; diff --git a/nym-wallet/src-tauri/src/operations/signatures/mod.rs b/nym-wallet/src-tauri/src/operations/signatures/mod.rs new file mode 100644 index 0000000000..66ccbc9eb6 --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/signatures/mod.rs @@ -0,0 +1 @@ +pub mod sign; diff --git a/nym-wallet/src-tauri/src/operations/signatures/sign.rs b/nym-wallet/src-tauri/src/operations/signatures/sign.rs new file mode 100644 index 0000000000..4ebca7aa56 --- /dev/null +++ b/nym-wallet/src-tauri/src/operations/signatures/sign.rs @@ -0,0 +1,140 @@ +use std::str::FromStr; + +use crate::error::BackendError; +use crate::state::WalletState; +use cosmrs::crypto::secp256k1::{Signature, VerifyingKey}; +use cosmrs::crypto::PublicKey; +use cosmrs::AccountId; +use k256::ecdsa::signature::Verifier; +use serde::Serialize; +use serde_json::json; + +#[derive(Debug, Serialize)] +pub struct SignatureOutputJson { + pub account_id: String, + pub public_key: PublicKey, + pub signature: String, +} + +#[tauri::command] +pub async fn sign( + message: String, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let client = guard.current_client()?; + let wallet = client.nymd.signer(); + let derived_accounts = wallet.try_derive_accounts()?; + let account = derived_accounts.first().ok_or_else(|| { + log::error!(">>> Unable to derive account"); + BackendError::SignatureError("unable to derive account".to_string()) + })?; + + log::info!("<<< Signing message"); + let signature = wallet.sign_raw_with_account(account, message.as_bytes())?; + let signature_as_hex_string = signature.to_string(); + let output = SignatureOutputJson { + account_id: account.address().to_string(), + public_key: account.public_key(), + signature: signature_as_hex_string.to_string(), + }; + log::info!(">>> Signing data {}", json!(output),); + Ok(signature_as_hex_string) +} + +async fn get_pubkey_from_account_address( + address: &AccountId, + state: &tauri::State<'_, WalletState>, +) -> Result { + log::info!("Getting public key for address {} from chain...", address); + let guard = state.read().await; + let client = guard.current_client()?; + let account = client + .nymd + .get_account_details(address) + .await? + .ok_or_else(|| { + log::error!("No account associated with address {}", address); + BackendError::SignatureError(format!("No account associated with address {}", address)) + })?; + let base_account = account.try_get_base_account()?; + + base_account.pubkey.ok_or_else(|| { + log::error!("No pubkey found for address {}", address); + BackendError::SignatureError(format!("No pubkey found for address {}", address)) + }) +} + +enum VerifyInputKind { + PublicKey(PublicKey), + AccountAddress(String), + CurrentAccountAddress, +} + +impl TryFrom> for VerifyInputKind { + type Error = BackendError; + + fn try_from(value: Option) -> Result { + let key = match value { + Some(key) => key, + None => return Ok(VerifyInputKind::CurrentAccountAddress), + }; + if key.trim().is_empty() { + return Err(BackendError::SignatureError( + "Please ensure the public key or address is not empty or whitespace".to_string(), + )); + } + let account_id = AccountId::from_str(&key); + let key_from_json = PublicKey::from_json(&key); + if account_id.is_err() && key_from_json.is_err() { + return Err(BackendError::SignatureError( + "Please ensure the public key or address is valid".to_string(), + )); + } + if let Ok(k) = key_from_json { + Ok(VerifyInputKind::PublicKey(k)) + } else { + Ok(VerifyInputKind::AccountAddress(key)) + } + } +} + +#[tauri::command] +pub async fn verify( + public_key_as_json_or_account_address: Option, + signature_as_hex: String, + message: String, + state: tauri::State<'_, WalletState>, +) -> Result<(), BackendError> { + let public_key = match VerifyInputKind::try_from(public_key_as_json_or_account_address)? { + VerifyInputKind::PublicKey(key) => key, + VerifyInputKind::AccountAddress(address) => { + // get public key from the given account address + get_pubkey_from_account_address(&AccountId::from_str(&address)?, &state).await? + } + VerifyInputKind::CurrentAccountAddress => { + // get public key from current account address + let guard = state.read().await; + let client = guard.current_client()?; + let address = client.nymd.address(); + get_pubkey_from_account_address(address, &state).await? + } + }; + + if public_key.type_url() != PublicKey::SECP256K1_TYPE_URL { + return Err(BackendError::SignatureError( + "Sorry, we only support secp256k1 public keys at the moment".to_string(), + )); + } + + log::info!("<<< Verifying signature [{}]", signature_as_hex); + 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) + .map_err(|e| { + log::error!(">>> Verification failed, wrong signature"); + e + })?) +} diff --git a/nym-wallet/src-tauri/src/operations/simulate/admin.rs b/nym-wallet/src-tauri/src/operations/simulate/admin.rs index 50a559105b..29636bebf2 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/admin.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/admin.rs @@ -13,14 +13,18 @@ pub async fn simulate_update_contract_settings( state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let mixnet_contract_settings_params: ContractStateParams = params.try_into()?; + let reg = guard.registered_coins()?; + let mixnet_contract_settings_params: ContractStateParams = + params.try_convert_to_mixnet_contract_params(reg)?; let client = guard.current_client()?; let mixnet_contract = client.nymd.mixnet_contract_address(); let msg = client.nymd.wrap_contract_execute_message( mixnet_contract, - &ExecuteMsg::UpdateContractStateParams(mixnet_contract_settings_params), + &ExecuteMsg::UpdateContractStateParams { + updated_parameters: mixnet_contract_settings_params, + }, vec![], )?; diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index 794a1f09fe..51912e1332 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -4,9 +4,36 @@ use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; -use mixnet_contract_common::IdentityKey; -use mixnet_contract_common::{ExecuteMsg, Gateway, MixNode}; +use mixnet_contract_common::MixNodeConfigUpdate; +use mixnet_contract_common::{ExecuteMsg, Gateway, MixId, MixNode}; use nym_types::currency::DecCoin; +use nym_types::mixnode::MixNodeCostParams; + +async fn simulate_mixnet_operation( + msg: ExecuteMsg, + raw_funds: Option, + state: &WalletState, +) -> Result { + let guard = state.read().await; + let funds = if let Some(funds) = raw_funds + .map(|c| guard.attempt_convert_to_base_coin(c)) + .transpose()? + { + vec![funds] + } else { + Vec::new() + }; + + let client = guard.current_client()?; + let mixnet_contract = client.nymd.mixnet_contract_address(); + + let msg = client + .nymd + .wrap_contract_execute_message(mixnet_contract, &msg, funds)?; + + let result = client.nymd.simulate(vec![msg]).await?; + guard.create_detailed_fee(result) +} #[tauri::command] pub async fn simulate_bond_gateway( @@ -15,202 +42,118 @@ pub async fn simulate_bond_gateway( owner_signature: String, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let pledge = guard.attempt_convert_to_base_coin(pledge)?; - - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); - - // TODO: I'm still not 100% convinced whether this should be exposed here or handled somewhere else in the client code - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::BondGateway { + simulate_mixnet_operation( + ExecuteMsg::BondGateway { gateway, owner_signature, }, - vec![pledge], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + Some(pledge), + &state, + ) + .await } #[tauri::command] pub async fn simulate_unbond_gateway( state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UnbondGateway {}, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_mixnet_operation(ExecuteMsg::UnbondGateway {}, None, &state).await } #[tauri::command] pub async fn simulate_bond_mixnode( mixnode: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, pledge: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let pledge = guard.attempt_convert_to_base_coin(pledge)?; + let reg = guard.registered_coins()?; + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::BondMixnode { + simulate_mixnet_operation( + ExecuteMsg::BondMixnode { mix_node: mixnode, + cost_params, owner_signature, }, - vec![pledge], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + Some(pledge), + &state, + ) + .await } #[tauri::command] pub async fn simulate_unbond_mixnode( state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UnbondMixnode {}, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_mixnet_operation(ExecuteMsg::UnbondMixnode {}, None, &state).await } #[tauri::command] -pub async fn simulate_update_mixnode( - profit_margin_percent: u8, +pub async fn simulate_update_mixnode_cost_params( + new_costs: MixNodeCostParams, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); + let reg = guard.registered_coins()?; + let new_costs = new_costs.try_convert_to_mixnet_contract_cost_params(reg)?; - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }, - vec![], - )?; + simulate_mixnet_operation( + ExecuteMsg::UpdateMixnodeCostParams { new_costs }, + None, + &state, + ) + .await +} - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) +#[tauri::command] +pub async fn simulate_update_mixnode_config( + update: MixNodeConfigUpdate, + state: tauri::State<'_, WalletState>, +) -> Result { + simulate_mixnet_operation( + ExecuteMsg::UpdateMixnodeConfig { new_config: update }, + None, + &state, + ) + .await } #[tauri::command] pub async fn simulate_delegate_to_mixnode( - identity: &str, + mix_id: MixId, amount: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let delegation = guard.attempt_convert_to_base_coin(amount)?; - - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::DelegateToMixnode { - mix_identity: identity.to_string(), - }, - vec![delegation], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_mixnet_operation( + ExecuteMsg::DelegateToMixnode { mix_id }, + Some(amount), + &state, + ) + .await } #[tauri::command] pub async fn simulate_undelegate_from_mixnode( - identity: &str, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - let mixnet_contract = client.nymd.mixnet_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - mixnet_contract, - &ExecuteMsg::UndelegateFromMixnode { - mix_identity: identity.to_string(), - }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_mixnet_operation(ExecuteMsg::UndelegateFromMixnode { mix_id }, None, &state).await } #[tauri::command] pub async fn simulate_claim_operator_reward( state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client.nymd.simulate_claim_operator_reward(None).await?; - guard.create_detailed_fee(result) -} - -#[tauri::command] -pub async fn simulate_compound_operator_reward( - state: tauri::State<'_, WalletState>, -) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client.nymd.simulate_compound_operator_reward(None).await?; - guard.create_detailed_fee(result) + simulate_mixnet_operation(ExecuteMsg::WithdrawOperatorReward {}, None, &state).await } #[tauri::command] pub async fn simulate_claim_delegator_reward( - mix_identity: IdentityKey, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client - .nymd - .simulate_claim_delegator_reward(mix_identity, None) - .await?; - guard.create_detailed_fee(result) -} - -#[tauri::command] -pub async fn simulate_compound_delegator_reward( - mix_identity: IdentityKey, - state: tauri::State<'_, WalletState>, -) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client - .nymd - .simulate_compound_delegator_reward(mix_identity, None) - .await?; - guard.create_detailed_fee(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 d8850d202c..c7cebf2ce5 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -4,11 +4,38 @@ use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; -use mixnet_contract_common::IdentityKey; -use mixnet_contract_common::{Gateway, MixNode}; +use mixnet_contract_common::MixNodeConfigUpdate; +use mixnet_contract_common::{Gateway, MixId, MixNode}; use nym_types::currency::DecCoin; +use nym_types::mixnode::MixNodeCostParams; use vesting_contract_common::ExecuteMsg; +async fn simulate_vesting_operation( + msg: ExecuteMsg, + raw_funds: Option, + state: &WalletState, +) -> Result { + let guard = state.read().await; + let funds = if let Some(funds) = raw_funds + .map(|c| guard.attempt_convert_to_base_coin(c)) + .transpose()? + { + vec![funds] + } else { + Vec::new() + }; + + let client = guard.current_client()?; + let vesting_contract = client.nymd.vesting_contract_address(); + + let msg = client + .nymd + .wrap_contract_execute_message(vesting_contract, &msg, funds)?; + + let result = client.nymd.simulate(vec![msg]).await?; + guard.create_detailed_fee(result) +} + #[tauri::command] pub async fn simulate_vesting_bond_gateway( gateway: Gateway, @@ -17,157 +44,113 @@ pub async fn simulate_vesting_bond_gateway( state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let pledge = guard.attempt_convert_to_base_coin(pledge)?; + let amount = guard.attempt_convert_to_base_coin(pledge)?.into(); - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::BondGateway { + simulate_vesting_operation( + ExecuteMsg::BondGateway { gateway, owner_signature, - amount: pledge.into(), + amount, }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + None, + &state, + ) + .await } #[tauri::command] pub async fn simulate_vesting_unbond_gateway( state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::UnbondGateway {}, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_vesting_operation(ExecuteMsg::UnbondGateway {}, None, &state).await } #[tauri::command] pub async fn simulate_vesting_bond_mixnode( mixnode: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, pledge: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let pledge = guard.attempt_convert_to_base_coin(pledge)?; + let reg = guard.registered_coins()?; + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; + let amount = guard.attempt_convert_to_base_coin(pledge)?.into(); - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::BondMixnode { + simulate_vesting_operation( + ExecuteMsg::BondMixnode { mix_node: mixnode, + cost_params, owner_signature, - amount: pledge.into(), + amount, }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + None, + &state, + ) + .await } #[tauri::command] pub async fn simulate_vesting_unbond_mixnode( state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::UnbondMixnode {}, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_vesting_operation(ExecuteMsg::UnbondMixnode {}, None, &state).await } #[tauri::command] -pub async fn simulate_vesting_update_mixnode( - profit_margin_percent: u8, +pub async fn simulate_vesting_update_mixnode_cost_params( + new_costs: MixNodeCostParams, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; + let reg = guard.registered_coins()?; + let new_costs = new_costs.try_convert_to_mixnet_contract_cost_params(reg)?; - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); + simulate_vesting_operation( + ExecuteMsg::UpdateMixnodeCostParams { new_costs }, + None, + &state, + ) + .await +} - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::UpdateMixnodeConfig { - profit_margin_percent, - }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) +#[tauri::command] +pub async fn simulate_vesting_update_mixnode_config( + update: MixNodeConfigUpdate, + state: tauri::State<'_, WalletState>, +) -> Result { + simulate_vesting_operation( + ExecuteMsg::UpdateMixnodeConfig { new_config: update }, + None, + &state, + ) + .await } #[tauri::command] pub async fn simulate_vesting_delegate_to_mixnode( - identity: &str, + mix_id: MixId, amount: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; let amount = guard.attempt_convert_to_base_coin(amount)?.into(); - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::DelegateToMixnode { - mix_identity: identity.to_string(), - amount, - }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_vesting_operation( + ExecuteMsg::DelegateToMixnode { mix_id, amount }, + None, + &state, + ) + .await } #[tauri::command] pub async fn simulate_vesting_undelegate_from_mixnode( - identity: &str, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::UndelegateFromMixnode { - mix_identity: identity.to_string(), - }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_vesting_operation(ExecuteMsg::UndelegateFromMixnode { mix_id }, None, &state).await } #[tauri::command] @@ -177,74 +160,20 @@ pub async fn simulate_withdraw_vested_coins( ) -> Result { let guard = state.read().await; let amount = guard.attempt_convert_to_base_coin(amount)?.into(); - - let client = guard.current_client()?; - let vesting_contract = client.nymd.vesting_contract_address(); - - let msg = client.nymd.wrap_contract_execute_message( - vesting_contract, - &ExecuteMsg::WithdrawVestedCoins { amount }, - vec![], - )?; - - let result = client.nymd.simulate(vec![msg]).await?; - guard.create_detailed_fee(result) + simulate_vesting_operation(ExecuteMsg::WithdrawVestedCoins { amount }, None, &state).await } #[tauri::command] pub async fn simulate_vesting_claim_operator_reward( state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client - .nymd - .simulate_vesting_claim_operator_reward(None) - .await?; - guard.create_detailed_fee(result) -} - -#[tauri::command] -pub async fn simulate_vesting_compound_operator_reward( - state: tauri::State<'_, WalletState>, -) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client - .nymd - .simulate_vesting_compound_operator_reward(None) - .await?; - guard.create_detailed_fee(result) + simulate_vesting_operation(ExecuteMsg::ClaimOperatorReward {}, None, &state).await } #[tauri::command] pub async fn simulate_vesting_claim_delegator_reward( - mix_identity: IdentityKey, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client - .nymd - .simulate_vesting_claim_delegator_reward(mix_identity, None) - .await?; - guard.create_detailed_fee(result) -} - -#[tauri::command] -pub async fn simulate_vesting_compound_delegator_reward( - mix_identity: IdentityKey, - state: tauri::State<'_, WalletState>, -) -> Result { - let guard = state.read().await; - let client = guard.current_client()?; - - let result = client - .nymd - .simulate_vesting_compound_delegator_reward(mix_identity, None) - .await?; - guard.create_detailed_fee(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 7ff60d3e6b..16c83cfb67 100644 --- a/nym-wallet/src-tauri/src/operations/validator_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/validator_api/status.rs @@ -4,67 +4,77 @@ use crate::api_client; use crate::error::BackendError; use crate::state::WalletState; +use mixnet_contract_common::{IdentityKeyRef, MixId}; use validator_client::models::{ - CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse, - RewardEstimationResponse, StakeSaturationResponse, + GatewayCoreStatusResponse, GatewayStatusReportResponse, InclusionProbabilityResponse, + MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, + StakeSaturationResponse, }; #[tauri::command] pub async fn mixnode_core_node_status( - identity: &str, + mix_id: MixId, since: Option, state: tauri::State<'_, WalletState>, -) -> Result { +) -> Result { Ok(api_client!(state) - .get_mixnode_core_status_count(identity, since) + .get_mixnode_core_status_count(mix_id, since) .await?) } #[tauri::command] pub async fn gateway_core_node_status( - identity: &str, + identity: IdentityKeyRef<'_>, since: Option, state: tauri::State<'_, WalletState>, -) -> Result { +) -> Result { Ok(api_client!(state) .get_gateway_core_status_count(identity, since) .await?) } +#[tauri::command] +pub async fn gateway_report( + identity: IdentityKeyRef<'_>, + state: tauri::State<'_, WalletState>, +) -> Result { + Ok(api_client!(state).get_gateway_report(identity).await?) +} + #[tauri::command] pub async fn mixnode_status( - identity: &str, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { - Ok(api_client!(state).get_mixnode_status(identity).await?) + Ok(api_client!(state).get_mixnode_status(mix_id).await?) } #[tauri::command] pub async fn mixnode_reward_estimation( - identity: &str, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) - .get_mixnode_reward_estimation(identity) + .get_mixnode_reward_estimation(mix_id) .await?) } #[tauri::command] pub async fn mixnode_stake_saturation( - identity: &str, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) - .get_mixnode_stake_saturation(identity) + .get_mixnode_stake_saturation(mix_id) .await?) } #[tauri::command] pub async fn mixnode_inclusion_probability( - identity: &str, + mix_id: MixId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) - .get_mixnode_inclusion_probability(identity) + .get_mixnode_inclusion_probability(mix_id) .await?) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index c567e4e908..99f0086d9c 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -2,8 +2,10 @@ use crate::error::BackendError; use crate::nymd_client; use crate::state::WalletState; use crate::{Gateway, MixNode}; +use mixnet_contract_common::MixNodeConfigUpdate; use nym_types::currency::DecCoin; +use nym_types::mixnode::MixNodeCostParams; use nym_types::transaction::TransactionExecuteResult; use validator_client::nymd::{Fee, VestingSigningClient}; @@ -60,26 +62,29 @@ pub async fn vesting_unbond_gateway( #[tauri::command] pub async fn vesting_bond_mixnode( mixnode: MixNode, + cost_params: MixNodeCostParams, owner_signature: String, pledge: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; + let reg = guard.registered_coins()?; let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; log::info!( - ">>> Bond mixnode with locked tokens: identity_key = {}, pledge_display = {}, pledge_base = {}, fee = {:?}", - mixnode.identity_key, - pledge, - pledge_base, - fee - ); + ">>> Bond mixnode with locked tokens: identity_key = {}, pledge_display = {}, pledge_base = {}, fee = {:?}", + mixnode.identity_key, + pledge, + pledge_base, + fee + ); let res = guard .current_client()? .nymd - .vesting_bond_mixnode(mixnode, &owner_signature, pledge_base, fee) + .vesting_bond_mixnode(mixnode, cost_params, &owner_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -140,22 +145,50 @@ pub async fn withdraw_vested_coins( } #[tauri::command] -pub async fn vesting_update_mixnode( - profit_margin_percent: u8, +pub async fn vesting_update_mixnode_cost_params( + new_costs: MixNodeCostParams, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; + let reg = guard.registered_coins()?; let fee_amount = guard.convert_tx_fee(fee.as_ref()); + let cost_params = new_costs.try_convert_to_mixnet_contract_cost_params(reg)?; + log::info!( - ">>> Update mixnode bonded with locked tokens: profit_margin_percent = {}, fee = {:?}", - profit_margin_percent, + ">>> Update mixnode cost params with locked tokens: parameters = {}, fee = {:?}", + cost_params.to_inline_json(), fee, ); let res = guard .current_client()? .nymd - .vesting_update_mixnode_config(profit_margin_percent, fee) + .vesting_update_mixnode_cost_params(cost_params, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[tauri::command] +pub async fn vesting_update_mixnode_config( + update: MixNodeConfigUpdate, + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( + ">>> Update mixnode config with locked tokens: update = {}, fee = {:?}", + update.to_inline_json(), + fee, + ); + let res = guard + .current_client()? + .nymd + .vesting_update_mixnode_config(update, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); diff --git a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs index 9ddde467a8..59183878e5 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs @@ -1,40 +1,16 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::error::BackendError; use crate::state::WalletState; +use mixnet_contract_common::MixId; use nym_types::currency::DecCoin; -use nym_types::delegation::DelegationEvent; use nym_types::transaction::TransactionExecuteResult; use validator_client::nymd::{Fee, VestingSigningClient}; -#[tauri::command] -pub async fn get_pending_vesting_delegation_events( - state: tauri::State<'_, WalletState>, -) -> Result, BackendError> { - log::info!(">>> Get pending delegations from vesting contract"); - - let guard = state.read().await; - let reg = guard.registered_coins()?; - let client = &guard.current_client()?.nymd; - let vesting_contract = client.vesting_contract_address(); - - let events = client - .get_pending_delegation_events( - client.address().to_string(), - Some(vesting_contract.to_string()), - ) - .await?; - - log::info!("<<< {} events", events.len()); - log::trace!("<<< {:?}", events); - - Ok(events - .into_iter() - .map(|event| DelegationEvent::from_mixnet_contract(event, reg)) - .collect::>()?) -} - #[tauri::command] pub async fn vesting_delegate_to_mixnode( - identity: &str, + mix_id: MixId, amount: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, @@ -44,16 +20,16 @@ pub async fn vesting_delegate_to_mixnode( let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Delegate to mixnode with locked tokens: identity_key = {}, amount_display = {}, amount_base = {}, fee = {:?}", - identity, - amount, - delegation, - fee - ); + ">>> Delegate to mixnode with locked tokens: mix_id = {}, amount_display = {}, amount_base = {}, fee = {:?}", + mix_id, + amount, + delegation, + fee + ); let res = guard .current_client()? .nymd - .vesting_delegate_to_mixnode(identity, delegation, fee) + .vesting_delegate_to_mixnode(mix_id, delegation, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -64,21 +40,21 @@ pub async fn vesting_delegate_to_mixnode( #[tauri::command] pub async fn vesting_undelegate_from_mixnode( - identity: &str, + mix_id: MixId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Undelegate from mixnode delegated with locked tokens: identity_key = {}, fee = {:?}", - identity, + ">>> Undelegate from mixnode delegated with locked tokens: mix_id = {}, fee = {:?}", + mix_id, fee, ); let res = guard .current_client()? .nymd - .vesting_undelegate_from_mixnode(identity, fee) + .vesting_undelegate_from_mixnode(mix_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); diff --git a/nym-wallet/src-tauri/src/operations/vesting/queries.rs b/nym-wallet/src-tauri/src/operations/vesting/queries.rs index 7d0d726a1c..d05608fb25 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/queries.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/queries.rs @@ -1,3 +1,6 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::error::BackendError; use crate::nymd_client; use crate::state::WalletState; diff --git a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs index b3a190c1ff..ad3760a364 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs @@ -1,6 +1,9 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::error::BackendError; use crate::state::WalletState; -use mixnet_contract_common::IdentityKey; +use mixnet_contract_common::MixId; use nym_types::transaction::TransactionExecuteResult; use validator_client::nymd::Fee; @@ -15,27 +18,7 @@ pub async fn vesting_claim_operator_reward( let res = guard .current_client()? .nymd - .execute_vesting_claim_operator_reward(None) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, fee_amount, - )?) -} - -#[tauri::command] -pub async fn vesting_compound_operator_reward( - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result { - log::info!(">>> Vesting account: compound operator reward"); - let guard = state.read().await; - let fee_amount = guard.convert_tx_fee(fee.as_ref()); - let res = guard - .current_client()? - .nymd - .execute_vesting_compound_operator_reward(fee) + .execute_vesting_withdraw_operator_reward(None) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -46,44 +29,20 @@ pub async fn vesting_compound_operator_reward( #[tauri::command] pub async fn vesting_claim_delegator_reward( - mix_identity: IdentityKey, + mix_id: MixId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { log::info!( - ">>> Vesting account: claim delegator reward: identity_key = {}", - mix_identity + ">>> Vesting account: claim delegator reward: mix_id = {}", + mix_id ); let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); let res = guard .current_client()? .nymd - .execute_vesting_claim_delegator_reward(mix_identity, fee) - .await?; - log::info!("<<< tx hash = {}", res.transaction_hash); - log::trace!("<<< {:?}", res); - Ok(TransactionExecuteResult::from_execute_result( - res, fee_amount, - )?) -} - -#[tauri::command] -pub async fn vesting_compound_delegator_reward( - mix_identity: IdentityKey, - fee: Option, - state: tauri::State<'_, WalletState>, -) -> Result { - log::info!( - ">>> Vesting account: compound delegator reward: identity_key = {}", - mix_identity - ); - let guard = state.read().await; - let fee_amount = guard.convert_tx_fee(fee.as_ref()); - let res = guard - .current_client()? - .nymd - .execute_vesting_compound_delegator_reward(mix_identity, fee) + .execute_vesting_withdraw_delegator_reward(mix_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); diff --git a/nym-wallet/src-tauri/src/platform_constants.rs b/nym-wallet/src-tauri/src/platform_constants.rs index b0ba4f3172..eaf87250ca 100644 --- a/nym-wallet/src-tauri/src/platform_constants.rs +++ b/nym-wallet/src-tauri/src/platform_constants.rs @@ -1,9 +1,6 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -// Specify filenames and other platform specific constants to respect platform conventions, or at -// least, something popular on each respective platform. - pub const CONFIG_DIR_NAME: &str = "nym-wallet"; pub const CONFIG_FILENAME: &str = "config.toml"; pub const STORAGE_DIR_NAME: &str = "nym-wallet"; diff --git a/nym-wallet/src-tauri/src/state.rs b/nym-wallet/src-tauri/src/state.rs index 3abcb29324..e36dfe1b5c 100644 --- a/nym-wallet/src-tauri/src/state.rs +++ b/nym-wallet/src-tauri/src/state.rs @@ -1,9 +1,11 @@ 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; -use nym_types::currency::{DecCoin, RegisteredCoins}; +use nym_types::currency::{DecCoin, Denom, RegisteredCoins}; use nym_types::fees::FeeDetails; use nym_wallet_types::network::Network; use nym_wallet_types::network_config; @@ -105,6 +107,24 @@ impl WalletStateInner { Ok(registered_coins.attempt_convert_to_display_dec_coin(coin)?) } + pub(crate) fn display_coin_from_base_decimal( + &self, + base_denom: &Denom, + base_amount: Decimal, + ) -> Result { + let registered_coins = self + .registered_coins + .get(&self.current_network) + .ok_or_else(|| BackendError::UnknownCoinDenom(base_denom.clone()))?; + + Ok(registered_coins + .attempt_create_display_coin_from_base_dec_amount(base_denom, base_amount)?) + } + + pub(crate) fn default_zero_mix_display_coin(&self) -> DecCoin { + self.current_network.default_zero_mix_display_coin() + } + pub(crate) fn registered_coins(&self) -> Result<&RegisteredCoins, BackendError> { self.registered_coins .get(&self.current_network) @@ -196,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) { @@ -516,7 +537,7 @@ mod tests { "http://nymd_url.com/".parse().unwrap(), "http://foo.com".parse().unwrap(), "http://bar.com".parse().unwrap(), - "https://rpc.nyx.nodes.guru".parse().unwrap(), + "https://rpc.nymtech.net".parse().unwrap(), ], ); assert_eq!( @@ -538,7 +559,7 @@ mod tests { "http://nymd_url.com/".parse().unwrap(), "http://foo.com".parse().unwrap(), "http://bar.com".parse().unwrap(), - "https://rpc.nyx.nodes.guru".parse().unwrap(), + "https://rpc.nymtech.net".parse().unwrap(), ], ) } diff --git a/nym-wallet/src-tauri/src/utils.rs b/nym-wallet/src-tauri/src/utils.rs index 304004a6ef..f04d47722f 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -1,9 +1,16 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::error::BackendError; use crate::nymd_client; use crate::state::WalletState; +use cosmwasm_std::Decimal; +use mixnet_contract_common::{IdentityKey, MixId, Percent}; use nym_types::currency::DecCoin; +use nym_types::mixnode::MixNodeCostParams; use nym_wallet_types::app::AppEnv; use serde::{Deserialize, Serialize}; +use validator_client::nymd::traits::MixnetQueryClient; use validator_client::nymd::{tx, Coin, CosmosCoin, Gas, GasPrice}; fn get_env_as_option(key: &str) -> Option { @@ -25,19 +32,54 @@ pub fn get_env() -> AppEnv { #[tauri::command] pub async fn owns_mixnode(state: tauri::State<'_, WalletState>) -> Result { Ok(nymd_client!(state) - .owns_mixnode(nymd_client!(state).address()) + .get_owned_mixnode(nymd_client!(state).address()) .await? + .mixnode_details .is_some()) } #[tauri::command] pub async fn owns_gateway(state: tauri::State<'_, WalletState>) -> Result { Ok(nymd_client!(state) - .owns_gateway(nymd_client!(state).address()) + .get_owned_gateway(nymd_client!(state).address()) .await? + .gateway .is_some()) } +#[tauri::command] +pub async fn try_convert_pubkey_to_mix_id( + state: tauri::State<'_, WalletState>, + mix_identity: IdentityKey, +) -> Result, BackendError> { + let res = nymd_client!(state) + .get_mixnode_details_by_identity(mix_identity) + .await?; + Ok(res.map(|mixnode_details| mixnode_details.mix_id())) +} + +#[tauri::command] +pub async fn default_mixnode_cost_params( + state: tauri::State<'_, WalletState>, + profit_margin_percent: Percent, +) -> Result { + // attaches the old pre-update default operating cost of 40 nym per interval + let guard = state.read().await; + + // since this is only a temporary solution until users are required to provide their own cost + // params, we can make the assumption that it's always safe to use the mix denom here + let current_network = guard.current_network(); + let denom = current_network.mix_denom().display; + + Ok(MixNodeCostParams { + profit_margin_percent, + interval_operating_cost: DecCoin { + denom: denom.into(), + amount: Decimal::from_atomics(40u32, 0).unwrap(), + }, + }) +} + #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum Operation { Upload, diff --git a/nym-wallet/src-tauri/tauri.conf.json b/nym-wallet/src-tauri/tauri.conf.json index 238f534a1a..035117f5d8 100644 --- a/nym-wallet/src-tauri/tauri.conf.json +++ b/nym-wallet/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "nym-wallet", - "version": "1.0.8" + "version": "1.0.9" }, "build": { "distDir": "../dist", 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 }) => ( diff --git a/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx b/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx index 097f6d7ad0..61d6f7b9e5 100644 --- a/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx +++ b/nym-wallet/src/components/Accounts/modals/AddAccountModal.tsx @@ -115,13 +115,14 @@ const NameAccount = ({ onNext, onBack }: { onNext: (value: string) => void; onBa {error} { setValue(e.target.value); setError(undefined); }} fullWidth + InputLabelProps={{ shrink: true }} /> @@ -177,68 +178,72 @@ export const AddAccountModal = () => { }, [step]); return ( - - - - - {`${dialogToDisplay} new account`} - - - {dialogToDisplay === 'Add' ? createAccountSteps[step] : importAccountSteps[step]} - - - {(() => { - switch (step) { - case 0: - return dialogToDisplay === 'Add' ? ( - setStep((s) => s + 1)} - onBack={() => (step === 0 ? handleClose() : setStep((s) => s - 1))} - /> - ) : ( - setData((d) => ({ ...d, mnemonic: value }))} - onNext={() => setStep((s) => s + 1)} - onBack={() => (step === 0 ? handleClose() : setStep((s) => s - 1))} - /> - ); - case 1: - return ( - { - setData((d) => ({ ...d, accountName })); - setStep((s) => s + 1); - }} - onBack={() => setStep((s) => s - 1)} - /> - ); - case 2: - return ( - { - if (data.accountName && data.mnemonic) { - try { - await handleAddAccount({ accountName: data.accountName, mnemonic: data.mnemonic, password }); - setStep(0); - setDialogToDisplay('Accounts'); - } catch (e) { - Console.error(e as string); - } + + + + {`${dialogToDisplay} new account`} + + + {dialogToDisplay === 'Add' ? createAccountSteps[step] : importAccountSteps[step]} + + + {(() => { + switch (step) { + case 0: + return dialogToDisplay === 'Add' ? ( + setStep((s) => s + 1)} + onBack={() => (step === 0 ? handleClose() : setStep((s) => s - 1))} + /> + ) : ( + setData((d) => ({ ...d, mnemonic: value }))} + onNext={() => setStep((s) => s + 1)} + onBack={() => (step === 0 ? handleClose() : setStep((s) => s - 1))} + /> + ); + case 1: + return ( + { + setData((d) => ({ ...d, accountName })); + setStep((s) => s + 1); + }} + onBack={() => setStep((s) => s - 1)} + /> + ); + case 2: + return ( + { + if (data.accountName && data.mnemonic) { + try { + await handleAddAccount({ accountName: data.accountName, mnemonic: data.mnemonic, password }); + setStep(0); + setDialogToDisplay('Accounts'); + } catch (e) { + Console.error(e as string); } - }} - onCancel={() => setStep((s) => s - 1)} - isLoading={isLoading} - error={error} - /> - ); - default: - return null; - } - })()} - + } + }} + onCancel={() => setStep((s) => s - 1)} + isLoading={isLoading} + error={error} + /> + ); + default: + return null; + } + })()} ); }; diff --git a/nym-wallet/src/components/Accounts/modals/ConfirmPasswordModal.tsx b/nym-wallet/src/components/Accounts/modals/ConfirmPasswordModal.tsx index 5f9307bcdf..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,9 +14,17 @@ export const ConfirmPasswordModal = ({ onConfirm: (password: string) => Promise; }) => { const { isLoading, error } = useContext(AccountsContext); + const theme = useTheme(); return ( - + Switch account diff --git a/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx b/nym-wallet/src/components/Accounts/modals/EditAccountModal.tsx index c9d495a329..e5dbd0464e 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,12 +20,21 @@ export const EditAccountModal = () => { const { accountToEdit, dialogToDisplay, setDialogToDisplay, handleEditAccount } = useContext(AccountsContext); + const theme = useTheme(); + useEffect(() => { setAccountName(accountToEdit ? accountToEdit?.id : ''); }, [accountToEdit]); return ( - setDialogToDisplay('Accounts')} fullWidth> + setDialogToDisplay('Accounts')} + fullWidth + PaperProps={{ + style: { border: `1px solid ${theme.palette.nym.nymWallet.modal.border}` }, + }} + > @@ -45,6 +55,7 @@ export const EditAccountModal = () => { value={accountName} onChange={(e) => setAccountName(e.target.value)} autoFocus + InputLabelProps={{ shrink: true }} /> diff --git a/nym-wallet/src/components/Accounts/modals/MnemonicModal.tsx b/nym-wallet/src/components/Accounts/modals/MnemonicModal.tsx index 713a321f25..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(''); @@ -40,63 +41,65 @@ export const MnemonicModal = () => { }; return ( - - - - - Display mnemonic - - - - - theme.palette.text.disabled }}> - {`Display mnemonic for: ${accountMnemonic?.accountName}`} - - - - - {error && ( - - {error} - - )} - {!accountMnemonic.value ? ( - <> - Enter the password used to login to your wallet - setPassword(pswrd)} - autoFocus - /> - - ) : ( - - )} - - - - {!accountMnemonic.value && ( - + + + + Display mnemonic + + theme.palette.text.disabled }}> + {`Display mnemonic for: ${accountMnemonic?.accountName}`} + + + + + {error && ( + + {error} + )} - - + {!accountMnemonic.value ? ( + <> + Enter the password used to login to your wallet + setPassword(pswrd)} + autoFocus + /> + + ) : ( + + )} +
+ + + + {!accountMnemonic.value && ( + + )} + ); }; diff --git a/nym-wallet/src/components/ActionsMenu.tsx b/nym-wallet/src/components/ActionsMenu.tsx index 63e8193833..227af88407 100644 --- a/nym-wallet/src/components/ActionsMenu.tsx +++ b/nym-wallet/src/components/ActionsMenu.tsx @@ -13,7 +13,7 @@ export const ActionsMenu: React.FC<{ open: boolean; onOpen: () => void; onClose: return ( <> - + t.palette.nym.nymWallet.text.main }} /> {children} diff --git a/nym-wallet/src/components/Alert.tsx b/nym-wallet/src/components/Alert.tsx new file mode 100644 index 0000000000..38bd2cb989 --- /dev/null +++ b/nym-wallet/src/components/Alert.tsx @@ -0,0 +1,32 @@ +import React, { useState } from 'react'; +import { Alert as MuiAlert, IconButton } from '@mui/material'; +import { Close } from '@mui/icons-material'; + +export const Alert = ({ title, dismissable }: { title: string | React.ReactNode; dismissable?: boolean }) => { + const [displayAlert, setDisplayAlert] = useState(true); + const handleDismiss = () => setDisplayAlert(false); + + if (!displayAlert) return null; + + return ( + theme.palette.nym.nymWallet.text.blue, + '& .MuiAlert-icon': { color: 'nym.nymWallet.text.blue', mr: 1 }, + }} + action={ + dismissable && ( + + + + ) + } + > + {title} + + ); +}; diff --git a/nym-wallet/src/components/Bonding/Bond.tsx b/nym-wallet/src/components/Bonding/Bond.tsx index 3392cde43f..1f96da875a 100644 --- a/nym-wallet/src/components/Bonding/Bond.tsx +++ b/nym-wallet/src/components/Bonding/Bond.tsx @@ -14,11 +14,11 @@ export const Bond = ({ - Bond a mixnode or a gateway + Bond a mixnode or a gateway void; }) => { - const { name, bond, ip, identityKey } = gateway; + const { name, bond, ip, identityKey, routingScore } = gateway; const cells: Cell[] = [ - { - cell: ip, - id: 'stake-saturation-cell', - }, { cell: `${bond.amount} ${bond.denom}`, - id: 'stake-cell', + id: 'bond-cell', sx: { pl: 0 }, }, + { + cell: `${routingScore?.current || '- '}%`, + id: 'routing-score-cell', + }, + { + cell: `${routingScore?.average || '- '}%`, + id: 'average-score-cell', + }, + { + cell: ip, + id: 'ip-cell', + }, { cell: , id: 'actions-cell', @@ -56,7 +73,7 @@ export const BondedGateway = ({ + Gateway @@ -73,7 +90,7 @@ export const BondedGateway = ({ {network && ( - Check more stats of your node on the{' '} + Check more stats of your gateway on the{' '} explorer diff --git a/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx index d602eeacb8..a30e5557cb 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnode.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -1,14 +1,16 @@ import React from 'react'; -import { Box, Button, Stack, Typography } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import { Box, Button, Chip, Stack, Tooltip, Typography } from '@mui/material'; import { Link } from '@nymproject/react/link/Link'; +import { isMixnode, Network } from 'src/types'; import { TBondedMixnode, urls } from 'src/context'; import { NymCard } from 'src/components'; -import { Network } from 'src/types'; import { IdentityKey } from 'src/components/IdentityKey'; import { NodeStatus } from 'src/components/NodeStatus'; import { Node as NodeIcon } from '../../svg-icons/node'; import { Cell, Header, NodeTable } from './NodeTable'; import { BondedMixnodeActions, TBondedMixnodeActions } from './BondedMixnodeActions'; +import { NodeStats } from './NodeStats'; const headers: Header[] = [ { @@ -30,6 +32,12 @@ const headers: Header[] = [ tooltipText: 'The percentage of the node rewards that you as the node operator will take before the rest of the reward is shared between you and the delegators.', }, + { + header: 'Operating cost', + id: 'operator-cost', + tooltipText: + 'Monthly operational costs of running your node. The cost also influences how the rewards are split between you and your delegators. ', + }, { header: 'Operator rewards', id: 'operator-rewards', @@ -55,8 +63,20 @@ export const BondedMixnode = ({ network?: Network; onActionSelect: (action: TBondedMixnodeActions) => void; }) => { - const { name, stake, bond, stakeSaturation, profitMargin, operatorRewards, delegators, status, identityKey } = - mixnode; + const navigate = useNavigate(); + const { + name, + stake, + bond, + stakeSaturation, + profitMargin, + operatorRewards, + operatorCost, + delegators, + status, + identityKey, + host, + } = mixnode; const cells: Cell[] = [ { cell: `${stake.amount} ${stake.denom}`, @@ -75,7 +95,11 @@ export const BondedMixnode = ({ id: 'pm-cell', }, { - cell: `${operatorRewards.amount} ${operatorRewards.denom}`, + cell: operatorCost ? `${operatorCost.amount} ${operatorCost.denom}` : '-', + id: 'operator-cost-cell', + }, + { + cell: operatorRewards ? `${operatorRewards.amount} ${operatorRewards.denom}` : '-', id: 'operator-rewards-cell', }, { @@ -83,10 +107,13 @@ export const BondedMixnode = ({ id: 'delegators-cell', }, { - cell: ( + cell: mixnode.isUnbonding ? ( + + ) : ( ), id: 'actions-cell', @@ -95,44 +122,56 @@ export const BondedMixnode = ({ ]; return ( - - - - Mix node - - - - {name && ( - - {name} - - )} - - - } - Action={ - - } - > - - {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..bf0511d51a 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 ( - + - + @@ -89,7 +94,7 @@ const NodeFormData = ({ gatewayData, onNext }: { gatewayData: GatewayData; onNex label="Show advanced options" /> {showAdvancedOptions && ( - + )} @@ -156,8 +163,8 @@ const AmountFormData = ({ }, []); return ( - - + + {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..721d114961 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 ( - + - + @@ -80,7 +84,7 @@ const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNex label="Show advanced options" /> {showAdvancedOptions && ( - + )} @@ -156,8 +163,8 @@ const AmountFormData = ({ }, []); return ( - - + + {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 +236,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 d54f75ce24..d983da0f51 100644 --- a/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx @@ -11,7 +11,7 @@ import { simulateBondGateway, simulateVestingBondGateway } from 'src/requests'; import { TBondGatewayArgs } from 'src/types'; import { BondGatewayForm } from '../forms/BondGatewayForm'; -const defaultMixnodeValues: GatewayData = { +const defaultGatewayValues: GatewayData = { identityKey: '', sphinxKey: '', ownerSignature: '', @@ -19,11 +19,12 @@ const defaultMixnodeValues: GatewayData = { host: '', version: '', mixPort: 1789, - clientsPort: 1790, + clientsPort: 9000, }; const defaultAmountValues = (denom: CurrencyDenom) => ({ amount: { amount: '100', denom }, + operatorCost: { amount: '40', denom }, tokenPool: 'balance', }); @@ -43,7 +44,7 @@ export const BondGatewayModal = ({ onError: (e: string) => void; }) => { const [step, setStep] = useState<1 | 2 | 3>(1); - const [gatewayData, setGatewayData] = useState(defaultMixnodeValues); + const [gatewayData, setGatewayData] = useState(defaultGatewayValues); const [amountData, setAmountData] = useState(defaultAmountValues(denom)); const { fee, getFee, resetFeeState, feeError } = useGetFee(); diff --git a/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx b/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx index fd0de36051..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,6 +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 { toPercentFloatString } from '../../../utils'; const defaultMixnodeValues: MixnodeData = { identityKey: '', @@ -24,7 +24,8 @@ const defaultMixnodeValues: MixnodeData = { const defaultAmountValues = (denom: CurrencyDenom) => ({ amount: { amount: '100', denom }, - profitMargin: 10, + operatorCost: { amount: '40', denom }, + profitMargin: '10', tokenPool: 'balance', }); @@ -70,7 +71,8 @@ export const BondMixnodeModal = ({ }; const handleUpdateAmountData = async (data: MixnodeAmount) => { - setAmountData(data); + setAmountData({ ...data }); + const payload = { pledge: data.amount, ownerSignature: mixnodeData.ownerSignature, @@ -81,7 +83,13 @@ export const BondMixnodeModal = ({ verloc_port: mixnodeData.verlocPort, sphinx_key: mixnodeData.sphinxKey, identity_key: mixnodeData.identityKey, - profit_margin_percent: data.profitMargin, + }, + costParams: { + profit_margin_percent: toPercentFloatString(data.profitMargin), + interval_operating_cost: { + amount: data.operatorCost.amount.toString(), + denom: data.operatorCost.denom, + }, }, }; @@ -104,7 +112,13 @@ export const BondMixnodeModal = ({ verloc_port: mixnodeData.verlocPort, sphinx_key: mixnodeData.sphinxKey, identity_key: mixnodeData.identityKey, - profit_margin_percent: amountData.profitMargin, + }, + costParams: { + profit_margin_percent: toPercentFloatString(amountData.profitMargin), + interval_operating_cost: { + amount: amountData.operatorCost.amount, + denom: amountData.operatorCost.denom, + }, }, }, amountData.tokenPool as TPoolOption, @@ -143,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..88518abc28 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)} />} - setSignature(e.target.value)} /> + setSignature(e.target.value)} + InputLabelProps={{ shrink: true }} + /> {errorSignature && Invalid signature} diff --git a/nym-wallet/src/components/Bonding/modals/CompoundRewardsModal.tsx b/nym-wallet/src/components/Bonding/modals/CompoundRewardsModal.tsx deleted file mode 100644 index 999f1c3302..0000000000 --- a/nym-wallet/src/components/Bonding/modals/CompoundRewardsModal.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useEffect } from 'react'; -import { FeeDetails } from '@nymproject/types'; -import { ModalFee } from 'src/components/Modals/ModalFee'; -import { ModalListItem } from 'src/components/Modals/ModalListItem'; -import { SimpleModal } from 'src/components/Modals/SimpleModal'; -import { TBondedMixnode } from 'src/context'; -import { useGetFee } from 'src/hooks/useGetFee'; -import { simulateCompoundOperatorReward, simulateVestingCompoundOperatorReward } from 'src/requests'; - -export const CompoundRewardsModal = ({ - node, - onConfirm, - onClose, - onError, -}: { - node: TBondedMixnode; - onClose: () => void; - onConfirm: (fee?: FeeDetails) => void; - onError: (err: string) => void; -}) => { - const { fee, getFee, feeError, isFeeLoading } = useGetFee(); - - useEffect(() => { - if (feeError) onError(feeError); - }, [feeError]); - - useEffect(() => { - if (node.proxy) getFee(simulateVestingCompoundOperatorReward, {}); - else getFee(simulateCompoundOperatorReward, {}); - }, []); - - const handleOnOK = async () => onConfirm(fee); - - return ( - - - - - - ); -}; diff --git a/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx b/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx index 1a1903d205..3a8dca5576 100644 --- a/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx @@ -5,13 +5,14 @@ import { Node as NodeIcon } from 'src/svg-icons/node'; import { TBondedMixnode } from 'src/context'; import { Tabs } from 'src/components/Tabs'; import { ModalListItem } from 'src/components/Modals/ModalListItem'; -import { isDecimal } from 'src/utils'; +import { attachDefaultOperatingCost, isDecimal, toPercentFloatString } from 'src/utils'; import { useGetFee } from 'src/hooks/useGetFee'; import { ConfirmTx } from 'src/components/ConfirmTX'; -import { simulateUpdateMixnode, simulateVestingUpdateMixnode } from 'src/requests'; +import { simulateUpdateMixnodeCostParams, simulateVestingUpdateMixnodeCostParams } from 'src/requests'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { FeeDetails } from '@nymproject/types'; +//Now we are using the node setting page instead of this modal export const NodeSettings = ({ currentPm, isVesting, @@ -19,13 +20,13 @@ export const NodeSettings = ({ onClose, onError, }: { - currentPm: TBondedMixnode['profitMargin']; - isVesting: boolean; - onConfirm: (profitMargin: number, fee?: FeeDetails) => Promise; + currentPm?: TBondedMixnode['profitMargin']; + isVesting?: boolean; + onConfirm: (profitMargin: string, fee?: FeeDetails) => 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,10 +53,15 @@ export const NodeSettings = ({ return; } - if (isVesting) { - await getFee(simulateVestingUpdateMixnode, { profitMarginPercent: pmAsNumber }); - } else { - await getFee(simulateUpdateMixnode, { profitMarginPercent: pmAsNumber }); + 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); + } } }; @@ -71,7 +77,7 @@ export const NodeSettings = ({ if (isFeeLoading) return ; - if (fee) + if (fee && pm) return ( onConfirm(Number(pm), fee)} + onConfirm={() => onConfirm(pm, fee)} > @@ -102,19 +108,25 @@ export const NodeSettings = ({ okLabel="Next" onClose={onClose} > - + Set profit margin - setPm(e.target.value)} fullWidth /> + setPm(e.target.value)} + fullWidth + InputLabelProps={{ shrink: true }} + /> {error && ( - Profit margin should be a whole number between 0 and 100 + Profit margin should be a number between 0 and 100 )} - Your new profit margin will be applied in the next epoch + Your new profit margin will be applied in the next interval diff --git a/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx b/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx index 1a312cdc07..76b9192df9 100644 --- a/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx @@ -43,7 +43,9 @@ export const RedeemRewardsModal = ({ > diff --git a/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx index 1c105139bc..15b46b517b 100644 --- a/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Typography } from '@mui/material'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { TBondedGateway, TBondedMixnode } from 'src/context'; import { useGetFee } from 'src/hooks/useGetFee'; import { isGateway, isMixnode } from 'src/types'; @@ -57,14 +57,7 @@ export const UnbondModal = ({ node, onConfirm, onClose, onError }: Props) => { onOk={onConfirm} onClose={onClose} > - - {isMixnode(node) && ( - - )} + 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 066c271e48..a6c1bb0dbf 100644 --- a/nym-wallet/src/components/Delegation/DelegateModal.tsx +++ b/nym-wallet/src/components/Delegation/DelegateModal.tsx @@ -1,11 +1,12 @@ -import React, { useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { Box, Typography, SxProps } from '@mui/material'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; -import { CurrencyDenom, FeeDetails, DecCoin } from '@nymproject/types'; +import { CurrencyDenom, FeeDetails, DecCoin, decimalToFloatApproximation } from '@nymproject/types'; import { Console } from 'src/utils/console'; import { useGetFee } from 'src/hooks/useGetFee'; -import { simulateDelegateToMixnode, simulateVestingDelegateToMixnode } from 'src/requests'; +import { simulateDelegateToMixnode, simulateVestingDelegateToMixnode, tryConvertIdentityToMixId } from 'src/requests'; +import { debounce } from 'lodash'; import { SimpleModal } from '../Modals/SimpleModal'; import { ModalListItem } from '../Modals/ModalListItem'; import { checkTokenBalance, validateAmount, validateKey } from '../../utils'; @@ -20,7 +21,13 @@ const MIN_AMOUNT_TO_DELEGATE = 10; export const DelegateModal: React.FC<{ open: boolean; onClose: () => void; - onOk?: (identityKey: string, amount: DecCoin, tokenPool: TPoolOption, fee?: FeeDetails) => Promise; + onOk?: ( + mixId: number, + identityKey: string, + amount: DecCoin, + tokenPool: TPoolOption, + fee?: FeeDetails, + ) => Promise; identityKey?: string; onIdentityKeyChanged?: (identityKey: string) => void; onAmountChanged?: (amount: string) => void; @@ -29,7 +36,7 @@ export const DelegateModal: React.FC<{ rewardInterval: string; accountBalance?: string; estimatedReward?: number; - profitMarginPercentage?: number | null; + profitMarginPercentage?: string | null; nodeUptimePercentage?: number | null; denom: CurrencyDenom; initialAmount?: string; @@ -56,20 +63,24 @@ export const DelegateModal: React.FC<{ sx, backdropProps, }) => { + const [mixId, setMixId] = useState(); const [identityKey, setIdentityKey] = useState(initialIdentityKey); const [amount, setAmount] = useState(initialAmount); const [isValidated, setValidated] = useState(false); const [errorAmount, setErrorAmount] = useState(); const [tokenPool, setTokenPool] = useState('balance'); const [errorIdentityKey, setErrorIdentityKey] = useState(); + const [mixIdError, setMixIdError] = useState(); const { fee, getFee, resetFeeState, feeError } = useGetFee(); - const handleCheckStakeSaturation = async (identity: string) => { + const handleCheckStakeSaturation = async (newMixId: number) => { try { - const newSaturation = await getMixnodeStakeSaturation(identity); - if (newSaturation && newSaturation.saturation > 1) { - const saturationPercentage = Math.round(newSaturation.saturation * 100); + const newSaturation = decimalToFloatApproximation( + (await getMixnodeStakeSaturation(newMixId)).uncapped_saturation, + ); + if (newSaturation && newSaturation > 1) { + const saturationPercentage = Math.round(newSaturation * 100); return { isOverSaturated: true, saturationPercentage }; } return { isOverSaturated: false, saturationPercentage: undefined }; @@ -89,8 +100,8 @@ export const DelegateModal: React.FC<{ errorIdentityKeyMessage = undefined; } - if (identityKey && validateKey(identityKey, 32)) { - const { isOverSaturated, saturationPercentage } = await handleCheckStakeSaturation(identityKey); + if (identityKey && mixId && validateKey(identityKey, 32)) { + const { isOverSaturated, saturationPercentage } = await handleCheckStakeSaturation(mixId); if (isOverSaturated) { newValidatedValue = false; errorIdentityKeyMessage = `This node is over saturated (${saturationPercentage}%), please select another node`; @@ -111,18 +122,25 @@ export const DelegateModal: React.FC<{ newValidatedValue = false; } + if (!mixId) { + newValidatedValue = false; + } + setErrorIdentityKey(errorIdentityKeyMessage); + if (mixIdError && !errorIdentityKeyMessage) { + setErrorIdentityKey(mixIdError); + } setErrorAmount(errorAmountMessage); setValidated(newValidatedValue); }; const handleOk = async () => { - if (onOk && amount && identityKey) { - onOk(identityKey, { amount, denom }, tokenPool, fee); + if (onOk && amount && identityKey && mixId) { + onOk(mixId, identityKey, { amount, denom }, tokenPool, fee); } }; - const handleConfirm = async ({ identity, value }: { identity: string; value: DecCoin }) => { + const handleConfirm = async ({ mixId: id, value }: { mixId: number; value: DecCoin }) => { const hasEnoughTokens = await checkTokenBalance(tokenPool, value.amount); if (!hasEnoughTokens) { @@ -131,11 +149,11 @@ export const DelegateModal: React.FC<{ } if (tokenPool === 'balance') { - getFee(simulateDelegateToMixnode, { identity, amount: value }); + getFee(simulateDelegateToMixnode, { mixId: id, amount: value }); } if (tokenPool === 'locked') { - getFee(simulateVestingDelegateToMixnode, { identity, amount: value }); + getFee(simulateVestingDelegateToMixnode, { mixId: id, amount: value }); } }; @@ -157,7 +175,33 @@ export const DelegateModal: React.FC<{ React.useEffect(() => { validate(); - }, [amount, identityKey]); + }, [amount, identityKey, mixIdError]); + + const resolveMixId = useCallback( + debounce(async (idKey) => { + if (!idKey || !validateKey(idKey, 32)) { + return; + } + let res; + try { + res = await tryConvertIdentityToMixId(idKey); + } catch (e) { + Console.warn(`failed to resolve mix_id for "${idKey}": ${e}`); + return; + } + if (res) { + setMixId(res); + setMixIdError(undefined); + } else { + setMixIdError('Mixnode with this identity does not seem to be currently bonded'); + } + }, 500), + [], + ); + + React.useEffect(() => { + resolveMixId(identityKey); + }, [identityKey]); if (fee) { return ( @@ -169,8 +213,8 @@ export const DelegateModal: React.FC<{ onPrev={resetFeeState} onConfirm={handleOk} > - - + + ); } @@ -192,8 +236,8 @@ export const DelegateModal: React.FC<{ open={open} onClose={onClose} onOk={async () => { - if (identityKey && amount) { - handleConfirm({ identity: identityKey, value: { amount, denom } }); + if (mixId && amount) { + handleConfirm({ mixId, value: { amount, denom } }); } }} header={header || 'Delegate'} @@ -202,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/DelegationActions.tsx b/nym-wallet/src/components/Delegation/DelegationActions.tsx index 2bf0070d98..653e2a80e2 100644 --- a/nym-wallet/src/components/Delegation/DelegationActions.tsx +++ b/nym-wallet/src/components/Delegation/DelegationActions.tsx @@ -66,10 +66,9 @@ export const DelegationActions: React.FC<{ export const DelegationsActionsMenu: React.FC<{ onActionClick?: (action: DelegationListItemActions) => void; - isPending?: DelegationEventKind; disableRedeemingRewards?: boolean; - disableCompoundRewards?: boolean; -}> = ({ disableRedeemingRewards, disableCompoundRewards, onActionClick, isPending }) => { + disableDelegateMore?: boolean | null; +}> = ({ disableRedeemingRewards, disableDelegateMore, onActionClick }) => { const [isOpen, setIsOpen] = useState(false); const handleOpenMenu = () => setIsOpen(true); @@ -80,36 +79,23 @@ export const DelegationsActionsMenu: React.FC<{ handleOnClose(); }; - if (isPending) { - return ( - - - - Pending {isPending === 'Delegate' ? 'delegation' : 'undelegation'}... - - - - ); - } - return ( - } onClick={() => handleActionSelect('delegate')} /> + } + onClick={() => handleActionSelect('delegate')} + disabled={Boolean(disableDelegateMore)} + /> } onClick={() => handleActionSelect('undelegate')} /> R} onClick={() => handleActionSelect('redeem')} disabled={disableRedeemingRewards} /> - C} - onClick={() => handleActionSelect('compound')} - disabled={disableCompoundRewards} - /> ); }; diff --git a/nym-wallet/src/components/Delegation/DelegationItem.tsx b/nym-wallet/src/components/Delegation/DelegationItem.tsx new file mode 100644 index 0000000000..2906ed2abd --- /dev/null +++ b/nym-wallet/src/components/Delegation/DelegationItem.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { Chip, IconButton, TableCell, TableRow, Tooltip, Typography } from '@mui/material'; +import { Link } from '@nymproject/react/link/Link'; +import { decimalToPercentage, DelegationWithEverything } from '@nymproject/types'; +import { isDelegation } from 'src/context/delegations'; +import { toPercentIntegerString } from 'src/utils'; +import { format } from 'date-fns'; +import { Undelegate } from 'src/svg-icons'; +import { DelegationListItemActions, DelegationsActionsMenu } from './DelegationActions'; + +const getStakeSaturation = (item: DelegationWithEverything) => + !item.stake_saturation ? '-' : `${decimalToPercentage(item.stake_saturation)}%`; + +const getRewardValue = (item: DelegationWithEverything) => { + const { unclaimed_rewards } = item; + return !unclaimed_rewards ? '-' : `${unclaimed_rewards.amount} ${unclaimed_rewards.denom}`; +}; + +export const DelegationItem = ({ + item, + explorerUrl, + nodeIsUnbonded, + onItemActionClick, +}: { + item: DelegationWithEverything; + explorerUrl: string; + nodeIsUnbonded: boolean; + onItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void; +}) => { + const operatingCost = isDelegation(item) && item.cost_params?.interval_operating_cost; + + return ( + + + + {nodeIsUnbonded ? ( + '-' + ) : ( + + )} + + + {isDelegation(item) && (!item.avg_uptime_percent ? '-' : `${item.avg_uptime_percent}%`)} + + + {isDelegation(item) && + (!item.cost_params?.profit_margin_percent + ? '-' + : `${toPercentIntegerString(item.cost_params.profit_margin_percent)}%`)} + + + + {operatingCost ? `${operatingCost.amount} ${operatingCost.denom}` : '-'} + + + {getStakeSaturation(item)} + + {format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')} + + + + {isDelegation(item) ? `${item.amount.amount} ${item.amount.denom}` : '-'} + + + {getRewardValue(item)} + + {!item.pending_events.length && !nodeIsUnbonded && ( + (onItemActionClick ? onItemActionClick(item, action) : undefined)} + disableRedeemingRewards={!item.unclaimed_rewards || item.unclaimed_rewards.amount === '0'} + disableDelegateMore={item.mixnode_is_unbonding} + /> + )} + {!item.pending_events.length && nodeIsUnbonded && ( + t.palette.nym.nymWallet.text.main }}> + (onItemActionClick ? onItemActionClick(item, 'undelegate') : undefined)} /> + + )} + {item.pending_events.length > 0 && ( + + + + )} + + + + ); +}; diff --git a/nym-wallet/src/components/Delegation/DelegationList.stories.tsx b/nym-wallet/src/components/Delegation/DelegationList.stories.tsx index 22e7a526e2..57ed73c380 100644 --- a/nym-wallet/src/components/Delegation/DelegationList.stories.tsx +++ b/nym-wallet/src/components/Delegation/DelegationList.stories.tsx @@ -13,36 +13,280 @@ const explorerUrl = 'https://sandbox-explorer.nymtech.net/network-components/mix export const items: DelegationWithEverything[] = [ { + mix_id: 1, node_identity: 'FiojKW7oY9WQmLCiYAsCA21tpowZHS6zcUoyYm319p6Z', delegated_on_iso_datetime: new Date(2021, 1, 1).toDateString(), - accumulated_rewards: { amount: '0.05', denom: 'nym' }, + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, amount: { amount: '10', denom: 'nym' }, - profit_margin_percent: 0.1122323949234, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, owner: '', block_height: BigInt(100), - stake_saturation: 0.5, + stake_saturation: '0.5', avg_uptime_percent: 0.5, - total_delegation: { amount: '0', denom: 'nym' }, - pledge_amount: { amount: '0', denom: 'nym' }, - pending_events: [], - history: [], uses_vesting_contract_tokens: false, + pending_events: [], + mixnode_is_unbonding: true, }, { + mix_id: 2, node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', - accumulated_rewards: { amount: '0.1', denom: 'nym' }, amount: { amount: '100', denom: 'nym' }, delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), - profit_margin_percent: 0.89, + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, owner: '', block_height: BigInt(4000), - stake_saturation: 0.5, + stake_saturation: '0.5', avg_uptime_percent: 0.1, - total_delegation: { amount: '0', denom: 'nym' }, - pledge_amount: { amount: '0', denom: 'nym' }, - pending_events: [], - history: [], uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 3, + node_identity: '', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 4, + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 5, + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 6, + node_identity: '', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 7, + node_identity: 'FiojKW7oY9WQmLCiYAsCA21tpowZHS6zcUoyYm319p6Z', + delegated_on_iso_datetime: new Date(2021, 1, 1).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + amount: { amount: '10', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(100), + stake_saturation: '0.5', + avg_uptime_percent: 0.5, + uses_vesting_contract_tokens: false, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 8, + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 9, + node_identity: '', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 10, + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 11, + node_identity: 'DT8S942S8AQs2zKHS9SVo1GyHmuca3pfL2uLhLksJ3D8', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, + }, + { + mix_id: 12, + node_identity: '', + amount: { amount: '100', denom: 'nym' }, + delegated_on_iso_datetime: new Date(2021, 1, 2).toDateString(), + unclaimed_rewards: { amount: '0.05', denom: 'nym' }, + cost_params: { + profit_margin_percent: '0.1122323949234', + interval_operating_cost: { + amount: '40', + denom: 'nym', + }, + }, + accumulated_by_delegates: { amount: '50', denom: 'nym' }, + accumulated_by_operator: { amount: '100', denom: 'nym' }, + owner: '', + block_height: BigInt(4000), + stake_saturation: '0.5', + avg_uptime_percent: 0.1, + uses_vesting_contract_tokens: true, + pending_events: [], + mixnode_is_unbonding: true, }, ]; @@ -52,4 +296,4 @@ export const Empty = () => ; -export const Loading = () => ; +export const Loading = () => ; diff --git a/nym-wallet/src/components/Delegation/DelegationList.tsx b/nym-wallet/src/components/Delegation/DelegationList.tsx index 465cd04dd5..a6c2ac1bcf 100644 --- a/nym-wallet/src/components/Delegation/DelegationList.tsx +++ b/nym-wallet/src/components/Delegation/DelegationList.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { Box, - Chip, CircularProgress, Table, TableBody, @@ -10,41 +9,25 @@ import { TableHead, TableRow, TableSortLabel, - Tooltip, - Typography, } from '@mui/material'; -import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; import { visuallyHidden } from '@mui/utils'; import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; import { DelegationWithEverything } from '@nymproject/types'; -import { Link } from '@nymproject/react/link/Link'; -import { format, formatDistanceToNow, parseISO } from 'date-fns'; -import { styled } from '@mui/material/styles'; -import { tableCellClasses } from '@mui/material/TableCell'; -import { DelegationListItemActions, DelegationsActionsMenu } from './DelegationActions'; - -const StyledTooltipTableCell = styled(TableCell)(({ theme }) => ({ - [`&.${tableCellClasses.head}`]: { - color: theme.palette.common.white, - opacity: 0.5, - fontSize: 12, - }, - [`&.${tableCellClasses.body}`]: { - color: theme.palette.common.white, - fontSize: 12, - }, -})); +import { DelegationListItemActions } from './DelegationActions'; +import { DelegationWithEvent, isDelegation, isPendingDelegation, TDelegations } from '../../context/delegations'; +import { DelegationItem } from './DelegationItem'; +import { PendingDelegationItem } from './PendingDelegationItem'; type Order = 'asc' | 'desc'; interface EnhancedTableProps { - onRequestSort: (event: React.MouseEvent, property: keyof DelegationWithEverything) => void; + onRequestSort: (event: React.MouseEvent, property: string) => void; order: Order; orderBy: string; } interface HeadCell { - id: keyof DelegationWithEverything; + id: string; label: string; sortable: boolean; disablePadding?: boolean; @@ -53,35 +36,17 @@ 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: 'operating_cost', label: 'Operating Cost', 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' }, { id: 'amount', label: 'Delegation', sortable: true, align: 'left' }, - { id: 'accumulated_rewards', label: 'Reward', sortable: true, align: 'left' }, + { id: 'unclaimed_rewards', label: 'Reward', sortable: true, align: 'left' }, ]; -function descendingComparator(a: T, b: T, orderBy: keyof T) { - if (b[orderBy] < a[orderBy]) { - return -1; - } - if (b[orderBy] > a[orderBy]) { - return 1; - } - return 0; -} - -function getComparator( - order: Order, - orderBy: Key, -): (a: DelegationWithEverything, b: DelegationWithEverything) => number { - return order === 'desc' - ? (a, b) => descendingComparator(a, b, orderBy) - : (a, b) => -descendingComparator(a, b, orderBy); -} - const EnhancedTableHead: React.FC = ({ order, orderBy, onRequestSort }) => { - const createSortHandler = (property: keyof DelegationWithEverything) => (event: React.MouseEvent) => { + const createSortHandler = (property: string) => (event: React.MouseEvent) => { onRequestSort(event, property); }; @@ -117,16 +82,22 @@ const EnhancedTableHead: React.FC = ({ order, orderBy, onReq ); }; +const sortByUnbondedMixnodeFirst = (a: DelegationWithEvent) => { + if (!a.node_identity) return -1; + return 1; +}; + export const DelegationList: React.FC<{ isLoading?: boolean; - items?: DelegationWithEverything[]; + items: TDelegations; onItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void; explorerUrl: string; + // eslint-disable-next-line @typescript-eslint/no-unused-vars }> = ({ isLoading, items, onItemActionClick, explorerUrl }) => { const [order, setOrder] = React.useState('asc'); - const [orderBy, setOrderBy] = React.useState('delegated_on_iso_datetime'); + const [orderBy, setOrderBy] = React.useState('delegated_on_iso_datetime'); - const handleRequestSort = (event: React.MouseEvent, property: keyof DelegationWithEverything) => { + const handleRequestSort = (event: React.MouseEvent, property: string) => { const isAsc = orderBy === property && order === 'asc'; setOrder(isAsc ? 'desc' : 'asc'); setOrderBy(property); @@ -137,92 +108,20 @@ export const DelegationList: React.FC<{ - {items?.length ? ( - items.sort(getComparator(order, orderBy)).map((item) => ( - - - { + if (isPendingDelegation(item)) return ; + if (isDelegation(item)) + return ( + - - {!item.avg_uptime_percent ? '-' : `${item.avg_uptime_percent}%`} - {!item.profit_margin_percent ? '-' : `${item.profit_margin_percent}%`} - - {!item.stake_saturation ? '-' : `${Math.round(item.stake_saturation * 100000) / 1000}%`} - - {format(new Date(item.delegated_on_iso_datetime), 'dd/MM/yyyy')} - - -
- - - Date - Amount - Block Height - - - - {item.history.map((historyItem) => ( - - - {formatDistanceToNow(parseISO(historyItem.delegated_on_iso_datetime), { - addSuffix: true, - })} - - - - {`${historyItem.amount.amount} ${historyItem.amount.denom}`} - {historyItem.uses_vesting_contract_tokens && ( - - )} - - - {historyItem.block_height} - - ))} - -
- - } - arrow - > - {`${item.amount.amount} ${item.amount.denom}`} - -
- - {!item.accumulated_rewards - ? '-' - : `${item.accumulated_rewards.amount} ${item.accumulated_rewards.denom}`} - - - - {!item.pending_events.length ? ( - (onItemActionClick ? onItemActionClick(item, action) : undefined)} - disableRedeemingRewards={!item.accumulated_rewards || item.accumulated_rewards.amount === '0'} - disableCompoundRewards={!item.accumulated_rewards || item.accumulated_rewards.amount === '0'} - /> - ) : ( - - - - )} - -
- )) + ); + return null; + }) ) : ( diff --git a/nym-wallet/src/components/Delegation/Delegations.tsx b/nym-wallet/src/components/Delegation/Delegations.tsx index 35a35a22a5..0a6781e44a 100644 --- a/nym-wallet/src/components/Delegation/Delegations.tsx +++ b/nym-wallet/src/components/Delegation/Delegations.tsx @@ -7,7 +7,7 @@ import { DelegationListItemActions } from './DelegationActions'; export const Delegations: React.FC<{ isLoading?: boolean; - items?: DelegationWithEverything[]; + items: DelegationWithEverything[]; explorerUrl: string; onDelegationItemActionClick?: (item: DelegationWithEverything, action: DelegationListItemActions) => void; }> = ({ isLoading, items, explorerUrl, onDelegationItemActionClick }) => ( diff --git a/nym-wallet/src/components/Delegation/Modals.stories.tsx b/nym-wallet/src/components/Delegation/Modals.stories.tsx index 0ca52166b7..218194841b 100644 --- a/nym-wallet/src/components/Delegation/Modals.stories.tsx +++ b/nym-wallet/src/components/Delegation/Modals.stories.tsx @@ -65,8 +65,8 @@ export const Delegate = () => { estimatedReward={50.423} accountBalance="425.2345053" nodeUptimePercentage={99.28394} - profitMarginPercentage={11.12334234} - rewardInterval="weekly" + profitMarginPercentage="11.12334234" + rewardInterval="monthlyish" hasVestingContract={false} {...storybookStyles(theme)} /> @@ -87,8 +87,8 @@ export const DelegateBelowMinimum = () => { denom="nym" estimatedReward={425.2345053} nodeUptimePercentage={99.28394} - profitMarginPercentage={11.12334234} - rewardInterval="weekly" + profitMarginPercentage="11.12334234" + rewardInterval="monthlyish" initialAmount="0.1" hasVestingContract={false} {...storybookStyles(theme)} @@ -113,8 +113,8 @@ export const DelegateMore = () => { estimatedReward={50.423} accountBalance="425.2345053" nodeUptimePercentage={99.28394} - profitMarginPercentage={11.12334234} - rewardInterval="weekly" + profitMarginPercentage="11.12334234" + rewardInterval="monthlyish" hasVestingContract={false} {...storybookStyles(theme)} /> @@ -134,6 +134,7 @@ export const Undelegate = () => { onOk={async () => setOpen(false)} currency="nym" amount={150} + mixId={1234} identityKey="AA6RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujyxx" usesVestingContractTokens={false} {...storybookStyles(theme)} diff --git a/nym-wallet/src/components/Delegation/PendingDelegationItem.tsx b/nym-wallet/src/components/Delegation/PendingDelegationItem.tsx new file mode 100644 index 0000000000..ef4b71390a --- /dev/null +++ b/nym-wallet/src/components/Delegation/PendingDelegationItem.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { Chip, TableCell, TableRow, Tooltip } from '@mui/material'; +import { WrappedDelegationEvent } from '@nymproject/types'; +import { Link } from '@nymproject/react/link/Link'; + +export const PendingDelegationItem = ({ item, explorerUrl }: { item: WrappedDelegationEvent; explorerUrl: string }) => ( + + + + + - + - + - + - + - + - + - + + + + + + +); diff --git a/nym-wallet/src/components/Delegation/PendingEvents.tsx b/nym-wallet/src/components/Delegation/PendingEvents.tsx deleted file mode 100644 index ac3afa1be5..0000000000 --- a/nym-wallet/src/components/Delegation/PendingEvents.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React, { FC } from 'react'; -import LockOutlinedIcon from '@mui/icons-material/LockOutlined'; -import { - Box, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - TableSortLabel, - Tooltip, - Typography, -} from '@mui/material'; -import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; -import { DelegationEvent } from '@nymproject/types'; -import { ArrowDropDown } from '@mui/icons-material'; -import { visuallyHidden } from '@mui/utils'; -import { Link } from '@nymproject/react/link/Link'; - -type Order = 'asc' | 'desc'; - -interface HeadCell { - id: keyof DelegationEvent; - label: string; - sortable: boolean; - disablePadding?: boolean; -} - -interface EnhancedTableProps { - onRequestSort: (event: React.MouseEvent, property: keyof DelegationEvent) => void; - order: Order; - orderBy: string; -} - -const headCells: HeadCell[] = [ - { id: 'node_identity', label: 'Node ID', sortable: true }, - { id: 'amount', label: 'Delegation', sortable: true }, - { id: 'kind', label: 'Type', sortable: true }, -]; - -function descendingComparator(a: T, b: T, orderBy: keyof T) { - if (b[orderBy] < a[orderBy]) { - return -1; - } - if (b[orderBy] > a[orderBy]) { - return 1; - } - return 0; -} - -function getComparator( - order: Order, - orderBy: Key, -): (a: DelegationEvent, b: DelegationEvent) => number { - return order === 'desc' - ? (a, b) => descendingComparator(a, b, orderBy) - : (a, b) => -descendingComparator(a, b, orderBy); -} - -const EnhancedTableHead: React.FC = ({ order, orderBy, onRequestSort }) => { - const createSortHandler = (property: keyof DelegationEvent) => (event: React.MouseEvent) => { - onRequestSort(event, property); - }; - - return ( - - - {headCells.map((headCell) => ( - - - {headCell.label} - {orderBy === headCell.id ? ( - - {order === 'desc' ? 'sorted descending' : 'sorted ascending'} - - ) : null} - - - ))} - - - ); -}; - -export const PendingEvents: FC<{ pendingEvents: DelegationEvent[]; explorerUrl: string }> = ({ - pendingEvents, - explorerUrl, -}) => { - const [order, setOrder] = React.useState('asc'); - const [orderBy, setOrderBy] = React.useState('node_identity'); - - const handleRequestSort = (event: React.MouseEvent, property: keyof DelegationEvent) => { - const isAsc = orderBy === property && order === 'asc'; - setOrder(isAsc ? 'desc' : 'asc'); - setOrderBy(property); - }; - - if (pendingEvents.length === 0) return No pending events; - - return ( - - - - - {pendingEvents.sort(getComparator(order, orderBy)).map((item) => ( - - - - Copy identity key {item.node_identity} to clipboard - - } - /> - - Click to view {item.node_identity} in the Network Explorer - - } - placement="right" - arrow - > - - - - {!item.amount ? '-' : `${item.amount?.amount} ${item.amount?.denom.toUpperCase()}`} - - {item.kind === 'Delegate' ? 'Delegation' : 'Undelegation'} - {item.proxy && ( - - - - )} - - - ))} - -
-
- ); -}; diff --git a/nym-wallet/src/components/Delegation/UndelegateModal.tsx b/nym-wallet/src/components/Delegation/UndelegateModal.tsx index 2b21632162..99715d7425 100644 --- a/nym-wallet/src/components/Delegation/UndelegateModal.tsx +++ b/nym-wallet/src/components/Delegation/UndelegateModal.tsx @@ -1,4 +1,4 @@ -import { Box, Typography, SxProps } from '@mui/material'; +import { Box, SxProps } from '@mui/material'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import React, { useEffect } from 'react'; import { FeeDetails } from '@nymproject/types'; @@ -11,26 +11,27 @@ import { SimpleModal } from '../Modals/SimpleModal'; export const UndelegateModal: React.FC<{ open: boolean; onClose?: () => void; - onOk?: (identityKey: string, usesVestingContractTokens: boolean, fee?: FeeDetails) => void; + onOk?: (mixId: number, identityKey: string, usesVestingContractTokens: boolean, fee?: FeeDetails) => void; + mixId: number; identityKey: string; amount: number; currency: string; usesVestingContractTokens: boolean; sx?: SxProps; backdropProps?: object; -}> = ({ identityKey, open, onClose, onOk, amount, currency, usesVestingContractTokens, sx, backdropProps }) => { +}> = ({ mixId, identityKey, open, onClose, onOk, amount, currency, usesVestingContractTokens, sx, backdropProps }) => { const { fee, isFeeLoading, feeError, getFee } = useGetFee(); useEffect(() => { - if (usesVestingContractTokens) getFee(simulateVestingUndelegateFromMixnode, { identity: identityKey }); + if (usesVestingContractTokens) getFee(simulateVestingUndelegateFromMixnode, { mixId }); else { - getFee(simulateUndelegateFromMixnode, identityKey); + getFee(simulateUndelegateFromMixnode, mixId); } }, []); const handleOk = async () => { if (onOk) { - onOk(identityKey, usesVestingContractTokens, fee); + onOk(mixId, identityKey, usesVestingContractTokens, fee); } }; @@ -45,15 +46,8 @@ export const UndelegateModal: React.FC<{ sx={sx} backdropProps={backdropProps} > - - + diff --git a/nym-wallet/src/components/Error.tsx b/nym-wallet/src/components/Error.tsx index a6d042ddc2..0101291cf6 100644 --- a/nym-wallet/src/components/Error.tsx +++ b/nym-wallet/src/components/Error.tsx @@ -2,7 +2,12 @@ import React from 'react'; import { Alert } from '@mui/material'; export const Error = ({ message }: { message: string }) => ( - + {message} ); diff --git a/nym-wallet/src/components/InfoToolTip.tsx b/nym-wallet/src/components/InfoToolTip.tsx index 43bacff3bc..6b88f8fddd 100644 --- a/nym-wallet/src/components/InfoToolTip.tsx +++ b/nym-wallet/src/components/InfoToolTip.tsx @@ -6,12 +6,12 @@ export const InfoTooltip = ({ title, tooltipPlacement = 'bottom', light, - size = 'small', + size = 'inherit', }: { title: string; tooltipPlacement?: TooltipProps['placement']; light?: boolean; - size?: 'small' | 'medium' | 'large'; + size?: 'small' | 'medium' | 'large' | 'inherit'; }) => ( diff --git a/nym-wallet/src/components/LogViewer/index.tsx b/nym-wallet/src/components/LogViewer/index.tsx new file mode 100644 index 0000000000..f2a9d2e6d0 --- /dev/null +++ b/nym-wallet/src/components/LogViewer/index.tsx @@ -0,0 +1,97 @@ +import React, { FC, useEffect, useRef, useState } from 'react'; +import type { UnlistenFn } from '@tauri-apps/api/event'; +import { listen } from '@tauri-apps/api/event'; +import { Box, Paper, Chip, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material'; + +// see https://github.com/tauri-apps/tauri-plugin-log/blob/dev/webview-src/index.ts#L4 +enum LogLevel { + Trace = 1, + Debug, + Info, + Warn, + Error, +} + +const getLogLevelName = (value: LogLevel) => { + switch (value) { + case LogLevel.Trace: + return 'Trace'; + case LogLevel.Debug: + return 'Debug'; + case LogLevel.Info: + return 'Info'; + case LogLevel.Warn: + return 'Warn'; + case LogLevel.Error: + return 'Error'; + default: + return 'Unknown'; + } +}; + +// see https://github.com/tauri-apps/tauri-plugin-log/blob/dev/webview-src/index.ts#L147 +interface RecordPayload { + level: LogLevel; + message: string; +} + +export const LogViewer: FC = () => { + const unlisten = useRef(); + const messages = useRef([]); + const [messageCount, setMessageCount] = useState(0); + + useEffect(() => { + listen('log://log', (event) => { + console.log(event.payload); + const payload = event.payload as RecordPayload; + messages.current.unshift(payload); + setMessageCount((prev) => prev + 1); + }).then((fn) => { + unlisten.current = fn; + }); + + return () => { + if (unlisten.current) { + unlisten.current(); + } + }; + }, []); + + return ( + + + + + + + Severity + Log message + + + + {messages.current.map((m) => ( + + + + + {m.message} + + ))} + +
+
+
+ theme.palette.divider, + }} + > + {messageCount} log entries since opening this window + +
+ ); +}; diff --git a/nym-wallet/src/components/Mnemonic.tsx b/nym-wallet/src/components/Mnemonic.tsx index 7b65e7aabf..d445172e9a 100644 --- a/nym-wallet/src/components/Mnemonic.tsx +++ b/nym-wallet/src/components/Mnemonic.tsx @@ -18,7 +18,25 @@ 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 - +