diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml new file mode 100644 index 0000000000..9a546dd0a7 --- /dev/null +++ b/.github/workflows/audit.yml @@ -0,0 +1,36 @@ +name: Daily security audit + +on: workflow_dispatch +jobs: + security_audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/audit-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + notification: + if: ${{ failure() }} + needs: security_audit + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v2 + - name: Keybase - Node Install + run: npm install + working-directory: .github/workflows/support-files + - name: Keybase - Send Notification + env: + NYM_NOTIFICATION_KIND: nightly + NYM_PROJECT_NAME: "Nym daily audit" + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" + GIT_BRANCH: "${GITHUB_REF##*/}" + KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" + KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" + KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBTECH_TEAM }}" + KEYBASE_NYM_CHANNEL: "test" + uses: docker://keybaseio/client:stable-node + with: + args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/.github/workflows/network-explorer.yml b/.github/workflows/network-explorer.yml index e2f424798e..2f5b10de94 100644 --- a/.github/workflows/network-explorer.yml +++ b/.github/workflows/network-explorer.yml @@ -1,6 +1,7 @@ name: CI for Network Explorer on: + workflow_dispatch: push: paths: - 'explorer/**' @@ -75,3 +76,14 @@ jobs: uses: docker://keybaseio/client:stable-node with: args: .github/workflows/support-files/notifications/entry_point.sh + - name: Deploy + if: github.event_name == 'workflow_dispatch' + uses: easingthemes/ssh-deploy@main + env: + SSH_PRIVATE_KEY: ${{ secrets.CD_PROD_NE_SSH_PRIVATE_KEY }} + ARGS: "-rltgoDzvO --delete" + SOURCE: "explorer/dist/" + REMOTE_HOST: ${{ secrets.CD_PROD_NE_REMOTE_HOST }} + REMOTE_USER: ${{ secrets.CD_PROD_NE_REMOTE_USER }} + TARGET: ${{ secrets.CD_PROD_NE_REMOTE_TARGET }} + EXCLUDE: "/dist/, /node_modules/" diff --git a/.github/workflows/nightly_build_matrix_on_dispatch.json b/.github/workflows/nightly_build_matrix_on_dispatch.json new file mode 100644 index 0000000000..c292817b6a --- /dev/null +++ b/.github/workflows/nightly_build_matrix_on_dispatch.json @@ -0,0 +1,50 @@ +[ + { + "os":"ubuntu-latest", + "rust":"stable", + "runOnEvent":"workflow_dispatch" + }, + + { + "os":"windows-latest", + "rust":"stable", + "runOnEvent":"workflow_dispatch" + }, + { + "os":"macos-latest", + "rust":"stable", + "runOnEvent":"workflow_dispatch" + }, + + { + "os":"ubuntu-latest", + "rust":"beta", + "runOnEvent":"workflow_dispatch" + }, + { + "os":"windows-latest", + "rust":"beta", + "runOnEvent":"workflow_dispatch" + }, + { + "os":"macos-latest", + "rust":"beta", + "runOnEvent":"workflow_dispatch" + }, + + { + "os":"ubuntu-latest", + "rust":"nightly", + "runOnEvent":"workflow_dispatch" + }, + { + "os":"windows-latest", + "rust":"nightly", + "runOnEvent":"workflow_dispatch" + }, + { + "os":"macos-latest", + "rust":"nightly", + "runOnEvent":"workflow_dispatch" + } +] diff --git a/.github/workflows/nightly_build_on_dispatch.yml b/.github/workflows/nightly_build_on_dispatch.yml new file mode 100644 index 0000000000..76e141efed --- /dev/null +++ b/.github/workflows/nightly_build_on_dispatch.yml @@ -0,0 +1,174 @@ +name: Nightly builds on dispatch + +on: workflow_dispatch +jobs: + matrix_prep: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + # creates the matrix strategy from nightly_build_matrix_includes.json + - uses: actions/checkout@v2 + - id: set-matrix + uses: JoshuaTheMiller/conditional-build-matrix@main + with: + inputFile: '.github/workflows/nightly_build_matrix_on_dispatch.json' + filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]' + build: + needs: matrix_prep + strategy: + matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}} + runs-on: ${{ matrix.os }} + 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 + if: matrix.os == 'ubuntu-latest' + + - name: Check out repository code + uses: actions/checkout@v2 + + - name: Install rust toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + components: rustfmt, clippy + + - name: Build all binaries + uses: actions-rs/cargo@v1 + with: + command: build + args: --workspace + + - name: Run all tests + uses: actions-rs/cargo@v1 + with: + command: test + 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 expensive tests + if: github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master' + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace --all-features -- --ignored + + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - uses: actions-rs/clippy-check@v1 + name: Clippy checks + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features + + - name: Run clippy + uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} + with: + command: clippy + args: --workspace --all-targets -- -D warnings + + - name: Reclaim some disk space + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' || matrix.os == 'ubuntu-latest' }} + with: + command: clean + + # COCONUT stuff + - name: Build all binaries with coconut enabled + uses: actions-rs/cargo@v1 + with: + command: build + args: --workspace --features=coconut + + - name: Run all tests with coconut enabled + uses: actions-rs/cargo@v1 + with: + command: test + 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 clippy with coconut enabled + uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} + with: + command: clippy + args: --workspace --all-targets --features=coconut -- -D warnings + + # nym-wallet (the rust part) + - name: Build nym-wallet rust code + uses: actions-rs/cargo@v1 + with: + command: build + args: --manifest-path nym-wallet/Cargo.toml --workspace + + - name: Run nym-wallet tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path nym-wallet/Cargo.toml --workspace + + - name: Check nym-wallet formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --manifest-path nym-wallet/Cargo.toml --all -- --check + + - name: Run clippy for nym-wallet + uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} + with: + command: clippy + args: --manifest-path nym-wallet/Cargo.toml --workspace --all-targets -- -D warnings + + notification: + needs: build + runs-on: ubuntu-latest + steps: + - name: Collect jobs status + uses: technote-space/workflow-conclusion-action@v2 + - name: Check out repository code + uses: actions/checkout@v2 + - name: Keybase - Node Install + if: env.WORKFLOW_CONCLUSION == 'failure' + run: npm install + working-directory: .github/workflows/support-files + - name: Keybase - Send Notification + if: env.WORKFLOW_CONCLUSION == 'failure' + env: + NYM_NOTIFICATION_KIND: nightly + NYM_PROJECT_NAME: "Nym nightly build" + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" + GIT_BRANCH: "${GITHUB_REF##*/}" + 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" + IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}" + uses: docker://keybaseio/client:stable-node + with: + args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/.github/workflows/nym-release-publish.yml b/.github/workflows/nym-release-publish.yml index afad6bd78a..ed966ecffc 100644 --- a/.github/workflows/nym-release-publish.yml +++ b/.github/workflows/nym-release-publish.yml @@ -45,3 +45,4 @@ 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index ebf11cf19f..5f25c11931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,16 +2,20 @@ 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 -## [Unreleased] +### Changed + +- validator-client: made `fee` argument optional for `execute` and `execute_multiple` ([#1541]) + +[#1541]: https://github.com/nymtech/nym/pull/1541 + + +## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2) ### Added - socks5 client/websocket client: add `--force-register-gateway` flag, useful when rerunning init ([#1353]) -- 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 - all: added network compilation target to `--help` (or `--version`) commands ([#1256]). - explorer-api: learned how to sum the delegations by owner in a new endpoint. - explorer-api: add apy values to `mix_nodes` endpoint @@ -27,6 +31,9 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - 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,10 +44,12 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - native & socks5 clients: rerun init will now reuse previous gateway configuration instead of failing ([#1353]) - native & socks5 clients: deduplicate big chunks of init logic - 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]). ### Changed -- nym-connect: reuse config id instead of creating a new id on each connection - validator-client: created internal `Coin` type that replaces coins from `cosmrs` and `cosmwasm` for API entrypoints [[#1295]] - all: updated all `cosmwasm`-related dependencies to `1.0.0` and `cw-storage-plus` to `0.13.4` [[#1318]] - all: updated `rocket` to `0.5.0-rc.2`. @@ -49,6 +58,12 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - gateway & mixnode: move detailed build info back to `--version` from `--help`. - socks5 client/websocket client: upgrade to latest clap and switched to declarative commandline parsing. - validator-api: fee payment for multisig operations comes from the gateway account instead of the validator APIs' accounts ([#1419]) +- multisig-contract: Limit the proposal creating functionality to one address (coconut-bandwidth-contract address) ([#1457]) +- All binaries and cosmwasm blobs are configured at runtime now; binaries are configured using environment variables or .env files and contracts keep the configuration parameters in storage ([#1463]) +- 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]) + [#1249]: https://github.com/nymtech/nym/pull/1249 [#1256]: https://github.com/nymtech/nym/pull/1256 @@ -69,76 +84,14 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// [#1393]: https://github.com/nymtech/nym/pull/1393 [#1404]: https://github.com/nymtech/nym/pull/1404 [#1419]: https://github.com/nymtech/nym/pull/1419 -[#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]) +[#1457]: https://github.com/nymtech/nym/pull/1457 +[#1463]: https://github.com/nymtech/nym/pull/1463 +[#1478]: https://github.com/nymtech/nym/pull/1478 +[#1482]: https://github.com/nymtech/nym/pull/1482 +[#1487]: https://github.com/nymtech/nym/pull/1487 +[#1496]: https://github.com/nymtech/nym/pull/1496 +[#1503]: https://github.com/nymtech/nym/pull/1503 +[#1520]: https://github.com/nymtech/nym/pull/1520 ## [v1.0.1](https://github.com/nymtech/nym/tree/v1.0.1) (2022-05-04) @@ -171,77 +124,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) @@ -320,108 +206,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 640463a314..6d417fff8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -684,6 +684,7 @@ name = "coconut-bandwidth-contract-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "multisig-contract-common", "schemars", "serde", ] @@ -966,7 +967,6 @@ dependencies = [ "coconut-interface", "cosmrs", "crypto", - "network-defaults", "rand 0.7.3", "thiserror", "url", @@ -1635,6 +1635,7 @@ name = "explorer-api" version = "1.0.1" dependencies = [ "chrono", + "clap 3.2.8", "humantime-serde", "isocountry", "itertools", @@ -1650,6 +1651,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "task", "thiserror", "tokio", "validator-client", @@ -2546,6 +2548,14 @@ dependencies = [ "syn", ] +[[package]] +name = "inclusion-probability" +version = "0.1.0" +dependencies = [ + "rand 0.8.5", + "thiserror", +] + [[package]] name = "indenter" version = "0.3.3" @@ -2916,7 +2926,6 @@ dependencies = [ "cosmwasm-std", "fixed", "log", - "network-defaults", "schemars", "serde", "serde_repr", @@ -3007,6 +3016,7 @@ name = "network-defaults" version = "0.1.0" dependencies = [ "cfg-if 1.0.0", + "dotenv", "hex-literal", "once_cell", "serde", @@ -3094,7 +3104,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.0.1" +version = "1.0.2" dependencies = [ "clap 3.2.15", "client-core", @@ -3104,7 +3114,6 @@ dependencies = [ "credentials", "crypto", "dirs", - "dotenv", "futures", "gateway-client", "gateway-requests", @@ -3129,7 +3138,7 @@ dependencies = [ [[package]] name = "nym-gateway" -version = "1.0.1" +version = "1.0.2" dependencies = [ "anyhow", "async-trait", @@ -3177,7 +3186,7 @@ dependencies = [ [[package]] name = "nym-mixnode" -version = "1.0.1" +version = "1.0.2" dependencies = [ "anyhow", "bs58", @@ -3217,7 +3226,7 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.0.1" +version = "1.0.2" dependencies = [ "async-trait", "clap 2.34.0", @@ -3245,7 +3254,7 @@ dependencies = [ [[package]] name = "nym-network-statistics" -version = "0.1.0" +version = "1.0.2" dependencies = [ "dirs", "log", @@ -3261,7 +3270,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.0.1" +version = "1.0.2" dependencies = [ "clap 3.2.15", "client-core", @@ -3271,7 +3280,6 @@ dependencies = [ "credentials", "crypto", "dirs", - "dotenv", "futures", "gateway-client", "gateway-requests", @@ -3323,7 +3331,7 @@ dependencies = [ [[package]] name = "nym-validator-api" -version = "1.0.1" +version = "1.0.2" dependencies = [ "anyhow", "async-trait", @@ -3338,6 +3346,8 @@ dependencies = [ "credential-storage", "credentials", "crypto", + "cw-utils", + "cw3", "dirs", "dotenv", "futures", @@ -3363,6 +3373,7 @@ dependencies = [ "serde", "serde_json", "sqlx", + "task", "thiserror", "time 0.3.11", "tokio", @@ -5380,7 +5391,6 @@ version = "1.0.1" dependencies = [ "async-trait", "log", - "network-defaults", "reqwest", "serde", "serde_json", @@ -5649,18 +5659,18 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +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", @@ -6349,7 +6359,6 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.0.1" dependencies = [ - "config", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", diff --git a/Cargo.toml b/Cargo.toml index d351216627..6e3dc19265 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,22 +22,23 @@ 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/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/mixnode-common", "common/network-defaults", "common/nonexhaustive-delayqueue", @@ -53,9 +54,9 @@ 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", @@ -76,6 +77,7 @@ default-members = [ "clients/socks5", "gateway", "service-providers/network-requester", + "service-providers/network-statistics", "mixnode", "validator-api", "explorer-api", diff --git a/Makefile b/Makefile index d1c2520d42..fb1393d11a 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ +test: clippy-all cargo-test wasm fmt test-all: test cargo-test-expensive -test: build clippy-all cargo-test wasm fmt 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 diff --git a/clients/client-core/src/config/mod.rs b/clients/client-core/src/config/mod.rs index f797851ce3..1c81d5e079 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -1,7 +1,6 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use config::defaults::*; use config::NymConfig; use serde::{Deserialize, Serialize}; use std::marker::PhantomData; @@ -366,7 +365,7 @@ impl Default for Client { version: env!("CARGO_PKG_VERSION").to_string(), id: "".to_string(), disabled_credentials_mode: true, - validator_api_urls: default_api_endpoints(), + validator_api_urls: vec![], private_identity_key_file: Default::default(), public_identity_key_file: Default::default(), private_encryption_key_file: Default::default(), diff --git a/clients/credential/src/client.rs b/clients/credential/src/client.rs index 1c3286f7c5..005aab356e 100644 --- a/clients/credential/src/client.rs +++ b/clients/credential/src/client.rs @@ -4,7 +4,7 @@ use crate::error::Result; use crate::{MNEMONIC, NYMD_URL}; use bip39::Mnemonic; -use network_defaults::{DEFAULT_NETWORK, MIX_DENOM, VOUCHER_INFO}; +use network_defaults::{NymNetworkDetails, VOUCHER_INFO}; use std::str::FromStr; use url::Url; use validator_client::nymd; @@ -13,18 +13,23 @@ use validator_client::nymd::{Coin, Fee, NymdClient, SigningNymdClient}; pub(crate) struct Client { nymd_client: NymdClient, + mix_denom_base: String, } impl Client { pub fn new() -> Self { let nymd_url = Url::from_str(NYMD_URL).unwrap(); let mnemonic = Mnemonic::from_str(MNEMONIC).unwrap(); - let config = nymd::Config::try_from_nym_network_details(&DEFAULT_NETWORK.details()) + 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"); let nymd_client = NymdClient::connect_with_mnemonic(config, nymd_url.as_ref(), mnemonic, None).unwrap(); - Client { nymd_client } + Client { + nymd_client, + mix_denom_base: network_details.chain_details.mix_denom.base, + } } pub async fn deposit( @@ -34,7 +39,7 @@ impl Client { encryption_key: String, fee: Option, ) -> Result { - let amount = Coin::new(amount as u128, MIX_DENOM.base.to_string()); + let amount = Coin::new(amount as u128, self.mix_denom_base.clone()); Ok(self .nymd_client .deposit( diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 79b04a7d7b..4d712233b2 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.0.1" +version = "1.0.2" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" @@ -22,7 +22,6 @@ url = "2.2" clap = { version = "3.2.8", features = ["cargo", "derive"] } dirs = "4.0" -dotenv = "0.15.0" # for obtaining environmental variables (only used for RUST_LOG for time being) 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 diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index c63ada0383..06ee44ea13 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -3,8 +3,6 @@ use crate::client::config::{Config, SocketType}; use clap::{Parser, Subcommand}; -use network_defaults::DEFAULT_NETWORK; -use url::Url; #[cfg(not(feature = "coconut"))] pub(crate) const DEFAULT_ETH_ENDPOINT: &str = @@ -28,7 +26,6 @@ fn long_version() -> String { {:<20}{} {:<20}{} {:<20}{} -{:<20}{} "#, "Build Timestamp:", env!("VERGEN_BUILD_TIMESTAMP"), @@ -46,8 +43,6 @@ fn long_version() -> String { env!("VERGEN_RUSTC_CHANNEL"), "cargo Profile:", env!("VERGEN_CARGO_PROFILE"), - "Network:", - DEFAULT_NETWORK ) } @@ -58,6 +53,10 @@ fn long_version_static() -> &'static str { #[derive(Parser)] #[clap(author = "Nymtech", version, long_version = long_version_static(), about)] pub(crate) struct Cli { + /// Path pointing to an env file that configures the client. + #[clap(long)] + pub(crate) config_env_file: Option, + #[clap(subcommand)] command: Commands, } @@ -97,22 +96,17 @@ pub(crate) async fn execute(args: &Cli) { } } -fn parse_validators(raw: &str) -> Vec { - raw.split(',') - .map(|raw_validator| { - raw_validator - .trim() - .parse() - .expect("one of the provided validator api urls is invalid") - }) - .collect() -} - pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config { if let Some(raw_validators) = args.validators { config .get_base_mut() - .set_custom_validator_apis(parse_validators(&raw_validators)); + .set_custom_validator_apis(config::parse_validators(&raw_validators)); + } else if std::env::var(network_defaults::var_names::CONFIGURED).is_ok() { + let raw_validators = std::env::var(network_defaults::var_names::API_VALIDATOR) + .expect("api validator not set"); + config + .get_base_mut() + .set_custom_validator_apis(config::parse_validators(&raw_validators)); } if args.disable_socket { diff --git a/clients/native/src/commands/run.rs b/clients/native/src/commands/run.rs index b30561e004..1d2af52f8f 100644 --- a/clients/native/src/commands/run.rs +++ b/clients/native/src/commands/run.rs @@ -82,7 +82,7 @@ fn version_check(cfg: &Config) -> bool { if binary_version == config_version { true } else { - warn!("The mixnode binary has different version than what is specified in config file! {} and {}", binary_version, config_version); + warn!("The native-client binary has different version than what is specified in config file! {} and {}", binary_version, config_version); if is_minor_version_compatible(binary_version, config_version) { info!("but they are still semver compatible. However, consider running the `upgrade` command"); true diff --git a/clients/native/src/commands/upgrade.rs b/clients/native/src/commands/upgrade.rs index 2ac897cf23..c925b10f08 100644 --- a/clients/native/src/commands/upgrade.rs +++ b/clients/native/src/commands/upgrade.rs @@ -3,7 +3,7 @@ use crate::client::config::{Config, MISSING_VALUE}; -use config::{defaults::default_api_endpoints, NymConfig}; +use config::NymConfig; use version_checker::Version; use clap::Args; @@ -105,15 +105,6 @@ fn minor_0_12_upgrade( print_start_upgrade(&config_version, &to_version); - println!( - "Setting validator API endpoints to {:?}", - default_api_endpoints() - ); - - config - .get_base_mut() - .set_custom_validator_apis(default_api_endpoints()); - config .get_base_mut() .set_custom_version(to_version.to_string().as_ref()); diff --git a/clients/native/src/main.rs b/clients/native/src/main.rs index dd0f644e43..13e82ec660 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 network_defaults::setup_env; pub mod client; pub mod commands; @@ -9,11 +10,11 @@ pub mod websocket; #[tokio::main] async fn main() { - dotenv::dotenv().ok(); setup_logging(); println!("{}", banner()); let args = commands::Cli::parse(); + setup_env(args.config_env_file.clone()); commands::execute(&args).await; } diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 0c431871df..008db6fe85 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.0.1" +version = "1.0.2" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" @@ -13,7 +13,6 @@ path = "src/lib.rs" [dependencies] clap = { version = "3.2.8", features = ["cargo", "derive"] } dirs = "4.0" -dotenv = "0.15.0" futures = "0.3" log = "0.4" pin-project = "1.0" diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index ea9d5da052..cba489e00a 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -3,8 +3,7 @@ use crate::client::config::Config; use clap::{Parser, Subcommand}; -use network_defaults::DEFAULT_NETWORK; -use url::Url; +use config::parse_validators; pub mod init; pub(crate) mod run; @@ -28,7 +27,6 @@ fn long_version() -> String { {:<20}{} {:<20}{} {:<20}{} -{:<20}{} "#, "Build Timestamp:", env!("VERGEN_BUILD_TIMESTAMP"), @@ -46,8 +44,6 @@ fn long_version() -> String { env!("VERGEN_RUSTC_CHANNEL"), "cargo Profile:", env!("VERGEN_CARGO_PROFILE"), - "Network:", - DEFAULT_NETWORK ) } @@ -58,6 +54,10 @@ fn long_version_static() -> &'static str { #[derive(Parser)] #[clap(author = "Nymtech", version, long_version = long_version_static(), about)] pub(crate) struct Cli { + /// Path pointing to an env file that configures the client. + #[clap(long)] + pub(crate) config_env_file: Option, + #[clap(subcommand)] command: Commands, } @@ -96,22 +96,15 @@ pub(crate) async fn execute(args: &Cli) { } } -fn parse_validators(raw: &str) -> Vec { - raw.split(',') - .map(|raw_validator| { - raw_validator - .trim() - .parse() - .expect("one of the provided validator api urls is invalid") - }) - .collect() -} - pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config { if let Some(raw_validators) = args.validators { config .get_base_mut() .set_custom_validator_apis(parse_validators(&raw_validators)); + } else if let Ok(raw_validators) = std::env::var(network_defaults::var_names::API_VALIDATOR) { + config + .get_base_mut() + .set_custom_validator_apis(parse_validators(&raw_validators)); } if let Some(port) = args.port { diff --git a/clients/socks5/src/commands/upgrade.rs b/clients/socks5/src/commands/upgrade.rs index 849400d3d6..5e9517ac2e 100644 --- a/clients/socks5/src/commands/upgrade.rs +++ b/clients/socks5/src/commands/upgrade.rs @@ -3,7 +3,7 @@ use crate::client::config::{Config, MISSING_VALUE}; -use config::{defaults::default_api_endpoints, NymConfig}; +use config::NymConfig; use version_checker::Version; use clap::Args; @@ -104,15 +104,6 @@ fn minor_0_12_upgrade( print_start_upgrade(&config_version, &to_version); - println!( - "Setting validator API endpoints to {:?}", - default_api_endpoints() - ); - - config - .get_base_mut() - .set_custom_validator_apis(default_api_endpoints()); - config .get_base_mut() .set_custom_version(to_version.to_string().as_ref()); diff --git a/clients/socks5/src/lib.rs b/clients/socks5/src/lib.rs index 39c657356c..2faec9d4e8 100644 --- a/clients/socks5/src/lib.rs +++ b/clients/socks5/src/lib.rs @@ -2,9 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 pub mod client; -// This is only used as we reach into the init functions in nym-connect. We need to refactor the -// init functions so that nym-connect can just call the same init function as the regular socks5 -// client. -#[allow(unused)] -pub mod commands; pub mod socks; diff --git a/clients/socks5/src/main.rs b/clients/socks5/src/main.rs index 7571a482e0..fb7a8bb607 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 network_defaults::setup_env; pub mod client; mod commands; @@ -9,11 +10,11 @@ pub mod socks; #[tokio::main] async fn main() { - dotenv::dotenv().ok(); setup_logging(); println!("{}", banner()); let args = commands::Cli::parse(); + setup_env(args.config_env_file.clone()); commands::execute(&args).await; } diff --git a/clients/socks5/src/socks/mixnet_responses.rs b/clients/socks5/src/socks/mixnet_responses.rs index 5ff06dada2..3146967711 100644 --- a/clients/socks5/src/socks/mixnet_responses.rs +++ b/clients/socks5/src/socks/mixnet_responses.rs @@ -5,7 +5,7 @@ use futures::StreamExt; use log::*; use nymsphinx::receiver::ReconstructedMessage; use proxy_helpers::connection_controller::{ControllerCommand, ControllerSender}; -use socks5_requests::Response; +use socks5_requests::Message; pub(crate) struct MixnetResponseListener { buffer_requester: ReceivedBufferRequestSender, @@ -44,12 +44,16 @@ impl MixnetResponseListener { warn!("this message had a surb - we didn't do anything with it"); } - let response = match Response::try_from_bytes(&raw_message) { + let response = match Message::try_from_bytes(&raw_message) { Err(err) => { warn!("failed to parse received response - {:?}", err); return; } - Ok(data) => data, + Ok(Message::Request(_)) => { + warn!("unexpected request"); + return; + } + Ok(Message::Response(data)) => data, }; self.controller_sender diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 45f2ec5a91..d820611bf0 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -5,8 +5,7 @@ use crate::{validator_api, ValidatorClientError}; use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixNodeBond}; use url::Url; use validator_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, - ProposeReleaseFundsRequestBody, ProposeReleaseFundsResponse, VerificationKeyResponse, + BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ @@ -29,7 +28,7 @@ use mixnet_contract_common::{ RewardedSetUpdateDetails, }; #[cfg(feature = "nymd-client")] -use network_defaults::{all::Network, NymNetworkDetails}; +use network_defaults::NymNetworkDetails; #[cfg(feature = "nymd-client")] use std::collections::{HashMap, HashSet}; @@ -114,9 +113,6 @@ impl Config { #[cfg(feature = "nymd-client")] pub struct Client { - // compatibility : ( - pub network: Network, - // TODO: we really shouldn't be storing a mnemonic here, but removing it would be // non-trivial amount of work and it's out of scope of the current branch mnemonic: Option, @@ -135,9 +131,6 @@ pub struct Client { impl Client { pub fn new_signing( config: Config, - // we need to provide network argument due to compatibility with other components (wallet...) - // that rely on its existence... - network: Network, mnemonic: bip39::Mnemonic, ) -> Result, ValidatorClientError> { let validator_api_client = validator_api::Client::new(config.api_url.clone()); @@ -149,7 +142,6 @@ impl Client { )?; Ok(Client { - network, mnemonic: Some(mnemonic), mixnode_page_limit: config.mixnode_page_limit, gateway_page_limit: config.gateway_page_limit, @@ -177,18 +169,12 @@ impl Client { #[cfg(feature = "nymd-client")] impl Client { - pub fn new_query( - config: Config, - // we need to provide network argument due to compatibility with other components (wallet...) - // that rely on its existence... - network: Network, - ) -> Result, ValidatorClientError> { + pub fn new_query(config: Config) -> Result, ValidatorClientError> { let validator_api_client = validator_api::Client::new(config.api_url.clone()); let nymd_client = NymdClient::connect(config.nymd_config.clone(), config.nymd_url.as_str())?; Ok(Client { - network, mnemonic: None, mixnode_page_limit: config.mixnode_page_limit, gateway_page_limit: config.gateway_page_limit, @@ -747,14 +733,4 @@ impl ApiClient { .verify_bandwidth_credential(request_body) .await?) } - - pub async fn propose_release_funds( - &self, - request_body: &ProposeReleaseFundsRequestBody, - ) -> Result { - Ok(self - .validator_api - .propose_release_funds(request_body) - .await?) - } } diff --git a/common/client-libs/validator-client/src/nymd/cosmwasm_client/client.rs b/common/client-libs/validator-client/src/nymd/cosmwasm_client/client.rs index 08a3ce22a2..8da9d3192c 100644 --- a/common/client-libs/validator-client/src/nymd/cosmwasm_client/client.rs +++ b/common/client-libs/validator-client/src/nymd/cosmwasm_client/client.rs @@ -26,7 +26,7 @@ use cosmrs::rpc::{self, HttpClient, Order}; use cosmrs::tendermint::abci::Code as AbciCode; use cosmrs::tendermint::abci::Transaction; use cosmrs::tendermint::{abci, block, chain}; -use cosmrs::{tx, AccountId, Coin as CosmosCoin, Denom, Tx}; +use cosmrs::{tx, AccountId, Coin as CosmosCoin, Tx}; use prost::Message; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; @@ -121,7 +121,7 @@ pub trait CosmWasmClient: rpc::Client { async fn get_balance( &self, address: &AccountId, - search_denom: Denom, + search_denom: String, ) -> Result, NymdError> { let path = Some("/cosmos.bank.v1beta1.Query/Balance".parse().unwrap()); diff --git a/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs b/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs index cc6bc9ce17..b1646f663e 100644 --- a/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/cosmwasm_client/signing_client.rs @@ -574,10 +574,10 @@ pub trait SigningCosmWasmClient: CosmWasmClient { let fee = match fee { Fee::Manual(fee) => fee, Fee::Auto(multiplier) => auto_fee(multiplier).await?, - Fee::PayerGranterAuto(multiplier, payer, granter) => { - let mut fee = auto_fee(multiplier).await?; - fee.payer = payer; - fee.granter = granter; + Fee::PayerGranterAuto(auto_feegrant) => { + let mut fee = auto_fee(auto_feegrant.gas_adjustment).await?; + fee.payer = auto_feegrant.payer; + fee.granter = auto_feegrant.granter; fee } }; diff --git a/common/client-libs/validator-client/src/nymd/fee/mod.rs b/common/client-libs/validator-client/src/nymd/fee/mod.rs index dfa869469e..411cac2365 100644 --- a/common/client-libs/validator-client/src/nymd/fee/mod.rs +++ b/common/client-libs/validator-client/src/nymd/fee/mod.rs @@ -1,9 +1,11 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::nymd::Coin; use crate::nymd::Gas; use cosmrs::{tx, AccountId}; use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; pub mod gas_price; @@ -11,11 +13,74 @@ pub type GasAdjustment = f32; pub const DEFAULT_SIMULATED_GAS_MULTIPLIER: GasAdjustment = 1.3; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AutoFeeGrant { + pub gas_adjustment: Option, + pub payer: Option, + pub granter: Option, +} + +impl Display for AutoFeeGrant { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if let Some(gas_adjustment) = self.gas_adjustment { + write!(f, "Feegrant in auto mode with {gas_adjustment} simulated multiplier with {:?} payer and {:?} granter", self.payer, self.granter) + } else { + write!(f, "Feegrant in auto mode with no custom simulated multiplier with {:?} payer and {:?} granter", self.payer, self.granter) + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Fee { Manual(#[serde(with = "sealed::TxFee")] tx::Fee), Auto(Option), - PayerGranterAuto(Option, Option, Option), + PayerGranterAuto(AutoFeeGrant), +} + +impl Display for Fee { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Fee::Manual(fee) => { + write!(f, "Fee in manual mode with ")?; + for fee in &fee.amount { + write!(f, "{}{} paid in fees, ", fee.amount, fee.denom)?; + } + write!(f, "{} set as gas limit, ", fee.gas_limit)?; + if let Some(payer) = &fee.payer { + write!(f, "{payer} set as payer, ")?; + } + if let Some(granter) = &fee.granter { + write!(f, "{granter} set as granter")?; + } + Ok(()) + } + Fee::Auto(Some(multiplier)) => { + write!(f, "Fee in auto mode with {multiplier} simulated multiplier") + } + Fee::Auto(None) => write!(f, "Fee in auto mode with no custom simulated multiplier"), + Fee::PayerGranterAuto(auto_feegrant) => write!(f, "{}", auto_feegrant), + } + } +} + +impl Fee { + pub fn new_payer_granter_auto( + gas_adjustment: Option, + payer: Option, + granter: Option, + ) -> Self { + Fee::PayerGranterAuto(AutoFeeGrant { + gas_adjustment, + payer, + granter, + }) + } + pub fn try_get_manual_amount(&self) -> Option> { + match self { + Fee::Manual(tx_fee) => Some(tx_fee.amount.iter().cloned().map(Into::into).collect()), + _ => None, + } + } } impl From for Fee { diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index ceeabd5133..2e714f3c38 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -389,7 +389,7 @@ impl NymdClient { pub async fn get_balance( &self, address: &AccountId, - denom: Denom, + denom: String, ) -> Result, NymdError> where C: CosmWasmClient + Sync, @@ -893,7 +893,7 @@ impl NymdClient { &self, contract_address: &AccountId, msg: &M, - fee: Fee, + fee: Option, memo: impl Into + Send + 'static, funds: Vec, ) -> Result @@ -901,6 +901,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 +911,7 @@ impl NymdClient { &self, contract_address: &AccountId, msgs: I, - fee: Fee, + fee: Option, memo: impl Into + Send + 'static, ) -> Result where @@ -918,6 +919,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,6 +1008,29 @@ 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 diff --git a/common/client-libs/validator-client/src/nymd/traits/coconut_bandwidth_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/coconut_bandwidth_query_client.rs new file mode 100644 index 0000000000..b2e38e12a5 --- /dev/null +++ b/common/client-libs/validator-client/src/nymd/traits/coconut_bandwidth_query_client.rs @@ -0,0 +1,33 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nymd::error::NymdError; +use crate::nymd::{CosmWasmClient, NymdClient}; + +use coconut_bandwidth_contract_common::msg::QueryMsg; +use coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; + +use async_trait::async_trait; + +#[async_trait] +pub trait CoconutBandwidthQueryClient { + async fn get_spent_credential( + &self, + blinded_serial_number: String, + ) -> Result; +} + +#[async_trait] +impl CoconutBandwidthQueryClient for NymdClient { + async fn get_spent_credential( + &self, + blinded_serial_number: String, + ) -> Result { + let request = QueryMsg::GetSpentCredential { + blinded_serial_number, + }; + self.client + .query_contract_smart(self.coconut_bandwidth_contract_address(), &request) + .await + } +} diff --git a/common/client-libs/validator-client/src/nymd/traits/coconut_bandwidth_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/coconut_bandwidth_signing_client.rs index 352ee6df26..0fc0d0e126 100644 --- a/common/client-libs/validator-client/src/nymd/traits/coconut_bandwidth_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/coconut_bandwidth_signing_client.rs @@ -5,6 +5,7 @@ pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient; use crate::nymd::cosmwasm_client::types::ExecuteResult; use crate::nymd::error::NymdError; use crate::nymd::{Coin, Fee, NymdClient}; +use coconut_bandwidth_contract_common::spend_credential::SpendCredentialData; use coconut_bandwidth_contract_common::{deposit::DepositData, msg::ExecuteMsg}; use async_trait::async_trait; @@ -19,6 +20,13 @@ pub trait CoconutBandwidthSigningClient { encryption_key: String, fee: Option, ) -> Result; + async fn spend_credential( + &self, + funds: Coin, + blinded_serial_number: String, + gateway_cosmos_address: String, + fee: Option, + ) -> Result; } #[async_trait] @@ -46,4 +54,30 @@ impl CoconutBandwidthSigningClient for N ) .await } + async fn spend_credential( + &self, + funds: Coin, + blinded_serial_number: String, + gateway_cosmos_address: String, + fee: Option, + ) -> Result { + let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); + let req = ExecuteMsg::SpendCredential { + data: SpendCredentialData::new( + funds.into(), + blinded_serial_number, + gateway_cosmos_address, + ), + }; + self.client + .execute( + self.address(), + self.coconut_bandwidth_contract_address(), + &req, + fee, + "CoconutBandwidth::SpendCredential", + vec![], + ) + .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 9198522d2d..f1cda7066b 100644 --- a/common/client-libs/validator-client/src/nymd/traits/mod.rs +++ b/common/client-libs/validator-client/src/nymd/traits/mod.rs @@ -1,14 +1,16 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +mod coconut_bandwidth_query_client; mod coconut_bandwidth_signing_client; mod multisig_query_client; mod multisig_signing_client; mod vesting_query_client; mod vesting_signing_client; +pub use coconut_bandwidth_query_client::CoconutBandwidthQueryClient; pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient; -pub use multisig_query_client::QueryClient; +pub use multisig_query_client::MultisigQueryClient; pub use multisig_signing_client::MultisigSigningClient; pub use vesting_query_client::VestingQueryClient; pub use vesting_signing_client::VestingSigningClient; diff --git a/common/client-libs/validator-client/src/nymd/traits/multisig_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/multisig_query_client.rs index 5869ada26a..9166897b18 100644 --- a/common/client-libs/validator-client/src/nymd/traits/multisig_query_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/multisig_query_client.rs @@ -9,12 +9,12 @@ use multisig_contract_common::msg::{ProposalResponse, QueryMsg}; use async_trait::async_trait; #[async_trait] -pub trait QueryClient { +pub trait MultisigQueryClient { async fn get_proposal(&self, proposal_id: u64) -> Result; } #[async_trait] -impl QueryClient for NymdClient { +impl MultisigQueryClient for NymdClient { async fn get_proposal(&self, proposal_id: u64) -> Result { let request = QueryMsg::Proposal { proposal_id }; self.client diff --git a/common/client-libs/validator-client/src/nymd/traits/multisig_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/multisig_signing_client.rs index 9b685a0f0e..46bb118ef0 100644 --- a/common/client-libs/validator-client/src/nymd/traits/multisig_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/multisig_signing_client.rs @@ -12,7 +12,6 @@ use multisig_contract_common::msg::ExecuteMsg; use async_trait::async_trait; use cosmwasm_std::{to_binary, Coin, CosmosMsg, WasmMsg}; use cw3::Vote; -use network_defaults::DEFAULT_NETWORK; #[async_trait] pub trait MultisigSigningClient { @@ -49,7 +48,10 @@ impl MultisigSigningClient for NymdClien ) -> Result { let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier))); let release_funds_req = CoconutBandwidthExecuteMsg::ReleaseFunds { - funds: Coin::new(voucher_value, DEFAULT_NETWORK.mix_denom().base), + funds: Coin::new( + voucher_value, + self.config.chain_details.mix_denom.base.clone(), + ), }; let release_funds_msg = CosmosMsg::Wasm(WasmMsg::Execute { contract_addr: self.coconut_bandwidth_contract_address().to_string(), 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 0ea6b081c2..247ffcd743 100644 --- a/common/client-libs/validator-client/src/nymd/wallet/mod.rs +++ b/common/client-libs/validator-client/src/nymd/wallet/mod.rs @@ -207,35 +207,20 @@ 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 prefixes = vec![ - MAINNET.bech32_prefix(), - SANDBOX.bech32_prefix(), - QA.bech32_prefix(), - ]; + let prefix = MAINNET.bech32_prefix(); - for prefix in prefixes { - let addrs = match prefix.as_ref() { - "nymt" => vec![ - "nymt1jw6mp7d5xqc7w6xm79lha27glmd0vdt339me94", - "nymt1h5hgn94nsq4kh99rjj794hr5h5q6yfm23rjshv", - "nymt17n9flp6jflljg6fp05dsy07wcprf2uuufgn4d4", - ], - "n" => vec![ - "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", - "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es", - "n17n9flp6jflljg6fp05dsy07wcprf2uuu8g40rf", - ], - _ => panic!("Test needs to be updated with new bech32 prefix"), - }; - for (idx, mnemonic) in mnemonics.iter().enumerate() { - let wallet = - DirectSecp256k1HdWallet::from_mnemonic(&prefix, mnemonic.parse().unwrap()) - .unwrap(); - assert_eq!( - wallet.try_derive_accounts().unwrap()[0].address, - addrs[idx].parse().unwrap() - ) - } + let addrs = vec![ + "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf", + "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es", + "n17n9flp6jflljg6fp05dsy07wcprf2uuu8g40rf", + ]; + for (idx, mnemonic) in mnemonics.iter().enumerate() { + let wallet = + DirectSecp256k1HdWallet::from_mnemonic(&prefix, mnemonic.parse().unwrap()).unwrap(); + assert_eq!( + wallet.try_derive_accounts().unwrap()[0].address, + addrs[idx].parse().unwrap() + ) } } } 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 f326904fb4..1fa3a50862 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -8,8 +8,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use url::Url; use validator_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, - ProposeReleaseFundsRequestBody, ProposeReleaseFundsResponse, VerificationKeyResponse, + BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ @@ -405,23 +404,6 @@ impl Client { ) .await } - - pub async fn propose_release_funds( - &self, - request_body: &ProposeReleaseFundsRequestBody, - ) -> Result { - self.post_validator_api( - &[ - routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_PROPOSE_RELEASE_FUNDS, - ], - NO_PARAMS, - request_body, - ) - .await - } } // utility function that should solve the double slash problem in validator API forever. diff --git a/common/client-libs/validator-client/src/validator_api/routes.rs b/common/client-libs/validator-client/src/validator_api/routes.rs index a0cd6011b9..8f1a125c80 100644 --- a/common/client-libs/validator-client/src/validator_api/routes.rs +++ b/common/client-libs/validator-client/src/validator_api/routes.rs @@ -19,7 +19,6 @@ pub const COCONUT_PARTIAL_BANDWIDTH_CREDENTIAL: &str = "partial-bandwidth-creden pub const COCONUT_VERIFICATION_KEY: &str = "verification-key"; pub const COCONUT_COSMOS_ADDRESS: &str = "cosmos-address"; pub const COCONUT_VERIFY_BANDWIDTH_CREDENTIAL: &str = "verify-bandwidth-credential"; -pub const COCONUT_PROPOSE_RELEASE_FUNDS: &str = "propose-release-funds"; pub const STATUS_ROUTES: &str = "status"; pub const MIXNODE: &str = "mixnode"; diff --git a/common/config/src/lib.rs b/common/config/src/lib.rs index 10cff06548..299208eebc 100644 --- a/common/config/src/lib.rs +++ b/common/config/src/lib.rs @@ -88,3 +88,14 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned { .map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err)) } } + +pub fn parse_validators(raw: &str) -> Vec { + raw.split(',') + .map(|raw_validator| { + raw_validator + .trim() + .parse() + .expect("one of the provided validator api urls is invalid") + }) + .collect() +} diff --git a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/Cargo.toml b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/Cargo.toml index 12b802d3b8..0d4cf1ef86 100644 --- a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/Cargo.toml @@ -9,3 +9,4 @@ edition = "2021" cosmwasm-std = "1.0.0" schemars = "0.8" serde = { version = "1.0.103", default-features = false, features = ["derive"] } +multisig-contract-common = { path = "../multisig-contract" } diff --git a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/lib.rs b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/lib.rs index b9afd74b5e..34b5556745 100644 --- a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/lib.rs @@ -1,3 +1,4 @@ pub mod deposit; pub mod events; pub mod msg; +pub mod spend_credential; diff --git a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/msg.rs index c3e8003894..20f1e2700d 100644 --- a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/msg.rs @@ -5,24 +5,34 @@ use cosmwasm_std::Coin; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::deposit::DepositData; +use crate::{deposit::DepositData, spend_credential::SpendCredentialData}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct InstantiateMsg { pub multisig_addr: String, pub pool_addr: String, + pub mix_denom: String, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ExecuteMsg { DepositFunds { data: DepositData }, + SpendCredential { data: SpendCredentialData }, ReleaseFunds { funds: Coin }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub enum QueryMsg {} +pub enum QueryMsg { + GetSpentCredential { + blinded_serial_number: String, + }, + GetAllSpentCredentials { + limit: Option, + start_after: Option, + }, +} #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] 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 new file mode 100644 index 0000000000..28842576c6 --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/spend_credential.rs @@ -0,0 +1,148 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{from_binary, to_binary, Addr, Coin, CosmosMsg, StdResult, WasmMsg}; +use multisig_contract_common::msg::ExecuteMsg as MultisigExecuteMsg; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::msg::ExecuteMsg; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] +pub struct SpendCredentialData { + funds: Coin, + blinded_serial_number: String, + gateway_cosmos_address: String, +} + +impl SpendCredentialData { + pub fn new(funds: Coin, blinded_serial_number: String, gateway_cosmos_address: String) -> Self { + SpendCredentialData { + funds, + blinded_serial_number, + gateway_cosmos_address, + } + } + + pub fn funds(&self) -> &Coin { + &self.funds + } + + pub fn blinded_serial_number(&self) -> &str { + &self.blinded_serial_number + } + + pub fn gateway_cosmos_address(&self) -> &str { + &self.gateway_cosmos_address + } +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] +pub enum SpendCredentialStatus { + InProgress, + Spent, +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct SpendCredential { + funds: Coin, + blinded_serial_number: String, + gateway_cosmos_address: Addr, + status: SpendCredentialStatus, +} + +impl SpendCredential { + pub fn new(funds: Coin, blinded_serial_number: String, gateway_cosmos_address: Addr) -> Self { + SpendCredential { + funds, + blinded_serial_number, + gateway_cosmos_address, + status: SpendCredentialStatus::InProgress, + } + } + + pub fn blinded_serial_number(&self) -> &str { + &self.blinded_serial_number + } + + pub fn status(&self) -> SpendCredentialStatus { + self.status + } + + pub fn mark_as_spent(&mut self) { + self.status = SpendCredentialStatus::Spent; + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct PagedSpendCredentialResponse { + pub spend_credentials: Vec, + pub per_page: usize, + pub start_next_after: Option, +} + +impl PagedSpendCredentialResponse { + pub fn new( + spend_credentials: Vec, + per_page: usize, + start_next_after: Option, + ) -> Self { + PagedSpendCredentialResponse { + spend_credentials, + per_page, + start_next_after, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct SpendCredentialResponse { + pub spend_credential: Option, +} + +impl SpendCredentialResponse { + pub fn new(spend_credential: Option) -> Self { + SpendCredentialResponse { spend_credential } + } +} + +pub fn to_cosmos_msg( + funds: Coin, + blinded_serial_number: String, + coconut_bandwidth_addr: String, + multisig_addr: String, +) -> StdResult { + let release_funds_req = ExecuteMsg::ReleaseFunds { funds }; + let release_funds_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: coconut_bandwidth_addr, + msg: to_binary(&release_funds_req)?, + funds: vec![], + }); + let req = MultisigExecuteMsg::Propose { + title: String::from("Release funds, as ordered by Coconut Bandwidth Contract"), + description: blinded_serial_number, + msgs: vec![release_funds_msg], + latest: None, + }; + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: multisig_addr, + msg: to_binary(&req)?, + funds: vec![], + }); + + Ok(msg) +} + +pub fn funds_from_cosmos_msgs(msgs: Vec) -> Option { + if let Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: _, + msg, + funds: _, + })) = msgs.get(0) + { + if let Ok(ExecuteMsg::ReleaseFunds { funds }) = from_binary::(msg) { + return Some(funds); + } + } + None +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index 33439678c5..20fc4ecb16 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -3,6 +3,7 @@ name = "mixnet-contract-common" version = "0.1.0" authors = ["Jędrzej Stuczyński "] edition = "2021" +rust-version = "1.62" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -13,7 +14,6 @@ serde = { version = "1.0", features = ["derive"] } serde_repr = "0.1" schemars = "0.8" thiserror = "1.0" -network-defaults = { path = "../../network-defaults" } fixed = { version = "1.1", features = ["serde"] } az = "1.1" log = "0.4.14" diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs index a6258e3ea6..d0d4372fa4 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs @@ -33,7 +33,7 @@ pub struct Delegation { pub node_identity: IdentityKey, pub amount: Coin, pub block_height: u64, - pub proxy: Option, // proxy address used to delegate the funds on behalf of anouther address + pub proxy: Option, // proxy address used to delegate the funds on behalf of another address } impl Eq for Delegation {} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 4772ca1a2e..752719e9c9 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -37,6 +37,16 @@ pub enum DelegationEvent { Undelegate(PendingUndelegate), } +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, + } + } +} + #[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)] pub struct PendingUndelegate { mix_identity: IdentityKey, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index d8f09d504a..a9b3e8fbf3 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -10,6 +10,7 @@ use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] pub struct InstantiateMsg { pub rewarding_validator_address: String, + pub mixnet_denom: String, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] @@ -32,6 +33,12 @@ pub enum ExecuteMsg { CompoundDelegatorReward { mix_identity: IdentityKey, }, + CompoundReward { + operator: Option, + delegator: Option, + mix_identity: Option, + proxy: Option, + }, BondMixnode { mix_node: MixNode, owner_signature: String, @@ -115,6 +122,7 @@ pub enum ExecuteMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum QueryMsg { + GetBlacklistedNodes {}, GetCurrentOperatorCost {}, GetRewardingValidatorAddress {}, GetAllDelegationKeys {}, @@ -207,4 +215,29 @@ pub enum QueryMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct MigrateMsg {} +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, Eq, 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() + } +} diff --git a/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs b/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs index 6a677f9c24..93fd9f4839 100644 --- a/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs @@ -14,6 +14,7 @@ use cw_utils::{Duration, Expiration, Threshold}; pub struct InstantiateMsg { // this is the group contract that contains the member list pub group_addr: String, + pub coconut_bandwidth_contract_address: String, pub threshold: Threshold, pub max_voting_period: Duration, } @@ -77,3 +78,8 @@ pub enum QueryMsg { limit: Option, }, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg { + pub coconut_bandwidth_address: String, +} diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs index 346162420d..da29e02cae 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs @@ -1,6 +1,5 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use config::defaults::MIX_DENOM; use cosmwasm_std::{Coin, Timestamp}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -10,8 +9,8 @@ pub use messages::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg}; pub mod events; pub mod messages; -pub fn one_ucoin() -> Coin { - Coin::new(1, MIX_DENOM.base) +pub fn one_ucoin(denom: String) -> Coin { + Coin::new(1, denom) } #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] @@ -28,8 +27,8 @@ pub enum Period { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct PledgeData { - amount: Coin, - block_time: Timestamp, + pub amount: Coin, + pub block_time: Timestamp, } impl PledgeData { @@ -48,9 +47,9 @@ impl PledgeData { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct OriginalVestingResponse { - amount: Coin, - number_of_periods: usize, - period_duration: u64, + pub amount: Coin, + pub number_of_periods: usize, + pub period_duration: u64, } impl OriginalVestingResponse { diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index 51802e77ac..a9dd627557 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -7,11 +7,14 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "snake_case")] pub struct InitMsg { pub mixnet_contract_address: String, + pub mix_denom: String, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct MigrateMsg {} +pub struct MigrateMsg { + pub mix_denom: String, +} #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, Default)] pub struct VestingSpecification { diff --git a/common/credentials/Cargo.toml b/common/credentials/Cargo.toml index 110d6ee224..4ff6b60d2b 100644 --- a/common/credentials/Cargo.toml +++ b/common/credentials/Cargo.toml @@ -14,7 +14,6 @@ url = "2.2" # I guess temporarily until we get serde support in coconut up and running coconut-interface = { path = "../coconut-interface" } crypto = { path = "../crypto", features = ["rand", "asymmetric", "symmetric", "hashing"] } -network-defaults = { path = "../network-defaults" } validator-api-requests = { path = "../../validator-api/validator-api-requests" } validator-client = { path = "../client-libs/validator-client" } diff --git a/common/crypto/src/bech32_address_validation.rs b/common/crypto/src/bech32_address_validation.rs index 21db7e5297..f432daab4c 100644 --- a/common/crypto/src/bech32_address_validation.rs +++ b/common/crypto/src/bech32_address_validation.rs @@ -1,4 +1,3 @@ -use config::defaults::DEFAULT_NETWORK; use subtle_encoding::bech32; #[derive(Debug, Clone, PartialEq, Eq)] @@ -15,16 +14,15 @@ pub fn try_bech32_decode(address: &str) -> Result { } } -pub fn validate_bech32_prefix(address: &str) -> Result<(), Bech32Error> { +pub fn validate_bech32_prefix(bech32_prefix: &str, address: &str) -> Result<(), Bech32Error> { let prefix = try_bech32_decode(address)?; - if prefix == DEFAULT_NETWORK.bech32_prefix() { + if prefix == bech32_prefix { Ok(()) } else { Err(Bech32Error::WrongPrefix(format!( "your bech32 address prefix should be {}, not {}", - DEFAULT_NETWORK.bech32_prefix(), - prefix + bech32_prefix, prefix ))) } } @@ -33,6 +31,8 @@ pub fn validate_bech32_prefix(address: &str) -> Result<(), Bech32Error> { mod tests { use super::*; + const TEST_BECH32_PREFIX: &str = "n"; + mod decoding_bech32_addresses { use super::*; @@ -71,9 +71,12 @@ mod tests { assert_eq!( Err(Bech32Error::WrongPrefix(format!( "your bech32 address prefix should be {}, not punk", - DEFAULT_NETWORK.bech32_prefix() + TEST_BECH32_PREFIX ))), - validate_bech32_prefix("punk1h3w4nj7kny5dfyjw2le4vm74z03v9vd4dstpu0") + validate_bech32_prefix( + TEST_BECH32_PREFIX, + "punk1h3w4nj7kny5dfyjw2le4vm74z03v9vd4dstpu0" + ) ) } @@ -82,6 +85,7 @@ mod tests { assert_eq!( Ok(()), validate_bech32_prefix( + TEST_BECH32_PREFIX, "n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g" ) ) diff --git a/common/inclusion-probability/Cargo.toml b/common/inclusion-probability/Cargo.toml new file mode 100644 index 0000000000..0df3bfe4e7 --- /dev/null +++ b/common/inclusion-probability/Cargo.toml @@ -0,0 +1,10 @@ +[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] +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..9e9908a585 --- /dev/null +++ b/common/inclusion-probability/src/error.rs @@ -0,0 +1,9 @@ +#[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 arrarys")] + NormDifferenceSizeArrays, +} diff --git a/common/inclusion-probability/src/lib.rs b/common/inclusion-probability/src/lib.rs new file mode 100644 index 0000000000..b392ab9803 --- /dev/null +++ b/common/inclusion-probability/src/lib.rs @@ -0,0 +1,278 @@ +//! Active set inclusion probability simulator + +use error::Error; + +mod error; + +const TOLERANCE_L2_NORM: f64 = 1e-4; +const TOLERANCE_MAX_NORM: f64 = 1e-3; + +pub struct SelectionProbability { + pub active_set_probability: Vec, + pub reserve_set_probability: Vec, + pub samples: u32, + pub delta_l2: f64, + pub delta_max: f64, +} + +pub fn simulate_selection_probability_mixnodes( + list_stake_for_mixnodes: &[u64], + active_set_size: usize, + reserve_set_size: usize, + max_samples: u32, +) -> Result { + // 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; + let mut rng = rand::thread_rng(); + + 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 { + let candidate = sample_candidate(&list_cumul_temp, &mut 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 { + let candidate = sample_candidate(&list_cumul_temp, &mut 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)? + / f64::from(samples); + delta_max = max_diff(&active_set_probability, &active_set_probability_previous)? + / f64::from(samples); + if samples > 10 && delta_l2 < TOLERANCE_L2_NORM && delta_max < TOLERANCE_MAX_NORM + || samples >= max_samples + { + break; + } + } + + active_set_probability + .iter_mut() + .for_each(|x| *x /= f64::from(samples)); + reserve_set_probability + .iter_mut() + .for_each(|x| *x /= f64::from(samples)); + + Ok(SelectionProbability { + active_set_probability, + reserve_set_probability, + samples, + 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: &[u64], rng: &mut rand::rngs::ThreadRng) -> Result { + 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 [u64]) { + 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 super::*; + + #[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 SelectionProbability { + active_set_probability, + reserve_set_probability, + samples, + delta_l2, + delta_max, + } = simulate_selection_probability_mixnodes( + &list_mix, + active_set_size, + standby_set_size, + max_samples, + ) + .unwrap(); + + // 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!(samples < 20_500); + assert!(delta_l2 < TOLERANCE_L2_NORM); + assert!(delta_max < TOLERANCE_MAX_NORM); + } +} diff --git a/common/network-defaults/Cargo.toml b/common/network-defaults/Cargo.toml index 85282473ce..0c84938c7b 100644 --- a/common/network-defaults/Cargo.toml +++ b/common/network-defaults/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] cfg-if = "1.0.0" +dotenv = "0.15.0" hex-literal = "0.3.3" once_cell = "1.7.2" serde = {version = "1.0", features = ["derive"]} diff --git a/common/network-defaults/build.rs b/common/network-defaults/build.rs deleted file mode 100644 index 2c71c67e3d..0000000000 --- a/common/network-defaults/build.rs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -fn main() { - match option_env!("NETWORK") { - None | Some("mainnet") => println!("cargo:rustc-cfg=network=\"mainnet\"",), - Some("sandbox") => println!("cargo:rustc-cfg=network=\"sandbox\"",), - Some("qa") => println!("cargo:rustc-cfg=network=\"qa\""), - _ => panic!("No such network"), - } -} diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index 5487899696..a156394e56 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -3,6 +3,7 @@ use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; +use std::{env::var, path::PathBuf}; use url::Url; pub mod all; @@ -10,36 +11,10 @@ pub mod eth_contract; pub mod mainnet; pub mod qa; pub mod sandbox; +pub mod var_names; -// The set of defaults that are decided at compile time. Ideally we want to reduce these to a -// minimum. -// Keep DENOM around mostly for use in contracts. (TODO: consider moving it there, or renaming?) -cfg_if::cfg_if! { - if #[cfg(network = "mainnet")] { - pub const DEFAULT_NETWORK: all::Network = all::Network::MAINNET; - pub const MIX_DENOM: DenomDetails = mainnet::MIX_DENOM; - pub const STAKE_DENOM: DenomDetails = mainnet::STAKE_DENOM; - - pub const ETH_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_CONTRACT_ADDRESS; - pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_ERC20_CONTRACT_ADDRESS; - - } else if #[cfg(network = "qa")] { - pub const DEFAULT_NETWORK: all::Network = all::Network::QA; - pub const MIX_DENOM: DenomDetails = qa::MIX_DENOM; - pub const STAKE_DENOM: DenomDetails = qa::STAKE_DENOM; - - pub const ETH_CONTRACT_ADDRESS: [u8; 20] = qa::_ETH_CONTRACT_ADDRESS; - pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = qa::_ETH_ERC20_CONTRACT_ADDRESS; - - } else if #[cfg(network = "sandbox")] { - pub const DEFAULT_NETWORK: all::Network = all::Network::SANDBOX; - pub const MIX_DENOM: DenomDetails = sandbox::MIX_DENOM; - pub const STAKE_DENOM: DenomDetails = sandbox::STAKE_DENOM; - - pub const ETH_CONTRACT_ADDRESS: [u8; 20] = sandbox::_ETH_CONTRACT_ADDRESS; - pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = sandbox::_ETH_ERC20_CONTRACT_ADDRESS; - } -} +pub const ETH_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_CONTRACT_ADDRESS; +pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_ERC20_CONTRACT_ADDRESS; #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub struct ChainDetails { @@ -82,23 +57,56 @@ impl NymNetworkDetails { NymNetworkDetails::default() } - pub fn new_qa() -> Self { - (&*QA_DEFAULTS).into() - } - - pub fn new_sandbox() -> Self { - (&*SANDBOX_DEFAULTS).into() + pub fn new_from_env() -> Self { + NymNetworkDetails::new() + .with_bech32_account_prefix( + var(var_names::BECH32_PREFIX).expect("bech32 prefix not set"), + ) + .with_mix_denom(DenomDetailsOwned { + base: var(var_names::MIX_DENOM).expect("mix denomination base not set"), + display: var(var_names::MIX_DENOM_DISPLAY) + .expect("mix denomination display not set"), + display_exponent: var(var_names::DENOMS_EXPONENT) + .expect("denomination exponent not set") + .parse() + .expect("denomination exponent is not u32"), + }) + .with_stake_denom(DenomDetailsOwned { + base: var(var_names::STAKE_DENOM).expect("stake denomination base not set"), + display: var(var_names::STAKE_DENOM_DISPLAY) + .expect("stake denomination display not set"), + display_exponent: var(var_names::DENOMS_EXPONENT) + .expect("denomination exponent not set") + .parse() + .expect("denomination exponent is not u32"), + }) + .with_validator_endpoint(ValidatorDetails::new( + var(var_names::NYMD_VALIDATOR).expect("nymd validator not set"), + Some(var(var_names::API_VALIDATOR).expect("api validator not set")), + )) + .with_mixnet_contract(Some( + var(var_names::MIXNET_CONTRACT_ADDRESS).expect("mixnet contract not set"), + )) + .with_vesting_contract(Some( + var(var_names::VESTING_CONTRACT_ADDRESS).expect("vesting contract not set"), + )) + .with_bandwidth_claim_contract(Some( + var(var_names::BANDWIDTH_CLAIM_CONTRACT_ADDRESS) + .expect("bandwidth claim contract not set"), + )) + .with_coconut_bandwidth_contract(Some( + var(var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS) + .expect("coconut bandwidth contract not set"), + )) + .with_multisig_contract(Some( + var(var_names::MULTISIG_CONTRACT_ADDRESS).expect("multisig contract not set"), + )) } pub fn new_mainnet() -> Self { (&*MAINNET_DEFAULTS).into() } - pub fn current_default() -> Self { - // backwards compatibility reasons - DEFAULT_NETWORK.details() - } - pub fn with_bech32_account_prefix>(mut self, prefix: S) -> Self { self.chain_details.bech32_account_prefix = prefix.into(); self @@ -267,7 +275,7 @@ impl DenomDetails { } } -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[derive(Debug, Serialize, Deserialize, Hash, Clone, PartialEq, Eq)] pub struct DenomDetailsOwned { pub base: String, pub display: String, @@ -333,27 +341,21 @@ impl ValidatorDetails { } } -pub fn default_statistics_service_url() -> Url { - DEFAULT_NETWORK - .statistics_service_url() - .parse() - .expect("the provided statistics service url is invalid!") -} - -pub fn default_nymd_endpoints() -> Vec { - DEFAULT_NETWORK - .validators() - .iter() - .map(ValidatorDetails::nymd_url) - .collect() -} - -pub fn default_api_endpoints() -> Vec { - DEFAULT_NETWORK - .validators() - .iter() - .filter_map(ValidatorDetails::api_url) - .collect() +pub fn setup_env(config_env_file: Option) { + match std::env::var(var_names::CONFIGURED) { + // if the configuration is not already set in the env vars + Err(std::env::VarError::NotPresent) => { + if let Some(config_env_file) = config_env_file { + dotenv::from_path(config_env_file) + .expect("Invalid path to environment configuration file"); + } else { + // if nothing is set, the use mainnet defaults + crate::mainnet::export_to_env(); + } + } + Err(_) => crate::mainnet::export_to_env(), + _ => {} + } } // 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 badce8e2bc..4a59806183 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::var_names; use crate::{DenomDetails, ValidatorDetails}; pub(crate) const BECH32_PREFIX: &str = "n"; @@ -23,10 +24,49 @@ pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000000"); pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy"; -pub(crate) const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "http://127.0.0.1:8090"; +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 API_VALIDATOR: &str = "https://validator.nymtech.net/api/"; pub(crate) fn validators() -> Vec { - vec![ValidatorDetails::new( - "https://rpc.nyx.nodes.guru/", - Some("https://validator.nymtech.net/api/"), - )] + vec![ValidatorDetails::new(NYMD_VALIDATOR, Some(API_VALIDATOR))] +} + +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( + var_names::DENOMS_EXPONENT, + STAKE_DENOM.display_exponent.to_string(), + ); + std::env::set_var(var_names::MIXNET_CONTRACT_ADDRESS, MIXNET_CONTRACT_ADDRESS); + std::env::set_var( + var_names::VESTING_CONTRACT_ADDRESS, + VESTING_CONTRACT_ADDRESS, + ); + std::env::set_var( + var_names::BANDWIDTH_CLAIM_CONTRACT_ADDRESS, + BANDWIDTH_CLAIM_CONTRACT_ADDRESS, + ); + std::env::set_var( + var_names::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, + COCONUT_BANDWIDTH_CONTRACT_ADDRESS, + ); + std::env::set_var( + var_names::MULTISIG_CONTRACT_ADDRESS, + MULTISIG_CONTRACT_ADDRESS, + ); + std::env::set_var( + var_names::REWARDING_VALIDATOR_ADDRESS, + REWARDING_VALIDATOR_ADDRESS, + ); + std::env::set_var( + 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); } diff --git a/common/network-defaults/src/var_names.rs b/common/network-defaults/src/var_names.rs new file mode 100644 index 0000000000..d1f3cf2736 --- /dev/null +++ b/common/network-defaults/src/var_names.rs @@ -0,0 +1,21 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// Environment variable that, if set, shows the environment is currently configured +pub const CONFIGURED: &str = "CONFIGURED"; + +pub const BECH32_PREFIX: &str = "BECH32_PREFIX"; +pub const MIX_DENOM: &str = "MIX_DENOM"; +pub const MIX_DENOM_DISPLAY: &str = "MIX_DENOM_DISPLAY"; +pub const STAKE_DENOM: &str = "STAKE_DENOM"; +pub const STAKE_DENOM_DISPLAY: &str = "STAKE_DENOM_DISPLAY"; +pub const DENOMS_EXPONENT: &str = "DENOMS_EXPONENT"; +pub const MIXNET_CONTRACT_ADDRESS: &str = "MIXNET_CONTRACT_ADDRESS"; +pub const VESTING_CONTRACT_ADDRESS: &str = "VESTING_CONTRACT_ADDRESS"; +pub const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = "BANDWIDTH_CLAIM_CONTRACT_ADDRESS"; +pub const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "COCONUT_BANDWIDTH_CONTRACT_ADDRESS"; +pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS"; +pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS"; +pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "STATISTICS_SERVICE_DOMAIN_ADDRESS"; +pub const NYMD_VALIDATOR: &str = "NYMD_VALIDATOR"; +pub const API_VALIDATOR: &str = "API_VALIDATOR"; diff --git a/common/nymcoconut/src/lib.rs b/common/nymcoconut/src/lib.rs index 936b27005f..ab1cab7cf7 100644 --- a/common/nymcoconut/src/lib.rs +++ b/common/nymcoconut/src/lib.rs @@ -34,8 +34,7 @@ mod error; mod impls; mod proofs; mod scheme; -#[cfg(test)] -mod tests; +pub mod tests; mod traits; mod utils; diff --git a/common/nymcoconut/src/tests/e2e.rs b/common/nymcoconut/src/tests/e2e.rs index 39c97184d7..ce4fe2ab57 100644 --- a/common/nymcoconut/src/tests/e2e.rs +++ b/common/nymcoconut/src/tests/e2e.rs @@ -1,23 +1,13 @@ use crate::{ - aggregate_signature_shares, aggregate_verification_keys, blind_sign, prepare_blind_sign, - prove_bandwidth_credential, setup, ttp_keygen, verify_credential, CoconutError, Signature, - SignatureShare, VerificationKey, + aggregate_verification_keys, setup, tests::helpers::theta_from_keys_and_attributes, ttp_keygen, + verify_credential, CoconutError, VerificationKey, }; -use itertools::izip; - #[test] fn main() -> Result<(), CoconutError> { let params = setup(5)?; let public_attributes = params.n_random_scalars(2); - let serial_number = params.random_scalar(); - let binding_number = params.random_scalar(); - let private_attributes = vec![serial_number, binding_number]; - - // generate commitment - let (commitments_openings, blind_sign_request) = - prepare_blind_sign(¶ms, &private_attributes, &public_attributes)?; // generate_keys let coconut_keypairs = ttp_keygen(¶ms, 2, 3)?; @@ -30,58 +20,8 @@ fn main() -> Result<(), CoconutError> { // aggregate verification keys let verification_key = aggregate_verification_keys(&verification_keys, Some(&[1, 2, 3]))?; - // generate blinded signatures - let mut blinded_signatures = Vec::new(); - - for keypair in coconut_keypairs { - let blinded_signature = blind_sign( - ¶ms, - &keypair.secret_key(), - &blind_sign_request, - &public_attributes, - )?; - blinded_signatures.push(blinded_signature) - } - - // Unblind - let unblinded_signatures: Vec = - izip!(blinded_signatures.iter(), verification_keys.iter()) - .map(|(s, vk)| { - s.unblind( - ¶ms, - vk, - &private_attributes, - &public_attributes, - &blind_sign_request.get_commitment_hash(), - &commitments_openings, - ) - .unwrap() - }) - .collect(); - - // Aggregate signatures - let signature_shares: Vec = unblinded_signatures - .iter() - .enumerate() - .map(|(idx, signature)| SignatureShare::new(*signature, (idx + 1) as u64)) - .collect(); - - let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len()); - attributes.extend_from_slice(&private_attributes); - attributes.extend_from_slice(&public_attributes); - - // Randomize credentials and generate any cryptographic material to verify them - let signature = - aggregate_signature_shares(¶ms, &verification_key, &attributes, &signature_shares)?; - // Generate cryptographic material to verify them - let theta = prove_bandwidth_credential( - ¶ms, - &verification_key, - &signature, - serial_number, - binding_number, - )?; + let theta = theta_from_keys_and_attributes(¶ms, &coconut_keypairs, &public_attributes)?; // Verify credentials assert!(verify_credential( diff --git a/common/nymcoconut/src/tests/helpers.rs b/common/nymcoconut/src/tests/helpers.rs new file mode 100644 index 0000000000..7d9c7098b9 --- /dev/null +++ b/common/nymcoconut/src/tests/helpers.rs @@ -0,0 +1,87 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::*; +use itertools::izip; + +pub fn theta_from_keys_and_attributes( + params: &Parameters, + coconut_keypairs: &Vec, + public_attributes: &Vec, +) -> Result { + let serial_number = params.random_scalar(); + let binding_number = params.random_scalar(); + let private_attributes = vec![serial_number, binding_number]; + + // generate commitment + let (commitments_openings, blind_sign_request) = + prepare_blind_sign(params, &private_attributes, public_attributes)?; + + let verification_keys: Vec = coconut_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + // aggregate verification keys + let indices: Vec = coconut_keypairs + .iter() + .enumerate() + .map(|(idx, _)| (idx + 1) as u64) + .collect(); + let verification_key = aggregate_verification_keys(&verification_keys, Some(&indices))?; + + // generate blinded signatures + let mut blinded_signatures = Vec::new(); + + for keypair in coconut_keypairs { + let blinded_signature = blind_sign( + params, + &keypair.secret_key(), + &blind_sign_request, + public_attributes, + )?; + blinded_signatures.push(blinded_signature) + } + + // Unblind + let unblinded_signatures: Vec = + izip!(blinded_signatures.iter(), verification_keys.iter()) + .map(|(s, vk)| { + s.unblind( + params, + vk, + &private_attributes, + public_attributes, + &blind_sign_request.get_commitment_hash(), + &commitments_openings, + ) + .unwrap() + }) + .collect(); + + // Aggregate signatures + let signature_shares: Vec = unblinded_signatures + .iter() + .enumerate() + .map(|(idx, signature)| SignatureShare::new(*signature, (idx + 1) as u64)) + .collect(); + + let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len()); + attributes.extend_from_slice(&private_attributes); + attributes.extend_from_slice(public_attributes); + + // Randomize credentials and generate any cryptographic material to verify them + let signature = + aggregate_signature_shares(params, &verification_key, &attributes, &signature_shares)?; + + // Generate cryptographic material to verify them + let theta = prove_bandwidth_credential( + params, + &verification_key, + &signature, + serial_number, + binding_number, + )?; + + Ok(theta) +} diff --git a/common/nymcoconut/src/tests/mod.rs b/common/nymcoconut/src/tests/mod.rs index b6ae44f843..c13b7a3b20 100644 --- a/common/nymcoconut/src/tests/mod.rs +++ b/common/nymcoconut/src/tests/mod.rs @@ -1 +1,3 @@ +#[cfg(test)] mod e2e; +pub mod helpers; diff --git a/common/statistics/Cargo.toml b/common/statistics/Cargo.toml index 1094962c21..9514edd29b 100644 --- a/common/statistics/Cargo.toml +++ b/common/statistics/Cargo.toml @@ -17,5 +17,3 @@ serde_json = "1" sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "chrono"]} thiserror = "1" tokio = { version = "1.19.1", features = [ "time" ] } - -network-defaults = { path = "../network-defaults" } diff --git a/common/statistics/src/lib.rs b/common/statistics/src/lib.rs index 9e107fc119..5423106648 100644 --- a/common/statistics/src/lib.rs +++ b/common/statistics/src/lib.rs @@ -34,12 +34,16 @@ pub enum StatsData { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct StatsGatewayData { + pub gateway_id: String, pub inbox_count: u32, } impl StatsGatewayData { - pub fn new(inbox_count: u32) -> Self { - StatsGatewayData { inbox_count } + pub fn new(gateway_id: String, inbox_count: u32) -> Self { + StatsGatewayData { + gateway_id, + inbox_count, + } } } diff --git a/common/types/src/account.rs b/common/types/src/account.rs index 00ec29a703..47c6fc0c6d 100644 --- a/common/types/src/account.rs +++ b/common/types/src/account.rs @@ -1,4 +1,5 @@ -use crate::currency::{CurrencyDenom, MajorCurrencyAmount}; +use crate::currency::{CurrencyDenom, DecCoin}; +use config::defaults::DenomDetails; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -9,17 +10,20 @@ use serde::{Deserialize, Serialize}; )] #[derive(Serialize, Deserialize, JsonSchema)] pub struct Account { - pub contract_address: String, pub client_address: String, - pub denom: CurrencyDenom, + pub base_mix_denom: String, + + // this should get refactored to just use a String, but for now it's fine as it reduces headache + // for others + pub display_mix_denom: CurrencyDenom, } impl Account { - pub fn new(contract_address: String, client_address: String, denom: CurrencyDenom) -> Self { + pub fn new(client_address: String, mix_denom: DenomDetails) -> Self { Account { - contract_address, client_address, - denom, + base_mix_denom: mix_denom.base.to_owned(), + display_mix_denom: mix_denom.display.parse().unwrap_or_default(), } } } @@ -53,6 +57,15 @@ pub struct AccountEntry { )] #[derive(Serialize, Deserialize)] pub struct Balance { - pub amount: MajorCurrencyAmount, + pub amount: DecCoin, pub printable_balance: String, } + +impl Balance { + pub fn new(amount: DecCoin) -> Self { + Balance { + printable_balance: amount.to_string(), + amount, + } + } +} diff --git a/common/types/src/currency.rs b/common/types/src/currency.rs index bad9ace0e6..d2bafe589c 100644 --- a/common/types/src/currency.rs +++ b/common/types/src/currency.rs @@ -1,24 +1,29 @@ use crate::error::TypesError; -use cosmrs::Denom as CosmosDenom; -use cosmwasm_std::Coin as CosmWasmCoin; +use config::defaults::all::Network; +use config::defaults::{DenomDetails, DenomDetailsOwned}; +use cosmwasm_std::Fraction; use cosmwasm_std::{Decimal, Uint128}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::cmp::Ordering; +use std::collections::HashMap; use std::convert::TryFrom; use std::fmt::{Display, Formatter}; -use std::ops::{Add, Mul}; -use std::str::FromStr; use strum::{Display, EnumString, EnumVariantNames}; -use validator_client::nymd::{Coin, CosmosCoin}; +use validator_client::nymd::Coin; + +#[cfg(feature = "generate-ts")] +use ts_rs::{Dependency, TS}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", ts(export_to = "ts-packages/types/src/types/rust/CurrencyDenom.ts") )] -#[cfg_attr(feature = "generate-ts", ts(rename_all = "UPPERCASE"))] +#[cfg_attr(feature = "generate-ts", ts(rename_all = "lowercase"))] #[derive( Display, + Default, Serialize, Deserialize, Clone, @@ -29,10 +34,12 @@ use validator_client::nymd::{Coin, CosmosCoin}; Eq, JsonSchema, )] -#[serde(rename_all = "UPPERCASE")] -#[strum(serialize_all = "UPPERCASE")] -// TODO: this shouldn't be an enum... +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] pub enum CurrencyDenom { + #[strum(ascii_case_insensitive)] + #[default] + Unknown, #[strum(ascii_case_insensitive)] Nym, #[strum(ascii_case_insensitive)] @@ -43,448 +50,469 @@ pub enum CurrencyDenom { Nyxt, } -impl CurrencyDenom { - pub fn parse(value: &str) -> Result { - let mut denom = value.to_string(); - if denom.starts_with('u') { - denom = denom[1..].to_string(); +pub type Denom = String; + +#[derive(Debug, Default)] +pub struct RegisteredCoins(HashMap); + +impl RegisteredCoins { + pub fn default_denoms(network: Network) -> 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()); + RegisteredCoins(network_coins) + } + + pub fn insert(&mut self, denom: Denom, metadata: CoinMetadata) -> Option { + self.0.insert(denom, metadata) + } + + pub fn remove(&mut self, denom: &Denom) -> Option { + self.0.remove(denom) + } + + 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) { + // if we're converting a base DecCoin it CANNOT fail, unless somebody is providing + // bullshit data on purpose : ) + return coin.try_into(); + } else { + // TODO: this kinda suggests we may need a better data structure + for registered_coin in self.0.values() { + if let Some(exponent) = registered_coin.get_exponent(&coin.denom) { + let amount = try_convert_decimal_to_u128(coin.try_scale_up_value(exponent)?)?; + return Ok(Coin::new(amount, ®istered_coin.base)); + } + } } - match CurrencyDenom::from_str(&denom) { - Ok(res) => Ok(res), - Err(_e) => Err(TypesError::InvalidDenom(value.to_string())), + Err(TypesError::UnknownCoinDenom(coin.denom)) + } + + pub fn attempt_convert_to_display_dec_coin(&self, coin: Coin) -> Result { + for registered_coin in self.0.values() { + if let Some(exponent) = registered_coin.get_exponent(&coin.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(coin.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::new_scaled_up( + coin.amount, + ®istered_coin.display, + exponent - display_exponent, + )?) + } + // we're already in the display denom + Ordering::Equal => Ok(coin.into()), + 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::new_scaled_down( + coin.amount, + ®istered_coin.display, + display_exponent - exponent, + )?) + } + }; + } } + + Err(TypesError::UnknownCoinDenom(coin.denom)) } } -impl TryFrom for CurrencyDenom { - type Error = TypesError; +// TODO: should this live here? +// attempts to replicate cosmos-sdk's coin metadata +// https://docs.cosmos.network/master/architecture/adr-024-coin-metadata.html +// this way we could more easily handle multiple coin types simultaneously (like nym/nyx/nymt/nyx + local currencies) +#[derive(Debug)] +pub struct DenomUnit { + pub denom: Denom, + pub exponent: u32, + // pub aliases: Vec, +} - fn try_from(value: CosmosDenom) -> Result { - CurrencyDenom::parse(value.as_ref()) +impl DenomUnit { + pub fn new(denom: Denom, exponent: u32) -> Self { + DenomUnit { denom, exponent } } } -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/CurrencyStringMajorAmount.ts") -)] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -pub struct MajorAmountString(String); // see https://github.com/Aleph-Alpha/ts-rs/issues/51 for exporting type aliases - -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Currency.ts") -)] -// #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] -pub struct MajorCurrencyAmount { - // temporarly going back to original impl to speed up merge - pub amount: MajorAmountString, - pub denom: CurrencyDenom, - // // temporary... - // #[cfg_attr(feature = "generate-ts", ts(skip))] - // pub coin: Coin, +#[derive(Debug)] +pub struct CoinMetadata { + pub denom_units: Vec, + pub base: Denom, + pub display: Denom, } -// impl JsonSchema for MajorCurrencyAmount { -// fn schema_name() -> String { -// todo!() -// } -// -// fn json_schema(gen: &mut SchemaGenerator) -> Schema { -// todo!() -// } -// } +impl CoinMetadata { + pub fn new(denom_units: Vec, base: Denom, display: Denom) -> Self { + CoinMetadata { + denom_units, + base, + display, + } + } + + pub fn get_exponent(&self, denom: &str) -> Option { + self.denom_units + .iter() + .find(|denom_unit| denom_unit.denom == denom) + .map(|denom_unit| denom_unit.exponent) + } +} + +impl From for CoinMetadata { + fn from(denom_details: DenomDetails) -> Self { + CoinMetadata::new( + vec![ + DenomUnit::new(denom_details.base.into(), 0), + DenomUnit::new(denom_details.display.into(), denom_details.display_exponent), + ], + denom_details.base.into(), + denom_details.display.into(), + ) + } +} + +impl From for CoinMetadata { + fn from(denom_details: DenomDetailsOwned) -> Self { + CoinMetadata::new( + vec![ + DenomUnit::new(denom_details.base.clone(), 0), + DenomUnit::new( + denom_details.display.clone(), + denom_details.display_exponent, + ), + ], + denom_details.base, + denom_details.display, + ) + } +} // tries to semi-replicate cosmos-sdk's DecCoin for being able to handle tokens with decimal amounts // https://github.com/cosmos/cosmos-sdk/blob/v0.45.4/types/dec_coin.go +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] pub struct DecCoin { - // + pub denom: Denom, + // Decimal is already serialized to string and using string in its schema, so lets also go straight to string for ts_rs + // todo: is `Decimal` the correct type to use? Do we want to depend on cosmwasm_std here? + pub amount: Decimal, } -impl MajorCurrencyAmount { - pub fn new(amount: &str, denom: CurrencyDenom) -> MajorCurrencyAmount { - MajorCurrencyAmount { - amount: MajorAmountString(amount.to_string()), - denom, +// I had to implement it manually to correctly set dependencies +#[cfg(feature = "generate-ts")] +impl TS for DecCoin { + const EXPORT_TO: Option<&'static str> = Some("ts-packages/types/src/types/rust/DecCoin.ts"); + + fn decl() -> String { + format!("type {} = {};", Self::name(), Self::inline()) + } + + fn name() -> String { + "DecCoin".into() + } + + fn inline() -> String { + "{ denom: CurrencyDenom, amount: string }".into() + } + + fn dependencies() -> Vec { + vec![Dependency::from_ty::() + .expect("TS was incorrectly defined on `CurrencyDenom`")] + } + + fn transparent() -> bool { + false + } +} + +impl DecCoin { + pub fn new_base>(amount: impl Into, denom: S) -> Self { + DecCoin { + denom: denom.into(), + amount: Decimal::from_atomics(amount, 0).unwrap(), } } - pub fn zero(denom: &CurrencyDenom) -> MajorCurrencyAmount { - MajorCurrencyAmount::new("0", denom.clone()) + pub fn zero>(denom: S) -> Self { + DecCoin { + denom: denom.into(), + amount: Decimal::zero(), + } } - // - // pub fn from_cosmrs_coin(coin: &CosmosCoin) -> Result { - // MajorCurrencyAmount::from_cosmrs_decimal_and_denom(coin.amount, coin.denom.to_string()) - // } - // - // pub fn from_minor_uint128_and_denom( - // amount_minor: Uint128, - // denom_minor: &str, - // ) -> Result { - // MajorCurrencyAmount::from_minor_decimal_and_denom( - // Decimal::from_atomics(amount_minor, 0)?, - // denom_minor, - // ) - // } - // - // pub fn from_minor_decimal_and_denom( - // amount_minor: Decimal, - // denom_minor: &str, - // ) -> Result { - // if !(denom_minor.starts_with('u') || denom_minor.starts_with('U')) { - // return Err(TypesError::InvalidDenom(denom_minor.to_string())); - // } - // let major = amount_minor / Uint128::from(1_000_000u64); - // if let Ok(denom) = CurrencyDenom::from_str(&denom_minor[1..].to_string()) { - // return Ok(MajorCurrencyAmount { - // amount: MajorAmountString(major.to_string()), - // denom, - // }); - // } - // Err(TypesError::InvalidDenom(denom_minor.to_string())) - // } - // pub fn from_decimal_and_denom( - // amount: Decimal, - // denom: String, - // ) -> Result { - // if denom.starts_with('u') || denom.starts_with('U') { - // return MajorCurrencyAmount::from_minor_decimal_and_denom(amount, &denom); - // } - // if let Ok(denom) = CurrencyDenom::from_str(denom.as_str()) { - // return Ok(MajorCurrencyAmount { - // amount: MajorAmountString(amount.to_string()), - // denom, - // }); - // } - // Err(TypesError::InvalidDenom(denom)) - // } - // pub fn from_cosmrs_decimal_and_denom( - // amount: CosmosDecimal, - // denom: String, - // ) -> Result { - // if denom.starts_with('u') || denom.starts_with('U') { - // return match Decimal::from_str(&amount.to_string()) { - // Ok(amount) => MajorCurrencyAmount::from_minor_decimal_and_denom(amount, &denom), - // Err(_e) => Err(TypesError::InvalidAmount(amount.to_string())), - // }; - // } - // - // if let Ok(denom) = CurrencyDenom::from_str(denom.as_str()) { - // return Ok(MajorCurrencyAmount { - // amount: MajorAmountString(amount.to_string()), - // denom, - // }); - // } - // Err(TypesError::InvalidDenom(denom)) - // } - // - // pub fn into_cosmos_coin(self) -> CosmosCoin { - // self.coin.into() - // } - // - // pub fn to_minor_uint128(&self) -> Result { - // if self.amount.0.contains('.') { - // // has a decimal point (Cosmos assumes "." is the decimal separator) - // let parts = self.amount.0.split('.'); - // let str = parts.collect_vec(); - // if str.is_empty() || str.len() > 2 { - // return Err(TypesError::InvalidAmount("Amount is invalid".to_string())); - // } - // if str.len() == 2 { - // // has a decimal, so check decimal places first - // if str[1].len() > 6 { - // return Err(TypesError::InvalidDenom( - // "Amount is invalid, only 6 decimal places of precision are allowed" - // .to_string(), - // )); - // } - // - // // so multiple whole part by 1e6 and add decimal part - // let whole_part = Uint128::from_str(str[0])? * Uint128::from(1_000_000u64); - // - // // TODO: has Rust got anything that deals with fixed point values, or parsing from format strings? Leading zeroes are causing issues - // return match format!("0.{}", str[1]).parse::() { - // Ok(decimal_part_float) => { - // // this makes an assumption that 6 decimal places of f64 can never lose precision - // let truncated = (decimal_part_float * 1_000_000.).trunc() as u32; - // let decimal_part = Uint128::from(truncated); - // let sum = whole_part + decimal_part; - // Ok(sum) - // } - // Err(_e) => Err(TypesError::InvalidAmount( - // "Amount decimal part is invalid".to_string(), - // )), - // }; - // } - // } - // - // let major = Uint128::from_str(&self.amount.0)?; - // let scaled = major * Uint128::new(1_000_000u128); - // Ok(scaled) - // } - // pub fn denom_to_string(&self) -> String { - // self.denom.to_string() - // } + pub fn new_scaled_up>( + base_amount: impl Into, + denom: S, + exponent: u32, + ) -> Result { + let base_amount = Decimal::from_atomics(base_amount, 0).unwrap(); + Ok(DecCoin { + denom: denom.into(), + amount: try_scale_up_decimal(base_amount, exponent)?, + }) + } + + pub fn new_scaled_down>( + base_amount: impl Into, + denom: S, + exponent: u32, + ) -> Result { + let base_amount = Decimal::from_atomics(base_amount, 0).unwrap(); + Ok(DecCoin { + denom: denom.into(), + amount: try_scale_down_decimal(base_amount, exponent)?, + }) + } + + pub fn try_scale_down_value(&self, exponent: u32) -> Result { + try_scale_down_decimal(self.amount, exponent) + } + + pub fn try_scale_up_value(&self, exponent: u32) -> Result { + try_scale_up_decimal(self.amount, exponent) + } } -impl Display for MajorCurrencyAmount { +// TODO: should thoese live here? +pub fn try_scale_down_decimal(dec: Decimal, exponent: u32) -> Result { + let rhs = 10u128 + .checked_pow(exponent) + .ok_or(TypesError::UnsupportedExponent(exponent))?; + let denominator = dec + .denominator() + .checked_mul(rhs.into()) + .map_err(|_| TypesError::UnsupportedExponent(exponent))?; + + Ok(Decimal::from_ratio(dec.numerator(), denominator)) +} + +pub fn try_scale_up_decimal(dec: Decimal, exponent: u32) -> Result { + let rhs = 10u128 + .checked_pow(exponent) + .ok_or(TypesError::UnsupportedExponent(exponent))?; + let denominator = dec + .denominator() + .checked_div(rhs.into()) + .map_err(|_| TypesError::UnsupportedExponent(exponent))?; + + Ok(Decimal::from_ratio(dec.numerator(), denominator)) +} + +pub fn try_convert_decimal_to_u128(dec: Decimal) -> Result { + let whole = dec.numerator() / dec.denominator(); + + // unwrap is fine as we're not dividing by zero here + let fractional = (dec.numerator()).checked_rem(dec.denominator()).unwrap(); + + // we cannot convert as we'd lose our decimal places + // (for example if somebody attempted to represent our gas price (WHICH YOU SHOULDN'T DO) as DecCoin) + if fractional != Uint128::zero() { + return Err(TypesError::LossyCoinConversion); + } + Ok(whole.u128()) +} + +impl Display for DecCoin { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{} {}", self.amount.0, self.denom) + write!(f, "{} {}", self.amount, self.denom) } } -// TODO: cleanup after merge -impl From for MajorCurrencyAmount { - fn from(c: CosmosCoin) -> Self { - MajorCurrencyAmount::from(Coin::from(c)) - } -} - -impl From for MajorCurrencyAmount { - fn from(c: CosmWasmCoin) -> Self { - MajorCurrencyAmount::from(Coin::from(c)) - } -} - -impl From for MajorCurrencyAmount { +impl From for DecCoin { fn from(coin: Coin) -> Self { - // current assumption: MajorCurrencyAmount is represented as decimal with 6 decimal points - // unwrap is fine as we haven't exceeded decimal range since our coins are at max 1B in value - // (this is a weak assumption, but for solving this merge conflict it's good enough temporary workaround) - let amount = Decimal::from_atomics(coin.amount, 6).unwrap(); - MajorCurrencyAmount { - amount: MajorAmountString(amount.to_string()), - denom: CurrencyDenom::parse(&coin.denom).expect("this will go away after the merge..."), - } + DecCoin::new_base(coin.amount, coin.denom) } } -// temporary... -impl From for CosmosCoin { - fn from(c: MajorCurrencyAmount) -> CosmosCoin { - let c: Coin = c.into(); - c.into() - } -} +// this conversion assumes same denomination +impl TryFrom for Coin { + type Error = TypesError; -impl From for CosmWasmCoin { - fn from(c: MajorCurrencyAmount) -> CosmWasmCoin { - let c: Coin = c.into(); - c.into() - } -} - -impl From for Coin { - fn from(c: MajorCurrencyAmount) -> Coin { - let decimal: Decimal = c - .amount - .0 - .parse() - .expect("stringified amount should have been a valid decimal"); - - // again, temporary - let exp = Uint128::new(1000000); - let val = decimal.mul(exp); - - // again, terrible assumption for denom, but it works temporarily... - Coin { - amount: val.u128(), - denom: format!("u{}", c.denom).to_lowercase(), - } - } -} - -impl Add for MajorCurrencyAmount { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - // again, temporary workaround to help with merge - (Coin::from(self).try_add(&Coin::from(rhs))) - .expect("provided coins had different denoms") - .into() + fn try_from(value: DecCoin) -> Result { + Ok(Coin { + amount: try_convert_decimal_to_u128(value.try_scale_down_value(0)?)?, + denom: value.denom, + }) } } #[cfg(test)] mod test { use super::*; - use cosmrs::Coin as CosmosCoin; - use cosmrs::Decimal as CosmosDecimal; - use cosmrs::Denom as CosmosDenom; - use cosmwasm_std::Coin as CosmWasmCoin; - use cosmwasm_std::Decimal as CosmWasmDecimal; - use serde_json::json; - use std::str::FromStr; + use std::convert::TryFrom; use std::string::ToString; #[test] - fn json_to_major_currency_amount() { - let nym = json!({ - "amount": "1", - "denom": "NYM" - }); - let nymt = json!({ - "amount": "1", - "denom": "NYMT" - }); + fn dec_value_scale_down() { + let dec = DecCoin { + denom: "foo".to_string(), + amount: "1234007000".parse().unwrap(), + }; - let test_nym_amount = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); - let test_nymt_amount = MajorCurrencyAmount::new("1", CurrencyDenom::Nymt); - - let nym_amount = serde_json::from_value::(nym).unwrap(); - let nymt_amount = serde_json::from_value::(nymt).unwrap(); - - assert_eq!(nym_amount, test_nym_amount); - assert_eq!(nymt_amount, test_nymt_amount); - } - - #[test] - fn minor_amount_json_to_major_currency_amount() { - let one_micro_nym = json!({ - "amount": "0.000001", - "denom": "NYM" - }); - - let expected_nym_amount = MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym); - let actual_nym_amount = - serde_json::from_value::(one_micro_nym).unwrap(); - - assert_eq!(expected_nym_amount, actual_nym_amount); - } - - #[test] - fn denom_from_str() { - assert_eq!(CurrencyDenom::from_str("nym").unwrap(), CurrencyDenom::Nym); assert_eq!( - CurrencyDenom::from_str("nymt").unwrap(), - CurrencyDenom::Nymt - ); - assert_eq!(CurrencyDenom::from_str("NYM").unwrap(), CurrencyDenom::Nym); - assert_eq!( - CurrencyDenom::from_str("NYMT").unwrap(), - CurrencyDenom::Nymt - ); - assert_eq!(CurrencyDenom::from_str("NyM").unwrap(), CurrencyDenom::Nym); - assert_eq!( - CurrencyDenom::from_str("NYmt").unwrap(), - CurrencyDenom::Nymt - ); - - assert!(matches!( - CurrencyDenom::from_str("foo").unwrap_err(), - strum::ParseError::VariantNotFound, - )); - - // denominations must all be major - assert!(matches!( - CurrencyDenom::from_str("unym").unwrap_err(), - strum::ParseError::VariantNotFound, - )); - assert!(matches!( - CurrencyDenom::from_str("unymt").unwrap_err(), - strum::ParseError::VariantNotFound, - )); - } - - #[test] - fn to_string() { - assert_eq!( - MajorCurrencyAmount::new("1", CurrencyDenom::Nym).to_string(), - "1 NYM" + "1234007000".parse::().unwrap(), + dec.try_scale_down_value(0).unwrap() ); assert_eq!( - MajorCurrencyAmount::new("1", CurrencyDenom::Nymt).to_string(), - "1 NYMT" + "123400700".parse::().unwrap(), + dec.try_scale_down_value(1).unwrap() ); assert_eq!( - MajorCurrencyAmount::new("1000000000000", CurrencyDenom::Nym).to_string(), - "1000000000000 NYM" + "12340070".parse::().unwrap(), + dec.try_scale_down_value(2).unwrap() + ); + assert_eq!( + "123400.7".parse::().unwrap(), + dec.try_scale_down_value(4).unwrap() + ); + + let dec = DecCoin { + denom: "foo".to_string(), + amount: "10000000000".parse().unwrap(), + }; + + assert_eq!( + "100".parse::().unwrap(), + dec.try_scale_down_value(8).unwrap() + ); + assert_eq!( + "1".parse::().unwrap(), + dec.try_scale_down_value(10).unwrap() + ); + assert_eq!( + "0.01".parse::().unwrap(), + dec.try_scale_down_value(12).unwrap() ); } #[test] - fn minor_coin_to_major_currency() { - let cosmos_coin = CosmosCoin { - amount: CosmosDecimal::from(1u64), - denom: CosmosDenom::from_str("unym").unwrap(), + fn dec_value_scale_up() { + let dec = DecCoin { + denom: "foo".to_string(), + amount: "1234.56".parse().unwrap(), }; - let c = MajorCurrencyAmount::from(cosmos_coin); - assert_eq!(c, MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym)); - } - #[test] - fn minor_cosmwasm_coin_to_major_currency() { - let coin = CosmWasmCoin { - amount: Uint128::from(1u64), - denom: "unym".to_string(), - }; - println!( - "from_atomics = {}", - CosmWasmDecimal::from_atomics(coin.amount, 6).unwrap() + assert_eq!( + "1234.56".parse::().unwrap(), + dec.try_scale_up_value(0).unwrap() ); - let c: MajorCurrencyAmount = coin.into(); - assert_eq!(c, MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym)); - } - - #[test] - fn minor_cosmwasm_coin_to_major_currency_2() { - let coin = CosmWasmCoin { - amount: Uint128::from(1_000_000u64), - denom: "unym".to_string(), - }; - println!( - "from_atomics = {:?}", - CosmWasmDecimal::from_atomics(coin.amount, 6) - .unwrap() - .to_string() + assert_eq!( + "12345.6".parse::().unwrap(), + dec.try_scale_up_value(1).unwrap() + ); + assert_eq!( + "123456".parse::().unwrap(), + dec.try_scale_up_value(2).unwrap() + ); + assert_eq!( + "1234560".parse::().unwrap(), + dec.try_scale_up_value(3).unwrap() + ); + assert_eq!( + "12345600".parse::().unwrap(), + dec.try_scale_up_value(4).unwrap() ); - let c: MajorCurrencyAmount = coin.into(); - assert_eq!(c, MajorCurrencyAmount::new("1", CurrencyDenom::Nym)); - } - #[test] - fn major_currency_to_minor_cosmos_coin() { - let expected_cosmos_coin = CosmosCoin { - amount: CosmosDecimal::from(1u64), - denom: CosmosDenom::from_str("unym").unwrap(), + let dec = DecCoin { + denom: "foo".to_string(), + amount: "0.00000123".parse().unwrap(), }; - let c = MajorCurrencyAmount::new("0.000001", CurrencyDenom::Nym); - let minor_cosmos_coin = c.into(); - assert_eq!(expected_cosmos_coin, minor_cosmos_coin); - assert_eq!("unym", minor_cosmos_coin.denom.to_string()); + + assert_eq!( + "0.0000123".parse::().unwrap(), + dec.try_scale_up_value(1).unwrap() + ); + assert_eq!( + "0.000123".parse::().unwrap(), + dec.try_scale_up_value(2).unwrap() + ); + assert_eq!( + "123".parse::().unwrap(), + dec.try_scale_up_value(8).unwrap() + ); + assert_eq!( + "1230".parse::().unwrap(), + dec.try_scale_up_value(9).unwrap() + ); + assert_eq!( + "12300".parse::().unwrap(), + dec.try_scale_up_value(10).unwrap() + ); } #[test] - fn major_currency_to_minor_cosmos_coin_2() { - let expected_cosmos_coin = CosmosCoin { - amount: CosmosDecimal::from(1000000u64), - denom: CosmosDenom::from_str("unym").unwrap(), + fn coin_to_dec_coin() { + let coin = Coin::new(123, "foo"); + let dec = DecCoin::from(coin.clone()); + assert_eq!(coin.denom, dec.denom); + assert_eq!(dec.amount, Decimal::from_atomics(coin.amount, 0).unwrap()); + } + + #[test] + fn dec_coin_to_coin() { + let dec = DecCoin { + denom: "foo".to_string(), + amount: "123".parse().unwrap(), }; - let c = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); - let minor_cosmos_coin = c.into(); - assert_eq!(expected_cosmos_coin, minor_cosmos_coin); - assert_eq!("unym", minor_cosmos_coin.denom.to_string()); + let coin = Coin::try_from(dec.clone()).unwrap(); + assert_eq!(dec.denom, coin.denom); + assert_eq!(coin.amount, 123u128); } #[test] - fn minor_cosmos_coin_to_major_currency_string() { - // check minor cosmos coin is converted to major value - let cosmos_coin = CosmosCoin { - amount: CosmosDecimal::from(1u64), - denom: CosmosDenom::from_str("unym").unwrap(), - }; - let c = MajorCurrencyAmount::from(cosmos_coin); - assert_eq!(c.to_string(), "0.000001 NYM"); + fn converting_to_display() { + let reg = RegisteredCoins::default_denoms(Network::MAINNET); + let values = vec![ + (1u128, "0.000001"), + (10u128, "0.00001"), + (100u128, "0.0001"), + (1000u128, "0.001"), + (10000u128, "0.01"), + (100000u128, "0.1"), + (1000000u128, "1"), + (1234567u128, "1.234567"), + (123456700u128, "123.4567"), + ]; + + for (raw, expected) in values { + let coin = Coin::new(raw, Network::MAINNET.mix_denom().base); + let display = reg.attempt_convert_to_display_dec_coin(coin).unwrap(); + assert_eq!(Network::MAINNET.mix_denom().display, display.denom); + assert_eq!(expected, display.amount.to_string()); + } } #[test] - fn denom_to_string() { - let c = MajorCurrencyAmount::new("1", CurrencyDenom::Nym); - let denom = c.denom.to_string(); - assert_eq!(denom, "NYM".to_string()); + fn converting_to_base() { + let reg = RegisteredCoins::default_denoms(Network::MAINNET); + let values = vec![ + (1u128, "0.000001"), + (10u128, "0.00001"), + (100u128, "0.0001"), + (1000u128, "0.001"), + (10000u128, "0.01"), + (100000u128, "0.1"), + (1000000u128, "1"), + (1234567u128, "1.234567"), + (123456700u128, "123.4567"), + ]; + + for (expected, raw_display) in values { + let coin = DecCoin { + denom: Network::MAINNET.mix_denom().display, + 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!(expected, base.amount); + } } } diff --git a/common/types/src/delegation.rs b/common/types/src/delegation.rs index d8302ff3b9..0f5c5c0c96 100644 --- a/common/types/src/delegation.rs +++ b/common/types/src/delegation.rs @@ -1,13 +1,10 @@ -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use log::error; +use crate::currency::{DecCoin, RegisteredCoins}; +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::currency::MajorCurrencyAmount; -use crate::error::TypesError; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( @@ -18,31 +15,22 @@ use crate::error::TypesError; pub struct Delegation { pub owner: String, pub node_identity: String, - pub amount: MajorCurrencyAmount, + pub amount: DecCoin, pub block_height: u64, pub proxy: Option, // proxy address used to delegate the funds on behalf of another address } -impl TryFrom for Delegation { - type Error = TypesError; - - fn try_from(value: MixnetContractDelegation) -> Result { - let MixnetContractDelegation { - owner, - node_identity, - amount, - block_height, - proxy, - } = value; - - let amount: MajorCurrencyAmount = amount.into(); - +impl Delegation { + pub fn from_mixnet_contract( + delegation: MixnetContractDelegation, + reg: &RegisteredCoins, + ) -> Result { Ok(Delegation { - owner: owner.into_string(), - node_identity, - amount, - block_height, - proxy: proxy.map(|p| p.into_string()), + owner: delegation.owner.to_string(), + node_identity: delegation.node_identity, + amount: reg.attempt_convert_to_display_dec_coin(delegation.amount.into())?, + block_height: delegation.block_height, + proxy: delegation.proxy.map(|d| d.to_string()), }) } } @@ -54,7 +42,7 @@ impl TryFrom for Delegation { )] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct DelegationRecord { - pub amount: MajorCurrencyAmount, + pub amount: DecCoin, pub block_height: u64, pub delegated_on_iso_datetime: String, pub uses_vesting_contract_tokens: bool, @@ -69,16 +57,16 @@ pub struct DelegationRecord { pub struct DelegationWithEverything { pub owner: String, pub node_identity: String, - pub amount: MajorCurrencyAmount, - pub total_delegation: Option, - pub pledge_amount: Option, + pub amount: DecCoin, + pub total_delegation: Option, + pub pledge_amount: Option, pub block_height: u64, pub delegated_on_iso_datetime: String, pub profit_margin_percent: Option, pub avg_uptime_percent: Option, pub stake_saturation: Option, pub uses_vesting_contract_tokens: bool, - pub accumulated_rewards: Option, + pub accumulated_rewards: Option, pub pending_events: Vec, pub history: Vec, } @@ -92,34 +80,7 @@ pub struct DelegationWithEverything { pub struct DelegationResult { source_address: String, target_address: String, - amount: Option, -} - -impl DelegationResult { - pub fn new( - source_address: &str, - target_address: &str, - amount: Option, - ) -> DelegationResult { - DelegationResult { - source_address: source_address.to_string(), - target_address: target_address.to_string(), - amount, - } - } -} - -impl TryFrom for DelegationResult { - type Error = TypesError; - - fn try_from(delegation: MixnetContractDelegation) -> Result { - let amount: MajorCurrencyAmount = delegation.amount.clone().into(); - Ok(DelegationResult { - source_address: delegation.owner().to_string(), - target_address: delegation.node_identity(), - amount: Some(amount), - }) - } + amount: Option, } #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] @@ -143,36 +104,34 @@ pub struct DelegationEvent { pub kind: DelegationEventKind, pub node_identity: String, pub address: String, - pub amount: Option, + pub amount: Option, pub block_height: u64, pub proxy: Option, } -impl TryFrom for DelegationEvent { - type Error = TypesError; - - fn try_from(event: ContractDelegationEvent) -> Result { - match event { - ContractDelegationEvent::Delegate(delegation) => { - let amount: MajorCurrencyAmount = delegation.amount.into(); - Ok(DelegationEvent { - kind: DelegationEventKind::Delegate, - block_height: delegation.block_height, - address: delegation.owner.into_string(), - node_identity: delegation.node_identity, - amount: Some(amount), - proxy: delegation.proxy.map(|p| p.into_string()), - }) - } - ContractDelegationEvent::Undelegate(pending_undelegate) => Ok(DelegationEvent { +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()), - }), - } + }, + }) } } @@ -200,30 +159,6 @@ impl From for PendingUndelegate { } } -pub fn from_contract_delegation_events( - events: Vec, -) -> Result, TypesError> { - let (events, errors): (Vec<_>, Vec<_>) = events - .into_iter() - .map(|delegation_event| delegation_event.try_into()) - .partition(Result::is_ok); - - if errors.is_empty() { - let events = events - .into_iter() - .filter_map(|e| e.ok()) - .collect::>(); - return Ok(events); - } - let errors = errors - .into_iter() - .filter_map(|e| e.err()) - .collect::>(); - - error!("Failed to convert delegations: {:?}", errors); - Err(TypesError::DelegationsInvalid) -} - #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", @@ -232,6 +167,6 @@ pub fn from_contract_delegation_events( #[derive(Deserialize, Serialize)] pub struct DelegationsSummaryResponse { pub delegations: Vec, - pub total_delegations: MajorCurrencyAmount, - pub total_rewards: MajorCurrencyAmount, + pub total_delegations: DecCoin, + pub total_rewards: DecCoin, } diff --git a/common/types/src/error.rs b/common/types/src/error.rs index 1bc8323e4a..20c3736152 100644 --- a/common/types/src/error.rs +++ b/common/types/src/error.rs @@ -4,6 +4,7 @@ use thiserror::Error; use validator_client::validator_api::error::ValidatorAPIError; use validator_client::{nymd::error::NymdError, ValidatorClientError}; +// TODO: ask @MS why this even exists #[derive(Error, Debug)] pub enum TypesError { #[error("{source}")] @@ -63,6 +64,12 @@ pub enum TypesError { InvalidGatewayBond(), #[error("Invalid delegations")] DelegationsInvalid, + #[error("Attempted to use too huge currency exponent ({0})")] + UnsupportedExponent(u32), + #[error("Attempted to convert coin that would have resulted in loss of precision")] + LossyCoinConversion, + #[error("The provided coin has an unknown denomination - {0}")] + UnknownCoinDenom(String), } impl Serialize for TypesError { diff --git a/common/types/src/fees.rs b/common/types/src/fees.rs index 95045fcc70..747693ce9c 100644 --- a/common/types/src/fees.rs +++ b/common/types/src/fees.rs @@ -1,4 +1,4 @@ -use crate::currency::MajorCurrencyAmount; +use crate::currency::DecCoin; use serde::{Deserialize, Serialize}; use validator_client::nymd::Fee; @@ -8,10 +8,16 @@ use ts_rs::{Dependency, TS}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct FeeDetails { // expected to be used by the wallet in order to display detailed fee information to the user - pub amount: Option, + pub amount: Option, pub fee: Fee, } +impl FeeDetails { + pub fn new(amount: Option, fee: Fee) -> Self { + FeeDetails { amount, fee } + } +} + #[cfg(feature = "generate-ts")] impl TS for FeeDetails { const EXPORT_TO: Option<&'static str> = Some("ts-packages/types/src/types/rust/FeeDetails.ts"); @@ -25,13 +31,12 @@ impl TS for FeeDetails { } fn inline() -> String { - "{ amount: MajorCurrencyAmount | null, fee: Fee }".into() + "{ amount: DecCoin | null, fee: Fee }".into() } fn dependencies() -> Vec { vec![ - Dependency::from_ty::() - .expect("TS was incorrectly defined on `CurrencyDenom`"), + Dependency::from_ty::().expect("TS was incorrectly defined on `DecCoin`"), Dependency::from_ty::() .expect("TS was incorrectly defined on `ts_type_helpers::Fee`"), ] diff --git a/common/types/src/gas.rs b/common/types/src/gas.rs index 02a2807a7e..9441008569 100644 --- a/common/types/src/gas.rs +++ b/common/types/src/gas.rs @@ -1,48 +1,29 @@ -use crate::currency::MajorCurrencyAmount; -use crate::error::TypesError; use cosmrs::tx::Gas as CosmrsGas; use serde::{Deserialize, Serialize}; use validator_client::nymd::cosmwasm_client::types::GasInfo as ValidatorClientGasInfo; -use validator_client::nymd::GasPrice; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", ts(export_to = "ts-packages/types/src/types/rust/Gas.ts") )] -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Copy, Clone, Debug)] pub struct Gas { /// units of gas used pub gas_units: u64, - // - // /// gas units converted to fee as major coin amount - // pub amount: MajorCurrencyAmount, } impl Gas { - pub fn from_cosmrs_gas(value: CosmrsGas, _denom_minor: &str) -> Result { - Ok(Gas { - gas_units: value.value(), - }) - - // // TODO: use simulator struct to do conversion to fee - // let value_u128 = Uint128::from(value.value()); - // let amount = Decimal::new(value_u128) * Decimal::from_str("0.0025")?; - // Ok(Gas { - // gas_units: value.value(), - // amount: MajorCurrencyAmount::from_minor_decimal_and_denom(amount, denom_minor)?, - // }) + pub fn from_u64(value: u64) -> Gas { + Gas { gas_units: value } } - pub fn from_u64(value: u64, _denom_minor: &str) -> Result { - Ok(Gas { gas_units: value }) - // todo!() - // // TODO: use simulator struct to do conversion to fee - // let value_u128 = Uint128::from(value); - // let amount = Decimal::new(value_u128) * Decimal::from_str("0.0025")?; - // Ok(Gas { - // gas_units: value, - // amount: MajorCurrencyAmount::from_minor_decimal_and_denom(amount, denom_minor)?, - // }) +} + +impl From for Gas { + fn from(gas: CosmrsGas) -> Self { + Gas { + gas_units: gas.value(), + } } } @@ -51,44 +32,29 @@ impl Gas { feature = "generate-ts", ts(export_to = "ts-packages/types/src/types/rust/GasInfo.ts") )] -#[derive(Deserialize, Serialize, Debug)] +#[derive(Deserialize, Serialize, Copy, Clone, Debug)] pub struct GasInfo { /// GasWanted is the maximum units of work we allow this tx to perform. - pub gas_wanted: u64, + pub gas_wanted: Gas, /// GasUsed is the amount of gas actually consumed. - pub gas_used: u64, + pub gas_used: Gas, +} - /// gas units converted to fee as major coin amount - pub fee: MajorCurrencyAmount, +impl From for GasInfo { + fn from(info: ValidatorClientGasInfo) -> Self { + GasInfo { + gas_wanted: info.gas_wanted.into(), + gas_used: info.gas_used.into(), + } + } } impl GasInfo { - pub fn from_validator_client_gas_info( - value: ValidatorClientGasInfo, - denom_minor: &str, - ) -> Result { - // terrible workaround, but I don't want to break the current flow (just yet) - let gas_price = GasPrice::new_with_default_price(denom_minor)?; - let fee = (&gas_price) * value.gas_used; - Ok(GasInfo { - gas_wanted: value.gas_wanted.value(), - gas_used: value.gas_used.value(), - fee: fee.into(), - }) - } - pub fn from_u64( - gas_wanted: u64, - gas_used: u64, - denom_minor: &str, - ) -> Result { - // terrible workaround, but I don't want to break the current flow (just yet) - let gas_price = GasPrice::new_with_default_price(denom_minor)?; - let fee = (&gas_price) * CosmrsGas::from(gas_used); - Ok(GasInfo { - gas_wanted, - gas_used, - fee: fee.into(), - }) + pub fn from_u64(gas_wanted: u64, gas_used: u64) -> GasInfo { + GasInfo { + gas_wanted: Gas::from_u64(gas_wanted), + gas_used: Gas::from_u64(gas_used), + } } } diff --git a/common/types/src/gateway.rs b/common/types/src/gateway.rs index 577c738378..8895a69761 100644 --- a/common/types/src/gateway.rs +++ b/common/types/src/gateway.rs @@ -1,4 +1,4 @@ -use crate::currency::MajorCurrencyAmount; +use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; use mixnet_contract_common::{ Gateway as MixnetContractGateway, GatewayBond as MixnetContractGatewayBond, @@ -54,7 +54,7 @@ impl From for Gateway { )] #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] pub struct GatewayBond { - pub pledge_amount: MajorCurrencyAmount, + pub pledge_amount: DecCoin, pub owner: String, pub block_height: u64, pub gateway: Gateway, @@ -63,38 +63,15 @@ pub struct GatewayBond { impl GatewayBond { pub fn from_mixnet_contract_gateway_bond( - bond: Option, - ) -> Result, TypesError> { - match bond { - Some(bond) => { - let bond: GatewayBond = bond.try_into()?; - Ok(Some(bond)) - } - None => Ok(None), - } - } -} - -impl TryFrom for GatewayBond { - type Error = TypesError; - - fn try_from(value: MixnetContractGatewayBond) -> Result { - let MixnetContractGatewayBond { - pledge_amount, - owner, - block_height, - gateway, - proxy, - } = value; - - let pledge_amount: MajorCurrencyAmount = pledge_amount.into(); - + bond: MixnetContractGatewayBond, + reg: &RegisteredCoins, + ) -> Result { Ok(GatewayBond { - pledge_amount, - owner: owner.into_string(), - block_height, - gateway: gateway.into(), - proxy: proxy.map(|p| p.into_string()), + pledge_amount: reg.attempt_convert_to_display_dec_coin(bond.pledge_amount.into())?, + owner: bond.owner.to_string(), + block_height: bond.block_height, + gateway: bond.gateway.into(), + proxy: bond.proxy.map(|p| p.into_string()), }) } } diff --git a/common/types/src/mixnode.rs b/common/types/src/mixnode.rs index 402062dbb9..d1a302d489 100644 --- a/common/types/src/mixnode.rs +++ b/common/types/src/mixnode.rs @@ -1,11 +1,11 @@ -use crate::currency::MajorCurrencyAmount; +use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; use mixnet_contract_common::{ - Coin as CosmWasmCoin, MixNode as MixnetContractMixNode, - MixNodeBond as MixnetContractMixNodeBond, + MixNode as MixnetContractMixNode, MixNodeBond as MixnetContractMixNodeBond, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use validator_client::nymd::Coin; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( @@ -58,67 +58,39 @@ impl From for MixNode { )] #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] pub struct MixNodeBond { - pub pledge_amount: MajorCurrencyAmount, - pub total_delegation: MajorCurrencyAmount, + pub pledge_amount: DecCoin, + pub total_delegation: DecCoin, pub owner: String, pub layer: String, pub block_height: u64, pub mix_node: MixNode, pub proxy: Option, - pub accumulated_rewards: Option, + pub accumulated_rewards: Option, } impl MixNodeBond { pub fn from_mixnet_contract_mixnode_bond( - bond: Option, - ) -> Result, TypesError> { - match bond { - Some(bond) => { - let bond: MixNodeBond = bond.try_into()?; - Ok(Some(bond)) - } - None => Ok(None), - } - } -} - -impl TryFrom for MixNodeBond { - type Error = TypesError; - - fn try_from(value: MixnetContractMixNodeBond) -> Result { - let MixnetContractMixNodeBond { - pledge_amount, - total_delegation, - owner, - layer, - block_height, - mix_node, - proxy, - accumulated_rewards, - } = value; - - if pledge_amount.denom != total_delegation.denom { - return Err(TypesError::InvalidDenom( - "The pledge and delegation denominations do not match".to_string(), - )); - } - - let denom = total_delegation.denom.clone(); - - let pledge_amount: MajorCurrencyAmount = pledge_amount.into(); - let total_delegation: MajorCurrencyAmount = total_delegation.into(); - let accumulated_rewards: Option = - accumulated_rewards.map(|r| CosmWasmCoin::new(r.u128(), denom).into()); - + bond: MixnetContractMixNodeBond, + reg: &RegisteredCoins, + ) -> Result { + let denom = bond.pledge_amount.denom.clone(); Ok(MixNodeBond { - pledge_amount, - total_delegation, - owner: owner.into_string(), - layer: layer.into(), - block_height, - mix_node: mix_node.into(), - proxy: proxy.map(|p| p.into_string()), - accumulated_rewards, + 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())?, + owner: bond.owner.into_string(), + 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()?, }) } } diff --git a/common/types/src/transaction.rs b/common/types/src/transaction.rs index 58ee9b1cca..b5dd50a4ae 100644 --- a/common/types/src/transaction.rs +++ b/common/types/src/transaction.rs @@ -1,6 +1,6 @@ -use crate::currency::MajorCurrencyAmount; +use crate::currency::DecCoin; use crate::error::TypesError; -use crate::gas::GasInfo; +use crate::gas::{Gas, GasInfo}; use serde::{Deserialize, Serialize}; use validator_client::nymd::cosmwasm_client::types::ExecuteResult; use validator_client::nymd::TxResponse; @@ -15,31 +15,23 @@ pub struct SendTxResult { pub block_height: u64, pub code: u32, pub details: TransactionDetails, - pub gas_used: u64, - pub gas_wanted: u64, + pub gas_used: Gas, + pub gas_wanted: Gas, pub tx_hash: String, - // pub fee: MajorCurrencyAmount, + pub fee: Option, } impl SendTxResult { - pub fn new( - t: TxResponse, - details: TransactionDetails, - _denom_minor: &str, - ) -> Result { - Ok(SendTxResult { + pub fn new(t: TxResponse, details: TransactionDetails, fee: Option) -> SendTxResult { + SendTxResult { block_height: t.height.value(), code: t.tx_result.code.value(), details, - gas_used: t.tx_result.gas_used.value(), - gas_wanted: t.tx_result.gas_wanted.value(), + gas_used: t.tx_result.gas_used.into(), + gas_wanted: t.tx_result.gas_wanted.into(), tx_hash: t.hash.to_string(), - // that is completely wrong: fee is what you told the validator to use beforehand - // fee: MajorCurrencyAmount::from_decimal_and_denom( - // Decimal::new(Uint128::from(t.tx_result.gas_used.value())), - // denom_minor.to_string(), - // )?, - }) + fee, + } } } @@ -50,11 +42,21 @@ impl SendTxResult { )] #[derive(Deserialize, Serialize, Debug)] pub struct TransactionDetails { - pub amount: MajorCurrencyAmount, + pub amount: DecCoin, pub from_address: String, pub to_address: String, } +impl TransactionDetails { + pub fn new(amount: DecCoin, from_address: String, to_address: String) -> Self { + TransactionDetails { + amount, + from_address, + to_address, + } + } +} + #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", @@ -66,18 +68,16 @@ pub struct TransactionExecuteResult { pub data_json: String, pub transaction_hash: String, pub gas_info: GasInfo, - pub fee: MajorCurrencyAmount, + pub fee: Option, } impl TransactionExecuteResult { pub fn from_execute_result( value: ExecuteResult, - denom_minor: &str, + fee: Option, ) -> Result { - let gas_info = GasInfo::from_validator_client_gas_info(value.gas_info, denom_minor)?; - let fee = gas_info.fee.clone(); Ok(TransactionExecuteResult { - gas_info, + gas_info: value.gas_info.into(), transaction_hash: value.transaction_hash.to_string(), data_json: ::serde_json::to_string_pretty(&value.data)?, logs_json: ::serde_json::to_string_pretty(&value.logs)?, @@ -97,30 +97,24 @@ pub struct RpcTransactionResponse { pub tx_result_json: String, pub block_height: u64, pub transaction_hash: String, - pub gas_info: GasInfo, - // pub fee: MajorCurrencyAmount, + pub gas_used: Gas, + pub gas_wanted: Gas, + pub fee: Option, } impl RpcTransactionResponse { pub fn from_tx_response( - value: &TxResponse, - denom_minor: &str, + t: &TxResponse, + fee: Option, ) -> Result { Ok(RpcTransactionResponse { - index: value.index, - gas_info: GasInfo::from_u64( - value.tx_result.gas_wanted.value(), - value.tx_result.gas_used.value(), - denom_minor, - )?, - transaction_hash: value.hash.to_string(), - tx_result_json: ::serde_json::to_string_pretty(&value.tx_result)?, - block_height: value.height.value(), - // wrong - // fee: MajorCurrencyAmount::from_decimal_and_denom( - // Decimal::new(Uint128::from(value.tx_result.gas_used.value())), - // denom_minor.to_string(), - // )?, + index: t.index, + gas_used: t.tx_result.gas_used.into(), + gas_wanted: t.tx_result.gas_wanted.into(), + transaction_hash: t.hash.to_string(), + tx_result_json: ::serde_json::to_string_pretty(&t.tx_result)?, + block_height: t.height.value(), + fee, }) } } diff --git a/common/types/src/vesting.rs b/common/types/src/vesting.rs index c20b0845e0..0c6b6a4c20 100644 --- a/common/types/src/vesting.rs +++ b/common/types/src/vesting.rs @@ -1,10 +1,10 @@ -use crate::currency::MajorCurrencyAmount; +use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; use serde::{Deserialize, Serialize}; -use vesting_contract::vesting::Account as VestingAccount; -use vesting_contract::vesting::VestingPeriod as VestingVestingPeriod; -use vesting_contract_common::OriginalVestingResponse as VestingOriginalVestingResponse; -use vesting_contract_common::PledgeData as VestingPledgeData; +use vesting_contract::vesting::Account as ContractVestingAccount; +use vesting_contract::vesting::VestingPeriod as ContractVestingPeriod; +use vesting_contract_common::OriginalVestingResponse as ContractOriginalVestingResponse; +use vesting_contract_common::PledgeData as ContractPledgeData; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( @@ -13,25 +13,19 @@ use vesting_contract_common::PledgeData as VestingPledgeData; )] #[derive(Serialize, Deserialize, Debug)] pub struct PledgeData { - pub amount: MajorCurrencyAmount, + pub amount: DecCoin, pub block_time: u64, } -impl TryFrom for PledgeData { - type Error = TypesError; - - fn try_from(data: VestingPledgeData) -> Result { - let amount: MajorCurrencyAmount = data.amount().into(); - Ok(Self { - amount, - block_time: data.block_time().seconds(), - }) - } -} - impl PledgeData { - pub fn and_then(data: VestingPledgeData) -> Option { - data.try_into().ok() + pub fn from_vesting_contract( + pledge: ContractPledgeData, + reg: &RegisteredCoins, + ) -> Result { + Ok(PledgeData { + amount: reg.attempt_convert_to_display_dec_coin(pledge.amount.into())?, + block_time: pledge.block_time.seconds(), + }) } } @@ -42,20 +36,20 @@ impl PledgeData { )] #[derive(Serialize, Deserialize, Debug)] pub struct OriginalVestingResponse { - amount: MajorCurrencyAmount, + amount: DecCoin, number_of_periods: usize, period_duration: u64, } -impl TryFrom for OriginalVestingResponse { - type Error = TypesError; - - fn try_from(data: VestingOriginalVestingResponse) -> Result { - let amount = data.amount().into(); - Ok(Self { - amount, - number_of_periods: data.number_of_periods(), - period_duration: data.period_duration(), +impl OriginalVestingResponse { + pub fn from_vesting_contract( + res: ContractOriginalVestingResponse, + reg: &RegisteredCoins, + ) -> Result { + Ok(OriginalVestingResponse { + amount: reg.attempt_convert_to_display_dec_coin(res.amount.into())?, + number_of_periods: res.number_of_periods, + period_duration: res.period_duration, }) } } @@ -71,24 +65,20 @@ pub struct VestingAccountInfo { staking_address: Option, start_time: u64, periods: Vec, - amount: MajorCurrencyAmount, + amount: DecCoin, } -impl TryFrom for VestingAccountInfo { - type Error = TypesError; - - fn try_from(account: VestingAccount) -> Result { - let mut periods = Vec::new(); - for period in account.periods() { - periods.push(period.into()); - } - let amount: MajorCurrencyAmount = account.coin().into(); - Ok(Self { +impl VestingAccountInfo { + pub fn from_vesting_contract( + account: ContractVestingAccount, + reg: &RegisteredCoins, + ) -> Result { + Ok(VestingAccountInfo { owner_address: account.owner_address().to_string(), staking_address: account.staking_address().map(|a| a.to_string()), start_time: account.start_time().seconds(), - periods, - amount, + periods: account.periods().into_iter().map(Into::into).collect(), + amount: reg.attempt_convert_to_display_dec_coin(account.coin.into())?, }) } } @@ -104,8 +94,8 @@ pub struct VestingPeriod { period_seconds: u64, } -impl From for VestingPeriod { - fn from(period: VestingVestingPeriod) -> Self { +impl From for VestingPeriod { + fn from(period: ContractVestingPeriod) -> Self { Self { start_time: period.start_time, period_seconds: period.period_seconds, diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md new file mode 100644 index 0000000000..bed698076b --- /dev/null +++ b/contracts/CHANGELOG.md @@ -0,0 +1,32 @@ +## [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 406c2b6814..e0676f0832 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -50,7 +50,6 @@ name = "bandwidth-claim" version = "1.0.0" dependencies = [ "bandwidth-claim-contract", - "config", "cosmwasm-std", "cosmwasm-storage", "schemars", @@ -221,12 +220,11 @@ version = "0.1.0" dependencies = [ "bandwidth-claim-contract", "coconut-bandwidth-contract-common", - "config", "cosmwasm-std", "cosmwasm-storage", "cw-controllers", - "cw-multi-test", "cw-storage-plus", + "multisig-contract-common", "schemars", "serde", "thiserror", @@ -237,10 +235,32 @@ name = "coconut-bandwidth-contract-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "multisig-contract-common", "schemars", "serde", ] +[[package]] +name = "coconut-test" +version = "0.1.0" +dependencies = [ + "bandwidth-claim-contract", + "coconut-bandwidth", + "coconut-bandwidth-contract-common", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-multi-test", + "cw-storage-plus", + "cw-utils", + "cw3-flex-multisig", + "cw4-group", + "multisig-contract-common", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "config" version = "0.1.0" @@ -592,6 +612,12 @@ 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" @@ -1056,7 +1082,6 @@ dependencies = [ "cosmwasm-std", "fixed", "log", - "network-defaults", "schemars", "serde", "serde_repr", @@ -1082,6 +1107,7 @@ name = "network-defaults" version = "0.1.0" dependencies = [ "cfg-if", + "dotenv", "hex-literal", "once_cell", "serde", @@ -1366,18 +1392,18 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.4" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "rfc6979" @@ -1828,7 +1854,6 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.0.1" dependencies = [ - "config", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 6d5188e04e..ccf7857d21 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"] +members = ["bandwidth-claim", "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 index 5d530a5452..b6491dda88 100644 --- a/contracts/bandwidth-claim/Cargo.toml +++ b/contracts/bandwidth-claim/Cargo.toml @@ -9,8 +9,6 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dev-dependencies] -config = { path = "../../common/config"} - [dependencies] bandwidth-claim-contract = { path = "../../common/bandwidth-claim-contract" } diff --git a/contracts/bandwidth-claim/src/lib.rs b/contracts/bandwidth-claim/src/lib.rs index 9dd88197e7..91deceaefa 100644 --- a/contracts/bandwidth-claim/src/lib.rs +++ b/contracts/bandwidth-claim/src/lib.rs @@ -62,10 +62,11 @@ pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult}; +use cosmwasm_std::{ + entry_point, to_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, +}; use coconut_bandwidth_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; use crate::error::ContractError; +use crate::queries::{query_all_spent_credentials_paged, query_spent_credential}; use crate::state::{Config, ADMIN, CONFIG}; use crate::transactions; @@ -22,12 +25,14 @@ pub fn instantiate( ) -> Result { let multisig_addr = deps.api.addr_validate(&msg.multisig_addr)?; let pool_addr = deps.api.addr_validate(&msg.pool_addr)?; + let mix_denom = msg.mix_denom; ADMIN.set(deps.branch(), Some(multisig_addr.clone()))?; let cfg = Config { multisig_addr, pool_addr, + mix_denom, }; CONFIG.save(deps.storage, &cfg)?; @@ -44,13 +49,23 @@ pub fn execute( ) -> Result { match msg { ExecuteMsg::DepositFunds { data } => transactions::deposit_funds(deps, env, info, data), + ExecuteMsg::SpendCredential { data } => { + transactions::spend_credential(deps, env, info, data) + } ExecuteMsg::ReleaseFunds { funds } => transactions::release_funds(deps, env, info, funds), } } #[entry_point] -pub fn query(_deps: Deps, _env: Env, _msg: QueryMsg) -> StdResult { - unimplemented!(); +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::GetAllSpentCredentials { limit, start_after } => to_binary( + &query_all_spent_credentials_paged(deps, start_after, limit)?, + ), + QueryMsg::GetSpentCredential { + blinded_serial_number, + } => to_binary(&query_spent_credential(deps, blinded_serial_number)?), + } } #[entry_point] @@ -61,12 +76,10 @@ pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result +// SPDX-License-Identifier: Apache-2.0 + +use coconut_bandwidth_contract_common::spend_credential::{ + PagedSpendCredentialResponse, SpendCredential, SpendCredentialResponse, +}; +use cosmwasm_std::{Deps, Order, StdResult}; +use cw_storage_plus::Bound; + +use crate::storage::{self, SPEND_CREDENTIAL_PAGE_DEFAULT_LIMIT, SPEND_CREDENTIAL_PAGE_MAX_LIMIT}; + +pub(crate) fn query_all_spent_credentials_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(SPEND_CREDENTIAL_PAGE_DEFAULT_LIMIT) + .min(SPEND_CREDENTIAL_PAGE_MAX_LIMIT) as usize; + + let start = start_after.as_deref().map(Bound::exclusive); + + let nodes = storage::spent_credentials() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + let start_next_after = nodes + .last() + .map(|spend_credential| spend_credential.blinded_serial_number().to_string()); + + Ok(PagedSpendCredentialResponse::new( + nodes, + limit, + start_next_after, + )) +} + +pub(crate) fn query_spent_credential( + deps: Deps<'_>, + blinded_serial_number: String, +) -> StdResult { + let spend_credential = + storage::spent_credentials().may_load(deps.storage, &blinded_serial_number)?; + Ok(SpendCredentialResponse::new(spend_credential)) +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::support::tests::fixtures::spend_credential_data_fixture; + use crate::support::tests::helpers::init_contract; + use crate::transactions::spend_credential; + use cosmwasm_std::testing::{mock_env, mock_info}; + + #[test] + fn spent_credentials_empty_on_init() { + let deps = init_contract(); + let response = + query_all_spent_credentials_paged(deps.as_ref(), None, Option::from(2)).unwrap(); + assert_eq!(0, response.spend_credentials.len()); + } + + #[test] + fn spent_credentials_paged_retrieval_obeys_limits() { + let mut deps = init_contract(); + let env = mock_env(); + let info = mock_info("requester", &[]); + let limit = 2; + for n in 0..1000 { + let data = spend_credential_data_fixture(&format!("blinded_serial_number{}", n)); + spend_credential(deps.as_mut(), env.clone(), info.clone(), data).unwrap(); + } + + let page1 = + query_all_spent_credentials_paged(deps.as_ref(), None, Option::from(limit)).unwrap(); + assert_eq!(limit, page1.spend_credentials.len() as u32); + } + + #[test] + fn spent_credentials_paged_retrieval_has_default_limit() { + let mut deps = init_contract(); + let env = mock_env(); + let info = mock_info("requester", &[]); + for n in 0..1000 { + let data = spend_credential_data_fixture(&format!("blinded_serial_number{}", n)); + spend_credential(deps.as_mut(), env.clone(), info.clone(), data).unwrap(); + } + + // query without explicitly setting a limit + let page1 = query_all_spent_credentials_paged(deps.as_ref(), None, None).unwrap(); + + assert_eq!( + SPEND_CREDENTIAL_PAGE_DEFAULT_LIMIT, + page1.spend_credentials.len() as u32 + ); + } + + #[test] + fn spent_credentials_paged_retrieval_has_max_limit() { + let mut deps = init_contract(); + let env = mock_env(); + let info = mock_info("requester", &[]); + for n in 0..1000 { + let data = spend_credential_data_fixture(&format!("blinded_serial_number{}", n)); + spend_credential(deps.as_mut(), env.clone(), info.clone(), data).unwrap(); + } + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000 * SPEND_CREDENTIAL_PAGE_MAX_LIMIT; + let page1 = + query_all_spent_credentials_paged(deps.as_ref(), None, Option::from(crazy_limit)) + .unwrap(); + + // we default to a decent sized upper bound instead + let expected_limit = SPEND_CREDENTIAL_PAGE_MAX_LIMIT; + assert_eq!(expected_limit, page1.spend_credentials.len() as u32); + } + + #[test] + fn spent_credentials_pagination_works() { + let mut deps = init_contract(); + let env = mock_env(); + let info = mock_info("requester", &[]); + + let data = spend_credential_data_fixture("blinded_serial_number1"); + spend_credential(deps.as_mut(), env.clone(), info.clone(), data).unwrap(); + + let per_page = 2; + let page1 = + query_all_spent_credentials_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.spend_credentials.len()); + + // save another + let data = spend_credential_data_fixture("blinded_serial_number2"); + spend_credential(deps.as_mut(), env.clone(), info.clone(), data).unwrap(); + + // page1 should have 2 results on it + let page1 = + query_all_spent_credentials_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.spend_credentials.len()); + + let data = spend_credential_data_fixture("blinded_serial_number3"); + spend_credential(deps.as_mut(), env.clone(), info.clone(), data).unwrap(); + + // page1 still has 2 results + let page1 = + query_all_spent_credentials_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + assert_eq!(2, page1.spend_credentials.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_all_spent_credentials_paged( + deps.as_ref(), + Option::from(start_after.clone()), + Option::from(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.spend_credentials.len()); + + let data = spend_credential_data_fixture("blinded_serial_number4"); + spend_credential(deps.as_mut(), env, info, data).unwrap(); + + let page2 = query_all_spent_credentials_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.spend_credentials.len()); + } +} diff --git a/contracts/coconut-bandwidth/src/state.rs b/contracts/coconut-bandwidth/src/state.rs index 3ce43ebe36..74b969341c 100644 --- a/contracts/coconut-bandwidth/src/state.rs +++ b/contracts/coconut-bandwidth/src/state.rs @@ -13,6 +13,7 @@ pub const ADMIN: Admin = Admin::new("admin"); pub struct Config { pub multisig_addr: Addr, pub pool_addr: Addr, + pub mix_denom: String, } pub const CONFIG: Item = Item::new("config"); diff --git a/contracts/coconut-bandwidth/src/storage.rs b/contracts/coconut-bandwidth/src/storage.rs new file mode 100644 index 0000000000..40e33b9f8d --- /dev/null +++ b/contracts/coconut-bandwidth/src/storage.rs @@ -0,0 +1,106 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use coconut_bandwidth_contract_common::spend_credential::SpendCredential; +use cw_storage_plus::{Index, IndexList, IndexedMap, UniqueIndex}; + +// storage prefixes +const SPEND_CREDENTIAL_PK_NAMESPACE: &str = "sc"; +const SPEND_CREDENTIAL_BLINDED_SERIAL_NO_IDX_NAMESPACE: &str = "scn"; + +// paged retrieval limits for all queries and transactions +pub(crate) const SPEND_CREDENTIAL_PAGE_MAX_LIMIT: u32 = 75; +pub(crate) const SPEND_CREDENTIAL_PAGE_DEFAULT_LIMIT: u32 = 50; + +pub(crate) struct SpendCredentialIndex<'a> { + pub(crate) blinded_serial_number: UniqueIndex<'a, String, SpendCredential>, +} + +// 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 SpendCredentialIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.blinded_serial_number]; + Box::new(v.into_iter()) + } +} + +// gateways() is the storage access function. +pub(crate) fn spent_credentials<'a>( +) -> IndexedMap<'a, &'a str, SpendCredential, SpendCredentialIndex<'a>> { + let indexes = SpendCredentialIndex { + blinded_serial_number: UniqueIndex::new( + |d| d.blinded_serial_number().to_string(), + SPEND_CREDENTIAL_BLINDED_SERIAL_NO_IDX_NAMESPACE, + ), + }; + IndexedMap::new(SPEND_CREDENTIAL_PK_NAMESPACE, indexes) +} + +// currently not used outside tests +#[cfg(test)] +mod tests { + use super::super::storage; + use crate::storage::SpendCredential; + use crate::support::tests::fixtures; + use crate::support::tests::fixtures::TEST_MIX_DENOM; + use coconut_bandwidth_contract_common::spend_credential::SpendCredentialStatus; + use cosmwasm_std::testing::MockStorage; + use cosmwasm_std::Addr; + use cosmwasm_std::Coin; + + #[test] + fn spend_credential_single_read_retrieval() { + let mut storage = MockStorage::new(); + let blind_serial_number1 = "number1"; + let blind_serial_number2 = "number2"; + let spend1 = fixtures::spend_credential_fixture(blind_serial_number1); + let spend2 = fixtures::spend_credential_fixture(blind_serial_number2); + storage::spent_credentials() + .save(&mut storage, blind_serial_number1, &spend1) + .unwrap(); + storage::spent_credentials() + .save(&mut storage, blind_serial_number2, &spend2) + .unwrap(); + + let res1 = storage::spent_credentials() + .load(&storage, blind_serial_number1) + .unwrap(); + let res2 = storage::spent_credentials() + .load(&storage, blind_serial_number2) + .unwrap(); + assert_eq!(spend1, res1); + assert_eq!(spend2, res2); + } + + #[test] + fn mark_as_spent_credential() { + let mut mock_storage = MockStorage::new(); + let funds = Coin::new(100, TEST_MIX_DENOM); + let blind_serial_number = "blind_serial_number"; + let gateway_cosmos_address: Addr = Addr::unchecked("gateway_cosmos_address"); + + let res = storage::spent_credentials() + .may_load(&mock_storage, blind_serial_number) + .unwrap(); + assert!(res.is_none()); + + let mut spend_credential = SpendCredential::new( + funds.clone(), + blind_serial_number.to_string(), + gateway_cosmos_address.clone(), + ); + spend_credential.mark_as_spent(); + + storage::spent_credentials() + .save(&mut mock_storage, blind_serial_number, &spend_credential) + .unwrap(); + + let ret = storage::spent_credentials() + .load(&mock_storage, blind_serial_number) + .unwrap(); + + assert_eq!(ret, spend_credential); + assert_eq!(ret.status(), SpendCredentialStatus::Spent); + } +} diff --git a/contracts/coconut-bandwidth/src/support/mod.rs b/contracts/coconut-bandwidth/src/support/mod.rs index 3e1ec563d5..2f17cb1c46 100644 --- a/contracts/coconut-bandwidth/src/support/mod.rs +++ b/contracts/coconut-bandwidth/src/support/mod.rs @@ -1,3 +1,5 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] pub mod tests; diff --git a/contracts/coconut-bandwidth/src/support/tests.rs b/contracts/coconut-bandwidth/src/support/tests.rs deleted file mode 100644 index 99bfba148d..0000000000 --- a/contracts/coconut-bandwidth/src/support/tests.rs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -#[cfg(test)] -pub mod helpers { - pub const OWNER: &str = "admin0001"; - pub const MULTISIG_CONTRACT: &str = "multisig contract address"; - pub const POOL_CONTRACT: &str = "mix pool contract address"; - - use crate::contract::instantiate; - use coconut_bandwidth_contract_common::msg::InstantiateMsg; - use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}; - use cosmwasm_std::{Addr, Coin, Empty, MemoryStorage, OwnedDeps}; - use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper}; - - pub fn init_contract() -> OwnedDeps> { - let mut deps = mock_dependencies(); - let msg = InstantiateMsg { - multisig_addr: String::from(MULTISIG_CONTRACT), - pool_addr: String::from(POOL_CONTRACT), - }; - let env = mock_env(); - let info = mock_info("creator", &[]); - instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); - deps - } - - pub fn mock_app(init_funds: &[Coin]) -> App { - AppBuilder::new().build(|router, _, storage| { - router - .bank - .init_balance(storage, &Addr::unchecked(OWNER), init_funds.to_vec()) - .unwrap(); - }) - } - - pub fn contract_bandwidth() -> Box> { - let contract = ContractWrapper::new( - crate::contract::execute, - crate::contract::instantiate, - crate::contract::query, - ); - Box::new(contract) - } -} diff --git a/contracts/coconut-bandwidth/src/support/tests/fixtures.rs b/contracts/coconut-bandwidth/src/support/tests/fixtures.rs new file mode 100644 index 0000000000..c1c68a7f0c --- /dev/null +++ b/contracts/coconut-bandwidth/src/support/tests/fixtures.rs @@ -0,0 +1,23 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use coconut_bandwidth_contract_common::spend_credential::{SpendCredential, SpendCredentialData}; +use cosmwasm_std::{Addr, Coin}; + +pub const TEST_MIX_DENOM: &str = "unym"; + +pub fn spend_credential_fixture(blinded_serial_number: &str) -> SpendCredential { + SpendCredential::new( + Coin::new(100, TEST_MIX_DENOM), + blinded_serial_number.to_string(), + Addr::unchecked("gateway_owner_addr"), + ) +} + +pub fn spend_credential_data_fixture(blinded_serial_number: &str) -> SpendCredentialData { + SpendCredentialData::new( + Coin::new(100, TEST_MIX_DENOM), + blinded_serial_number.to_string(), + "gateway_owner_addr".to_string(), + ) +} diff --git a/contracts/coconut-bandwidth/src/support/tests/helpers.rs b/contracts/coconut-bandwidth/src/support/tests/helpers.rs new file mode 100644 index 0000000000..31a4a6f279 --- /dev/null +++ b/contracts/coconut-bandwidth/src/support/tests/helpers.rs @@ -0,0 +1,25 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub const MULTISIG_CONTRACT: &str = "multisig contract address"; +pub const POOL_CONTRACT: &str = "mix pool contract address"; + +use crate::contract::instantiate; +use coconut_bandwidth_contract_common::msg::InstantiateMsg; +use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}; +use cosmwasm_std::{Empty, MemoryStorage, OwnedDeps}; + +use super::fixtures::TEST_MIX_DENOM; + +pub fn init_contract() -> OwnedDeps> { + let mut deps = mock_dependencies(); + let msg = InstantiateMsg { + multisig_addr: String::from(MULTISIG_CONTRACT), + pool_addr: String::from(POOL_CONTRACT), + mix_denom: TEST_MIX_DENOM.to_string(), + }; + let env = mock_env(); + let info = mock_info("creator", &[]); + instantiate(deps.as_mut(), env.clone(), info, msg).unwrap(); + deps +} diff --git a/contracts/coconut-bandwidth/src/support/tests/mod.rs b/contracts/coconut-bandwidth/src/support/tests/mod.rs new file mode 100644 index 0000000000..1e96465ed5 --- /dev/null +++ b/contracts/coconut-bandwidth/src/support/tests/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod fixtures; +pub mod helpers; diff --git a/contracts/coconut-bandwidth/src/transactions.rs b/contracts/coconut-bandwidth/src/transactions.rs index 46941bcc14..92d3a28bd1 100644 --- a/contracts/coconut-bandwidth/src/transactions.rs +++ b/contracts/coconut-bandwidth/src/transactions.rs @@ -1,20 +1,23 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use coconut_bandwidth_contract_common::spend_credential::{ + to_cosmos_msg, SpendCredential, SpendCredentialData, +}; use cosmwasm_std::{BankMsg, Coin, DepsMut, Env, Event, MessageInfo, Response}; use crate::error::ContractError; use crate::state::{ADMIN, CONFIG}; +use crate::storage; use coconut_bandwidth_contract_common::deposit::DepositData; use coconut_bandwidth_contract_common::events::{ DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO, DEPOSIT_VALUE, }; -use config::defaults::MIX_DENOM; pub(crate) fn deposit_funds( - _deps: DepsMut<'_>, + deps: DepsMut<'_>, _env: Env, info: MessageInfo, data: DepositData, @@ -25,8 +28,9 @@ pub(crate) fn deposit_funds( if info.funds.len() > 1 { return Err(ContractError::MultipleDenoms); } - if info.funds[0].denom != MIX_DENOM.base { - return Err(ContractError::WrongDenom); + let mix_denom = CONFIG.load(deps.storage)?.mix_denom; + if info.funds[0].denom != mix_denom { + return Err(ContractError::WrongDenom { mix_denom }); } let voucher_value = info.funds.last().unwrap(); @@ -39,18 +43,55 @@ pub(crate) fn deposit_funds( Ok(Response::new().add_event(event)) } +pub(crate) fn spend_credential( + deps: DepsMut<'_>, + env: Env, + _info: MessageInfo, + data: SpendCredentialData, +) -> Result { + let mix_denom = CONFIG.load(deps.storage)?.mix_denom; + if data.funds().denom != mix_denom { + return Err(ContractError::WrongDenom { mix_denom }); + } + if storage::spent_credentials().has(deps.storage, data.blinded_serial_number()) { + return Err(ContractError::DuplicateBlindedSerialNumber); + } + let cfg = CONFIG.load(deps.storage)?; + + let gateway_cosmos_address = deps.api.addr_validate(data.gateway_cosmos_address())?; + storage::spent_credentials().save( + deps.storage, + data.blinded_serial_number(), + &SpendCredential::new( + data.funds().to_owned(), + data.blinded_serial_number().to_owned(), + gateway_cosmos_address, + ), + )?; + + let msg = to_cosmos_msg( + data.funds().clone(), + data.blinded_serial_number().to_string(), + env.contract.address.into_string(), + cfg.multisig_addr.into_string(), + )?; + + Ok(Response::new().add_message(msg)) +} + pub(crate) fn release_funds( deps: DepsMut<'_>, env: Env, info: MessageInfo, funds: Coin, ) -> Result { - if funds.denom != MIX_DENOM.base { - return Err(ContractError::WrongDenom); + let mix_denom = CONFIG.load(deps.storage)?.mix_denom; + if funds.denom != mix_denom { + return Err(ContractError::WrongDenom { mix_denom }); } let current_balance = deps .querier - .query_balance(env.contract.address, MIX_DENOM.base)?; + .query_balance(env.contract.address, mix_denom)?; if funds.amount > current_balance.amount { return Err(ContractError::NotEnoughFunds); } @@ -70,11 +111,13 @@ pub(crate) fn release_funds( #[cfg(test)] mod tests { use super::*; - use crate::support::tests::helpers; - use crate::support::tests::helpers::{MULTISIG_CONTRACT, POOL_CONTRACT}; + use crate::support::tests::fixtures::spend_credential_data_fixture; + use crate::support::tests::helpers::{self, MULTISIG_CONTRACT, POOL_CONTRACT}; + use coconut_bandwidth_contract_common::msg::ExecuteMsg; use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::{Coin, CosmosMsg}; + use cosmwasm_std::{from_binary, Coin, CosmosMsg, WasmMsg}; use cw_controllers::AdminError; + use multisig_contract_common::msg::ExecuteMsg as MultisigExecuteMsg; #[test] fn invalid_deposit() { @@ -92,7 +135,7 @@ mod tests { Err(ContractError::NoCoin) ); - let coin = Coin::new(1000000, MIX_DENOM.base); + let coin = Coin::new(1000000, crate::support::tests::fixtures::TEST_MIX_DENOM); let second_coin = Coin::new(1000000, "some_denom"); let info = mock_info("requester", &[coin, second_coin.clone()]); @@ -104,7 +147,9 @@ mod tests { let info = mock_info("requester", &[second_coin]); assert_eq!( deposit_funds(deps.as_mut(), env, info, data), - Err(ContractError::WrongDenom) + Err(ContractError::WrongDenom { + mix_denom: crate::support::tests::fixtures::TEST_MIX_DENOM.to_string() + }) ); } @@ -122,7 +167,10 @@ mod tests { verification_key.clone(), encryption_key.clone(), ); - let coin = Coin::new(deposit_value, MIX_DENOM.base); + let coin = Coin::new( + deposit_value, + crate::support::tests::fixtures::TEST_MIX_DENOM, + ); let info = mock_info("requester", &[coin]); let tx = deposit_funds(deps.as_mut(), env.clone(), info, data).unwrap(); @@ -171,7 +219,7 @@ mod tests { let mut deps = helpers::init_contract(); let env = mock_env(); let invalid_admin = "invalid admin"; - let funds = Coin::new(1, MIX_DENOM.base); + let funds = Coin::new(1, crate::support::tests::fixtures::TEST_MIX_DENOM); let err = release_funds( deps.as_mut(), @@ -180,7 +228,12 @@ mod tests { Coin::new(1, "invalid denom"), ) .unwrap_err(); - assert_eq!(err, ContractError::WrongDenom); + assert_eq!( + err, + ContractError::WrongDenom { + mix_denom: crate::support::tests::fixtures::TEST_MIX_DENOM.to_string() + } + ); let err = release_funds( deps.as_mut(), @@ -207,7 +260,7 @@ mod tests { fn valid_release() { let mut deps = helpers::init_contract(); let env = mock_env(); - let coin = Coin::new(1, MIX_DENOM.base); + let coin = Coin::new(1, crate::support::tests::fixtures::TEST_MIX_DENOM); deps.querier .update_balance(env.contract.address.clone(), vec![coin.clone()]); @@ -226,4 +279,96 @@ mod tests { }) ); } + #[test] + fn valid_spend() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let info = mock_info("requester", &[]); + let data = spend_credential_data_fixture("blinded_serial_number"); + let res = spend_credential(deps.as_mut(), env.clone(), info, data.clone()).unwrap(); + if let CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + msg, + funds, + }) = &res.messages[0].msg + { + assert_eq!(contract_addr, MULTISIG_CONTRACT); + assert!(funds.is_empty()); + let multisig_msg: MultisigExecuteMsg = from_binary(&msg).unwrap(); + if let MultisigExecuteMsg::Propose { + title: _, + description, + msgs, + latest, + } = multisig_msg + { + assert_eq!(description, data.blinded_serial_number().to_string()); + assert!(latest.is_none()); + if let CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr, + msg, + funds, + }) = &msgs[0] + { + assert_eq!(*contract_addr, env.contract.address.into_string()); + assert!(funds.is_empty()); + let release_funds_req: ExecuteMsg = from_binary(&msg).unwrap(); + if let ExecuteMsg::ReleaseFunds { funds } = release_funds_req { + assert_eq!(funds, *data.funds()); + } else { + panic!("Could not extract release funds message from proposal"); + } + } + } else { + panic!("Could not extract proposal from binary blob"); + } + } else { + panic!("Wasm execute message not found"); + } + } + + #[test] + fn invalid_spend_attempts() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + let info = mock_info("requester", &[]); + + let invalid_data = SpendCredentialData::new( + Coin::new(1, "invalid_denom".to_string()), + String::new(), + String::new(), + ); + let ret = spend_credential(deps.as_mut(), env.clone(), info.clone(), invalid_data); + assert_eq!( + ret.unwrap_err(), + ContractError::WrongDenom { + mix_denom: crate::support::tests::fixtures::TEST_MIX_DENOM.to_string() + } + ); + + let invalid_data = SpendCredentialData::new( + Coin::new(1, crate::support::tests::fixtures::TEST_MIX_DENOM), + String::new(), + "Blinded Serial Number".to_string(), + ); + let ret = spend_credential(deps.as_mut(), env.clone(), info.clone(), invalid_data); + assert_eq!( + ret.unwrap_err().to_string(), + "Generic error: Invalid input: address not normalized".to_string() + ); + + let invalid_data = spend_credential_data_fixture("blined_serial_number"); + spend_credential( + deps.as_mut(), + env.clone(), + info.clone(), + invalid_data.clone(), + ) + .unwrap(); + let ret = spend_credential(deps.as_mut(), env, info, invalid_data); + assert_eq!( + ret.unwrap_err(), + ContractError::DuplicateBlindedSerialNumber + ); + } } diff --git a/contracts/coconut-test/Cargo.toml b/contracts/coconut-test/Cargo.toml new file mode 100644 index 0000000000..5955391250 --- /dev/null +++ b/contracts/coconut-test/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "coconut-test" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bandwidth-claim-contract = { path = "../../common/bandwidth-claim-contract" } +coconut-bandwidth-contract-common = { path = "../../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" } +multisig-contract-common = { path = "../../common/cosmwasm-smart-contracts/multisig-contract" } + +cosmwasm-std = "1.0.0" +cosmwasm-storage = "1.0.0" +cw-storage-plus = "0.13.4" +cw-controllers = "0.13.4" +cw-utils = "0.13.4" + +schemars = "0.8" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +thiserror = "1.0.23" + +coconut-bandwidth = { path = "../coconut-bandwidth" } +cw-multi-test = { version = "0.13.2" } +cw3-flex-multisig = { path = "../multisig/cw3-flex-multisig" } +cw4-group = { path = "../multisig/cw4-group" } + +[[test]] +name = "coconut-test" +path = "src/tests.rs" diff --git a/contracts/coconut-test/src/deposit_and_release.rs b/contracts/coconut-test/src/deposit_and_release.rs new file mode 100644 index 0000000000..0212741261 --- /dev/null +++ b/contracts/coconut-test/src/deposit_and_release.rs @@ -0,0 +1,101 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::*; +use coconut_bandwidth::error::ContractError; +use coconut_bandwidth_contract_common::{ + deposit::DepositData, + msg::{ExecuteMsg, InstantiateMsg}, +}; +use cosmwasm_std::{coins, Addr}; +use cw_controllers::AdminError; +use cw_multi_test::Executor; + +const TEST_MIX_DENOM: &str = "unym"; + +#[test] +fn deposit_and_release() { + let init_funds = coins(10, TEST_MIX_DENOM); + let deposit_funds = coins(1, TEST_MIX_DENOM); + let release_funds = coins(2, TEST_MIX_DENOM); + let mut app = mock_app(&init_funds); + let multisig_addr = String::from(MULTISIG_CONTRACT); + let pool_addr = String::from(POOL_CONTRACT); + let random_addr = String::from(RANDOM_ADDRESS); + + let code_id = app.store_code(contract_bandwidth()); + let msg = InstantiateMsg { + multisig_addr: multisig_addr.clone(), + pool_addr: pool_addr.clone(), + mix_denom: TEST_MIX_DENOM.to_string(), + }; + let contract_addr = app + .instantiate_contract( + code_id, + Addr::unchecked(OWNER), + &msg, + &[], + "bandwidth", + None, + ) + .unwrap(); + + let msg = ExecuteMsg::DepositFunds { + data: DepositData::new( + String::from("info"), + String::from("id"), + String::from("enc"), + ), + }; + app.execute_contract( + Addr::unchecked(OWNER), + contract_addr.clone(), + &msg, + &deposit_funds, + ) + .unwrap(); + + // try to release more then it's in the contract + let msg = ExecuteMsg::ReleaseFunds { + funds: release_funds[0].clone(), + }; + let err = app + .execute_contract( + Addr::unchecked(multisig_addr.clone()), + contract_addr.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!(ContractError::NotEnoughFunds, err.downcast().unwrap()); + + // try to call release from non-admin + let msg = ExecuteMsg::ReleaseFunds { + funds: deposit_funds[0].clone(), + }; + let err = app + .execute_contract( + Addr::unchecked(random_addr), + contract_addr.clone(), + &msg, + &[], + ) + .unwrap_err(); + assert_eq!( + ContractError::Admin(AdminError::NotAdmin {}), + err.downcast().unwrap() + ); + + let msg = ExecuteMsg::ReleaseFunds { + funds: deposit_funds[0].clone(), + }; + app.execute_contract( + Addr::unchecked(multisig_addr), + contract_addr.clone(), + &msg, + &[], + ) + .unwrap(); + let pool_bal = app.wrap().query_balance(pool_addr, TEST_MIX_DENOM).unwrap(); + assert_eq!(pool_bal, deposit_funds[0]); +} diff --git a/contracts/coconut-test/src/helpers.rs b/contracts/coconut-test/src/helpers.rs new file mode 100644 index 0000000000..2a8c0b24ed --- /dev/null +++ b/contracts/coconut-test/src/helpers.rs @@ -0,0 +1,64 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{entry_point, Addr, Coin, DepsMut, Empty, Env, Response}; +use cw3_flex_multisig::{state::CONFIG, ContractError}; +use cw_multi_test::{App, AppBuilder, Contract, ContractWrapper}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +pub const OWNER: &str = "admin0001"; +pub const MULTISIG_CONTRACT: &str = "multisig contract address"; +pub const POOL_CONTRACT: &str = "mix pool contract address"; +pub const RANDOM_ADDRESS: &str = "random address"; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg { + pub coconut_bandwidth_address: String, +} + +#[entry_point] +pub fn migrate(deps: DepsMut<'_>, _env: Env, msg: MigrateMsg) -> Result { + let mut cfg = CONFIG.load(deps.storage)?; + cfg.coconut_bandwidth_addr = deps.api.addr_validate(&msg.coconut_bandwidth_address)?; + CONFIG.save(deps.storage, &cfg)?; + Ok(Default::default()) +} + +pub fn mock_app(init_funds: &[Coin]) -> App { + AppBuilder::new().build(|router, _, storage| { + router + .bank + .init_balance(storage, &Addr::unchecked(OWNER), init_funds.to_vec()) + .unwrap(); + }) +} + +pub fn contract_bandwidth() -> Box> { + let contract = ContractWrapper::new( + coconut_bandwidth::contract::execute, + coconut_bandwidth::contract::instantiate, + coconut_bandwidth::contract::query, + ); + Box::new(contract) +} + +pub fn contract_multisig() -> Box> { + let contract = ContractWrapper::new( + cw3_flex_multisig::contract::execute, + cw3_flex_multisig::contract::instantiate, + cw3_flex_multisig::contract::query, + ) + .with_migrate(migrate); + Box::new(contract) +} + +pub fn contract_group() -> Box> { + let contract = ContractWrapper::new( + cw4_group::contract::execute, + cw4_group::contract::instantiate, + cw4_group::contract::query, + ); + Box::new(contract) +} diff --git a/contracts/coconut-test/src/spend_credential_creates_proposal.rs b/contracts/coconut-test/src/spend_credential_creates_proposal.rs new file mode 100644 index 0000000000..7d07905b97 --- /dev/null +++ b/contracts/coconut-test/src/spend_credential_creates_proposal.rs @@ -0,0 +1,160 @@ +use crate::helpers::*; +use coconut_bandwidth::error::ContractError; +use coconut_bandwidth_contract_common::{ + msg::{ + ExecuteMsg as CoconutBandwidthExecuteMsg, InstantiateMsg as CoconutBandwidthInstantiateMsg, + }, + spend_credential::SpendCredentialData, +}; +use cosmwasm_std::{coins, Addr, Coin, Decimal}; +use cw4_group::msg::InstantiateMsg as GroupInstantiateMsg; +use cw_multi_test::Executor; +use cw_utils::{Duration, Threshold}; +use multisig_contract_common::msg::InstantiateMsg as MultisigInstantiateMsg; + +pub const TEST_COIN_DENOM: &str = "unym"; +pub const TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = + "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; + +#[test] +fn spend_credential_creates_proposal() { + let init_funds = coins(10, TEST_COIN_DENOM); + let mut app = mock_app(&init_funds); + let pool_addr = String::from(POOL_CONTRACT); + + let group_code_id = app.store_code(contract_group()); + let msg = GroupInstantiateMsg { + admin: Some(OWNER.to_string()), + members: vec![], + }; + let group_contract_addr = app + .instantiate_contract( + group_code_id, + Addr::unchecked(OWNER), + &msg, + &[], + "group", + None, + ) + .unwrap(); + + let multisig_code_id = app.store_code(contract_multisig()); + let msg = MultisigInstantiateMsg { + group_addr: group_contract_addr.into_string(), + threshold: Threshold::AbsolutePercentage { + percentage: Decimal::from_ratio(2u128, 3u128), + }, + max_voting_period: Duration::Height(1000), + coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), + }; + let multisig_contract_addr = app + .instantiate_contract( + multisig_code_id, + Addr::unchecked(OWNER), + &msg, + &[], + "multisig", + Some(OWNER.to_string()), + ) + .unwrap(); + + let coconut_bandwidth_code_id = app.store_code(contract_bandwidth()); + let msg = CoconutBandwidthInstantiateMsg { + multisig_addr: multisig_contract_addr.to_string(), + pool_addr, + mix_denom: TEST_COIN_DENOM.to_string(), + }; + let coconut_bandwidth_contract_addr = app + .instantiate_contract( + coconut_bandwidth_code_id, + Addr::unchecked(OWNER), + &msg, + &[], + "coconut bandwidth", + None, + ) + .unwrap(); + + let msg = MigrateMsg { + coconut_bandwidth_address: coconut_bandwidth_contract_addr.to_string(), + }; + app.migrate_contract( + Addr::unchecked(OWNER), + multisig_contract_addr, + &msg, + multisig_code_id, + ) + .unwrap(); + + let msg = CoconutBandwidthExecuteMsg::SpendCredential { + data: SpendCredentialData::new( + Coin::new(1, TEST_COIN_DENOM), + String::from("blinded_serial_number"), + String::from("gateway_cosmos_address"), + ), + }; + let res = app + .execute_contract( + Addr::unchecked(OWNER), + coconut_bandwidth_contract_addr.clone(), + &msg, + &vec![], + ) + .unwrap(); + let proposal_id = res + .events + .into_iter() + .find(|e| &e.ty == "wasm") + .unwrap() + .attributes + .into_iter() + .find(|attr| &attr.key == "proposal_id") + .unwrap() + .value + .parse::() + .unwrap(); + assert_eq!(1, proposal_id); + + // Trying with the same blinded serial number will detect the double spend attempt + let err = app + .execute_contract( + Addr::unchecked(OWNER), + coconut_bandwidth_contract_addr.clone(), + &msg, + &vec![], + ) + .unwrap_err(); + assert_eq!( + ContractError::DuplicateBlindedSerialNumber, + err.downcast().unwrap() + ); + + let msg = CoconutBandwidthExecuteMsg::SpendCredential { + data: SpendCredentialData::new( + Coin::new(1, TEST_COIN_DENOM), + String::from("blinded_serial_number2"), + String::from("gateway_cosmos_address"), + ), + }; + let res = app + .execute_contract( + Addr::unchecked(OWNER), + coconut_bandwidth_contract_addr.clone(), + &msg, + &vec![], + ) + .unwrap(); + let proposal_id = res + .events + .into_iter() + .find(|e| &e.ty == "wasm") + .unwrap() + .attributes + .into_iter() + .find(|attr| &attr.key == "proposal_id") + .unwrap() + .value + .parse::() + .unwrap(); + assert_eq!(2, proposal_id); +} diff --git a/contracts/coconut-test/src/tests.rs b/contracts/coconut-test/src/tests.rs new file mode 100644 index 0000000000..575a2c2ac1 --- /dev/null +++ b/contracts/coconut-test/src/tests.rs @@ -0,0 +1,6 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +mod deposit_and_release; +mod helpers; +mod spend_credential_creates_proposal; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index d95f364e50..3eaa1f6035 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -27,16 +27,18 @@ 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::rewards::storage as rewards_storage; use cosmwasm_std::{ entry_point, to_binary, Addr, Api, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, - Uint128, + Storage, Uint128, }; use mixnet_contract_common::{ - ContractStateParams, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, + ContractStateParams, ExecuteMsg, InstantiateMsg, MigrateMsg, NodeToRemove, QueryMsg, }; use time::OffsetDateTime; @@ -61,9 +63,14 @@ 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, rewarding_validator_address: Addr) -> ContractState { +fn default_initial_state( + owner: Addr, + mix_denom: String, + rewarding_validator_address: Addr, +) -> ContractState { ContractState { owner, + mix_denom, rewarding_validator_address, params: ContractStateParams { minimum_mixnode_pledge: INITIAL_MIXNODE_PLEDGE, @@ -88,7 +95,7 @@ pub fn instantiate( msg: InstantiateMsg, ) -> Result { let rewarding_validator_address = deps.api.addr_validate(&msg.rewarding_validator_address)?; - let state = default_initial_state(info.sender, rewarding_validator_address); + let state = default_initial_state(info.sender, msg.mixnet_denom, rewarding_validator_address); init_epoch(deps.storage, env)?; mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &state)?; @@ -107,6 +114,19 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { + ExecuteMsg::CompoundReward { + operator, + delegator, + mix_identity, + proxy, + } => crate::rewards::transactions::try_compound_reward( + deps, + env, + operator, + delegator, + mix_identity, + proxy, + ), ExecuteMsg::UpdateRewardingValidatorAddress { address } => { try_update_rewarding_validator_address(deps, info, address) } @@ -122,7 +142,7 @@ pub fn execute( owner_signature, ), ExecuteMsg::UnbondMixnode {} => { - crate::mixnodes::transactions::try_remove_mixnode(env, deps, info) + crate::mixnodes::transactions::try_remove_mixnode(&env, deps.storage, deps.api, info) } ExecuteMsg::UpdateMixnodeConfig { profit_margin_percent, @@ -216,7 +236,13 @@ pub fn execute( owner_signature, ), ExecuteMsg::UnbondMixnodeOnBehalf { owner } => { - crate::mixnodes::transactions::try_remove_mixnode_on_behalf(env, deps, info, owner) + crate::mixnodes::transactions::try_remove_mixnode_on_behalf( + &env, + deps.storage, + deps.api, + info, + owner, + ) } ExecuteMsg::BondGatewayOnBehalf { gateway, @@ -275,7 +301,7 @@ pub fn execute( ) } ExecuteMsg::ReconcileDelegations {} => { - crate::delegations::transactions::try_reconcile_all_delegation_events(deps, info) + crate::delegations::transactions::try_reconcile_all_delegation_events(deps) } ExecuteMsg::CheckpointMixnodes {} => { crate::mixnodes::transactions::try_checkpoint_mixnodes( @@ -316,6 +342,9 @@ pub fn execute( #[entry_point] 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::GetRewardingValidatorAddress {} => { to_binary(&query_rewarding_validator_address(deps)?) } @@ -443,16 +472,61 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result 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 { - Ok(Default::default()) +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) } #[cfg(test)] pub mod tests { use super::*; use crate::support::tests; - use config::defaults::{DEFAULT_NETWORK, MIX_DENOM}; + 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; @@ -462,7 +536,8 @@ pub mod tests { let mut deps = mock_dependencies(); let env = mock_env(); let msg = InstantiateMsg { - rewarding_validator_address: DEFAULT_NETWORK.rewarding_validator_address().to_string(), + rewarding_validator_address: TEST_REWARDING_VALIDATOR_ADDRESS.to_string(), + mixnet_denom: TEST_COIN_DENOM.to_string(), }; let info = mock_info("creator", &[]); @@ -484,7 +559,7 @@ pub mod tests { // Contract balance should match what we initialized it as assert_eq!( - coins(0, MIX_DENOM.base), + coins(0, TEST_COIN_DENOM), tests::queries::query_contract_balance(env.contract.address, deps) ); } diff --git a/contracts/mixnet/src/delegations/queries.rs b/contracts/mixnet/src/delegations/queries.rs index 654e7fd85f..8b60091e4f 100644 --- a/contracts/mixnet/src/delegations/queries.rs +++ b/contracts/mixnet/src/delegations/queries.rs @@ -199,8 +199,7 @@ pub(crate) fn query_mixnode_delegations_paged( #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::support::tests::test_helpers; - use config::defaults::MIX_DENOM; + use crate::support::tests::{fixtures::TEST_COIN_DENOM, test_helpers}; use cosmwasm_std::{coin, Addr, Storage}; use rand::Rng; @@ -385,7 +384,7 @@ pub(crate) mod tests { let delegation = Delegation::new( delegation_owner.clone(), node_identity.clone(), - coin(1234, MIX_DENOM.base), + coin(1234, TEST_COIN_DENOM), 1234, None, ); @@ -433,7 +432,7 @@ pub(crate) mod tests { let delegation = Delegation::new( delegation_owner2, node_identity1.clone(), - coin(1234, MIX_DENOM.base), + coin(1234, TEST_COIN_DENOM), 1234, None, ); @@ -460,7 +459,7 @@ pub(crate) mod tests { let delegation = Delegation::new( delegation_owner1.clone(), node_identity2, - coin(1234, MIX_DENOM.base), + coin(1234, TEST_COIN_DENOM), 1234, None, ); diff --git a/contracts/mixnet/src/delegations/storage.rs b/contracts/mixnet/src/delegations/storage.rs index 52dd95d834..5715bc2c27 100644 --- a/contracts/mixnet/src/delegations/storage.rs +++ b/contracts/mixnet/src/delegations/storage.rs @@ -76,8 +76,8 @@ mod tests { #[cfg(test)] mod reverse_mix_delegations { use super::*; + use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::test_helpers; - use config::defaults::MIX_DENOM; use cosmwasm_std::testing::mock_env; use cosmwasm_std::{coin, Order}; use mixnet_contract_common::Delegation; @@ -87,7 +87,7 @@ mod tests { let mut deps = test_helpers::init_contract(); let node_identity: IdentityKey = "foo".into(); let delegation_owner = Addr::unchecked("bar"); - let delegation = coin(12345, MIX_DENOM.base); + let delegation = coin(12345, TEST_COIN_DENOM); let dummy_data = Delegation::new( delegation_owner.clone(), @@ -125,7 +125,7 @@ mod tests { let node_identity2: IdentityKey = "foo2".into(); let delegation_owner1 = Addr::unchecked("bar"); let delegation_owner2 = Addr::unchecked("bar2"); - let delegation = coin(12345, MIX_DENOM.base); + let delegation = coin(12345, TEST_COIN_DENOM); assert!(test_helpers::read_delegation( deps.as_ref().storage, diff --git a/contracts/mixnet/src/delegations/transactions.rs b/contracts/mixnet/src/delegations/transactions.rs index 4d817f683f..c5e462929e 100644 --- a/contracts/mixnet/src/delegations/transactions.rs +++ b/contracts/mixnet/src/delegations/transactions.rs @@ -4,9 +4,8 @@ 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 as mixnet_params_storage; +use crate::mixnet_contract_settings::storage::{self as mixnet_params_storage}; use crate::mixnodes::storage as mixnodes_storage; -use config::defaults::MIX_DENOM; use cosmwasm_std::{ coins, wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, Event, MessageInfo, Order, Response, Storage, Uint128, WasmMsg, @@ -20,16 +19,7 @@ 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<'_>, - info: MessageInfo, -) -> Result { - let state = mixnet_params_storage::CONTRACT_STATE.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); - } - +pub fn try_reconcile_all_delegation_events(deps: DepsMut<'_>) -> Result { _try_reconcile_all_delegation_events(deps.storage, deps.api) } @@ -74,7 +64,10 @@ pub(crate) fn _try_reconcile_all_delegation_events( Ok(response) } -fn validate_delegation_stake(mut delegation: Vec) -> Result { +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); @@ -85,8 +78,8 @@ fn validate_delegation_stake(mut delegation: Vec) -> Result Result { // check if the delegation contains any funds of the appropriate denomination - let amount = validate_delegation_stake(info.funds)?; + let amount = + validate_delegation_stake(info.funds, mixnet_params_storage::mix_denom(deps.storage)?)?; _try_delegate_to_mixnode( deps, @@ -124,7 +118,8 @@ pub(crate) fn try_delegate_to_mixnode_on_behalf( delegate: String, ) -> Result { // check if the delegation contains any funds of the appropriate denomination - let amount = validate_delegation_stake(info.funds)?; + let amount = + validate_delegation_stake(info.funds, mixnet_params_storage::mix_denom(deps.storage)?)?; _try_delegate_to_mixnode( deps, @@ -260,6 +255,7 @@ pub(crate) fn try_reconcile_undelegation( pending_undelegate: &PendingUndelegate, ) -> Result { let delegation_map = storage::delegations(); + let mix_denom = mixnet_params_storage::mix_denom(storage)?; // debug_with_visibility(api, "Reconciling undelegations"); @@ -389,7 +385,7 @@ pub(crate) fn try_reconcile_undelegation( .as_ref() .unwrap_or(&pending_undelegate.delegate()) .to_string(), - amount: coins(total_funds.u128(), MIX_DENOM.base), + amount: coins(total_funds.u128(), mix_denom.clone()), }) } else { None @@ -401,10 +397,10 @@ pub(crate) fn try_reconcile_undelegation( 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.base), + amount: Coin::new(total_funds.u128(), mix_denom.clone()), }); - wasm_msg = Some(wasm_execute(proxy, &msg, vec![one_ucoin()])?); + wasm_msg = Some(wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom)])?); } let event = new_undelegation_event( @@ -468,9 +464,9 @@ 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::storage; use super::*; #[cfg(test)] @@ -483,7 +479,7 @@ mod tests { fn stake_cant_be_empty() { assert_eq!( Err(ContractError::EmptyDelegation), - validate_delegation_stake(vec![]) + validate_delegation_stake(vec![], TEST_COIN_DENOM.to_string()) ) } @@ -491,19 +487,24 @@ mod tests { fn stake_must_have_single_coin_type() { assert_eq!( Err(ContractError::MultipleDenoms), - validate_delegation_stake(vec![ - coin(123, MIX_DENOM.base), - coin(123, "BTC"), - coin(123, "DOGE") - ]) + 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 {}), - validate_delegation_stake(coins(123, "DOGE")) + Err(ContractError::WrongDenom { + mix_denom: TEST_COIN_DENOM.to_string() + }), + validate_delegation_stake(coins(123, "DOGE"), TEST_COIN_DENOM.to_string()) ) } @@ -511,16 +512,28 @@ mod tests { fn stake_coin_must_have_value_greater_than_zero() { assert_eq!( Err(ContractError::EmptyDelegation), - validate_delegation_stake(coins(0, MIX_DENOM.base)) + 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, MIX_DENOM.base)).is_ok()); - assert!(validate_delegation_stake(coins(123, MIX_DENOM.base)).is_ok()); - assert!(validate_delegation_stake(coins(10000000000, MIX_DENOM.base)).is_ok()); + 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()); } } @@ -543,7 +556,7 @@ mod tests { try_delegate_to_mixnode( deps.as_mut(), mock_env(), - mock_info("sender", &coins(123, MIX_DENOM.base)), + mock_info("sender", &coins(123, TEST_COIN_DENOM)), "non-existent-mix-identity".into(), ) ); @@ -559,7 +572,7 @@ mod tests { deps.as_mut(), ); let delegation_owner = Addr::unchecked("sender"); - let delegation = coin(123, MIX_DENOM.base); + let delegation = coin(123, TEST_COIN_DENOM); assert!(try_delegate_to_mixnode( deps.as_mut(), mock_env(), @@ -608,7 +621,14 @@ mod tests { deps.as_mut(), ); let delegation_owner = Addr::unchecked("sender"); - try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).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() @@ -616,7 +636,7 @@ mod tests { try_delegate_to_mixnode( deps.as_mut(), mock_env(), - mock_info(delegation_owner.as_str(), &coins(123, MIX_DENOM.base)), + mock_info(delegation_owner.as_str(), &coins(123, TEST_COIN_DENOM)), identity, ) ); @@ -631,13 +651,20 @@ mod tests { tests::fixtures::good_mixnode_pledge(), deps.as_mut(), ); - try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap(); + 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, MIX_DENOM.base); + let delegation = coin(123, TEST_COIN_DENOM); let delegation_owner = Addr::unchecked("sender"); assert!(try_delegate_to_mixnode( deps.as_mut(), @@ -687,8 +714,8 @@ mod tests { deps.as_mut(), ); let delegation_owner = Addr::unchecked("sender"); - let delegation1 = coin(100, MIX_DENOM.base); - let delegation2 = coin(50, MIX_DENOM.base); + let delegation1 = coin(100, TEST_COIN_DENOM); + let delegation2 = coin(50, TEST_COIN_DENOM); let mut env = mock_env(); @@ -715,7 +742,7 @@ mod tests { // let expected = Delegation::new( // delegation_owner.clone(), // identity.clone(), - // coin(delegation1.amount.u128() + delegation2.amount.u128(), MIX_DENOM.base), + // coin(delegation1.amount.u128() + delegation2.amount.u128(), TEST_COIN_DENOM), // mock_env().block.height, // None, // ); @@ -750,7 +777,7 @@ mod tests { deps.as_mut(), ); let delegation_owner = Addr::unchecked("sender"); - let delegation = coin(100, MIX_DENOM.base); + let delegation = coin(100, TEST_COIN_DENOM); let env1 = mock_env(); let mut env2 = mock_env(); let initial_height = env1.block.height; @@ -815,8 +842,8 @@ mod tests { ); let delegation_owner1 = Addr::unchecked("sender1"); let delegation_owner2 = Addr::unchecked("sender2"); - let delegation1 = coin(100, MIX_DENOM.base); - let delegation2 = coin(120, MIX_DENOM.base); + 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; @@ -891,11 +918,18 @@ mod tests { try_delegate_to_mixnode( deps.as_mut(), mock_env(), - mock_info(delegation_owner.as_str(), &coins(100, MIX_DENOM.base)), + mock_info(delegation_owner.as_str(), &coins(100, TEST_COIN_DENOM)), identity.clone(), ) .unwrap(); - try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).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() @@ -903,7 +937,7 @@ mod tests { try_delegate_to_mixnode( deps.as_mut(), mock_env(), - mock_info(delegation_owner.as_str(), &coins(50, MIX_DENOM.base)), + mock_info(delegation_owner.as_str(), &coins(50, TEST_COIN_DENOM)), identity, ) ); @@ -928,14 +962,14 @@ mod tests { assert!(try_delegate_to_mixnode( deps.as_mut(), mock_env(), - mock_info(delegation_owner.as_str(), &coins(123, MIX_DENOM.base)), + 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, MIX_DENOM.base)), + mock_info(delegation_owner.as_str(), &coins(42, TEST_COIN_DENOM)), identity2.clone(), ) .is_ok()); @@ -945,7 +979,7 @@ mod tests { let expected1 = Delegation::new( delegation_owner.clone(), identity1.clone(), - coin(123, MIX_DENOM.base), + coin(123, TEST_COIN_DENOM), mock_env().block.height, None, ); @@ -953,7 +987,7 @@ mod tests { let expected2 = Delegation::new( delegation_owner.clone(), identity2.clone(), - coin(42, MIX_DENOM.base), + coin(42, TEST_COIN_DENOM), mock_env().block.height, None, ); @@ -989,8 +1023,8 @@ mod tests { tests::fixtures::good_mixnode_pledge(), deps.as_mut(), ); - let delegation1 = coin(123, MIX_DENOM.base); - let delegation2 = coin(234, MIX_DENOM.base); + let delegation1 = coin(123, TEST_COIN_DENOM); + let delegation2 = coin(234, TEST_COIN_DENOM); assert!(try_delegate_to_mixnode( deps.as_mut(), mock_env(), @@ -1026,7 +1060,7 @@ mod tests { deps.as_mut(), ); let delegation_owner = Addr::unchecked("sender"); - let delegation_amount = coin(100, MIX_DENOM.base); + let delegation_amount = coin(100, TEST_COIN_DENOM); try_delegate_to_mixnode( deps.as_mut(), mock_env(), @@ -1036,8 +1070,14 @@ mod tests { .unwrap(); _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap(); + let api = deps.api.clone(); + try_remove_mixnode( + &mock_env(), + deps.as_mut().storage, + &api, + mock_info(mixnode_owner, &[]), + ) + .unwrap(); let expected = Delegation::new( delegation_owner.clone(), @@ -1062,182 +1102,12 @@ mod tests { #[cfg(test)] mod removing_mix_stake_delegation { - use crate::delegations::queries::query_mixnode_delegation; + use super::*; + use crate::support::tests; use cosmwasm_std::coin; use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::mock_info; use cosmwasm_std::Addr; - use cosmwasm_std::Uint128; - - use crate::mixnodes::transactions::try_remove_mixnode; - use crate::support::tests; - - use super::storage; - use super::*; - - // TODO: Probably delete due to reconciliation logic - //#[ignore] - //#[test] - //fn fails_if_delegation_never_existed() { - // 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_owner = Addr::unchecked("sender"); - // assert_eq!( - // Err(ContractError::NoMixnodeDelegationFound { - // identity: identity.clone(), - // address: delegation_owner.to_string(), - // }), - // try_remove_delegation_from_mixnode( - // deps.as_mut(), - // env, - // mock_info(delegation_owner.as_str(), &[]), - // identity, - // ) - // ); - //} - - // TODO: Update to work with reconciliation - //#[ignore] - //#[test] - //fn succeeds_if_delegation_existed() { - // let mut deps = test_helpers::init_contract(); - // let mixnode_owner = "bob"; - // let env = mock_env(); - // 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, MIX_DENOM.base)), - // identity.clone(), - // ) - // .unwrap(); - - // _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - // let _delegation = query_mixnode_delegation( - // &deps.storage, - // &deps.api, - // identity.clone(), - // delegation_owner.clone().into_string(), - // None, - // ) - // .unwrap(); - - // let expected_response = Response::new() - // .add_message(BankMsg::Send { - // to_address: delegation_owner.clone().into(), - // amount: coins(100, MIX_DENOM.base), - // }) - // .add_event(new_undelegation_event( - // &delegation_owner, - // &None, - // &identity, - // Uint128::new(100), - // )); - - // assert_eq!( - // Ok(expected_response), - // try_remove_delegation_from_mixnode( - // deps.as_mut(), - // env, - // mock_info(delegation_owner.as_str(), &[]), - // identity.clone(), - // ) - // ); - // assert!(storage::delegations() - // .may_load( - // &deps.storage, - // (identity.clone(), delegation_owner.as_bytes().to_vec(), 0), - // ) - // .unwrap() - // .is_none()); - - // // and total delegation is cleared - // assert_eq!( - // Uint128::zero(), - // mixnodes_storage::TOTAL_DELEGATION - // .load(&deps.storage, &identity) - // .unwrap() - // ) - //} - - // TODO: Update to work with reconciliation - //#[ignore] - //#[test] - //fn succeeds_if_delegation_existed_even_if_node_unbonded() { - // let mut deps = test_helpers::init_contract(); - // let mixnode_owner = "bob"; - // let env = mock_env(); - // 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, MIX_DENOM.base)), - // identity.clone(), - // ) - // .unwrap(); - - // _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - // let delegation = query_mixnode_delegation( - // &deps.storage, - // &deps.api, - // identity.clone(), - // delegation_owner.clone().into_string(), - // None, - // ) - // .unwrap(); - - // let expected_response = Response::new() - // .add_message(BankMsg::Send { - // to_address: delegation_owner.clone().into(), - // amount: coins(100, MIX_DENOM.base), - // }) - // .add_event(new_undelegation_event( - // &delegation_owner, - // &None, - // &identity, - // Uint128::new(100), - // )); - - // try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap(); - - // assert_eq!( - // Ok(expected_response), - // try_remove_delegation_from_mixnode( - // deps.as_mut(), - // env, - // mock_info(delegation_owner.as_str(), &[]), - // identity.clone(), - // ) - // ); - - // _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap(); - - // assert!(test_helpers::read_delegation( - // &deps.storage, - // identity, - // delegation_owner.as_bytes(), - // mock_env().block.height - // ) - // .is_none()); - //} #[test] fn total_delegation_is_preserved_if_only_some_undelegate() { @@ -1251,8 +1121,8 @@ mod tests { ); let delegation_owner1 = Addr::unchecked("sender1"); let delegation_owner2 = Addr::unchecked("sender2"); - let delegation1 = coin(123, MIX_DENOM.base); - let delegation2 = coin(234, MIX_DENOM.base); + let delegation1 = coin(123, TEST_COIN_DENOM); + let delegation2 = coin(234, TEST_COIN_DENOM); assert!(try_delegate_to_mixnode( deps.as_mut(), mock_env(), diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs index c4d4c85fda..e1d3a22335 100644 --- a/contracts/mixnet/src/error.rs +++ b/contracts/mixnet/src/error.rs @@ -1,7 +1,6 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use config::defaults::MIX_DENOM; use cosmwasm_std::{Addr, StdError}; use mixnet_contract_common::{error::MixnetContractError, IdentityKey}; use thiserror::Error; @@ -33,14 +32,14 @@ pub enum ContractError { #[error("MIXNET ({}): Unauthorized", line!())] Unauthorized, - #[error("MIXNET ({}): Wrong coin denomination, you must send {}", line!(), MIX_DENOM.base)] - WrongDenom, + #[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 {}", line!(), MIX_DENOM.base)] - NoBondFound, + #[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, @@ -179,4 +178,11 @@ pub enum ContractError { 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/storage.rs b/contracts/mixnet/src/gateways/storage.rs index 6b79f5ed4e..89786a3019 100644 --- a/contracts/mixnet/src/gateways/storage.rs +++ b/contracts/mixnet/src/gateways/storage.rs @@ -36,7 +36,7 @@ pub(crate) fn gateways<'a>() -> IndexedMap<'a, IdentityKeyRef<'a>, GatewayBond, mod tests { use super::super::storage; use crate::support::tests; - use config::defaults::MIX_DENOM; + use crate::support::tests::fixtures::TEST_COIN_DENOM; use cosmwasm_std::testing::MockStorage; use cosmwasm_std::StdResult; use cosmwasm_std::Storage; @@ -84,7 +84,7 @@ mod tests { let pledge_amount = 1000; let gateway_bond = GatewayBond { - pledge_amount: coin(pledge_amount, MIX_DENOM.base), + pledge_amount: coin(pledge_amount, TEST_COIN_DENOM), owner: node_owner, block_height: 12_345, gateway: Gateway { diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index 76689422bd..6315a2b2e2 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -5,7 +5,6 @@ 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 config::defaults::MIX_DENOM; use cosmwasm_std::{ wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Uint128, }; @@ -26,7 +25,8 @@ pub fn try_add_gateway( .load(deps.storage)? .params .minimum_mixnode_pledge; - let pledge = validate_gateway_pledge(info.funds, minimum_pledge)?; + let mix_denom = mixnet_params_storage::mix_denom(deps.storage)?; + let pledge = validate_gateway_pledge(info.funds, minimum_pledge, mix_denom)?; _try_add_gateway( deps, @@ -52,7 +52,8 @@ pub fn try_add_gateway_on_behalf( .load(deps.storage)? .params .minimum_mixnode_pledge; - let pledge = validate_gateway_pledge(info.funds, minimum_pledge)?; + let mix_denom = mixnet_params_storage::mix_denom(deps.storage)?; + let pledge = validate_gateway_pledge(info.funds, minimum_pledge, mix_denom)?; let proxy = info.sender; _try_add_gateway( @@ -138,6 +139,7 @@ pub(crate) fn _try_remove_gateway( proxy: Option, ) -> Result { let owner = deps.api.addr_validate(owner)?; + let mix_denom = mixnet_params_storage::mix_denom(deps.storage)?; // try to find the node of the sender let gateway_bond = match storage::gateways() .idx @@ -177,7 +179,7 @@ pub(crate) fn _try_remove_gateway( amount: gateway_bond.pledge_amount(), }; - let track_unbond_message = wasm_execute(proxy, &msg, vec![one_ucoin()])?; + let track_unbond_message = wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom)])?; response = response.add_message(track_unbond_message); } @@ -192,10 +194,11 @@ 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); + return Err(ContractError::NoBondFound { mix_denom }); } if pledge.len() > 1 { @@ -203,8 +206,8 @@ fn validate_gateway_pledge( } // check that the denomination is correct - if pledge[0].denom != MIX_DENOM.base { - return Err(ContractError::WrongDenom {}); + if pledge[0].denom != mix_denom { + return Err(ContractError::WrongDenom { mix_denom }); } // check that we have at least 100 coins in our pledge @@ -225,8 +228,8 @@ pub mod tests { use crate::error::ContractError; use crate::gateways::transactions::validate_gateway_pledge; use crate::support::tests; + use crate::support::tests::fixtures::TEST_COIN_DENOM; use crate::support::tests::test_helpers; - use config::defaults::MIX_DENOM; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coins, BankMsg, Response}; use cosmwasm_std::{from_binary, Addr, Uint128}; @@ -238,7 +241,7 @@ pub mod tests { // 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, MIX_DENOM.base)); + let info = mock_info("anyone", &coins(insufficient_bond, TEST_COIN_DENOM)); let (msg, _) = tests::messages::valid_bond_gateway_msg("anyone"); // we are informed that we didn't send enough funds @@ -544,13 +547,26 @@ pub mod tests { #[test] fn validating_gateway_bond() { // you must send SOME funds - let result = validate_gateway_pledge(Vec::new(), INITIAL_GATEWAY_PLEDGE); - assert_eq!(result, Err(ContractError::NoBondFound)); + 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); + let result = validate_gateway_pledge( + bond.clone(), + INITIAL_GATEWAY_PLEDGE, + TEST_COIN_DENOM.to_string(), + ); assert_eq!( result, Err(ContractError::InsufficientGatewayBond { @@ -562,18 +578,40 @@ pub mod tests { // 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); + 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); - assert_eq!(result, Err(ContractError::WrongDenom {})); + 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); - assert_eq!(result, Err(ContractError::WrongDenom {})); + 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/lib.rs b/contracts/mixnet/src/lib.rs index 281675a90d..5b0d0c8d0c 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -9,5 +9,6 @@ mod gateways; mod interval; mod mixnet_contract_settings; mod mixnodes; +mod queued_migrations; mod rewards; mod support; diff --git a/contracts/mixnet/src/mixnet_contract_settings/models.rs b/contracts/mixnet/src/mixnet_contract_settings/models.rs index 105475a2e1..d271972be7 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/models.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/models.rs @@ -9,6 +9,7 @@ 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 9ed2ac8a2e..845be0dc67 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/queries.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/queries.rs @@ -45,6 +45,7 @@ pub(crate) mod tests { let dummy_state = ContractState { owner: Addr::unchecked("someowner"), + mix_denom: String::from("unym"), rewarding_validator_address: Addr::unchecked("monitor"), params: ContractStateParams { minimum_mixnode_pledge: 123u128.into(), diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 94f6a73db2..5f4c736d13 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -1,7 +1,6 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::ContractError; use crate::mixnet_contract_settings::models::ContractState; use cosmwasm_std::StdResult; use cosmwasm_std::Storage; @@ -11,10 +10,14 @@ use mixnet_contract_common::{Layer, LayerDistribution}; pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new("config"); pub(crate) const LAYERS: Item<'_, LayerDistribution> = Item::new("layers"); -pub fn rewarding_validator_address(storage: &dyn Storage) -> Result { - Ok(CONTRACT_STATE +pub fn rewarding_validator_address(storage: &dyn Storage) -> StdResult { + CONTRACT_STATE .load(storage) - .map(|state| state.rewarding_validator_address.to_string())?) + .map(|state| state.rewarding_validator_address.to_string()) +} + +pub fn mix_denom(storage: &dyn Storage) -> StdResult { + CONTRACT_STATE.load(storage).map(|state| state.mix_denom) } pub fn increment_layer_count(storage: &mut dyn Storage, layer: Layer) -> StdResult<()> { diff --git a/contracts/mixnet/src/mixnodes/bonding_queries.rs b/contracts/mixnet/src/mixnodes/bonding_queries.rs index 2519045675..3c5085e04e 100644 --- a/contracts/mixnet/src/mixnodes/bonding_queries.rs +++ b/contracts/mixnet/src/mixnodes/bonding_queries.rs @@ -8,6 +8,13 @@ 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, @@ -252,10 +259,13 @@ pub(crate) mod tests { 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(), + &env, + deps.as_mut().storage, + &api, mock_info("fred", &[]), ) .unwrap(); diff --git a/contracts/mixnet/src/mixnodes/storage.rs b/contracts/mixnet/src/mixnodes/storage.rs index a45e8c74a2..ad54790b8b 100644 --- a/contracts/mixnet/src/mixnodes/storage.rs +++ b/contracts/mixnet/src/mixnodes/storage.rs @@ -1,7 +1,6 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use config::defaults::MIX_DENOM; use cosmwasm_std::{StdResult, Storage, Uint128}; use cw_storage_plus::{Index, IndexList, IndexedSnapshotMap, Map, Strategy, UniqueIndex}; use mixnet_contract_common::{ @@ -11,6 +10,8 @@ use mixnet_contract_common::{SphinxKey, U128}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; +use crate::mixnet_contract_settings::storage::mix_denom; + // storage prefixes const TOTAL_DELEGATION_NAMESPACE: &str = "td"; const MIXNODES_PK_NAMESPACE: &str = "mn"; @@ -18,6 +19,7 @@ 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"; const LAST_PM_UPDATE_NAMESPACE: &str = "lpm"; @@ -31,6 +33,9 @@ pub(crate) const TOTAL_DELEGATION: Map<'_, IdentityKeyRef<'_>, Uint128> = 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>, @@ -171,7 +176,7 @@ pub(crate) fn read_full_mixnode_bond( Ok(Some(MixNodeBond { pledge_amount: stored_bond.pledge_amount, total_delegation: Coin { - denom: MIX_DENOM.base.to_owned(), + denom: mix_denom(storage)?, amount: total_delegation.unwrap_or_default(), }, owner: stored_bond.owner, @@ -190,7 +195,8 @@ mod tests { use super::super::storage; use super::*; use crate::support::tests; - use config::defaults::MIX_DENOM; + 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}; @@ -211,7 +217,7 @@ mod tests { #[test] fn reading_mixnode_bond() { - let mut mock_storage = MockStorage::new(); + let mut mock_storage = init_contract().storage; let node_owner: Addr = Addr::unchecked("node-owner"); let node_identity: IdentityKey = "nodeidentity".into(); @@ -223,7 +229,7 @@ mod tests { let pledge_value = 1000000000; let mixnode_bond = StoredMixnodeBond { - pledge_amount: coin(pledge_value, MIX_DENOM.base), + pledge_amount: coin(pledge_value, TEST_COIN_DENOM), owner: node_owner, layer: Layer::One, block_height: 12_345, diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index e0ff269665..0b8b6153e3 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -3,18 +3,17 @@ use super::storage::{self, LAST_PM_UPDATE_TIME}; use crate::error::ContractError; -use crate::mixnet_contract_settings::storage as mixnet_params_storage; +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 config::defaults::MIX_DENOM; use cosmwasm_std::{ - wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Storage, Uint128, + wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Storage, Uint128, }; use mixnet_contract_common::events::{ new_checkpoint_mixnodes_event, new_mixnode_bonding_event, new_mixnode_unbonding_event, }; -use mixnet_contract_common::MixNode; +use mixnet_contract_common::{IdentityKeyRef, MixNode}; use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg; use vesting_contract_common::one_ucoin; @@ -48,7 +47,7 @@ pub fn try_add_mixnode( .load(deps.storage)? .params .minimum_mixnode_pledge; - let pledge = validate_mixnode_pledge(info.funds, minimum_pledge)?; + let pledge = validate_mixnode_pledge(info.funds, minimum_pledge, mix_denom(deps.storage)?)?; _try_add_mixnode( deps, @@ -74,7 +73,7 @@ pub fn try_add_mixnode_on_behalf( .load(deps.storage)? .params .minimum_mixnode_pledge; - let pledge = validate_mixnode_pledge(info.funds, minimum_pledge)?; + let pledge = validate_mixnode_pledge(info.funds, minimum_pledge, mix_denom(deps.storage)?)?; let proxy = info.sender; _try_add_mixnode( @@ -88,6 +87,15 @@ pub fn try_add_mixnode_on_behalf( ) } +pub fn is_blacklisted( + storage: &dyn Storage, + identity: IdentityKeyRef, +) -> Result { + Ok(storage::MIXNODES_BOND_BLACKLIST + .may_load(storage, identity)? + .is_some()) +} + fn _try_add_mixnode( deps: DepsMut<'_>, env: Env, @@ -101,6 +109,12 @@ fn _try_add_mixnode( // if the client has an active bonded mixnode or gateway, don't allow bonding 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( @@ -168,45 +182,47 @@ fn _try_add_mixnode( } pub fn try_remove_mixnode_on_behalf( - env: Env, - deps: DepsMut<'_>, + env: &Env, + storage: &mut dyn Storage, + api: &dyn Api, info: MessageInfo, owner: String, ) -> Result { let proxy = info.sender; - _try_remove_mixnode(env, deps, &owner, Some(proxy)) + _try_remove_mixnode(env, storage, api, &owner, Some(proxy), true) } pub fn try_remove_mixnode( - env: Env, - deps: DepsMut<'_>, + env: &Env, + storage: &mut dyn Storage, + api: &dyn Api, info: MessageInfo, ) -> Result { - _try_remove_mixnode(env, deps, info.sender.as_ref(), None) + _try_remove_mixnode(env, storage, api, info.sender.as_ref(), None, true) } pub(crate) fn _try_remove_mixnode( - env: Env, - deps: DepsMut<'_>, + env: &Env, + storage: &mut dyn Storage, + api: &dyn Api, owner: &str, proxy: Option, + collect_rewards: bool, ) -> Result { - let owner = deps.api.addr_validate(owner)?; + let owner = api.addr_validate(owner)?; - crate::rewards::transactions::_try_compound_operator_reward( - deps.storage, - deps.api, - env.block.height, - &owner, - None, - )?; + if collect_rewards { + crate::rewards::transactions::_try_compound_operator_reward( + storage, + api, + env.block.height, + &owner, + None, + )?; + } // try to find the node of the sender - let mixnode_bond = match storage::mixnodes() - .idx - .owner - .item(deps.storage, owner.clone())? - { + let mixnode_bond = match storage::mixnodes().idx.owner.item(storage, owner.clone())? { Some(record) => record.1, None => return Err(ContractError::NoAssociatedMixNodeBond { owner }), }; @@ -226,10 +242,10 @@ pub(crate) fn _try_remove_mixnode( }; // remove the bond - storage::mixnodes().remove(deps.storage, mixnode_bond.identity(), env.block.height)?; + storage::mixnodes().remove(storage, mixnode_bond.identity(), env.block.height)?; // decrement layer count - mixnet_params_storage::decrement_layer_count(deps.storage, mixnode_bond.layer)?; + mixnet_params_storage::decrement_layer_count(storage, mixnode_bond.layer)?; let mut response = Response::new(); @@ -239,7 +255,7 @@ pub(crate) fn _try_remove_mixnode( amount: mixnode_bond.pledge_amount(), }; - let track_unbond_message = wasm_execute(proxy, &msg, vec![one_ucoin()])?; + let track_unbond_message = wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom(storage)?)])?; response = response.add_message(track_unbond_message); } @@ -288,6 +304,7 @@ pub(crate) fn _try_update_mixnode_config( .item(deps.storage, owner.clone())? .ok_or(ContractError::NoAssociatedMixNodeBond { owner })? .1; + let mix_denom = mix_denom(deps.storage)?; if proxy != mixnode_bond.proxy { return Err(ContractError::ProxyMismatch { @@ -329,7 +346,9 @@ pub(crate) fn _try_update_mixnode_config( mixnode_bond.block_height = env.block.height; mixnode_bond }) - .ok_or(ContractError::NoBondFound) + .ok_or(ContractError::NoBondFound { + mix_denom: mix_denom.clone(), + }) }, )?; @@ -342,7 +361,7 @@ pub(crate) fn _try_update_mixnode_config( // 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()], + amount: vec![one_ucoin(mix_denom)], }; response = response.add_message(return_one_ucoint); } @@ -353,10 +372,11 @@ pub(crate) fn _try_update_mixnode_config( 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); + return Err(ContractError::NoBondFound { mix_denom }); } if pledge.len() > 1 { @@ -364,8 +384,8 @@ fn validate_mixnode_pledge( } // check that the denomination is correct - if pledge[0].denom != MIX_DENOM.base { - return Err(ContractError::WrongDenom {}); + if pledge[0].denom != mix_denom { + return Err(ContractError::WrongDenom { mix_denom }); } // check that we have at least MIXNODE_BOND coins in our pledge @@ -386,8 +406,8 @@ pub mod tests { 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 config::defaults::MIX_DENOM; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coins, BankMsg, Response}; use cosmwasm_std::{from_binary, Addr, Uint128}; @@ -402,7 +422,7 @@ pub mod tests { // if we don't send enough funds let insufficient_bond = Into::::into(INITIAL_MIXNODE_PLEDGE) - 1; - let info = mock_info("anyone", &coins(insufficient_bond, MIX_DENOM.base)); + let info = mock_info("anyone", &coins(insufficient_bond, TEST_COIN_DENOM)); let (msg, _) = tests::messages::valid_bond_mixnode_msg("anyone"); // we are informed that we didn't send enough funds @@ -808,13 +828,26 @@ pub mod tests { #[test] fn validating_mixnode_bond() { // you must send SOME funds - let result = validate_mixnode_pledge(Vec::new(), INITIAL_MIXNODE_PLEDGE); - assert_eq!(result, Err(ContractError::NoBondFound)); + 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); + let result = validate_mixnode_pledge( + bond.clone(), + INITIAL_MIXNODE_PLEDGE, + TEST_COIN_DENOM.to_string(), + ); assert_eq!( result, Err(ContractError::InsufficientMixNodeBond { @@ -826,19 +859,41 @@ pub mod tests { // 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); + 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); - assert_eq!(result, Err(ContractError::WrongDenom {})); + 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); - assert_eq!(result, Err(ContractError::WrongDenom {})); + 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] diff --git a/contracts/mixnet/src/queued_migrations.rs b/contracts/mixnet/src/queued_migrations.rs new file mode 100644 index 0000000000..56e4a9f890 --- /dev/null +++ b/contracts/mixnet/src/queued_migrations.rs @@ -0,0 +1,37 @@ +// 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 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/queries.rs b/contracts/mixnet/src/rewards/queries.rs index 035382ddb7..9abcd66a0f 100644 --- a/contracts/mixnet/src/rewards/queries.rs +++ b/contracts/mixnet/src/rewards/queries.rs @@ -83,7 +83,7 @@ pub(crate) mod tests { 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 config::defaults::MIX_DENOM; + use crate::support::tests::fixtures::TEST_COIN_DENOM; use cosmwasm_std::{coin, Addr}; use mixnet_contract_common::{ Interval, RewardingResult, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT, @@ -187,7 +187,7 @@ pub(crate) mod tests { env.clone(), mock_info( &*format!("delegator{:04}", i), - &[coin(200_000000, MIX_DENOM.base)], + &[coin(200_000000, TEST_COIN_DENOM)], ), node_identity.clone(), ) diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index e3ba1b96a0..e0f59de6e9 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -10,11 +10,12 @@ use crate::contract::debug_with_visibility; 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::rewards::helpers; use crate::support::helpers::{is_authorized, operator_cost_at_epoch}; -use config::defaults::MIX_DENOM; use cosmwasm_std::{ coins, wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, MessageInfo, Order, Response, Storage, Uint128, @@ -69,6 +70,7 @@ fn _try_claim_operator_reward( proxy: Option, ) -> Result { let owner = api.addr_validate(owner)?; + let mix_denom = mix_denom(storage)?; let bond = match crate::mixnodes::storage::mixnodes() .idx @@ -105,7 +107,7 @@ fn _try_claim_operator_reward( let return_tokens = BankMsg::Send { to_address: proxy.as_ref().unwrap_or(&owner).to_string(), - amount: coins(reward.u128(), MIX_DENOM.base), + amount: coins(reward.u128(), mix_denom.clone()), }; let mut response = Response::default() @@ -115,10 +117,10 @@ fn _try_claim_operator_reward( if let Some(proxy) = proxy { let msg = Some(VestingContractExecuteMsg::TrackReward { address: owner.to_string(), - amount: Coin::new(reward.u128(), MIX_DENOM.base), + amount: Coin::new(reward.u128(), mix_denom.clone()), }); - let wasm_msg = wasm_execute(proxy, &msg, vec![one_ucoin()])?; + let wasm_msg = wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom)])?; response = response.add_message(wasm_msg); } @@ -134,6 +136,7 @@ pub fn _try_claim_delegator_reward( 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)?; @@ -153,7 +156,7 @@ pub fn _try_claim_delegator_reward( let return_tokens = BankMsg::Send { to_address: proxy.as_ref().unwrap_or(&owner).to_string(), - amount: coins(reward.u128(), MIX_DENOM.base), + amount: coins(reward.u128(), mix_denom.clone()), }; let mut response = @@ -169,10 +172,10 @@ pub fn _try_claim_delegator_reward( if let Some(proxy) = proxy { let msg = Some(VestingContractExecuteMsg::TrackReward { address: owner.to_string(), - amount: Coin::new(reward.u128(), MIX_DENOM.base), + amount: Coin::new(reward.u128(), mix_denom.clone()), }); - let wasm_msg = wasm_execute(proxy, &msg, vec![one_ucoin()])?; + let wasm_msg = wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom)])?; response = response.add_message(wasm_msg); } @@ -370,6 +373,56 @@ pub fn try_compound_delegator_reward_on_behalf( ) } +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, @@ -403,13 +456,14 @@ pub fn _try_compound_delegator_reward( 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 compounded_delegation = reward; + let mut total_delegation_delegate = Uint128::zero(); // Might want to introduce paging here let delegation_heights = delegation_map @@ -421,7 +475,7 @@ pub fn _try_compound_delegator_reward( for h in delegation_heights { let delegation = delegation_map.load(deps.storage, (mix_identity.to_string(), key.clone(), h))?; - compounded_delegation += delegation.amount.amount; + total_delegation_delegate += delegation.amount.amount; delegation_map.replace( deps.storage, (mix_identity.to_string(), key.clone(), h), @@ -430,7 +484,22 @@ pub fn _try_compound_delegator_reward( )?; } + 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, @@ -438,7 +507,7 @@ pub fn _try_compound_delegator_reward( owner_address, Coin { amount: compounded_delegation, - denom: MIX_DENOM.base.to_string(), + denom: mix_denom, }, proxy, )?; @@ -469,6 +538,10 @@ pub fn calculate_delegator_reward( 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); @@ -724,9 +797,9 @@ pub mod tests { 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 config::defaults::MIX_DENOM; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coin, coins, Addr, StdError, Timestamp, Uint128}; use mixnet_contract_common::events::{ @@ -744,7 +817,7 @@ pub mod tests { 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, MIX_DENOM.base)); + let info = mock_info(&sender, &coins(1000, TEST_COIN_DENOM)); crate::interval::transactions::init_epoch(&mut deps.storage, env.clone()).unwrap(); // bond the node @@ -885,7 +958,7 @@ pub mod tests { let initial_bond = 10000_000000; let initial_delegation = 20000_000000; let mixnode_bond = StoredMixnodeBond { - pledge_amount: coin(initial_bond, MIX_DENOM.base), + pledge_amount: coin(initial_bond, TEST_COIN_DENOM), owner: node_owner, layer: Layer::One, block_height: env.block.height, @@ -925,7 +998,7 @@ pub mod tests { &Delegation::new( Addr::unchecked("delegator"), node_identity.clone(), - coin(initial_delegation, MIX_DENOM.base), + coin(initial_delegation, TEST_COIN_DENOM), env.block.height, None, ), @@ -1109,7 +1182,7 @@ pub mod tests { assert_eq!(staking_supply, 100_000_000_000_000u128); let sender = Addr::unchecked("alice"); - let stake = coins(10_000_000_000, MIX_DENOM.base); + let stake = coins(10_000_000_000, TEST_COIN_DENOM); let keypair = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); let owner_signature = keypair @@ -1160,14 +1233,14 @@ pub mod tests { let node_owner: Addr = Addr::unchecked("johnny"); let node_identity_2 = test_helpers::add_mixnode( node_owner.as_str(), - coins(10_000_000_000, MIX_DENOM.base), + 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, MIX_DENOM.base)]), + mock_info("alice_d1", &[coin(8000_000000, TEST_COIN_DENOM)]), node_identity_1.clone(), ) .unwrap(); @@ -1175,7 +1248,7 @@ pub mod tests { try_delegate_to_mixnode( deps.as_mut(), mock_env(), - mock_info("alice_d2", &[coin(2000_000000, MIX_DENOM.base)]), + mock_info("alice_d2", &[coin(2000_000000, TEST_COIN_DENOM)]), node_identity_1.clone(), ) .unwrap(); @@ -1183,7 +1256,7 @@ pub mod tests { try_delegate_to_mixnode( deps.as_mut(), mock_env(), - mock_info("bob_d1", &[coin(8000_000000, MIX_DENOM.base)]), + mock_info("bob_d1", &[coin(8000_000000, TEST_COIN_DENOM)]), node_identity_2.clone(), ) .unwrap(); @@ -1191,7 +1264,7 @@ pub mod tests { try_delegate_to_mixnode( deps.as_mut(), mock_env(), - mock_info("bob_d2", &[coin(2000_000000, MIX_DENOM.base)]), + mock_info("bob_d2", &[coin(2000_000000, TEST_COIN_DENOM)]), node_identity_2.clone(), ) .unwrap(); @@ -1199,14 +1272,14 @@ pub mod tests { 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, MIX_DENOM.base), + 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, MIX_DENOM.base)]), + mock_info("alicebob_d1", &[coin(8000_000000 * 2, TEST_COIN_DENOM)]), node_identity_3.clone(), ) .unwrap(); @@ -1214,7 +1287,7 @@ pub mod tests { try_delegate_to_mixnode( deps.as_mut(), mock_env(), - mock_info("alicebob_d2", &[coin(2000_000000 * 2, MIX_DENOM.base)]), + mock_info("alicebob_d2", &[coin(2000_000000 * 2, TEST_COIN_DENOM)]), node_identity_3.clone(), ) .unwrap(); @@ -1225,7 +1298,6 @@ pub mod tests { ) .unwrap(); - let info = mock_info(rewarding_validator_address.as_ref(), &[]); env.block.height += 2 * constants::MINIMUM_BLOCK_AGE_FOR_REWARDING; let mix_1 = mixnodes_storage::read_full_mixnode_bond(&deps.storage, &node_identity_1) @@ -1321,7 +1393,7 @@ pub mod tests { try_delegate_to_mixnode( deps.as_mut(), env.clone(), - mock_info("alice_d1", &[coin(8000_000000, MIX_DENOM.base)]), + mock_info("alice_d1", &[coin(8000_000000, TEST_COIN_DENOM)]), node_identity_1.clone(), ) .unwrap(); @@ -1531,7 +1603,7 @@ pub mod tests { assert_eq!(mix_2_reward_result.lambda(), U128::from_num(0.0001f64)); assert_eq!(mix_2_reward_result.reward().int(), 974456u128); - let mix_3_reward_result = mix_3.reward(¶ms3); + let _mix_3_reward_result = mix_3.reward(¶ms3); // assert_eq!(mix_3_reward_result.reward().int(), mix_1_reward_result.reward().int() + mix_2_reward_result.reward().int()); } @@ -1560,14 +1632,14 @@ pub mod tests { let node_owner: Addr = Addr::unchecked("alice"); let node_identity = test_helpers::add_mixnode( node_owner.as_str(), - coins(10_000_000_000, MIX_DENOM.base), + 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, MIX_DENOM.base)]), + mock_info("alice_d1", &[coin(8000_000000, TEST_COIN_DENOM)]), node_identity.clone(), ) .unwrap(); @@ -1575,7 +1647,7 @@ pub mod tests { try_delegate_to_mixnode( deps.as_mut(), mock_env(), - mock_info("alice_d2", &[coin(2000_000000, MIX_DENOM.base)]), + mock_info("alice_d2", &[coin(2000_000000, TEST_COIN_DENOM)]), node_identity.clone(), ) .unwrap(); @@ -1751,7 +1823,7 @@ pub mod tests { #[allow(clippy::inconsistent_digit_grouping)] let node_identity = test_helpers::add_mixnode( node_owner.as_str(), - coins(10000_000_000, MIX_DENOM.base), + coins(10000_000_000, TEST_COIN_DENOM), deps.as_mut(), ); @@ -1778,7 +1850,7 @@ pub mod tests { #[allow(clippy::inconsistent_digit_grouping)] let node_identity = test_helpers::add_mixnode( node_owner.as_str(), - coins(10000_000_000, MIX_DENOM.base), + coins(10000_000_000, TEST_COIN_DENOM), deps.as_mut(), ); @@ -1788,7 +1860,7 @@ pub mod tests { env.clone(), mock_info( &*format!("delegator{:04}", i), - &[coin(2000_000000, MIX_DENOM.base)], + &[coin(2000_000000, TEST_COIN_DENOM)], ), node_identity.clone(), ) diff --git a/contracts/mixnet/src/support/tests/fixtures.rs b/contracts/mixnet/src/support/tests/fixtures.rs index f8c5513c32..813a68e537 100644 --- a/contracts/mixnet/src/support/tests/fixtures.rs +++ b/contracts/mixnet/src/support/tests/fixtures.rs @@ -1,11 +1,13 @@ use crate::contract::INITIAL_MIXNODE_PLEDGE; use crate::mixnodes::storage as mixnodes_storage; use crate::{mixnodes::storage::StoredMixnodeBond, support::tests}; -use config::defaults::MIX_DENOM; use cosmwasm_std::{coin, Addr, Coin}; use mixnet_contract_common::reward_params::NodeRewardParams; use mixnet_contract_common::{Gateway, GatewayBond, Layer, MixNode}; +pub const TEST_COIN_DENOM: &str = "unym"; +pub const TEST_REWARDING_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; + pub fn mix_node_fixture() -> MixNode { MixNode { host: "mix.node.org".to_string(), @@ -37,7 +39,7 @@ pub fn gateway_bond_fixture(owner: &str) -> GatewayBond { ..tests::fixtures::gateway_fixture() }; GatewayBond::new( - coin(50, MIX_DENOM.base), + coin(50, TEST_COIN_DENOM), Addr::unchecked(owner), 12_345, gateway, @@ -47,7 +49,7 @@ pub fn gateway_bond_fixture(owner: &str) -> GatewayBond { pub(crate) fn stored_mixnode_bond_fixture(owner: &str) -> mixnodes_storage::StoredMixnodeBond { StoredMixnodeBond::new( - coin(50, MIX_DENOM.base), + coin(50, TEST_COIN_DENOM), Addr::unchecked(owner), Layer::One, 12_345, @@ -64,14 +66,14 @@ pub(crate) fn stored_mixnode_bond_fixture(owner: &str) -> mixnodes_storage::Stor pub fn good_mixnode_pledge() -> Vec { vec![Coin { - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), amount: INITIAL_MIXNODE_PLEDGE, }] } pub fn good_gateway_pledge() -> Vec { vec![Coin { - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), amount: INITIAL_MIXNODE_PLEDGE, }] } diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index e8a1108317..19fe66dd35 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -17,7 +17,6 @@ pub mod test_helpers { use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::transactions::try_add_mixnode; use crate::support::tests; - use config::defaults::{DEFAULT_NETWORK, MIX_DENOM}; use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::mock_info; @@ -32,6 +31,9 @@ pub mod test_helpers { use mixnet_contract_common::{Delegation, Gateway, IdentityKeyRef, InstantiateMsg, MixNode}; use rand::thread_rng; + use super::fixtures::TEST_COIN_DENOM; + use super::fixtures::TEST_REWARDING_VALIDATOR_ADDRESS; + pub fn add_mixnode(sender: &str, stake: Vec, deps: DepsMut<'_>) -> String { let keypair = crypto::asymmetric::identity::KeyPair::new(&mut thread_rng()); let owner_signature = keypair @@ -85,7 +87,8 @@ pub mod test_helpers { pub fn init_contract() -> OwnedDeps> { let mut deps = mock_dependencies(); let msg = InstantiateMsg { - rewarding_validator_address: DEFAULT_NETWORK.rewarding_validator_address().to_string(), + rewarding_validator_address: TEST_REWARDING_VALIDATOR_ADDRESS.to_string(), + mixnet_denom: TEST_COIN_DENOM.to_string(), }; let env = mock_env(); let info = mock_info("creator", &[]); @@ -111,7 +114,7 @@ pub mod test_helpers { let delegation = Delegation { owner: Addr::unchecked(owner.into()), node_identity: mix.into(), - amount: coin(12345, MIX_DENOM.base), + amount: coin(12345, TEST_COIN_DENOM), block_height: block_height, proxy: None, }; diff --git a/contracts/mixnet/src/support/tests/queries.rs b/contracts/mixnet/src/support/tests/queries.rs index c42dd9b267..e53caf148e 100644 --- a/contracts/mixnet/src/support/tests/queries.rs +++ b/contracts/mixnet/src/support/tests/queries.rs @@ -1,4 +1,3 @@ -use config::defaults::MIX_DENOM; use cosmwasm_std::{ from_binary, testing::{mock_env, MockApi, MockQuerier, MockStorage}, @@ -10,6 +9,8 @@ use mixnet_contract_common::{ 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(), @@ -45,5 +46,5 @@ pub fn query_contract_balance( deps: OwnedDeps, ) -> Vec { let querier = deps.as_ref().querier; - vec![querier.query_balance(address, MIX_DENOM.base).unwrap()] + vec![querier.query_balance(address, TEST_COIN_DENOM).unwrap()] } diff --git a/contracts/multisig/cw3-flex-multisig/src/contract.rs b/contracts/multisig/cw3-flex-multisig/src/contract.rs index 0af5001f12..c85d1362a7 100644 --- a/contracts/multisig/cw3-flex-multisig/src/contract.rs +++ b/contracts/multisig/cw3-flex-multisig/src/contract.rs @@ -19,7 +19,7 @@ use cw_utils::{maybe_addr, Expiration, ThresholdResponse}; use crate::error::ContractError; use crate::state::{Config, CONFIG}; -use multisig_contract_common::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; +use multisig_contract_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; // version info for migration info const CONTRACT_NAME: &str = "crates.io:cw3-flex-multisig"; @@ -37,6 +37,11 @@ pub fn instantiate( addr: msg.group_addr.clone(), } })?); + // This might need to be changed via a migration, due to circular dependency + // of deploying the two contracts + let coconut_bandwidth_addr = deps + .api + .addr_validate(&msg.coconut_bandwidth_contract_address)?; let total_weight = group_addr.total_weight(&deps.querier)?; msg.threshold.validate(total_weight)?; @@ -46,12 +51,21 @@ pub fn instantiate( threshold: msg.threshold, max_voting_period: msg.max_voting_period, group_addr, + coconut_bandwidth_addr, }; CONFIG.save(deps.storage, &cfg)?; Ok(Response::default()) } +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn migrate(deps: DepsMut<'_>, _env: Env, msg: MigrateMsg) -> Result { + let mut cfg = CONFIG.load(deps.storage)?; + cfg.coconut_bandwidth_addr = deps.api.addr_validate(&msg.coconut_bandwidth_address)?; + CONFIG.save(deps.storage, &cfg)?; + Ok(Default::default()) +} + #[cfg_attr(not(feature = "library"), entry_point)] pub fn execute( deps: DepsMut, @@ -88,15 +102,12 @@ pub fn execute_propose( // only members of the multisig can create a proposal let cfg = CONFIG.load(deps.storage)?; - // Only members of the multisig can create a proposal - // Non-voting members are special - they are allowed to create a proposal and - // therefore "vote", but they aren't allowed to vote otherwise. - // Such vote is also special, because despite having 0 weight it still counts when - // counting threshold passing - let vote_power = cfg - .group_addr - .is_member(&deps.querier, &info.sender, None)? - .ok_or(ContractError::Unauthorized {})?; + // Only the coconut bandwidth contract can create proposals + if info.sender != cfg.coconut_bandwidth_addr { + return Err(ContractError::Unauthorized {}); + } + // The contract doesn't have any say in the voting outcome + let vote_power = 0; // max expires also used as default let max_expires = cfg.max_voting_period.after(&env.block); @@ -433,7 +444,9 @@ mod tests { use cw2::{query_contract_info, ContractVersion}; use cw4::{Cw4ExecuteMsg, Member}; use cw4_group::helpers::Cw4GroupContract; - use cw_multi_test::{next_block, App, AppBuilder, Contract, ContractWrapper, Executor}; + use cw_multi_test::{ + next_block, App, AppBuilder, AppResponse, Contract, ContractWrapper, Executor, + }; use cw_utils::{Duration, Threshold}; use super::*; @@ -445,6 +458,8 @@ mod tests { const VOTER4: &str = "voter0004"; const VOTER5: &str = "voter0005"; const SOMEBODY: &str = "somebody"; + const TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = + "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; fn member>(addr: T, weight: u64) -> Member { Member { @@ -491,6 +506,26 @@ mod tests { .unwrap() } + fn propose_and_vote( + app: &mut App, + flex_addr: Addr, + proposal: ExecuteMsg, + voter: &str, + ) -> AppResponse { + let proposer = TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(); + let res = app + .execute_contract(Addr::unchecked(proposer), flex_addr.clone(), &proposal, &[]) + .unwrap(); + + let yes_vote = ExecuteMsg::Vote { + proposal_id: res.custom_attrs(1)[2].value.parse().unwrap(), + vote: Vote::Yes, + }; + let _ = app.execute_contract(Addr::unchecked(voter), flex_addr, &yes_vote, &[]); + + res + } + #[track_caller] fn instantiate_flex( app: &mut App, @@ -503,6 +538,7 @@ mod tests { group_addr: group.to_string(), threshold, max_voting_period, + coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), }; app.instantiate_contract(flex_id, Addr::unchecked(OWNER), &msg, &[], "flex", None) .unwrap() @@ -538,8 +574,10 @@ mod tests { init_funds: Vec, multisig_as_group_admin: bool, ) -> (Addr, Addr) { - // 1. Instantiate group contract with members (and OWNER as admin) + let coconut_bandwidth_contract = TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(); + // 1. Instantiate group contract with members (and OWNER as admin) and coconut bandwidth contract let members = vec![ + member(coconut_bandwidth_contract, 0), member(OWNER, 0), member(VOTER1, 1), member(VOTER2, 2), @@ -616,6 +654,7 @@ mod tests { quorum: Decimal::percent(1), }, max_voting_period, + coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), }; let err = app .instantiate_contract( @@ -637,6 +676,7 @@ mod tests { group_addr: group_addr.to_string(), threshold: Threshold::AbsoluteCount { weight: 100 }, max_voting_period, + coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), }; let err = app .instantiate_contract( @@ -658,6 +698,7 @@ mod tests { group_addr: group_addr.to_string(), threshold: Threshold::AbsoluteCount { weight: 1 }, max_voting_period, + coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), }; let flex_addr = app .instantiate_contract( @@ -730,41 +771,13 @@ mod tests { }; let err = app .execute_contract( - Addr::unchecked(OWNER), + Addr::unchecked(TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string()), flex_addr.clone(), &proposal_wrong_exp, &[], ) .unwrap_err(); assert_eq!(ContractError::WrongExpiration {}, err.downcast().unwrap()); - - // Proposal from voter works - let res = app - .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[]) - .unwrap(); - assert_eq!( - res.custom_attrs(1), - [ - ("action", "propose"), - ("sender", VOTER3), - ("proposal_id", "1"), - ("status", "Open"), - ], - ); - - // Proposal from voter with enough vote power directly passes - let res = app - .execute_contract(Addr::unchecked(VOTER4), flex_addr, &proposal, &[]) - .unwrap(); - assert_eq!( - res.custom_attrs(1), - [ - ("action", "propose"), - ("sender", VOTER4), - ("proposal_id", "2"), - ("status", "Passed"), - ], - ); } fn get_tally(app: &App, flex_addr: &str, proposal_id: u64) -> u64 { @@ -805,6 +818,54 @@ mod tests { } } + #[test] + fn test_proposer_limited_to_coconut_bandwidth() { + let init_funds = coins(10, "BTC"); + let mut app = mock_app(&init_funds); + + let voting_period = Duration::Time(2000000); + let threshold = Threshold::ThresholdQuorum { + threshold: Decimal::percent(80), + quorum: Decimal::percent(20), + }; + let (flex_addr, _) = setup_test_case(&mut app, threshold, voting_period, init_funds, false); + let proposal = pay_somebody_proposal(); + + let err = app + .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[]) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + + let err = app + .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal, &[]) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + + let err = app + .execute_contract(Addr::unchecked(SOMEBODY), flex_addr.clone(), &proposal, &[]) + .unwrap_err(); + assert_eq!(ContractError::Unauthorized {}, err.downcast().unwrap()); + + let proposer = TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(); + let res = app + .execute_contract( + Addr::unchecked(&proposer), + flex_addr.clone(), + &proposal, + &[], + ) + .unwrap(); + assert_eq!( + res.custom_attrs(1), + [ + ("action", "propose"), + ("sender", &proposer), + ("proposal_id", "1"), + ("status", "Open"), + ], + ); + } + #[test] fn test_proposal_queries() { let init_funds = coins(10, "BTC"); @@ -819,17 +880,13 @@ mod tests { // create proposal with 1 vote power let proposal = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal, VOTER1); let proposal_id1: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); // another proposal immediately passes app.update_block(next_block); let proposal = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(VOTER4), flex_addr.clone(), &proposal, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal, VOTER4); let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); // expire them both @@ -837,9 +894,7 @@ mod tests { // add one more open proposal, 2 votes let proposal = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(VOTER2), flex_addr.clone(), &proposal, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal, VOTER2); let proposal_id3: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); let proposed_at = app.block_info(); @@ -914,9 +969,7 @@ mod tests { // create proposal with 0 vote power let proposal = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal, OWNER); // Get the proposal id from the logs let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); @@ -1017,22 +1070,6 @@ mod tests { assert_eq!(ContractError::NotOpen {}, err.downcast().unwrap()); // query individual votes - // initial (with 0 weight) - let voter = OWNER.into(); - let vote: VoteResponse = app - .wrap() - .query_wasm_smart(&flex_addr, &QueryMsg::Vote { proposal_id, voter }) - .unwrap(); - assert_eq!( - vote.vote.unwrap(), - VoteInfo { - proposal_id, - voter: OWNER.into(), - vote: Vote::Yes, - weight: 0 - } - ); - // nay sayer let voter = VOTER2.into(); let vote: VoteResponse = app @@ -1059,9 +1096,7 @@ mod tests { // create proposal with 0 vote power let proposal = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal, OWNER); // Get the proposal id from the logs let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); @@ -1109,9 +1144,7 @@ mod tests { // create proposal with 0 vote power let proposal = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal, OWNER); // Get the proposal id from the logs let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); @@ -1215,9 +1248,7 @@ mod tests { // create proposal with 0 vote power let proposal = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal, OWNER); // Get the proposal id from the logs let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); @@ -1286,9 +1317,7 @@ mod tests { // create proposal with 0 vote power let proposal = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(OWNER), flex_addr.clone(), &proposal, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal, OWNER); // Get the proposal id from the logs let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); @@ -1338,9 +1367,7 @@ mod tests { // VOTER1 starts a proposal to send some tokens (1/4 votes) let proposal = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal, VOTER1); // Get the proposal id from the logs let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); let prop_status = |app: &App, proposal_id: u64| -> Status { @@ -1400,9 +1427,7 @@ mod tests { // make a second proposal let proposal2 = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(VOTER1), flex_addr.clone(), &proposal2, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal2, VOTER1); // Get the proposal id from the logs let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); @@ -1472,14 +1497,7 @@ mod tests { msgs: vec![update_msg], latest: None, }; - let res = app - .execute_contract( - Addr::unchecked(VOTER1), - flex_addr.clone(), - &update_proposal, - &[], - ) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), update_proposal, VOTER1); // Get the proposal id from the logs let update_proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); @@ -1488,14 +1506,7 @@ mod tests { // VOTER1 starts a proposal to send some tokens let cash_proposal = pay_somebody_proposal(); - let res = app - .execute_contract( - Addr::unchecked(VOTER1), - flex_addr.clone(), - &cash_proposal, - &[], - ) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), cash_proposal, VOTER1); // Get the proposal id from the logs let cash_proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); assert_ne!(cash_proposal_id, update_proposal_id); @@ -1584,9 +1595,7 @@ mod tests { // VOTER3 starts a proposal to send some tokens (3/12 votes) let proposal = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal, VOTER3); // Get the proposal id from the logs let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); let prop_status = |app: &App| -> Status { @@ -1628,9 +1637,7 @@ mod tests { // new proposal can be passed single-handedly by newbie let proposal = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(newbie), flex_addr.clone(), &proposal, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal, newbie); // Get the proposal id from the logs let proposal_id2: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); @@ -1667,9 +1674,7 @@ mod tests { // VOTER3 starts a proposal to send some tokens (3 votes) let proposal = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(VOTER3), flex_addr.clone(), &proposal, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal, VOTER3); // Get the proposal id from the logs let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); let prop_status = |app: &App| -> Status { @@ -1737,9 +1742,7 @@ mod tests { // create proposal let proposal = pay_somebody_proposal(); - let res = app - .execute_contract(Addr::unchecked(VOTER5), flex_addr.clone(), &proposal, &[]) - .unwrap(); + let res = propose_and_vote(&mut app, flex_addr.clone(), proposal, VOTER5); // Get the proposal id from the logs let proposal_id: u64 = res.custom_attrs(1)[2].value.parse().unwrap(); let prop_status = |app: &App| -> Status { diff --git a/contracts/multisig/cw3-flex-multisig/src/error.rs b/contracts/multisig/cw3-flex-multisig/src/error.rs index 4936a8923f..f7addae37b 100644 --- a/contracts/multisig/cw3-flex-multisig/src/error.rs +++ b/contracts/multisig/cw3-flex-multisig/src/error.rs @@ -14,6 +14,9 @@ pub enum ContractError { #[error("Group contract invalid address '{addr}'")] InvalidGroup { addr: String }, + #[error("Coconut bandwidth contract address not found")] + InvalidCoconutBandwidth {}, + #[error("Unauthorized")] Unauthorized {}, diff --git a/contracts/multisig/cw3-flex-multisig/src/state.rs b/contracts/multisig/cw3-flex-multisig/src/state.rs index 023662b8bc..96e6146c2f 100644 --- a/contracts/multisig/cw3-flex-multisig/src/state.rs +++ b/contracts/multisig/cw3-flex-multisig/src/state.rs @@ -1,3 +1,4 @@ +use cosmwasm_std::Addr; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -11,6 +12,7 @@ pub struct Config { pub max_voting_period: Duration, // Total weight and voters are queried from this contract pub group_addr: Cw4Contract, + pub coconut_bandwidth_addr: Addr, } // unique items diff --git a/contracts/vesting/Cargo.toml b/contracts/vesting/Cargo.toml index 5d371cbf8b..3f29aa7e94 100644 --- a/contracts/vesting/Cargo.toml +++ b/contracts/vesting/Cargo.toml @@ -16,7 +16,6 @@ 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" } cosmwasm-std = { version = "1.0.0 "} cw-storage-plus = { version = "0.13.4", features = ["iterator"] } diff --git a/contracts/vesting/src/contract.rs b/contracts/vesting/src/contract.rs index 15626909fd..b1dad8944c 100644 --- a/contracts/vesting/src/contract.rs +++ b/contracts/vesting/src/contract.rs @@ -1,13 +1,13 @@ use crate::errors::ContractError; +use crate::queued_migrations::migrate_config_from_env; use crate::storage::{ account_from_address, locked_pledge_cap, update_locked_pledge_cap, ADMIN, - MIXNET_CONTRACT_ADDRESS, + MIXNET_CONTRACT_ADDRESS, MIX_DENOM, }; use crate::traits::{ DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, VestingAccount, }; use crate::vesting::{populate_vesting_periods, Account}; -use config::defaults::MIX_DENOM; use cosmwasm_std::{ coin, entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Timestamp, Uint128, @@ -36,11 +36,13 @@ pub fn instantiate( // ADMIN is set to the address that instantiated the contract, TODO: make this updatable 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)?; Ok(Response::default()) } #[entry_point] pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result { + migrate_config_from_env(_deps, _env, _msg)?; Ok(Response::default()) } @@ -167,11 +169,9 @@ pub fn try_withdraw_vested_coins( info: MessageInfo, deps: DepsMut<'_>, ) -> Result { - if amount.denom != MIX_DENOM.base { - return Err(ContractError::WrongDenom( - amount.denom, - MIX_DENOM.base.to_string(), - )); + let mix_denom = MIX_DENOM.load(deps.storage)?; + if amount.denom != mix_denom { + return Err(ContractError::WrongDenom(amount.denom, mix_denom)); } let address = info.sender.clone(); @@ -245,7 +245,8 @@ pub fn try_bond_gateway( env: Env, deps: DepsMut<'_>, ) -> Result { - let pledge = validate_funds(&[amount])?; + 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_gateway(gateway, owner_signature, pledge, &env, deps.storage) } @@ -285,7 +286,8 @@ pub fn try_bond_mixnode( env: Env, deps: DepsMut<'_>, ) -> Result { - let pledge = validate_funds(&[amount])?; + 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) } @@ -345,7 +347,8 @@ fn try_delegate_to_mixnode( env: Env, deps: DepsMut<'_>, ) -> Result { - let amount = validate_funds(&[amount])?; + 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) } @@ -396,6 +399,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(); if account_exists { @@ -406,7 +410,7 @@ fn try_create_periodic_vesting_account( let vesting_spec = vesting_spec.unwrap_or_default(); - let coin = validate_funds(&info.funds)?; + let coin = validate_funds(&info.funds, mix_denom)?; let owner_address = deps.api.addr_validate(owner_address)?; let staking_address = if let Some(staking_address) = staking_address { @@ -573,7 +577,7 @@ pub fn try_get_vested_coins( deps: Deps<'_>, ) -> Result { let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - account.get_vested_coins(block_time, &env) + account.get_vested_coins(block_time, &env, deps.storage) } pub fn try_get_vesting_coins( @@ -583,7 +587,7 @@ pub fn try_get_vesting_coins( deps: Deps<'_>, ) -> Result { let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; - account.get_vesting_coins(block_time, &env) + account.get_vesting_coins(block_time, &env, deps.storage) } pub fn try_get_start_time( @@ -630,7 +634,7 @@ pub fn try_get_delegated_vesting( account.get_delegated_vesting(block_time, &env, deps.storage) } -fn validate_funds(funds: &[Coin]) -> Result { +fn validate_funds(funds: &[Coin], mix_denom: String) -> Result { if funds.is_empty() || funds[0].amount.is_zero() { return Err(ContractError::EmptyFunds); } @@ -639,11 +643,8 @@ fn validate_funds(funds: &[Coin]) -> Result { return Err(ContractError::MultipleDenoms); } - if funds[0].denom != MIX_DENOM.base { - return Err(ContractError::WrongDenom( - funds[0].denom.clone(), - MIX_DENOM.base.to_string(), - )); + if funds[0].denom != mix_denom { + return Err(ContractError::WrongDenom(funds[0].denom.clone(), mix_denom)); } Ok(funds[0].clone()) diff --git a/contracts/vesting/src/lib.rs b/contracts/vesting/src/lib.rs index 4e57232fe5..4e44a69be8 100644 --- a/contracts/vesting/src/lib.rs +++ b/contracts/vesting/src/lib.rs @@ -1,5 +1,6 @@ pub mod contract; mod errors; +mod queued_migrations; mod storage; mod support; mod traits; diff --git a/contracts/vesting/src/queued_migrations.rs b/contracts/vesting/src/queued_migrations.rs new file mode 100644 index 0000000000..75d63ba648 --- /dev/null +++ b/contracts/vesting/src/queued_migrations.rs @@ -0,0 +1,17 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{DepsMut, Env, Response}; +use vesting_contract_common::MigrateMsg; + +use crate::{errors::ContractError, storage::MIX_DENOM}; + +pub fn migrate_config_from_env( + deps: DepsMut<'_>, + _env: Env, + msg: MigrateMsg, +) -> Result { + MIX_DENOM.save(deps.storage, &msg.mix_denom)?; + + Ok(Default::default()) +} diff --git a/contracts/vesting/src/storage.rs b/contracts/vesting/src/storage.rs index 9e845a58c6..400d90fc3e 100644 --- a/contracts/vesting/src/storage.rs +++ b/contracts/vesting/src/storage.rs @@ -17,6 +17,7 @@ const GATEWAY_PLEDGES: Map<'_, u32, PledgeData> = Map::new("gtw"); pub const DELEGATIONS: Map<'_, (u32, IdentityKey, BlockHeight), Uint128> = Map::new("dlg"); 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 { diff --git a/contracts/vesting/src/support/tests.rs b/contracts/vesting/src/support/tests.rs index eee85a9070..8084e48739 100644 --- a/contracts/vesting/src/support/tests.rs +++ b/contracts/vesting/src/support/tests.rs @@ -2,15 +2,17 @@ pub mod helpers { use crate::contract::instantiate; use crate::vesting::{populate_vesting_periods, Account}; - use config::defaults::MIX_DENOM; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier}; use cosmwasm_std::{Addr, Coin, Empty, Env, MemoryStorage, OwnedDeps, Storage, Uint128}; use vesting_contract_common::messages::{InitMsg, VestingSpecification}; + pub const TEST_COIN_DENOM: &str = "unym"; + pub fn init_contract() -> OwnedDeps> { let mut deps = mock_dependencies(); let msg = InitMsg { mixnet_contract_address: "test".to_string(), + mix_denom: TEST_COIN_DENOM.to_string(), }; let env = mock_env(); let info = mock_info("admin", &[]); @@ -31,7 +33,7 @@ pub mod helpers { Some(Addr::unchecked("staking")), Coin { amount: Uint128::new(1_000_000_000_000), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }, start_time_ts, periods, @@ -50,7 +52,7 @@ pub mod helpers { Some(Addr::unchecked("staking")), Coin { amount: Uint128::new(1_000_000_000_000), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }, start_time, periods, diff --git a/contracts/vesting/src/traits/vesting_account.rs b/contracts/vesting/src/traits/vesting_account.rs index 840b5735f0..95f445114e 100644 --- a/contracts/vesting/src/traits/vesting_account.rs +++ b/contracts/vesting/src/traits/vesting_account.rs @@ -34,11 +34,13 @@ pub trait VestingAccount { &self, block_time: Option, env: &Env, + storage: &dyn Storage, ) -> Result; fn get_vesting_coins( &self, block_time: Option, env: &Env, + storage: &dyn Storage, ) -> Result; fn get_start_time(&self) -> Timestamp; diff --git a/contracts/vesting/src/vesting/account/delegating_account.rs b/contracts/vesting/src/vesting/account/delegating_account.rs index 8dede6014e..48f32207ac 100644 --- a/contracts/vesting/src/vesting/account/delegating_account.rs +++ b/contracts/vesting/src/vesting/account/delegating_account.rs @@ -2,6 +2,7 @@ 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}; @@ -28,7 +29,7 @@ impl DelegatingAccount for Account { let compound_delegator_reward_msg = wasm_execute( MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, - vec![one_ucoin()], + vec![one_ucoin(MIX_DENOM.load(storage)?)], )?; Ok(Response::new().add_message(compound_delegator_reward_msg)) @@ -46,7 +47,7 @@ impl DelegatingAccount for Account { let compound_delegator_reward_msg = wasm_execute( MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, - vec![one_ucoin()], + vec![one_ucoin(MIX_DENOM.load(storage)?)], )?; Ok(Response::new().add_message(compound_delegator_reward_msg)) @@ -118,7 +119,7 @@ impl DelegatingAccount for Account { let undelegate_from_mixnode = wasm_execute( MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, - vec![one_ucoin()], + vec![one_ucoin(MIX_DENOM.load(storage)?)], )?; Ok(Response::new() diff --git a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs index 05b27f8786..33af1d6506 100644 --- a/contracts/vesting/src/vesting/account/gateway_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/gateway_bonding_account.rs @@ -2,6 +2,7 @@ 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}; @@ -76,7 +77,7 @@ impl GatewayBondingAccount for Account { let unbond_msg = wasm_execute( MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, - vec![one_ucoin()], + vec![one_ucoin(MIX_DENOM.load(storage)?)], )?; Ok(Response::new() diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index a031dc58f6..6312647a9a 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -1,6 +1,7 @@ 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}; @@ -24,7 +25,7 @@ impl MixnodeBondingAccount for Account { let compound_operator_reward_msg = wasm_execute( MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, - vec![one_ucoin()], + vec![one_ucoin(MIX_DENOM.load(storage)?)], )?; Ok(Response::new().add_message(compound_operator_reward_msg)) @@ -41,7 +42,7 @@ impl MixnodeBondingAccount for Account { let compound_operator_reward_msg = wasm_execute( MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, - vec![one_ucoin()], + vec![one_ucoin(MIX_DENOM.load(storage)?)], )?; Ok(Response::new().add_message(compound_operator_reward_msg)) @@ -60,7 +61,7 @@ impl MixnodeBondingAccount for Account { let update_mixnode_config_msg = wasm_execute( MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, - vec![one_ucoin()], + vec![one_ucoin(MIX_DENOM.load(storage)?)], )?; Ok(Response::new() @@ -130,7 +131,7 @@ impl MixnodeBondingAccount for Account { let unbond_msg = wasm_execute( MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, - vec![one_ucoin()], + vec![one_ucoin(MIX_DENOM.load(storage)?)], )?; Ok(Response::new() diff --git a/contracts/vesting/src/vesting/account/mod.rs b/contracts/vesting/src/vesting/account/mod.rs index b25628deea..b5369aa410 100644 --- a/contracts/vesting/src/vesting/account/mod.rs +++ b/contracts/vesting/src/vesting/account/mod.rs @@ -25,11 +25,11 @@ fn generate_storage_key(storage: &mut dyn Storage) -> Result #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)] pub struct Account { - owner_address: Addr, - staking_address: Option, - start_time: Timestamp, - periods: Vec, - coin: Coin, + pub owner_address: Addr, + pub staking_address: Option, + pub start_time: Timestamp, + pub periods: Vec, + pub coin: Coin, storage_key: u32, } @@ -101,10 +101,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 { diff --git a/contracts/vesting/src/vesting/account/vesting_account.rs b/contracts/vesting/src/vesting/account/vesting_account.rs index a4ba2f062c..dc93627d1d 100644 --- a/contracts/vesting/src/vesting/account/vesting_account.rs +++ b/contracts/vesting/src/vesting/account/vesting_account.rs @@ -1,7 +1,6 @@ use crate::errors::ContractError; -use crate::storage::{delete_account, save_account, DELEGATIONS}; +use crate::storage::{delete_account, save_account, DELEGATIONS, MIX_DENOM}; use crate::traits::VestingAccount; -use config::defaults::MIX_DENOM; use cosmwasm_std::{Addr, Coin, Env, Order, Storage, Timestamp, Uint128}; use vesting_contract_common::{OriginalVestingResponse, Period}; @@ -33,7 +32,7 @@ impl VestingAccount for Account { // 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)? + self.get_vesting_coins(block_time, env, storage)? .amount .u128() .saturating_sub( @@ -47,7 +46,7 @@ impl VestingAccount for Account { .u128(), ), ), - denom: MIX_DENOM.base.to_string(), + denom: MIX_DENOM.load(storage)?, }) } @@ -63,7 +62,7 @@ impl VestingAccount for Account { .u128() .saturating_sub(self.locked_coins(block_time, env, storage)?.amount.u128()), ), - denom: MIX_DENOM.base.to_string(), + denom: MIX_DENOM.load(storage)?, }) } @@ -71,22 +70,24 @@ impl VestingAccount for Account { &self, block_time: Option, env: &Env, + storage: &dyn Storage, ) -> Result { let block_time = block_time.unwrap_or(env.block.time); let period = self.get_current_vesting_period(block_time); + let denom = MIX_DENOM.load(storage)?; let amount = match period { Period::Before => Coin { amount: Uint128::new(0), - denom: MIX_DENOM.base.to_string(), + denom, }, Period::In(idx) => Coin { amount: Uint128::new(self.tokens_per_period()? * idx as u128), - denom: MIX_DENOM.base.to_string(), + denom, }, Period::After => Coin { amount: self.coin.amount, - denom: MIX_DENOM.base.to_string(), + denom, }, }; Ok(amount) @@ -96,11 +97,12 @@ impl VestingAccount for Account { &self, block_time: Option, env: &Env, + storage: &dyn Storage, ) -> Result { Ok(Coin { amount: self.get_original_vesting().amount().amount - - self.get_vested_coins(block_time, env)?.amount, - denom: MIX_DENOM.base.to_string(), + - self.get_vested_coins(block_time, env, storage)?.amount, + denom: MIX_DENOM.load(storage)?, }) } @@ -130,7 +132,7 @@ impl VestingAccount for Account { 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)? + .get_vested_coins(Some(block_time), env, storage)? .amount .saturating_sub(withdrawn); let start_time = match period { @@ -152,7 +154,7 @@ impl VestingAccount for Account { Ok(Coin { amount, - denom: MIX_DENOM.base.to_string(), + denom: MIX_DENOM.load(storage)?, }) } @@ -170,7 +172,7 @@ impl VestingAccount for Account { Ok(Coin { amount, - denom: MIX_DENOM.base.to_string(), + denom: MIX_DENOM.load(storage)?, }) } @@ -182,7 +184,7 @@ impl VestingAccount for Account { ) -> Result { let block_time = block_time.unwrap_or(env.block.time); let period = self.get_current_vesting_period(block_time); - let max_vested = self.get_vested_coins(Some(block_time), env)?; + let max_vested = self.get_vested_coins(Some(block_time), env, storage)?; let start_time = match period { Period::Before => 0, Period::After => u64::MAX, @@ -206,7 +208,7 @@ impl VestingAccount for Account { Ok(Coin { amount, - denom: MIX_DENOM.base.to_string(), + denom: MIX_DENOM.load(storage)?, }) } @@ -226,12 +228,12 @@ impl VestingAccount for Account { let amount = bond.amount().amount - bonded_free.amount; Ok(Coin { amount, - denom: MIX_DENOM.base.to_string(), + denom: MIX_DENOM.load(storage)?, }) } else { Ok(Coin { amount: Uint128::zero(), - denom: MIX_DENOM.base.to_string(), + denom: MIX_DENOM.load(storage)?, }) } } diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index 0ac5ba7562..9dc389864d 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -39,12 +39,11 @@ mod tests { use crate::contract::execute; use crate::storage::load_account; use crate::support::tests::helpers::{ - init_contract, vesting_account_mid_fixture, vesting_account_new_fixture, + 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 config::defaults::MIX_DENOM; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coins, Addr, Coin, Timestamp, Uint128}; use mixnet_contract_common::{Gateway, MixNode}; @@ -55,7 +54,7 @@ mod tests { fn test_account_creation() { let mut deps = init_contract(); let env = mock_env(); - let info = mock_info("not_admin", &coins(1_000_000_000_000, MIX_DENOM.base)); + let info = mock_info("not_admin", &coins(1_000_000_000_000, TEST_COIN_DENOM)); let msg = ExecuteMsg::CreateAccount { owner_address: "owner".to_string(), staking_address: Some("staking".to_string()), @@ -65,7 +64,7 @@ mod tests { let response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone()); assert!(response.is_err()); - let info = mock_info("admin", &coins(1_000_000_000_000, MIX_DENOM.base)); + 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 created_account = load_account(&Addr::unchecked("owner"), &deps.storage) .unwrap() @@ -131,7 +130,7 @@ mod tests { let msg = ExecuteMsg::WithdrawVestedCoins { amount: Coin { amount: Uint128::new(1), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }, }; let info = mock_info("new_owner", &[]); @@ -186,8 +185,12 @@ mod tests { Timestamp::from_seconds(account.start_time().seconds() + vesting_period + 1); let current_period = account.get_current_vesting_period(block_time); assert_eq!(current_period, Period::In(1)); - let vested_coins = account.get_vested_coins(Some(block_time), &env).unwrap(); - let vesting_coins = account.get_vesting_coins(Some(block_time), &env).unwrap(); + let vested_coins = account + .get_vested_coins(Some(block_time), &env, &deps.storage) + .unwrap(); + let vesting_coins = account + .get_vesting_coins(Some(block_time), &env, &deps.storage) + .unwrap(); assert_eq!( vested_coins.amount, Uint128::new( @@ -207,8 +210,12 @@ mod tests { Timestamp::from_seconds(account.start_time().seconds() + 5 * vesting_period + 1); let current_period = account.get_current_vesting_period(block_time); assert_eq!(current_period, Period::In(5)); - let vested_coins = account.get_vested_coins(Some(block_time), &env).unwrap(); - let vesting_coins = account.get_vesting_coins(Some(block_time), &env).unwrap(); + let vested_coins = account + .get_vested_coins(Some(block_time), &env, &deps.storage) + .unwrap(); + let vesting_coins = account + .get_vesting_coins(Some(block_time), &env, &deps.storage) + .unwrap(); assert_eq!( vested_coins.amount, Uint128::new( @@ -230,8 +237,12 @@ mod tests { ); let current_period = account.get_current_vesting_period(block_time); assert_eq!(current_period, Period::After); - let vested_coins = account.get_vested_coins(Some(block_time), &env).unwrap(); - let vesting_coins = account.get_vesting_coins(Some(block_time), &env).unwrap(); + let vested_coins = account + .get_vested_coins(Some(block_time), &env, &deps.storage) + .unwrap(); + let vesting_coins = account + .get_vesting_coins(Some(block_time), &env, &deps.storage) + .unwrap(); assert_eq!( vested_coins.amount, Uint128::new(account.get_original_vesting().amount().amount.u128()) @@ -245,8 +256,10 @@ mod tests { let env = mock_env(); let account = vesting_account_mid_fixture(&mut deps.storage, &env); - let vested_coins = account.get_vested_coins(None, &env).unwrap(); - let vesting_coins = account.get_vesting_coins(None, &env).unwrap(); + let vested_coins = account.get_vested_coins(None, &env, &deps.storage).unwrap(); + let vesting_coins = account + .get_vesting_coins(None, &env, &deps.storage) + .unwrap(); let locked_coins = account.locked_coins(None, &env, &mut 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)); @@ -262,7 +275,7 @@ mod tests { let delegation = Coin { amount: Uint128::new(90_000_000_000), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }; let ok = account.try_delegate_to_mixnode( @@ -273,8 +286,10 @@ mod tests { ); assert!(ok.is_ok()); - let vested_coins = account.get_vested_coins(None, &env).unwrap(); - let vesting_coins = account.get_vesting_coins(None, &env).unwrap(); + let vested_coins = account.get_vested_coins(None, &env, &deps.storage).unwrap(); + let vesting_coins = 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)); @@ -333,8 +348,10 @@ mod tests { let withdrawn = account.load_withdrawn(&deps.storage).unwrap(); assert_eq!(withdrawn, Uint128::new(250_000_000_000)); - let vested_coins = account.get_vested_coins(None, &env).unwrap(); - let vesting_coins = account.get_vesting_coins(None, &env).unwrap(); + let vested_coins = account.get_vested_coins(None, &env, &deps.storage).unwrap(); + let vesting_coins = account + .get_vesting_coins(None, &env, &deps.storage) + .unwrap(); let locked_coins = account.locked_coins(None, &env, &mut 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)); @@ -352,8 +369,10 @@ mod tests { ); assert!(ok.is_ok()); - let vested_coins = account.get_vested_coins(None, &env).unwrap(); - let vesting_coins = account.get_vesting_coins(None, &env).unwrap(); + let vested_coins = account.get_vested_coins(None, &env, &deps.storage).unwrap(); + let vesting_coins = account + .get_vesting_coins(None, &env, &deps.storage) + .unwrap(); let locked_coins = account.locked_coins(None, &env, &mut 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)); @@ -388,7 +407,7 @@ mod tests { "alice".to_string(), Coin { amount: Uint128::new(1_000_000_000_001), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }, &env, &mut deps.storage, @@ -399,7 +418,7 @@ mod tests { "alice".to_string(), Coin { amount: Uint128::new(90_000_000_000), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }, &env, &mut deps.storage, @@ -411,7 +430,7 @@ mod tests { "alice".to_string(), Coin { amount: Uint128::new(20_000_000_000), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }, &env, &mut deps.storage, @@ -426,7 +445,7 @@ mod tests { "alice".to_string(), Coin { amount: Uint128::new(500_000_000_001), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }, &env, &mut deps.storage, @@ -522,7 +541,7 @@ mod tests { "alice".to_string(), Coin { amount: Uint128::new(1_000_000_000_001), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }, &env, &mut deps.storage, @@ -534,7 +553,7 @@ mod tests { "alice".to_string(), Coin { amount: Uint128::new(90_000_000_000), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }, &env, &mut deps.storage, @@ -550,7 +569,7 @@ mod tests { "alice".to_string(), Coin { amount: Uint128::new(10_000_000_001), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }, &env, &mut deps.storage, @@ -642,7 +661,7 @@ mod tests { "alice".to_string(), Coin { amount: Uint128::new(1_000_000_000_001), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }, &env, &mut deps.storage, @@ -654,7 +673,7 @@ mod tests { "alice".to_string(), Coin { amount: Uint128::new(90_000_000_000), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }, &env, &mut deps.storage, @@ -670,7 +689,7 @@ mod tests { "alice".to_string(), Coin { amount: Uint128::new(500_000_000_001), - denom: MIX_DENOM.base.to_string(), + denom: TEST_COIN_DENOM.to_string(), }, &env, &mut deps.storage, diff --git a/envs/mainnet.env b/envs/mainnet.env new file mode 100644 index 0000000000..a8dc60dd44 --- /dev/null +++ b/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="https://mainnet-stats.nymte.ch:8090" +NYMD_VALIDATOR="https://rpc.nyx.nodes.guru/" +API_VALIDATOR="https://validator.nymtech.net/api/" diff --git a/envs/qa.env b/envs/qa.env new file mode 100644 index 0000000000..842fb54c4d --- /dev/null +++ b/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/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 7e0400693f..f65a46db03 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] chrono = { version = "0.4.19", features = ["serde"] } +clap = { version = "3.2.8", features = ["cargo", "derive"] } humantime-serde = "1.0" isocountry = "0.3.2" itertools = "0.10.3" @@ -25,4 +26,5 @@ tokio = {version = "1.19.1", features = ["full"] } mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } network-defaults = { path = "../common/network-defaults" } +task = { path = "../common/task" } validator-client = { path = "../common/client-libs/validator-client", features=["nymd-client"] } diff --git a/explorer-api/src/client.rs b/explorer-api/src/client.rs index 1f15891c16..129a394d31 100644 --- a/explorer-api/src/client.rs +++ b/explorer-api/src/client.rs @@ -1,6 +1,9 @@ -use network_defaults::{default_api_endpoints, default_nymd_endpoints, DEFAULT_NETWORK}; +use network_defaults::{ + var_names::{API_VALIDATOR, NYMD_VALIDATOR}, + NymNetworkDetails, +}; use reqwest::Url; -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; use validator_client::nymd::QueryNymdClient; // since this is just a query client, we don't need any locking mechanism to keep sequence numbers in check @@ -23,16 +26,17 @@ impl ThreadsafeValidatorClient { } pub(crate) fn new_validator_client() -> ThreadsafeValidatorClient { - let nymd_url = default_nymd_endpoints()[0].clone(); - let api_url = default_api_endpoints()[0].clone(); + let nymd_url = Url::from_str(&std::env::var(NYMD_VALIDATOR).expect("nymd validator not set")) + .expect("nymd validator not in url format"); + let api_url = Url::from_str(&std::env::var(API_VALIDATOR).expect("nymd validator not set")) + .expect("nymd validator not in url format"); - let client_config = - validator_client::Config::try_from_nym_network_details(&DEFAULT_NETWORK.details()) - .expect("failed to construct valid validator client config with the provided network") - .with_urls(nymd_url, api_url); + let details = NymNetworkDetails::new_from_env(); + let client_config = validator_client::Config::try_from_nym_network_details(&details) + .expect("failed to construct valid validator client config with the provided network") + .with_urls(nymd_url, api_url); ThreadsafeValidatorClient(Arc::new( - validator_client::Client::new_query(client_config, DEFAULT_NETWORK) - .expect("Failed to connect to nymd!"), + validator_client::Client::new_query(client_config).expect("Failed to connect to nymd!"), )) } diff --git a/explorer-api/src/commands/mod.rs b/explorer-api/src/commands/mod.rs new file mode 100644 index 0000000000..3c63e989cf --- /dev/null +++ b/explorer-api/src/commands/mod.rs @@ -0,0 +1,12 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; + +#[derive(Parser)] +#[clap(author = "Nymtech", version, about)] +pub(crate) struct Cli { + /// Path pointing to an env file that configures the explorer api. + #[clap(long)] + pub(crate) config_env_file: Option, +} diff --git a/explorer-api/src/country_statistics/distribution.rs b/explorer-api/src/country_statistics/distribution.rs index ebbc1eca9b..f48e87eebb 100644 --- a/explorer-api/src/country_statistics/distribution.rs +++ b/explorer-api/src/country_statistics/distribution.rs @@ -1,4 +1,5 @@ use log::info; +use task::ShutdownListener; use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution; use crate::COUNTRY_DATA_REFRESH_INTERVAL; @@ -7,11 +8,12 @@ use crate::state::ExplorerApiStateContext; pub(crate) struct CountryStatisticsDistributionTask { state: ExplorerApiStateContext, + shutdown: ShutdownListener, } impl CountryStatisticsDistributionTask { - pub(crate) fn new(state: ExplorerApiStateContext) -> Self { - CountryStatisticsDistributionTask { state } + pub(crate) fn new(state: ExplorerApiStateContext, shutdown: ShutdownListener) -> Self { + CountryStatisticsDistributionTask { state, shutdown } } pub(crate) fn start(mut self) { @@ -20,10 +22,15 @@ impl CountryStatisticsDistributionTask { let mut interval_timer = tokio::time::interval(std::time::Duration::from_secs( COUNTRY_DATA_REFRESH_INTERVAL, )); - loop { - // wait for the next interval tick - interval_timer.tick().await; - self.calculate_nodes_per_country().await; + while !self.shutdown.is_shutdown() { + tokio::select! { + _ = interval_timer.tick() => { + self.calculate_nodes_per_country().await; + } + _ = self.shutdown.recv() => { + trace!("Listener: Received shutdown"); + } + } } }); } diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index 41b7eb6f3a..fd84a8c380 100644 --- a/explorer-api/src/country_statistics/geolocate.rs +++ b/explorer-api/src/country_statistics/geolocate.rs @@ -5,15 +5,17 @@ use crate::mix_nodes::location::{GeoLocation, 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, + shutdown: ShutdownListener, } impl GeoLocateTask { - pub(crate) fn new(state: ExplorerApiStateContext) -> Self { - GeoLocateTask { state } + pub(crate) fn new(state: ExplorerApiStateContext, shutdown: ShutdownListener) -> Self { + GeoLocateTask { state, shutdown } } pub(crate) fn start(mut self) { @@ -27,10 +29,15 @@ impl GeoLocateTask { info!("Spawning mix node locator task runner..."); tokio::spawn(async move { let mut interval_timer = tokio::time::interval(std::time::Duration::from_millis(50)); - loop { - // wait for the next interval tick - interval_timer.tick().await; - self.locate_mix_nodes().await; + while !self.shutdown.is_shutdown() { + tokio::select! { + _ = interval_timer.tick() => { + self.locate_mix_nodes().await; + } + _ = self.shutdown.recv() => { + trace!("Listener: Received shutdown"); + } + } } }); } diff --git a/explorer-api/src/main.rs b/explorer-api/src/main.rs index ed96dd4fff..7e6779794b 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -3,10 +3,14 @@ extern crate rocket; #[macro_use] extern crate rocket_okapi; +use clap::Parser; use log::info; +use network_defaults::setup_env; +use task::ShutdownNotifier; pub(crate) mod cache; mod client; +pub(crate) mod commands; mod country_statistics; mod gateways; mod http; @@ -24,6 +28,8 @@ const COUNTRY_DATA_REFRESH_INTERVAL: u64 = 60 * 15; // every 15 minutes #[tokio::main] async fn main() { setup_logging(); + let args = commands::Cli::parse(); + setup_env(args.config_env_file); let mut explorer_api = ExplorerApi::new(); explorer_api.run().await; } @@ -45,29 +51,63 @@ impl ExplorerApi { let validator_api_url = self.state.inner.validator_client.api_endpoint(); info!("Using validator API - {}", validator_api_url); + let shutdown = ShutdownNotifier::default(); + // spawn concurrent tasks - crate::tasks::ExplorerApiTasks::new(self.state.clone()).start(); + crate::tasks::ExplorerApiTasks::new(self.state.clone(), shutdown.subscribe()).start(); country_statistics::distribution::CountryStatisticsDistributionTask::new( self.state.clone(), + shutdown.subscribe(), ) .start(); - country_statistics::geolocate::GeoLocateTask::new(self.state.clone()).start(); + country_statistics::geolocate::GeoLocateTask::new(self.state.clone(), shutdown.subscribe()) + .start(); + + // Rocket handles shutdown on it's own, but its shutdown handling should be incorporated + // with that of the rest of the tasks. + // Currently it's runtime is forcefully terminated once the explorer-api exits. http::start(self.state.clone()); // wait for user to press ctrl+C - self.wait_for_interrupt().await + self.wait_for_interrupt(shutdown).await } - async fn wait_for_interrupt(&self) { - if let Err(e) = tokio::signal::ctrl_c().await { - error!( - "There was an error while capturing SIGINT - {:?}. We will terminate regardless", - e - ); + async fn wait_for_interrupt(&self, mut shutdown: ShutdownNotifier) { + wait_for_signal().await; + + log::info!("Sending shutdown"); + shutdown.signal_shutdown().ok(); + log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); + shutdown.wait_for_shutdown().await; + log::info!("Stopping explorer API"); + } +} + +#[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"); } - info!( - "Received SIGINT - the mixnode will terminate now (threads are not yet nicely stopped, if you see stack traces that's alright)." - ); + _ = 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/explorer-api/src/tasks.rs b/explorer-api/src/tasks.rs index 83ed0d7d6a..bac19b7ee9 100644 --- a/explorer-api/src/tasks.rs +++ b/explorer-api/src/tasks.rs @@ -4,6 +4,7 @@ use std::future::Future; use mixnet_contract_common::GatewayBond; +use task::ShutdownListener; use validator_client::models::MixNodeBondAnnotated; use validator_client::nymd::error::NymdError; use validator_client::nymd::{Paging, QueryNymdClient, ValidatorResponse}; @@ -14,11 +15,12 @@ use crate::state::ExplorerApiStateContext; pub(crate) struct ExplorerApiTasks { state: ExplorerApiStateContext, + shutdown: ShutdownListener, } impl ExplorerApiTasks { - pub(crate) fn new(state: ExplorerApiStateContext) -> Self { - ExplorerApiTasks { state } + pub(crate) fn new(state: ExplorerApiStateContext, shutdown: ShutdownListener) -> Self { + ExplorerApiTasks { state, shutdown } } // a helper to remove duplicate code when grabbing active/rewarded/all mixnodes @@ -128,24 +130,28 @@ impl ExplorerApiTasks { } } - pub(crate) fn start(self) { + pub(crate) fn start(mut self) { info!("Spawning mix nodes task runner..."); tokio::spawn(async move { let mut interval_timer = tokio::time::interval(CACHE_REFRESH_RATE); - loop { - // wait for the next interval tick - interval_timer.tick().await; + while !self.shutdown.is_shutdown() { + tokio::select! { + _ = interval_timer.tick() => { + info!("Updating validator cache..."); + self.update_validators_cache().await; + info!("Done"); - info!("Updating validator cache..."); - self.update_validators_cache().await; - info!("Done"); + info!("Updating gateway cache..."); + self.update_gateways_cache().await; + info!("Done"); - info!("Updating gateway cache..."); - self.update_gateways_cache().await; - info!("Done"); - - info!("Updating mix node cache..."); - self.update_mixnode_cache().await; + info!("Updating mix node cache..."); + self.update_mixnode_cache().await; + } + _ = self.shutdown.recv() => { + trace!("Listener: Received shutdown"); + } + } } }); } diff --git a/explorer/.env.prod b/explorer/.env.prod index 17c51ea14e..e053bd085a 100644 --- a/explorer/.env.prod +++ b/explorer/.env.prod @@ -1,5 +1,6 @@ -EXPLORER_API_URL=https://sandbox-explorer.nymtech.net/api/v1 -VALIDATOR_API_URL=https://sandbox-validator.nymtech.net -BIG_DIPPER_URL=https://sandbox-blocks.nymtech.net -CURRENCY_DENOM=unymt -CURRENCY_STAKING_DENOM=unyxt +EXPLORER_API_URL=https://explorer.nymtech.net/api/v1 +VALIDATOR_API_URL=https://validator.nymtech.net +VALIDATOR_URL=https://rpc.nyx.nodes.guru +BIG_DIPPER_URL=https://blocks.nymtech.net +CURRENCY_DENOM=unym +CURRENCY_STAKING_DENOM=unyx diff --git a/explorer/.env.qa b/explorer/.env.qa index d345c26284..60b30e1bd1 100644 --- a/explorer/.env.qa +++ b/explorer/.env.qa @@ -1,5 +1,6 @@ EXPLORER_API_URL=https://qa-explorer.nymtech.net/api/v1 -VALIDATOR_API_URL=https://qa-validator.nymtech.net +VALIDATOR_API_URL=https://qa-validator-api.nymtech.net +VALIDATOR_URL=https://qa-validator.nymtech.net BIG_DIPPER_URL=https://qa-blocks.nymtech.net -CURRENCY_DENOM=unymt -CURRENCY_STAKING_DENOM=unyxt +CURRENCY_DENOM=unym +CURRENCY_STAKING_DENOM=unyx diff --git a/explorer/src/App.tsx b/explorer/src/App.tsx index a33b49c8ca..9ce60f06f9 100644 --- a/explorer/src/App.tsx +++ b/explorer/src/App.tsx @@ -1,13 +1,11 @@ import * as React from 'react'; -import { useMediaQuery } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; import { Nav } from './components/Nav'; import { MobileNav } from './components/MobileNav'; import { Routes } from './routes/index'; +import { useIsMobile } from './hooks/useIsMobile'; export const App: React.FC = () => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const isMobile = useIsMobile(); if (isMobile) { return ( diff --git a/explorer/src/api/constants.ts b/explorer/src/api/constants.ts index 5cff9cc000..109586c0fd 100644 --- a/explorer/src/api/constants.ts +++ b/explorer/src/api/constants.ts @@ -1,6 +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 BIG_DIPPER = process.env.BIG_DIPPER_URL; // specific API routes @@ -9,7 +10,7 @@ export const MIXNODE_PING = `${API_BASE_URL}/ping`; export const MIXNODES_API = `${API_BASE_URL}/mix-nodes`; export const MIXNODE_API = `${API_BASE_URL}/mix-node`; export const GATEWAYS_API = `${VALIDATOR_API_BASE_URL}/api/v1/gateways`; -export const VALIDATORS_API = `${VALIDATOR_API_BASE_URL}/validators`; +export const VALIDATORS_API = `${VALIDATOR_URL}/validators`; export const BLOCK_API = `${VALIDATOR_API_BASE_URL}/block`; export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`; export const UPTIME_STORY_API = `${VALIDATOR_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this. diff --git a/explorer/src/components/Filters/Filters.tsx b/explorer/src/components/Filters/Filters.tsx new file mode 100644 index 0000000000..65ea7112dd --- /dev/null +++ b/explorer/src/components/Filters/Filters.tsx @@ -0,0 +1,170 @@ +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { Tune } from '@mui/icons-material'; +import { + Button, + Dialog, + DialogContent, + DialogActions, + DialogTitle, + Slider, + Typography, + Box, + Snackbar, + Slide, + Alert, +} from '@mui/material'; +import { useParams } from 'react-router-dom'; +import { useMainContext } from '../../context/main'; +import { MixnodeStatusWithAll, toMixnodeStatus } from '../../typeDefs/explorer-api'; +import { EnumFilterKey, TFilterItem, TFilters } from '../../typeDefs/filters'; +import { formatOnSave, generateFilterSchema } from './filterSchema'; +import { Api } from '../../api'; +import { useIsMobile } from '../../hooks/useIsMobile'; + +const FilterItem = ({ + label, + id, + tooltipInfo, + value, + isSmooth, + marks, + scale, + min, + max, + onChange, +}: TFilterItem & { + onChange: (id: EnumFilterKey, newValue: number[]) => void; +}) => ( + + {label} + {tooltipInfo} + onChange(id, newValue as number[])} + valueLabelDisplay={isSmooth ? 'auto' : 'off'} + marks={marks} + step={isSmooth ? 1 : null} + scale={scale} + min={min} + max={max} + /> + +); + +export const Filters = () => { + const { filterMixnodes, fetchMixnodes, mixnodes } = useMainContext(); + const { status } = useParams<{ status: MixnodeStatusWithAll | undefined }>(); + const isMobile = useIsMobile(); + + const [showFilters, setShowFilters] = useState(false); + const [isFiltered, setIsFiltered] = useState(false); + const [filters, setFilters] = React.useState(); + + const baseFilters = useRef(); + const prevFilters = useRef(); + + 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); + baseFilters.current = initFilters; + prevFilters.current = initFilters; + setFilters(initFilters); + } + }, []); + + const handleOnChange = (id: EnumFilterKey, newValue: number[]) => { + setFilters((ftrs) => { + if (ftrs) return { ...ftrs, [id]: { ...ftrs[id], value: newValue } }; + return undefined; + }); + }; + + const handleOnSave = async () => { + setShowFilters(false); + await filterMixnodes(formatOnSave(filters!), status); + setIsFiltered(true); + prevFilters.current = filters; + }; + + const handleOnCancel = () => { + setShowFilters(false); + setFilters(prevFilters.current); + }; + + const resetFilters = () => { + setFilters(baseFilters.current); + setIsFiltered(false); + prevFilters.current = baseFilters.current; + }; + + const onClearFilters = async () => { + await fetchMixnodes(toMixnodeStatus(status)); + resetFilters(); + }; + + useEffect(() => { + initialiseFilters(); + }, [initialiseFilters]); + + useEffect(() => { + resetFilters(); + }, [status]); + + if (!filters) return null; + + return ( + <> + + t.palette.info.light }} + action={ + + } + > + {mixnodes?.data?.length} mixnodes matched your criteria + + + + + Mixnode filters + + {Object.values(filters).map((v) => ( + + ))} + + + + + + + + ); +}; diff --git a/explorer/src/components/Filters/filterSchema.ts b/explorer/src/components/Filters/filterSchema.ts new file mode 100644 index 0000000000..b5e7fbdf89 --- /dev/null +++ b/explorer/src/components/Filters/filterSchema.ts @@ -0,0 +1,83 @@ +import { EnumFilterKey, TFilters } from '../../typeDefs/filters'; + +export const generateFilterSchema = (upperSaturationValue?: number) => ({ + profitMargin: { + label: 'Profit margin (%)', + id: EnumFilterKey.profitMargin, + value: [0, 100], + isSmooth: true, + marks: [ + { label: '0', value: 0 }, + { label: '10', value: 10 }, + { label: '20', value: 20 }, + { label: '30', value: 30 }, + { label: '40', value: 40 }, + { label: '50', value: 50 }, + { label: '60', value: 60 }, + { label: '70', value: 70 }, + { label: '80', value: 80 }, + { label: '90', value: 90 }, + { label: '100', value: 100 }, + ], + tooltipInfo: + 'As a delegator you want to chose nodes with lower profit margin, meaning more payout for their delegators', + }, + 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, + 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], + marks: [ + { label: '0', value: 0 }, + { label: '10', value: 10 }, + { label: '20', value: 20 }, + { label: '30', value: 30 }, + { label: '40', value: 40 }, + { label: '50', value: 50 }, + { label: '60', value: 60 }, + { label: '70', value: 70 }, + { label: '80', value: 80 }, + { label: '90', value: 90 }, + { label: '100', value: 100 }, + ], + tooltipInfo: 'The higher the routing score the better the performance of the node and so its rewards', + }, +}); + +const formatStakeSaturationValues = ([value_1, value_2]: number[]) => { + const lowerValue = value_1 / 100; + const upperValue = value_2 / 100; + + return [lowerValue, upperValue]; +}; + +export const formatOnSave = (filters: TFilters) => ({ + routingScore: filters.routingScore.value, + profitMargin: filters.profitMargin.value, + stakeSaturation: formatStakeSaturationValues(filters.stakeSaturation.value), +}); diff --git a/explorer/src/components/Footer.tsx b/explorer/src/components/Footer.tsx index 1696def9ee..2ee7c77198 100644 --- a/explorer/src/components/Footer.tsx +++ b/explorer/src/components/Footer.tsx @@ -1,12 +1,11 @@ import * as React from 'react'; import Box from '@mui/material/Box'; -import { Typography, useMediaQuery } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; +import { Typography } from '@mui/material'; import { Socials } from './Socials'; +import { useIsMobile } from '../hooks/useIsMobile'; export const Footer: React.FC = () => { - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('md')); + const isMobile = useIsMobile(); return ( { sx={{ fontSize: 12, textAlign: isMobile ? 'center' : 'end', - color: theme.palette.nym.muted.onDarkBg, + color: 'nym.muted.onDarkBg', }} > © {new Date().getFullYear()} Nym Technologies SA, all rights reserved diff --git a/explorer/src/components/MixNodes/BondBreakdown.tsx b/explorer/src/components/MixNodes/BondBreakdown.tsx index 4b2dc58d92..06009be05d 100644 --- a/explorer/src/components/MixNodes/BondBreakdown.tsx +++ b/explorer/src/components/MixNodes/BondBreakdown.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Alert, Box, CircularProgress, useMediaQuery, Typography } from '@mui/material'; +import { Alert, Box, CircularProgress, Typography } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import Table from '@mui/material/Table'; import TableBody from '@mui/material/TableBody'; @@ -11,6 +11,7 @@ import Paper from '@mui/material/Paper'; import { ExpandMore } from '@mui/icons-material'; import { currencyToString } from '../../utils/currency'; import { useMixnodeContext } from '../../context/mixnode'; +import { useIsMobile } from '../../hooks/useIsMobile'; export const BondBreakdownTable: React.FC = () => { const { mixNode, delegations, uniqDelegations } = useMixnodeContext(); @@ -23,7 +24,7 @@ export const BondBreakdownTable: React.FC = () => { hasLoaded: false, }); const theme = useTheme(); - const matches = useMediaQuery(theme.breakpoints.down('sm')); + const isMobile = useIsMobile(); React.useEffect(() => { if (mixNode?.data) { @@ -83,7 +84,7 @@ export const BondBreakdownTable: React.FC = () => { - + { {uniqDelegations?.data?.map(({ owner, amount: { amount, denom } }) => ( - + {owner} {currencyToString(amount.toString(), denom)} diff --git a/explorer/src/components/MixNodes/DetailSection.tsx b/explorer/src/components/MixNodes/DetailSection.tsx index 6ffe7af34e..b338487fd2 100644 --- a/explorer/src/components/MixNodes/DetailSection.tsx +++ b/explorer/src/components/MixNodes/DetailSection.tsx @@ -1,10 +1,10 @@ -import { Box, Button, Grid, Typography, useMediaQuery } from '@mui/material'; import * as React from 'react'; +import { Box, Button, Grid, Typography, useTheme } from '@mui/material'; import Identicon from 'react-identicons'; -import { useTheme } from '@mui/material/styles'; import { MixnodeRowType } from '.'; import { getMixNodeStatusText, MixNodeStatus } from './Status'; import { MixNodeDescriptionResponse } from '../../typeDefs/explorer-api'; +import { useIsMobile } from '../../hooks/useIsMobile'; interface MixNodeDetailProps { mixNodeRow: MixnodeRowType; @@ -14,7 +14,7 @@ interface MixNodeDetailProps { export const MixNodeDetailSection: React.FC = ({ mixNodeRow, mixnodeDescription }) => { const theme = useTheme(); const palette = [theme.palette.text.primary]; - const matches = useMediaQuery(theme.breakpoints.down('sm')); + const isMobile = useIsMobile(); const statusText = React.useMemo(() => getMixNodeStatusText(mixNodeRow.status), [mixNodeRow.status]); return ( @@ -64,7 +64,7 @@ export const MixNodeDetailSection: React.FC = ({ mixNodeRow, - + Node status: diff --git a/explorer/src/components/MixNodes/Economics/Table.tsx b/explorer/src/components/MixNodes/Economics/Table.tsx index 12c67e9323..d60a83da84 100644 --- a/explorer/src/components/MixNodes/Economics/Table.tsx +++ b/explorer/src/components/MixNodes/Economics/Table.tsx @@ -1,15 +1,5 @@ import * as React from 'react'; -import { - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableRow, - Typography, - useMediaQuery, -} from '@mui/material'; +import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material'; import { Box } from '@mui/system'; import { useTheme, Theme } from '@mui/material/styles'; import { Tooltip } from '@nymproject/react/tooltip/Tooltip'; @@ -17,6 +7,7 @@ import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types'; import { EconomicsProgress } from './EconomicsProgress'; import { cellStyles } from '../../Universal-DataGrid'; import { UniversalTableProps } from '../../DetailTable'; +import { useIsMobile } from '../../../hooks/useIsMobile'; const threshold = 100; @@ -44,8 +35,8 @@ const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => { return theme.palette.nym.wallet.fee; }; -const formatCellValues = (value: EconomicsRowsType, field: string, theme: Theme) => { - const isTablet = useMediaQuery(theme.breakpoints.down('lg')); +const formatCellValues = (value: EconomicsRowsType, field: string) => { + const isTablet = useIsMobile('lg'); if (value.progressBarValue) { return ( @@ -130,7 +121,7 @@ export const DelegatorsInfoTable: React.FC - {formatCellValues(value, columnsData[index].field, theme)} + {formatCellValues(value, columnsData[index].field)} ); })} diff --git a/explorer/src/components/MixNodes/StatusDropdown.tsx b/explorer/src/components/MixNodes/StatusDropdown.tsx index 6856beca4a..1dba2c2f95 100644 --- a/explorer/src/components/MixNodes/StatusDropdown.tsx +++ b/explorer/src/components/MixNodes/StatusDropdown.tsx @@ -1,11 +1,11 @@ -import { MenuItem, useMediaQuery } from '@mui/material'; import * as React from 'react'; +import { MenuItem } from '@mui/material'; import Select from '@mui/material/Select'; import { SelectInputProps } from '@mui/material/Select/SelectInput'; -import { useTheme } from '@mui/material/styles'; import { SxProps } from '@mui/system'; import { MixNodeStatus } from './Status'; import { MixnodeStatus, MixnodeStatusWithAll } from '../../typeDefs/explorer-api'; +import { useIsMobile } from '../../hooks/useIsMobile'; // TODO: replace with i18n const ALL_NODES = 'All nodes'; @@ -17,8 +17,7 @@ interface MixNodeStatusDropdownProps { } export const MixNodeStatusDropdown: React.FC = ({ status, onSelectionChanged, sx }) => { - const theme = useTheme(); - const matches = useMediaQuery(theme.breakpoints.down('sm')); + const isMobile = useIsMobile(); const [statusValue, setStatusValue] = React.useState(status || MixnodeStatusWithAll.all); const onChange: SelectInputProps['onChange'] = React.useCallback( ({ target: { value } }) => { @@ -47,7 +46,7 @@ export const MixNodeStatusDropdown: React.FC = ({ st } }} sx={{ - width: matches ? 'auto' : 200, + width: isMobile ? 'auto' : 200, ...sx, }} > diff --git a/explorer/src/components/TableToolbar.tsx b/explorer/src/components/TableToolbar.tsx index 05f6b8b651..c6a64dd6c5 100644 --- a/explorer/src/components/TableToolbar.tsx +++ b/explorer/src/components/TableToolbar.tsx @@ -1,13 +1,15 @@ import * as React from 'react'; -import { Box, useMediaQuery, TextField, MenuItem } from '@mui/material'; -import { useTheme } from '@mui/material/styles'; +import { Box, TextField, MenuItem } from '@mui/material'; import Select, { SelectChangeEvent } from '@mui/material/Select'; +import { Filters } from './Filters/Filters'; +import { useIsMobile } from '../hooks/useIsMobile'; type TableToolBarProps = { onChangeSearch: (arg: string) => void; onChangePageSize: (event: SelectChangeEvent) => void; pageSize: string; searchTerm: string; + withFilters?: boolean; childrenBefore?: React.ReactNode; childrenAfter?: React.ReactNode; }; @@ -19,16 +21,16 @@ export const TableToolbar: React.FC = ({ pageSize, childrenBefore, childrenAfter, + withFilters, }) => { - const theme = useTheme(); - const matches = useMediaQuery(theme.breakpoints.down('sm')); + const isMobile = useIsMobile(); return ( @@ -40,7 +42,7 @@ export const TableToolbar: React.FC = ({ value={pageSize} onChange={onChangePageSize} sx={{ - width: matches ? 100 : 200, + width: isMobile ? 100 : 200, }} > @@ -57,17 +59,18 @@ export const TableToolbar: React.FC = ({ - + onChangeSearch(event.target.value)} /> + {withFilters && } {childrenAfter} diff --git a/explorer/src/context/main.tsx b/explorer/src/context/main.tsx index 4663cd2644..9255eb20e3 100644 --- a/explorer/src/context/main.tsx +++ b/explorer/src/context/main.tsx @@ -10,6 +10,7 @@ import { SummaryOverviewResponse, ValidatorsResponse, } from '../typeDefs/explorer-api'; +import { EnumFilterKey } from '../typeDefs/filters'; import { Api } from '../api'; import { NavOptionType, originalNavOptions } from './nav'; @@ -26,8 +27,8 @@ interface StateData { } interface StateApi { - fetchMixnodes: (status?: MixnodeStatus) => void; - filterMixnodes: (mixnodes: MixNodeResponse) => void; + fetchMixnodes: (status?: MixnodeStatus) => Promise; + filterMixnodes: (filters: any, status: any) => void; toggleMode: () => void; updateNavState: (id: number) => void; } @@ -40,7 +41,7 @@ export const MainContext = React.createContext({ navState: originalNavOptions, toggleMode: () => undefined, filterMixnodes: () => null, - fetchMixnodes: () => null, + fetchMixnodes: () => Promise.resolve(undefined), }); export const useMainContext = (): React.ContextType => React.useContext(MainContext); @@ -78,9 +79,10 @@ export const MainContextProvider: React.FC = ({ children }) => { }; const fetchMixnodes = async (status?: MixnodeStatus) => { + let data; setMixnodes((d) => ({ ...d, isLoading: true })); try { - const data = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes(); + data = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes(); setMixnodes({ data, isLoading: false }); } catch (error) { setMixnodes({ @@ -88,10 +90,24 @@ export const MainContextProvider: React.FC = ({ children }) => { isLoading: false, }); } + return data; }; - const filterMixnodes = (arr: MixNodeResponse) => { - setMixnodes({ data: arr, isLoading: false }); + + 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.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 () => { try { const data = await Api.fetchGateways(); @@ -144,6 +160,7 @@ export const MainContextProvider: React.FC = ({ children }) => { })); updateNav(updated); }; + React.useEffect(() => { Promise.all([fetchOverviewSummary(), fetchGateways(), fetchValidators(), fetchBlock(), fetchCountryData()]); }, []); diff --git a/explorer/src/hooks/useIsMobile.ts b/explorer/src/hooks/useIsMobile.ts new file mode 100644 index 0000000000..225964b464 --- /dev/null +++ b/explorer/src/hooks/useIsMobile.ts @@ -0,0 +1,9 @@ +import { Breakpoint, useMediaQuery } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; + +export const useIsMobile = (queryInput: number | Breakpoint = 'md') => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down(queryInput)); + + return isMobile; +}; diff --git a/explorer/src/pages/Mixnodes/index.tsx b/explorer/src/pages/Mixnodes/index.tsx index 4427ba8b46..33ebb84993 100644 --- a/explorer/src/pages/Mixnodes/index.tsx +++ b/explorer/src/pages/Mixnodes/index.tsx @@ -290,6 +290,7 @@ export const PageMixnodes: React.FC = () => { onChangePageSize={handlePageSize} pageSize={pageSize} searchTerm={searchTerm} + withFilters /> number; + tooltipInfo?: string; +}; + +export type TFilters = { [key in EnumFilterKey]: TFilterItem }; diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index d28f07c38f..fa000a3381 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-gateway" -version = "1.0.1" +version = "1.0.2" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Mixnet Gateway" edition = "2021" diff --git a/gateway/src/commands/init.rs b/gateway/src/commands/init.rs index 680e2770e7..eee994fbf2 100644 --- a/gateway/src/commands/init.rs +++ b/gateway/src/commands/init.rs @@ -156,6 +156,8 @@ pub async fn execute(args: &Init) { #[cfg(test)] mod tests { + use network_defaults::var_names::BECH32_PREFIX; + use crate::node::{storage::InMemStorage, Gateway}; use super::*; @@ -180,6 +182,7 @@ mod tests { #[cfg(all(feature = "eth", not(feature = "coconut")))] eth_endpoint: "".to_string(), }; + std::env::set_var(BECH32_PREFIX, "n"); let config = Config::new(&args.id); let config = override_config(config, OverrideConfig::from(args.clone())); diff --git a/gateway/src/commands/mod.rs b/gateway/src/commands/mod.rs index 2e3272674d..821c354733 100644 --- a/gateway/src/commands/mod.rs +++ b/gateway/src/commands/mod.rs @@ -6,8 +6,11 @@ use std::{process, str::FromStr}; use crate::{config::Config, Cli}; use clap::Subcommand; use colored::Colorize; +use config::parse_validators; use crypto::bech32_address_validation; -use url::Url; +use network_defaults::var_names::{ + API_VALIDATOR, BECH32_PREFIX, CONFIGURED, NYMD_VALIDATOR, STATISTICS_SERVICE_DOMAIN_ADDRESS, +}; pub(crate) mod init; pub(crate) mod node_details; @@ -66,17 +69,6 @@ pub(crate) async fn execute(args: Cli) { } } -fn parse_validators(raw: &str) -> Vec { - raw.split(',') - .map(|raw_validator| { - raw_validator - .trim() - .parse() - .expect("one of the provided validator api urls is invalid") - }) - .collect() -} - pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config { let mut was_host_overridden = false; if let Some(host) = args.host { @@ -109,14 +101,28 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi .parse() .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_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) = 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(wallet_address) = args.wallet_address { @@ -166,6 +172,7 @@ pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Confi /// Ensures that a given bech32 address is valid, or exits pub(crate) fn validate_bech32_address_or_exit(address: &str) { + let prefix = std::env::var(BECH32_PREFIX).expect("bech32 prefix not set"); if let Err(bech32_address_validation::Bech32Error::DecodeFailed(err)) = bech32_address_validation::try_bech32_decode(address) { @@ -176,7 +183,7 @@ pub(crate) fn validate_bech32_address_or_exit(address: &str) { } if let Err(bech32_address_validation::Bech32Error::WrongPrefix(err)) = - bech32_address_validation::validate_bech32_prefix(address) + bech32_address_validation::validate_bech32_prefix(&prefix, address) { let error_message = format!("Error: wallet address type is wrong, {}", err).red(); println!("{}", error_message); diff --git a/gateway/src/commands/upgrade.rs b/gateway/src/commands/upgrade.rs index d7f3d6e947..cc62c5219e 100644 --- a/gateway/src/commands/upgrade.rs +++ b/gateway/src/commands/upgrade.rs @@ -3,7 +3,6 @@ use crate::config::{Config, MISSING_VALUE}; use clap::Args; -use config::defaults::default_api_endpoints; use config::NymConfig; use std::fmt::Display; use std::process; @@ -104,14 +103,7 @@ fn minor_0_12_upgrade( print_start_upgrade(&config_version, &to_version); - println!( - "Setting validator API endpoints to {:?}", - default_api_endpoints() - ); - - let upgraded_config = config - .with_custom_version(to_version.to_string().as_ref()) - .with_custom_validator_apis(default_api_endpoints()); + 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); diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index 6922cbe75d..d3acfacf52 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::template::config_template; -use config::defaults::*; +use config::defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT}; use config::NymConfig; use log::error; use serde::{Deserialize, Serialize}; @@ -422,9 +422,9 @@ impl Default for Gateway { #[cfg(not(feature = "coconut"))] eth_endpoint: "".to_string(), enabled_statistics: false, - statistics_service_url: default_statistics_service_url(), - validator_api_urls: default_api_endpoints(), - validator_nymd_urls: default_nymd_endpoints(), + statistics_service_url: Url::from_str("http://127.0.0.1").unwrap(), + validator_api_urls: vec![], + validator_nymd_urls: vec![], cosmos_mnemonic: bip39::Mnemonic::from_str("exact antique hybrid width raise anchor puzzle degree fee quit long crack net vague hip despair write put useless civil mechanic broom music day").unwrap(), nym_root_directory: Config::default_root_directory(), persistent_storage: Default::default(), diff --git a/gateway/src/main.rs b/gateway/src/main.rs index a7ea4ff1da..0651b37237 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use clap::{crate_version, Parser}; -use network_defaults::DEFAULT_NETWORK; +use network_defaults::setup_env; use once_cell::sync::OnceCell; mod commands; @@ -19,13 +19,16 @@ fn long_version_static() -> &'static str { #[derive(Parser)] #[clap(author = "Nymtech", version, about, long_version = long_version_static())] struct Cli { + /// Path pointing to an env file that configures the gateway. + #[clap(long)] + pub(crate) config_env_file: Option, + #[clap(subcommand)] command: commands::Commands, } #[tokio::main] async fn main() { - dotenv::dotenv().ok(); setup_logging(); println!("{}", banner()); LONG_VERSION @@ -33,6 +36,7 @@ async fn main() { .expect("Failed to set long about text"); let args = Cli::parse(); + setup_env(args.config_env_file.clone()); commands::execute(args).await; } @@ -64,7 +68,6 @@ fn long_version() -> String { {:<20}{} {:<20}{} {:<20}{} -{:<20}{} "#, "Build Timestamp:", env!("VERGEN_BUILD_TIMESTAMP"), @@ -81,9 +84,7 @@ fn long_version() -> String { "rustc Channel:", env!("VERGEN_RUSTC_CHANNEL"), "cargo Profile:", - env!("VERGEN_CARGO_PROFILE"), - "Network:", - DEFAULT_NETWORK + env!("VERGEN_CARGO_PROFILE") ) } 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 b372cbcc94..cf11305bab 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -64,7 +64,7 @@ pub(crate) enum RequestHandlingError { EthContractError(#[from] web3::contract::Error), #[cfg(feature = "coconut")] - #[error("Validator API error")] + #[error("Validator API error - {0}")] APIError(#[from] validator_client::ValidatorClientError), #[cfg(feature = "coconut")] @@ -72,8 +72,8 @@ pub(crate) enum RequestHandlingError { NotEnoughValidatorAPIs { received: usize, needed: usize }, #[cfg(feature = "coconut")] - #[error("Validator API {url} misbehaved in the bandwidth redemption protocol: {reason}")] - MisbehavingAPI { url: String, reason: String }, + #[error("There was a problem with the proposal id: {reason}")] + ProposalIdError { reason: String }, #[cfg(feature = "coconut")] #[error("Coconut interface error - {0}")] diff --git a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs index 48106eda07..8f0ffcd098 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs @@ -6,10 +6,10 @@ use std::time::{Duration, SystemTime}; use log::*; use coconut_interface::{Credential, VerificationKey}; -use network_defaults::MIX_DENOM; use validator_client::{ nymd::{ - traits::{MultisigSigningClient, QueryClient}, + cosmwasm_client::logs::find_attribute, + traits::{CoconutBandwidthSigningClient, MultisigQueryClient, MultisigSigningClient}, Coin, Fee, NymdClient, SigningNymdClient, }, ApiClient, @@ -23,6 +23,7 @@ const MAX_FEEGRANT_UNYM: u128 = 10000; pub(crate) struct CoconutVerifier { api_clients: Vec, nymd_client: NymdClient, + mix_denom_base: String, aggregated_verification_key: VerificationKey, } @@ -30,6 +31,7 @@ impl CoconutVerifier { pub fn new( api_clients: Vec, nymd_client: NymdClient, + mix_denom_base: String, aggregated_verification_key: VerificationKey, ) -> Result { if api_clients.is_empty() { @@ -41,6 +43,7 @@ impl CoconutVerifier { Ok(CoconutVerifier { api_clients, nymd_client, + mix_denom_base, aggregated_verification_key, }) } @@ -54,45 +57,32 @@ impl CoconutVerifier { // isn't enough let revoke_fee = Some(Fee::Auto(Some(1.5))); - let first_api_client = self - .api_clients - .get(0) - .expect("This shouldn't happen, as we check for length in constructor"); - - let first_api_cosmos_addr = first_api_client.get_cosmos_address().await?.addr; - self.nymd_client - .grant_allowance( - &first_api_cosmos_addr, - vec![Coin::new(MAX_FEEGRANT_UNYM, MIX_DENOM.base)], - SystemTime::now().checked_add(Duration::from_secs(ONE_HOUR_SEC)), - // It would be nice to be able to filter deeper, but for now only the msg type filter is avaialable - vec![String::from("/cosmwasm.wasm.v1.MsgExecuteContract")], - "Create allowance to propose the release of funds".to_string(), + let res = self + .nymd_client + .spend_credential( + Coin::new( + credential.voucher_value().into(), + self.mix_denom_base.clone(), + ), + credential.blinded_serial_number(), + self.nymd_client.address().to_string(), None, ) .await?; - - let req = validator_api_requests::coconut::ProposeReleaseFundsRequestBody::new( - credential.clone(), - self.nymd_client.address().clone(), - ); - let ret = first_api_client.propose_release_funds(&req).await; - - self.nymd_client - .revoke_allowance( - &first_api_cosmos_addr, - "Cleanup the previous allowance for releasing funds".to_string(), - revoke_fee.clone(), - ) - .await?; - - let proposal_id = ret?.proposal_id; + let proposal_id = find_attribute(&res.logs, "wasm", "proposal_id") + .ok_or(RequestHandlingError::ProposalIdError { + reason: String::from("proposal id not found"), + })? + .value + .parse::() + .map_err(|_| RequestHandlingError::ProposalIdError { + reason: String::from("proposal id could not be parsed to u64"), + })?; let proposal = self.nymd_client.get_proposal(proposal_id).await?; if !credential.has_blinded_serial_number(&proposal.description)? { - return Err(RequestHandlingError::MisbehavingAPI { - url: first_api_client.validator_api.current_url().to_string(), - reason: String::from("Created proposal with different serial number"), + return Err(RequestHandlingError::ProposalIdError { + reason: String::from("proposal has different serial number"), }); } @@ -101,12 +91,12 @@ impl CoconutVerifier { proposal_id, self.nymd_client.address().clone(), ); - for client in self.api_clients.iter().skip(1) { + for client in self.api_clients.iter() { let api_cosmos_addr = client.get_cosmos_address().await?.addr; self.nymd_client .grant_allowance( &api_cosmos_addr, - vec![Coin::new(MAX_FEEGRANT_UNYM, MIX_DENOM.base)], + vec![Coin::new(MAX_FEEGRANT_UNYM, self.mix_denom_base.clone())], SystemTime::now().checked_add(Duration::from_secs(ONE_HOUR_SEC)), // It would be nice to be able to filter deeper, but for now only the msg type filter is avaialable vec![String::from("/cosmwasm.wasm.v1.MsgExecuteContract")], diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index b2354e7a3b..379aae5fe2 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -9,10 +9,10 @@ use crate::node::client_handling::websocket; use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; use crate::node::statistics::collector::GatewayStatisticsCollector; use crate::node::storage::Storage; -use config::defaults::DEFAULT_NETWORK; use crypto::asymmetric::{encryption, identity}; use log::*; use mixnet_client::forwarder::{MixForwardingSender, PacketForwarder}; +use network_defaults::NymNetworkDetails; use rand::seq::SliceRandom; use rand::thread_rng; use statistics_common::collector::StatisticsSender; @@ -249,7 +249,8 @@ where .choose(&mut thread_rng()) .expect("The list of validators is empty"); - let client_config = nymd::Config::try_from_nym_network_details(&DEFAULT_NETWORK.details()) + let network_details = NymNetworkDetails::new_from_env(); + let client_config = nymd::Config::try_from_nym_network_details(&network_details) .expect("failed to construct valid validator client config with the provided network"); validator_client::nymd::NymdClient::connect_with_mnemonic( @@ -316,6 +317,7 @@ where let coconut_verifier = CoconutVerifier::new( self.all_api_clients(), nymd_client, + std::env::var(network_defaults::var_names::MIX_DENOM).expect("mix denom base not set"), validators_verification_key, ) .expect("Could not create coconut verifier"); @@ -334,6 +336,7 @@ where if self.config.get_enabled_statistics() { let statistics_service_url = self.config.get_statistics_service_url(); let stats_collector = GatewayStatisticsCollector::new( + self.identity_keypair.public_key().to_base58_string(), active_clients_store.clone(), statistics_service_url, ); diff --git a/gateway/src/node/statistics/collector.rs b/gateway/src/node/statistics/collector.rs index 18f290d006..62014d0d16 100644 --- a/gateway/src/node/statistics/collector.rs +++ b/gateway/src/node/statistics/collector.rs @@ -14,13 +14,19 @@ use statistics_common::{ use crate::node::client_handling::active_clients::ActiveClientsStore; pub(crate) struct GatewayStatisticsCollector { + gateway_id: String, active_clients_store: ActiveClientsStore, statistics_service_url: Url, } impl GatewayStatisticsCollector { - pub fn new(active_clients_store: ActiveClientsStore, statistics_service_url: Url) -> Self { + pub fn new( + gateway_id: String, + active_clients_store: ActiveClientsStore, + statistics_service_url: Url, + ) -> Self { GatewayStatisticsCollector { + gateway_id, active_clients_store, statistics_service_url, } @@ -35,7 +41,10 @@ impl StatisticsCollector for GatewayStatisticsCollector { timestamp: DateTime, ) -> StatsMessage { let inbox_count = self.active_clients_store.size() as u32; - let stats_data = vec![StatsData::Gateway(StatsGatewayData { inbox_count })]; + let stats_data = vec![StatsData::Gateway(StatsGatewayData::new( + self.gateway_id.clone(), + inbox_count, + ))]; StatsMessage { stats_data, interval_seconds: interval.as_secs() as u32, diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index 4fb441b0ce..259649ce0b 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-mixnode" -version = "1.0.1" +version = "1.0.2" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ", diff --git a/mixnode/src/commands/mod.rs b/mixnode/src/commands/mod.rs index 1d924bcf63..62468ca809 100644 --- a/mixnode/src/commands/mod.rs +++ b/mixnode/src/commands/mod.rs @@ -6,8 +6,11 @@ use std::process; use crate::{config::Config, Cli}; use clap::Subcommand; use colored::Colorize; +use config::{ + defaults::var_names::{API_VALIDATOR, BECH32_PREFIX, CONFIGURED}, + parse_validators, +}; use crypto::bech32_address_validation; -use url::Url; mod describe; mod init; @@ -60,17 +63,6 @@ pub(crate) async fn execute(args: Cli) { } } -fn parse_validators(raw: &str) -> Vec { - raw.split(',') - .map(|raw_validator| { - raw_validator - .trim() - .parse() - .expect("one of the provided validator api urls is invalid") - }) - .collect() -} - fn override_config(mut config: Config, args: OverrideConfig) -> Config { let mut was_host_overridden = false; if let Some(host) = args.host { @@ -92,6 +84,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(ref announce_host) = args.announce_host { @@ -112,6 +107,7 @@ fn override_config(mut config: Config, args: OverrideConfig) -> Config { /// Ensures that a given bech32 address is valid, or exits pub(crate) fn validate_bech32_address_or_exit(address: &str) { + let prefix = std::env::var(BECH32_PREFIX).expect("bech32 prefix not set"); if let Err(bech32_address_validation::Bech32Error::DecodeFailed(err)) = bech32_address_validation::try_bech32_decode(address) { @@ -122,7 +118,7 @@ pub(crate) fn validate_bech32_address_or_exit(address: &str) { } if let Err(bech32_address_validation::Bech32Error::WrongPrefix(err)) = - bech32_address_validation::validate_bech32_prefix(address) + bech32_address_validation::validate_bech32_prefix(&prefix, address) { let error_message = format!("Error: wallet address type is wrong, {}", err).red(); println!("{}", error_message); diff --git a/mixnode/src/commands/upgrade.rs b/mixnode/src/commands/upgrade.rs index 0b0241e03c..85352e77c3 100644 --- a/mixnode/src/commands/upgrade.rs +++ b/mixnode/src/commands/upgrade.rs @@ -3,7 +3,6 @@ use crate::config::{missing_string_value, Config}; use clap::Args; -use config::defaults::default_api_endpoints; use config::NymConfig; use std::fmt::Display; use std::process; @@ -104,14 +103,7 @@ fn minor_0_12_upgrade( print_start_upgrade(&config_version, &to_version); - println!( - "Setting validator API endpoints to {:?}", - default_api_endpoints() - ); - - let upgraded_config = config - .with_custom_version(to_version.to_string().as_ref()) - .with_custom_validator_apis(default_api_endpoints()); + 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); diff --git a/mixnode/src/config/mod.rs b/mixnode/src/config/mod.rs index 7d3b520da9..14279ccdc2 100644 --- a/mixnode/src/config/mod.rs +++ b/mixnode/src/config/mod.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::template::config_template; -use config::defaults::*; +use config::defaults::{ + DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, +}; use config::NymConfig; use serde::{Deserialize, Deserializer, Serialize}; use std::net::{IpAddr, SocketAddr}; @@ -401,7 +403,7 @@ impl Default for MixNode { public_identity_key_file: Default::default(), private_sphinx_key_file: Default::default(), public_sphinx_key_file: Default::default(), - validator_api_urls: default_api_endpoints(), + validator_api_urls: vec![], nym_root_directory: Config::default_root_directory(), wallet_address: "nymXXXXXXXX".to_string(), } diff --git a/mixnode/src/main.rs b/mixnode/src/main.rs index 44637eace1..2e30e00976 100644 --- a/mixnode/src/main.rs +++ b/mixnode/src/main.rs @@ -4,7 +4,7 @@ extern crate rocket; // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use ::config::defaults::DEFAULT_NETWORK; +use ::config::defaults::setup_env; use clap::{crate_version, Parser}; use lazy_static::lazy_static; @@ -24,17 +24,21 @@ fn long_version_static() -> &'static str { #[derive(Parser)] #[clap(author = "Nymtech", version, about, long_version = long_version_static())] struct Cli { + /// Path pointing to an env file that configures the mixnode. + #[clap(long)] + pub(crate) config_env_file: Option, + #[clap(subcommand)] command: commands::Commands, } #[tokio::main] async fn main() { - dotenv::dotenv().ok(); setup_logging(); println!("{}", banner()); let args = Cli::parse(); + setup_env(args.config_env_file.clone()); commands::execute(args).await; } @@ -66,7 +70,6 @@ fn long_version() -> String { {:<20}{} {:<20}{} {:<20}{} -{:<20}{} "#, "Build Timestamp:", env!("VERGEN_BUILD_TIMESTAMP"), @@ -83,9 +86,7 @@ fn long_version() -> String { "rustc Channel:", env!("VERGEN_RUSTC_CHANNEL"), "cargo Profile:", - env!("VERGEN_CARGO_PROFILE"), - "Network:", - DEFAULT_NETWORK + env!("VERGEN_CARGO_PROFILE") ) } diff --git a/nym-chitchat/Cargo.lock b/nym-chitchat/Cargo.lock new file mode 100644 index 0000000000..408a2c30f8 --- /dev/null +++ b/nym-chitchat/Cargo.lock @@ -0,0 +1,1983 @@ +# This file is automatically @generated by Cargo. +# 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" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "opaque-debug", +] + +[[package]] +name = "aes-gcm" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" + +[[package]] +name = "assert_cmd" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e" +dependencies = [ + "bstr", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "async-trait" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", +] + +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + +[[package]] +name = "bytes" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" + +[[package]] +name = "bytes" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chitchat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e16bc6492a6744d6e322e22562c3ab89404ba326aedb65a570799d2e042687" +dependencies = [ + "anyhow", + "async-trait", + "bytes 1.2.0", + "rand", + "serde", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "chitchat-test" +version = "0.3.0" +dependencies = [ + "anyhow", + "assert_cmd", + "chitchat", + "cool-id-generator", + "env_logger", + "once_cell", + "poem", + "poem-openapi", + "predicates", + "reqwest", + "serde", + "serde_json", + "structopt", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time 0.1.44", + "winapi", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "clap" +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", + "unicode-width", + "vec_map", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" +dependencies = [ + "aes-gcm", + "base64 0.13.0", + "hkdf", + "hmac", + "percent-encoding", + "rand", + "sha2", + "subtle", + "time 0.3.11", + "version_check", +] + +[[package]] +name = "cool-id-generator" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "946cf88fb171128bc025588b9b408982814ace1a1fb31ffb9bfa63d955f0b43d" +dependencies = [ + "rand", +] + +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[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", + "syn", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "h2" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +dependencies = [ + "bytes 1.2.0", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "headers" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +dependencies = [ + "base64 0.13.0", + "bitflags", + "bytes 1.2.0", + "headers-core", + "http", + "httpdate", + "mime", + "sha-1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +dependencies = [ + "bytes 1.2.0", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes 1.2.0", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +dependencies = [ + "bytes 1.2.0", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" + +[[package]] +name = "js-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", +] + +[[package]] +name = "multer" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a30ba6d97eb198c5e8a35d67d5779d6680cca35652a60ee90fc23dc431d4fde8" +dependencies = [ + "bytes 1.2.0", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin", + "tokio", + "version_check", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "once_cell" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", + "windows-sys", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "poem" +version = "1.3.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01e479ed477ea11939bea754109eaa024e55bf1639e19c1753fb250026351da8" +dependencies = [ + "async-trait", + "bytes 1.2.0", + "chrono", + "cookie", + "futures-util", + "headers", + "http", + "hyper", + "mime", + "multer", + "parking_lot", + "percent-encoding", + "pin-project-lite", + "poem-derive", + "regex", + "rfc7239", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tempfile", + "thiserror", + "time 0.3.11", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "typed-headers", +] + +[[package]] +name = "poem-derive" +version = "1.3.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d454ac05b5299eb8e6261214fcb823f0c3c1b02f47167f8809c9dc2db0ee033" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "poem-openapi" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c27cd1c97d45a9552b0c80bab1d51f93a1050c3f7f91e9312239b3b87c336b0f" +dependencies = [ + "base64 0.13.0", + "bytes 1.2.0", + "derive_more", + "futures-util", + "mime", + "num-traits", + "poem", + "poem-openapi-derive", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "serde_yaml", + "thiserror", + "tokio", +] + +[[package]] +name = "poem-openapi-derive" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5105e426a8a747fefc4be06ab7705cb63bc3ab1b2db373867e1d83b1d95877a" +dependencies = [ + "Inflector", + "darling", + "http", + "indexmap", + "mime", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn", + "thiserror", +] + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "predicates" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" + +[[package]] +name = "predicates-tree" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" +dependencies = [ + "predicates-core", + "termtree", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "reqwest" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +dependencies = [ + "base64 0.13.0", + "bytes 1.2.0", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rfc7239" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "087317b3cf7eb481f13bd9025d729324b7cd068d6f470e2d76d049e191f5ba47" +dependencies = [ + "uncased", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "ryu" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" + +[[package]] +name = "serde" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "spin" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termtree" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e" +dependencies = [ + "autocfg", + "bytes 1.2.0", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "pin-project-lite", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-stream" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-util" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +dependencies = [ + "bytes 1.2.0", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a713421342a5a666b7577783721d3117f1b69a393df803ee17bb73b1e122a59" +dependencies = [ + "ansi_term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typed-headers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3179a61e9eccceead5f1574fd173cf2e162ac42638b9bf214c6ad0baf7efa24a" +dependencies = [ + "base64 0.11.0", + "bytes 0.5.6", + "chrono", + "http", + "mime", +] + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "uncased" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" + +[[package]] +name = "unicode-normalization" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" + +[[package]] +name = "web-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[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.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[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.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/nym-chitchat/Cargo.toml b/nym-chitchat/Cargo.toml new file mode 100644 index 0000000000..eae29e5738 --- /dev/null +++ b/nym-chitchat/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "chitchat-test" +version = "0.3.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chitchat = "0.4" +poem = "1" +poem-openapi = {version="2", features = ["swagger-ui"] } +structopt = "0.3" +tokio = { version = "1.14.0", features = ["net", "sync", "rt-multi-thread", "macros", "time"] } +serde = { version="1", features=["derive"] } +serde_json = "1" +anyhow = "1" +once_cell = "1" +tracing = "0.1" +tracing-subscriber = "0.3" +cool-id-generator = "1" +env_logger = "0.9" + +[dev-dependencies] +assert_cmd = "2" +predicates = "2" +reqwest = { version = "0.11", default-features=false, features = ["blocking", "json"] } + +[workspace] diff --git a/nym-chitchat/README.md b/nym-chitchat/README.md new file mode 100644 index 0000000000..2fd61cd169 --- /dev/null +++ b/nym-chitchat/README.md @@ -0,0 +1,15 @@ +# Chitchat test + +Runs simple chitchat servers, mostly copied over from https://github.com/quickwit-oss/chitchat + +## Example + +```bash +# Starts 5 servers and joins them into a cluster on localhost ports 10000-10004 +# All servers print cluster state on `/` ie 127.0.0.1:10000 +# `/docs` endpoint has an open api with a key value setter, set it on one node and observe how the state propagates to the other nodes +# NodeState is a regular BTreeMap +./run-servers.sh + +# run killall chitchat-test after you're done, as the servers will continue to run forever in the background +``` diff --git a/nym-chitchat/run-servers.sh b/nym-chitchat/run-servers.sh new file mode 100755 index 0000000000..9e08abac3c --- /dev/null +++ b/nym-chitchat/run-servers.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +killall chitchat-test + +cargo build --release + +for i in $(seq 10000 10004) +do + listen_addr="127.0.0.1:$i"; + echo ${listen_addr}; + cargo run --release -- --listen_addr ${listen_addr} --seed 127.0.0.1:10000 --node_id node_$i & +done; + +read +kill 0 diff --git a/nym-chitchat/src/main.rs b/nym-chitchat/src/main.rs new file mode 100644 index 0000000000..dc0899913d --- /dev/null +++ b/nym-chitchat/src/main.rs @@ -0,0 +1,123 @@ +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +use chitchat::transport::UdpTransport; +use chitchat::{spawn_chitchat, Chitchat, ChitchatConfig, FailureDetectorConfig, NodeId}; +use cool_id_generator::Size; +use poem::listener::TcpListener; +use poem::{Route, Server}; +use poem_openapi::param::Query; +use poem_openapi::payload::Json; +use poem_openapi::{OpenApi, OpenApiService}; +use structopt::StructOpt; +use tokio::sync::Mutex; + +use chitchat::ClusterStateSnapshot; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct ApiResponse { + pub cluster_id: String, + pub cluster_state: ClusterStateSnapshot, + pub live_nodes: Vec, + pub dead_nodes: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SetKeyValueResponse { + pub status: bool, +} + +struct Api { + chitchat: Arc>, +} + +#[OpenApi] +impl Api { + /// Chitchat state + #[oai(path = "/", method = "get")] + async fn index(&self) -> Json { + let chitchat_guard = self.chitchat.lock().await; + let response = ApiResponse { + cluster_id: chitchat_guard.cluster_id().to_string(), + cluster_state: chitchat_guard.state_snapshot(), + live_nodes: chitchat_guard.live_nodes().cloned().collect::>(), + dead_nodes: chitchat_guard.dead_nodes().cloned().collect::>(), + }; + Json(serde_json::to_value(&response).unwrap()) + } + + /// Set a key & value on this node (with no validation). + #[oai(path = "/set_kv/", method = "get")] + async fn set_kv(&self, key: Query, value: Query) -> Json { + let mut chitchat_guard = self.chitchat.lock().await; + + let cc_state = chitchat_guard.self_node_state(); + cc_state.set(key.as_str(), value.as_str()); + + Json(serde_json::to_value(&SetKeyValueResponse { status: true }).unwrap()) + } +} + +#[derive(Debug, StructOpt)] +#[structopt(name = "chitchat", about = "Chitchat test server.")] +struct Opt { + /// Defines the socket addr on which we should listen to. + #[structopt(long = "listen_addr", default_value = "127.0.0.1:10000")] + listen_addr: SocketAddr, + /// Defines the socket_address (host:port) other servers should use to + /// reach this server. + /// + /// It defaults to the listen address, but this is only valid + /// when all server are running on the same server. + #[structopt(long = "public_addr")] + public_addr: Option, + + /// Node id. Has to be unique. If None, the node_id will be generated from + /// the public_addr and a random suffix. + #[structopt(long = "node_id")] + node_id: Option, + + #[structopt(long = "seed")] + seeds: Vec, + + #[structopt(long = "interval_ms", default_value = "500")] + interval: u64, +} + +fn generate_server_id(public_addr: SocketAddr) -> String { + let cool_id = cool_id_generator::get_id(Size::Medium); + format!("server:{}-{}", public_addr, cool_id) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + let opt = Opt::from_args(); + println!("{:?}", opt); + let public_addr = opt.public_addr.unwrap_or(opt.listen_addr); + let node_id_str = opt + .node_id + .unwrap_or_else(|| generate_server_id(public_addr)); + let node_id = NodeId::new(node_id_str, public_addr); + let config = ChitchatConfig { + node_id, + cluster_id: "testing".to_string(), + gossip_interval: Duration::from_millis(opt.interval), + listen_addr: opt.listen_addr, + seed_nodes: opt.seeds.clone(), + failure_detector_config: FailureDetectorConfig::default(), + }; + let chitchat_handler = spawn_chitchat(config, Vec::new(), &UdpTransport).await?; + let chitchat = chitchat_handler.chitchat(); + let api = Api { chitchat }; + let api_service = OpenApiService::new(api, "Hello World", "1.0") + .server(&format!("http://{}/", opt.listen_addr)); + let docs = api_service.swagger_ui(); + let app = Route::new().nest("/", api_service).nest("/docs", docs); + Server::new(TcpListener::bind(&opt.listen_addr)) + .run(app) + .await?; + Ok(()) +} diff --git a/nym-connect/CHANGELOG.md b/nym-connect/CHANGELOG.md new file mode 100644 index 0000000000..0baec4fd35 --- /dev/null +++ b/nym-connect/CHANGELOG.md @@ -0,0 +1,14 @@ +## [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 3c331dab3e..229016b1ce 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -680,6 +680,7 @@ name = "coconut-bandwidth-contract-common" version = "0.1.0" dependencies = [ "cosmwasm-std", + "multisig-contract-common", "schemars", "serde", ] @@ -805,8 +806,7 @@ dependencies = [ [[package]] name = "cosmos-sdk-proto" version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ca04d3795c18023c221a2143b29de9c70668ecb22d17783bc02ee780c6c404" +source = "git+https://github.com/neacsu/cosmos-rust?branch=neacsu/feegrant_support#f63ded63ec13e753ebe8bdafe9dc503df265d67d" dependencies = [ "prost", "prost-types", @@ -816,8 +816,7 @@ dependencies = [ [[package]] name = "cosmrs" version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6989fdb6267eccb52762530b79ce0b385f4eaeb8b786522a95512e9bebb268c2" +source = "git+https://github.com/neacsu/cosmos-rust?branch=neacsu/feegrant_support#f63ded63ec13e753ebe8bdafe9dc503df265d67d" dependencies = [ "bip32", "cosmos-sdk-proto", @@ -927,7 +926,6 @@ dependencies = [ "bls12_381", "coconut-interface", "crypto", - "network-defaults", "thiserror", "url", "validator-api-requests", @@ -3201,7 +3199,6 @@ dependencies = [ "cosmwasm-std", "fixed", "log", - "network-defaults", "schemars", "serde", "serde_repr", @@ -3273,6 +3270,7 @@ name = "network-defaults" version = "0.1.0" dependencies = [ "cfg-if", + "dotenv", "hex-literal", "once_cell", "serde", @@ -3406,7 +3404,7 @@ dependencies = [ [[package]] name = "nym-connect" -version = "1.0.0" +version = "1.0.1" dependencies = [ "bip39", "client-core", @@ -3438,7 +3436,7 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.0.1" +version = "1.0.2" dependencies = [ "clap", "client-core", @@ -3446,7 +3444,6 @@ dependencies = [ "credential-storage", "crypto", "dirs", - "dotenv", "futures", "gateway-client", "gateway-requests", @@ -6485,7 +6482,6 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vesting-contract" version = "1.0.1" dependencies = [ - "config", "cosmwasm-std", "cw-storage-plus", "mixnet-contract-common", diff --git a/nym-connect/README.md b/nym-connect/README.md index 5433a366d6..5a247235ab 100644 --- a/nym-connect/README.md +++ b/nym-connect/README.md @@ -32,7 +32,7 @@ yarn install ## Development mode -You can compile nym-connectin development mode by running the following command inside the `nym-connect` directory: +You can compile nym-connect in development mode by running the following command inside the `nym-connect` directory: ``` yarn dev diff --git a/nym-connect/src-tauri/Cargo.toml b/nym-connect/src-tauri/Cargo.toml index 23bfeed153..a529005681 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.0" +version = "1.0.1" description = "nym-connect" authors = ["Nym Technologies SA"] license = "" @@ -39,7 +39,7 @@ tokio = { version = "1.19.1", features = ["sync", "time"] } url = "2.2" client-core = { path = "../../clients/client-core" } -config = { path = "../../common/config" } +config-common = { path = "../../common/config", package = "config" } 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 5f4ce22e21..532f861f9e 100644 --- a/nym-connect/src-tauri/src/config/mod.rs +++ b/nym-connect/src-tauri/src/config/mod.rs @@ -6,7 +6,7 @@ use tap::TapFallible; use tokio::sync::RwLock; use client_core::config::Config as BaseConfig; -use config::NymConfig; +use config_common::NymConfig; use nym_socks5::client::config::Config as Socks5Config; use crate::{ @@ -135,6 +135,12 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str .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() + .set_custom_validator_apis(config_common::parse_validators(&raw_validators)); + } + let gateway = setup_gateway( &id, register_gateway, diff --git a/nym-connect/src-tauri/src/main.rs b/nym-connect/src-tauri/src/main.rs index 795533287c..e1117c7516 100644 --- a/nym-connect/src-tauri/src/main.rs +++ b/nym-connect/src-tauri/src/main.rs @@ -5,6 +5,7 @@ use std::sync::Arc; +use config_common::defaults::setup_env; use tauri::Menu; use tokio::sync::RwLock; @@ -24,6 +25,7 @@ mod window; fn main() { setup_logging(); + setup_env(None); println!("Starting up..."); // As per breaking change description here diff --git a/nym-connect/src-tauri/src/state.rs b/nym-connect/src-tauri/src/state.rs index d9a14dc9d0..2fce640487 100644 --- a/nym-connect/src-tauri/src/state.rs +++ b/nym-connect/src-tauri/src/state.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use ::config::NymConfig; +use ::config_common::NymConfig; use futures::SinkExt; use tap::TapFallible; use tauri::Manager; diff --git a/nym-connect/src-tauri/src/tasks.rs b/nym-connect/src-tauri/src/tasks.rs index 8e44f1bb40..3729d4b4da 100644 --- a/nym-connect/src-tauri/src/tasks.rs +++ b/nym-connect/src-tauri/src/tasks.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use tap::TapFallible; use tokio::sync::RwLock; -use config::NymConfig; +use config_common::NymConfig; #[cfg(not(feature = "coconut"))] use nym_socks5::client::NymClient as Socks5NymClient; use nym_socks5::client::{config::Config as Socks5Config, Socks5ControlMessageSender}; diff --git a/nym-connect/src-tauri/tauri.conf.json b/nym-connect/src-tauri/tauri.conf.json index b2699a20b7..97ea3f1af8 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.0" + "version": "1.0.1" }, "build": { "distDir": "../dist", diff --git a/nym-connect/src/layouts/DefaultLayout.tsx b/nym-connect/src/layouts/DefaultLayout.tsx index 42b6357442..9c329206d3 100644 --- a/nym-connect/src/layouts/DefaultLayout.tsx +++ b/nym-connect/src/layouts/DefaultLayout.tsx @@ -22,11 +22,14 @@ export const DefaultLayout: React.FC<{ }; return ( - - Connect, your privacy will be 100% protected thanks to the Nym Mixnet + + This is experimental software.
+ Do not rely on it for strong anonymity (yet).
- - You are not protected now + + Connect to the +
+ Nym mixnet for privacy.
// SPDX-License-Identifier: Apache-2.0 -use cosmrs::Denom; +use config::defaults::all::Network as ConfigNetwork; +use config::defaults::{mainnet, qa, sandbox, DenomDetails}; use serde::{Deserialize, Serialize}; use std::fmt; -use std::str::FromStr; use strum::EnumIter; -use config::defaults::all::Network as ConfigNetwork; -use config::defaults::{mainnet, qa, sandbox}; - #[allow(clippy::upper_case_acronyms)] #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( @@ -28,13 +25,19 @@ impl Network { self.to_string().to_lowercase() } - // this should be returning just a `&str`, but don't want to cause too many conflicts just yet... - pub fn base_mix_denom(&self) -> Denom { + pub fn mix_denom(&self) -> DenomDetails { match self { - // network defaults should be correctly formatted - Network::QA => Denom::from_str(qa::MIX_DENOM.base).unwrap(), - Network::SANDBOX => Denom::from_str(sandbox::MIX_DENOM.base).unwrap(), - Network::MAINNET => Denom::from_str(mainnet::MIX_DENOM.base).unwrap(), + Network::QA => qa::MIX_DENOM, + Network::SANDBOX => sandbox::MIX_DENOM, + Network::MAINNET => mainnet::MIX_DENOM, + } + } + + pub fn base_mix_denom(&self) -> &str { + match self { + Network::QA => qa::MIX_DENOM.base, + Network::SANDBOX => sandbox::MIX_DENOM.base, + Network::MAINNET => mainnet::MIX_DENOM.base, } } diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index f6ea6170fe..1ecea8e852 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.7" +version = "1.0.8" description = "Nym Native Wallet" authors = ["Nym Technologies SA"] license = "" @@ -32,14 +32,14 @@ log = "0.4" once_cell = "1.7.2" pretty_env_logger = "0.4" rand = "0.6.5" -reqwest = "0.11.9" +reqwest = {version = "0.11.9", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" strum = { version = "0.23", features = ["derive"] } tauri = { version = "=1.0.0-rc.2", features = ["clipboard-all", "shell-open", "updater", "window-maximize"] } tendermint-rpc = "0.23.0" thiserror = "1.0" -tokio = { version = "1.10", features = ["sync", "time"] } +tokio = { version = "1.10", features = ["full"] } toml = "0.5.8" url = "2.2" diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 265563947d..64a9bd7fe1 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -1,4 +1,5 @@ use nym_types::error::TypesError; +use nym_wallet_types::network::Network; use serde::{Serialize, Serializer}; use std::io; use std::num::ParseIntError; @@ -77,7 +78,7 @@ pub enum BackendError { #[error("No balance available for address {0}")] NoBalance(String), #[error("The provided network is not supported (yet)")] - NetworkNotSupported(config::defaults::all::Network), + NetworkNotSupported, #[error("Could not access the local data storage directory")] UnknownStorageDirectory, #[error("The wallet file already exists")] @@ -106,6 +107,10 @@ pub enum BackendError { FailedToDeriveAddress, #[error("{0}")] ValueParseError(#[from] ParseIntError), + #[error("The provided coin has an unknown denomination - {0}")] + UnknownCoinDenom(String), + #[error("Network {network} doesn't have any associated registered coin denoms")] + NoCoinsRegistered { network: Network }, } impl Serialize for BackendError { diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 01ac86b95c..488b26e7a9 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -4,9 +4,7 @@ )] use mixnet_contract_common::{Gateway, MixNode}; -use std::sync::Arc; use tauri::Menu; -use tokio::sync::RwLock; mod config; mod error; @@ -24,7 +22,7 @@ use crate::operations::simulate; use crate::operations::validator_api; use crate::operations::vesting; -use crate::state::State; +use crate::state::WalletState; #[allow(clippy::too_many_lines)] fn main() { @@ -32,7 +30,7 @@ fn main() { setup_logging(); tauri::Builder::default() - .manage(Arc::new(RwLock::new(State::default()))) + .manage(WalletState::default()) .invoke_handler(tauri::generate_handler![ mixnet::account::add_account_for_password, mixnet::account::archive_wallet_file, @@ -60,6 +58,8 @@ fn main() { mixnet::bond::unbond_gateway, mixnet::bond::unbond_mixnode, mixnet::bond::update_mixnode, + mixnet::bond::get_number_of_mixnode_delegators, + mixnet::bond::get_mix_node_description, mixnet::delegate::delegate_to_mixnode, mixnet::delegate::get_delegator_rewards, mixnet::delegate::get_pending_delegation_events, diff --git a/nym-wallet/src-tauri/src/network_config.rs b/nym-wallet/src-tauri/src/network_config.rs index 2a621922d5..43e872b717 100644 --- a/nym-wallet/src-tauri/src/network_config.rs +++ b/nym-wallet/src-tauri/src/network_config.rs @@ -1,20 +1,15 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::sync::Arc; - -use tokio::sync::RwLock; - +use crate::error::BackendError; +use crate::state::WalletState; use nym_wallet_types::network::Network as WalletNetwork; use nym_wallet_types::network_config::{Validator, ValidatorUrl, ValidatorUrls}; -use crate::error::BackendError; -use crate::state::State; - #[tauri::command] pub async fn get_validator_nymd_urls( network: WalletNetwork, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let state = state.read().await; let urls: Vec = state.get_nymd_urls(network).collect(); @@ -24,7 +19,7 @@ pub async fn get_validator_nymd_urls( #[tauri::command] pub async fn get_validator_api_urls( network: WalletNetwork, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let state = state.read().await; let urls: Vec = state.get_api_urls(network).collect(); @@ -35,7 +30,7 @@ pub async fn get_validator_api_urls( pub async fn select_validator_nymd_url( url: &str, network: WalletNetwork, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result<(), BackendError> { log::debug!("Selecting new validator nymd_url for {network}: {url}"); state @@ -49,7 +44,7 @@ pub async fn select_validator_nymd_url( pub async fn select_validator_api_url( url: &str, network: WalletNetwork, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result<(), BackendError> { log::debug!("Selecting new validator api_url for {network}: {url}"); state.write().await.select_validator_api_url(url, network)?; @@ -60,7 +55,7 @@ pub async fn select_validator_api_url( pub async fn add_validator( validator: Validator, network: WalletNetwork, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result<(), BackendError> { log::debug!("Add validator for {network}: {validator}"); let url = validator.try_into()?; @@ -72,7 +67,7 @@ pub async fn add_validator( pub async fn remove_validator( validator: Validator, network: WalletNetwork, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result<(), BackendError> { log::debug!("Remove validator for {network}: {validator}"); let url = validator.try_into()?; @@ -83,7 +78,7 @@ pub async fn remove_validator( // Update the list of validators by fecthing additional ones remotely. If it fails, just ignore. #[tauri::command] pub async fn update_validator_urls( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result<(), BackendError> { let mut w_state = state.write().await; let _r = w_state.fetch_updated_validator_urls().await; diff --git a/nym-wallet/src-tauri/src/operations/mixnet/account.rs b/nym-wallet/src-tauri/src/operations/mixnet/account.rs index 07e52cbdb9..bb83b8a7fe 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/account.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/account.rs @@ -1,8 +1,7 @@ use crate::config::{Config, CUSTOM_SIMULATED_GAS_MULTIPLIER}; use crate::error::BackendError; use crate::network_config; -use crate::nymd_client; -use crate::state::{State, WalletAccountIds}; +use crate::state::{WalletAccountIds, WalletState}; use crate::wallet_storage::{self, DEFAULT_LOGIN_ID}; use bip39::{Language, Mnemonic}; use config::defaults::all::Network; @@ -10,15 +9,11 @@ use config::defaults::COSMOS_DERIVATION_PATH; use cosmrs::bip32::DerivationPath; use itertools::Itertools; use nym_types::account::{Account, AccountEntry, Balance}; -use nym_types::currency::MajorCurrencyAmount; use nym_wallet_types::network::Network as WalletNetwork; use rand::seq::SliceRandom; use std::collections::HashMap; -use std::convert::TryInto; use std::str::FromStr; -use std::sync::Arc; use strum::IntoEnumIterator; -use tokio::sync::RwLock; use url::Url; use validator_client::nymd::wallet::{AccountData, DirectSecp256k1HdWallet}; use validator_client::{nymd::SigningNymdClient, Client}; @@ -26,33 +21,30 @@ use validator_client::{nymd::SigningNymdClient, Client}; #[tauri::command] pub async fn connect_with_mnemonic( mnemonic: String, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let mnemonic = Mnemonic::from_str(&mnemonic)?; _connect_with_mnemonic(mnemonic, state).await } #[tauri::command] -pub async fn get_balance( - state: tauri::State<'_, Arc>>, -) -> Result { - let denom = state.read().await.current_network().base_mix_denom(); - match nymd_client!(state) - .get_balance(nymd_client!(state).address(), denom) - .await +pub async fn get_balance(state: tauri::State<'_, WalletState>) -> Result { + let guard = state.read().await; + let client = guard.current_client()?; + let address = client.nymd.address(); + let network = guard.current_network(); + let base_mix_denom = network.base_mix_denom(); + + match client + .nymd + .get_balance(address, base_mix_denom.to_string()) + .await? { - Ok(Some(coin)) => { - let amount = MajorCurrencyAmount::from(coin); - let printable_balance = amount.to_string(); - Ok(Balance { - amount, - printable_balance, - }) + Some(coin) => { + let amount = guard.attempt_convert_to_display_dec_coin(coin)?; + Ok(Balance::new(amount)) } - Ok(None) => Err(BackendError::NoBalance( - nymd_client!(state).address().to_string(), - )), - Err(e) => Err(BackendError::from(e)), + None => Err(BackendError::NoBalance(address.to_string())), } } @@ -68,19 +60,15 @@ pub fn validate_mnemonic(mnemonic: &str) -> bool { #[tauri::command] pub async fn switch_network( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, network: WalletNetwork, ) -> Result { let account = { let r_state = state.read().await; let client = r_state.client(network)?; - let denom = network.base_mix_denom(); + let denom = network.mix_denom(); - Account::new( - client.nymd.mixnet_contract_address().to_string(), - client.nymd.address().to_string(), - denom.try_into()?, - ) + Account::new(client.nymd.address().to_string(), denom) }; let mut w_state = state.write().await; @@ -90,7 +78,7 @@ pub async fn switch_network( } #[tauri::command] -pub async fn logout(state: tauri::State<'_, Arc>>) -> Result<(), BackendError> { +pub async fn logout(state: tauri::State<'_, WalletState>) -> Result<(), BackendError> { state.write().await.logout(); Ok(()) } @@ -102,7 +90,7 @@ fn random_mnemonic() -> Mnemonic { async fn _connect_with_mnemonic( mnemonic: Mnemonic, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { { let mut w_state = state.write().await; @@ -154,19 +142,16 @@ async fn _connect_with_mnemonic( )?; // Set the default account - let default_network: WalletNetwork = config::defaults::DEFAULT_NETWORK.into(); + let default_network = WalletNetwork::MAINNET; let client_for_default_network = clients .iter() - .find(|client| WalletNetwork::from(client.network.clone()) == default_network); + .find(|(network, _)| *network == default_network); let account_for_default_network = match client_for_default_network { - Some(client) => Ok(Account::new( - client.nymd.mixnet_contract_address().to_string(), + Some((_, client)) => Ok(Account::new( client.nymd.address().to_string(), - default_network.base_mix_denom().try_into()?, - )), - None => Err(BackendError::NetworkNotSupported( - config::defaults::DEFAULT_NETWORK, + default_network.mix_denom(), )), + None => Err(BackendError::NetworkNotSupported), }; // Register all the clients @@ -174,10 +159,10 @@ async fn _connect_with_mnemonic( let mut w_state = state.write().await; w_state.logout(); } - for client in clients { - let network: WalletNetwork = client.network.clone().into(); + for (network, client) in clients { let mut w_state = state.write().await; w_state.add_client(network, client); + w_state.register_default_denoms(network); } account_for_default_network @@ -218,7 +203,7 @@ fn create_clients( default_api_urls: &HashMap, config: &Config, mnemonic: &Mnemonic, -) -> Result>, BackendError> { +) -> Result)>, BackendError> { let mut clients = Vec::new(); for network in WalletNetwork::iter() { let nymd_url = if let Some(url) = config.get_selected_validator_nymd_url(network) { @@ -265,10 +250,9 @@ fn create_clients( let config = validator_client::Config::try_from_nym_network_details(&network_details)? .with_urls(nymd_url, api_url); - let mut client = - validator_client::Client::new_signing(config, network.into(), mnemonic.clone())?; + let mut client = validator_client::Client::new_signing(config, mnemonic.clone())?; client.set_nymd_simulated_gas_multiplier(CUSTOM_SIMULATED_GAS_MULTIPLIER); - clients.push(client); + clients.push((network, client)); } Ok(clients) } @@ -328,7 +312,7 @@ pub fn create_password(mnemonic: &str, password: String) -> Result<(), BackendEr #[tauri::command] pub async fn sign_in_with_password( password: String, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!("Signing in with password"); @@ -368,7 +352,7 @@ fn extract_first_mnemonic( pub async fn sign_in_with_password_and_account_id( account_id: &str, password: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!("Signing in with password"); @@ -420,7 +404,7 @@ pub async fn add_account_for_password( mnemonic: &str, password: &str, account_id: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!("Adding account for the current password: {account_id}"); let mnemonic = Mnemonic::from_str(mnemonic)?; @@ -462,7 +446,7 @@ pub async fn add_account_for_password( async fn set_state_with_all_accounts( stored_login: wallet_storage::StoredLogin, first_id_when_converting: wallet_storage::AccountId, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result<(), BackendError> { log::trace!("Set state with accounts:"); let all_accounts: Vec<_> = stored_login @@ -503,7 +487,7 @@ async fn set_state_with_all_accounts( pub async fn remove_account_for_password( password: &str, account_id: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result<(), BackendError> { log::info!("Removing account: {account_id}"); // Currently we only support a single, default, id in the wallet @@ -534,7 +518,7 @@ fn derive_address( #[tauri::command] pub async fn list_accounts( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::trace!("Listing accounts"); let state = state.read().await; diff --git a/nym-wallet/src-tauri/src/operations/mixnet/admin.rs b/nym-wallet/src-tauri/src/operations/mixnet/admin.rs index abfa3a48b5..faced61bce 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/admin.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/admin.rs @@ -1,19 +1,14 @@ -use std::convert::TryInto; -use std::sync::Arc; - -use tokio::sync::RwLock; - -use mixnet_contract_common::ContractStateParams; -use nym_wallet_types::admin::TauriContractStateParams; -use validator_client::nymd::Fee; - use crate::error::BackendError; use crate::nymd_client; -use crate::state::State; +use crate::state::WalletState; +use mixnet_contract_common::ContractStateParams; +use nym_wallet_types::admin::TauriContractStateParams; +use std::convert::TryInto; +use validator_client::nymd::Fee; #[tauri::command] pub async fn get_contract_settings( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Getting contract settings"); let res = nymd_client!(state).get_contract_settings().await?.into(); @@ -25,7 +20,7 @@ pub async fn get_contract_settings( pub async fn update_contract_settings( params: TauriContractStateParams, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let mixnet_contract_settings_params: ContractStateParams = params.try_into()?; log::info!( diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 099ea073cf..033186886a 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -1,56 +1,68 @@ +use std::time::Duration; + use crate::error::BackendError; -use crate::nymd_client; -use crate::state::State; +use crate::state::WalletState; use crate::{Gateway, MixNode}; -use nym_types::currency::MajorCurrencyAmount; +use nym_types::currency::DecCoin; use nym_types::gateway::GatewayBond; use nym_types::mixnode::MixNodeBond; use nym_types::transaction::TransactionExecuteResult; -use std::sync::Arc; -use tokio::sync::RwLock; -use validator_client::nymd::{CosmWasmCoin, Fee}; +use reqwest::Error as ReqwestError; +use serde::{Deserialize, Serialize}; +use validator_client::nymd::{Coin, Fee}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct NodeDescription { + name: String, + description: String, + link: String, + location: String, +} #[tauri::command] pub async fn bond_gateway( gateway: Gateway, - pledge: MajorCurrencyAmount, + pledge: DecCoin, owner_signature: String, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); - let pledge_minor = pledge.clone().into(); + let guard = state.read().await; + let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( - ">>> Bond gateway: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", - &gateway.identity_key, + ">>> Bond gateway: identity_key = {}, pledge_display = {}, pledge_base = {}, fee = {:?}", + gateway.identity_key, pledge, - &pledge_minor, + pledge_base, fee, ); - let res = nymd_client!(state) - .bond_gateway(gateway, owner_signature, pledge_minor, fee) + let res = guard + .current_client()? + .nymd + .bond_gateway(gateway, owner_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } #[tauri::command] pub async fn unbond_gateway( fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!(">>> Unbond gateway, fee = {:?}", fee); - let res = nymd_client!(state).unbond_gateway(fee).await?; + let res = guard.current_client()?.nymd.unbond_gateway(fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } @@ -58,43 +70,46 @@ pub async fn unbond_gateway( pub async fn bond_mixnode( mixnode: MixNode, owner_signature: String, - pledge: MajorCurrencyAmount, + pledge: DecCoin, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); - let pledge_minor = pledge.clone().into(); + let guard = state.read().await; + let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( - ">>> Bond mixnode: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", + ">>> Bond mixnode: identity_key = {}, pledge_display = {}, pledge_base = {}, fee = {:?}", mixnode.identity_key, pledge, - pledge_minor, + pledge_base, fee, ); - let res = nymd_client!(state) - .bond_mixnode(mixnode, owner_signature, pledge_minor, fee) + let res = guard + .current_client()? + .nymd + .bond_mixnode(mixnode, owner_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } #[tauri::command] pub async fn unbond_mixnode( fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!(">>> Unbond mixnode, fee = {:?}", fee); - let res = nymd_client!(state).unbond_mixnode(fee).await?; + let res = guard.current_client()?.nymd.unbond_mixnode(fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } @@ -102,34 +117,43 @@ pub async fn unbond_mixnode( pub async fn update_mixnode( profit_margin_percent: u8, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( ">>> Update mixnode: profit_margin_percent = {}, fee {:?}", profit_margin_percent, fee, ); - let res = nymd_client!(state) + let res = guard + .current_client()? + .nymd .update_mixnode_config(profit_margin_percent, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } #[tauri::command] pub async fn mixnode_bond_details( - state: tauri::State<'_, Arc>>, + 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 = MixNodeBond::from_mixnet_contract_mixnode_bond(bond)?; + let res = bond + .map(|bond| { + guard + .registered_coins() + .map(|reg| MixNodeBond::from_mixnet_contract_mixnode_bond(bond, reg)) + }) + .transpose()? + .transpose()?; log::info!( "<<< identity_key = {:?}", res.as_ref().map(|r| r.mix_node.identity_key.to_string()) @@ -140,13 +164,21 @@ pub async fn mixnode_bond_details( #[tauri::command] pub async fn gateway_bond_details( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { 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 res = GatewayBond::from_mixnet_contract_gateway_bond(bond)?; + let res = bond + .map(|bond| { + guard + .registered_coins() + .map(|reg| GatewayBond::from_mixnet_contract_gateway_bond(bond, reg)) + }) + .transpose()? + .transpose()?; + log::info!( "<<< identity_key = {:?}", res.as_ref().map(|r| r.gateway.identity_key.to_string()) @@ -158,17 +190,71 @@ pub async fn gateway_bond_details( #[tauri::command] pub async fn get_operator_rewards( address: String, - state: tauri::State<'_, Arc>>, -) -> Result { + state: tauri::State<'_, WalletState>, +) -> Result { log::info!(">>> Get operator rewards for {}", address); - let denom = state.read().await.current_network().base_mix_denom(); - let rewards_as_minor = nymd_client!(state).get_operator_rewards(address).await?; - let coin = CosmWasmCoin::new(rewards_as_minor.u128(), denom.as_ref()); - let amount: MajorCurrencyAmount = coin.into(); + let guard = state.read().await; + let network = guard.current_network(); + let denom = network.base_mix_denom(); + let reward_amount = guard + .current_client()? + .nymd + .get_operator_rewards(address) + .await?; + let base_coin = Coin::new(reward_amount.u128(), denom); + let display_coin: DecCoin = guard.attempt_convert_to_display_dec_coin(base_coin.clone())?; log::info!( - "<<< rewards_as_minor = {}, amount = {}", - rewards_as_minor, - amount + "<<< rewards_base = {}, rewards_display = {}", + base_coin, + display_coin ); - Ok(amount) + Ok(display_coin) +} + +#[tauri::command] +pub async fn get_number_of_mixnode_delegators( + identity: String, + 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), + } +} + +#[tauri::command] +pub async fn get_mix_node_description( + host: &str, + port: u16, +) -> Result { + fetch_mix_node_description(host, port) + .await + .map_err(|e| BackendError::ReqwestError { source: e }) } diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index fa81c32ab0..7e6f27ae8f 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -1,63 +1,66 @@ use crate::error::BackendError; -use crate::state::State; +use crate::state::WalletState; use crate::vesting::delegate::{ get_pending_vesting_delegation_events, vesting_undelegate_from_mixnode, }; use crate::{api_client, nymd_client}; -use cosmwasm_std::Coin as CosmWasmCoin; use mixnet_contract_common::IdentityKey; -use nym_types::currency::{CurrencyDenom, MajorCurrencyAmount}; +use nym_types::currency::DecCoin; use nym_types::delegation::{ - from_contract_delegation_events, Delegation, DelegationEvent, DelegationRecord, - DelegationWithEverything, DelegationsSummaryResponse, + Delegation, DelegationEvent, DelegationRecord, DelegationWithEverything, + DelegationsSummaryResponse, }; use nym_types::transaction::TransactionExecuteResult; use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::RwLock; -use validator_client::nymd::Fee; +use validator_client::nymd::{Coin, Fee}; #[tauri::command] pub async fn get_pending_delegation_events( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::info!(">>> Get pending delegation events"); - let events = nymd_client!(state) - .get_pending_delegation_events(nymd_client!(state).address().to_string(), None) + 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); - match from_contract_delegation_events(events) { - Ok(res) => Ok(res), - Err(e) => Err(e.into()), - } + Ok(events + .into_iter() + .map(|event| DelegationEvent::from_mixnet_contract(event, reg)) + .collect::>()?) } #[tauri::command] pub async fn delegate_to_mixnode( identity: &str, - amount: MajorCurrencyAmount, + amount: DecCoin, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); - let delegation = amount.clone().into(); + let guard = state.read().await; + 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 = {}, amount = {}, minor_amount = {}, fee = {:?}", + ">>> Delegate to mixnode: identity_key = {}, display_amount = {}, base_amount = {}, fee = {:?}", identity, amount, - delegation, + delegation_base, fee, ); let res = nymd_client!(state) - .delegate_to_mixnode(identity, delegation, fee) + .delegate_to_mixnode(identity, delegation_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } @@ -65,22 +68,25 @@ pub async fn delegate_to_mixnode( pub async fn undelegate_from_mixnode( identity: &str, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( ">>> Undelegate from mixnode: identity_key = {}, fee = {:?}", identity, fee ); - let res = nymd_client!(state) + let res = guard + .current_client()? + .nymd .remove_mixnode_delegation(identity, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } @@ -90,7 +96,7 @@ pub async fn undelegate_all_from_mixnode( uses_vesting_contract_tokens: bool, fee: Option, fee2: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::info!( ">>> Undelegate all from mixnode: identity_key = {}, uses_vesting_contract_tokens = {}, fee = {:?}", @@ -110,30 +116,31 @@ pub async fn undelegate_all_from_mixnode( struct DelegationWithHistory { pub delegation: Delegation, - pub amount_sum: MajorCurrencyAmount, + 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<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::info!(">>> Get all mixnode delegations"); + let guard = state.read().await; + 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 = api_client!(state).get_mixnodes().await?; + let mixnodes = client.validator_api.get_mixnodes().await?; - let address = nymd_client!(state).address().to_string(); - - let denom_minor = state.read().await.current_network().base_mix_denom(); - let denom: CurrencyDenom = denom_minor.clone().try_into()?; + 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(); log::info!(" >>> Get delegations"); - let delegations = nymd_client!(state) - .get_delegator_delegations_paged(address.clone(), None, None) // get all delegations, ignoring paging - .await? - .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) @@ -152,20 +159,21 @@ pub async fn get_all_mix_delegations( let mut map: HashMap = HashMap::new(); - for pending_event in pending_events_for_account.clone() { + 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 - .unwrap_or_else(|| MajorCurrencyAmount::zero(&denom)); + .clone() + .unwrap_or_else(|| DecCoin::zero(display_mix_denom)); let delegation = DelegationWithHistory { delegation: Delegation { amount: amount.clone(), - node_identity: pending_event.node_identity, - proxy: pending_event.proxy, - owner: pending_event.address, + 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, @@ -178,11 +186,13 @@ pub async fn get_all_mix_delegations( for d in delegations { // create record of delegation - let delegated_on_iso_datetime = nymd_client!(state) + let delegated_on_iso_datetime = client + .nymd .get_block_timestamp(Some(d.block_height as u32)) .await? .to_rfc3339(); - let amount: MajorCurrencyAmount = d.amount.clone().into(); + let amount = guard.attempt_convert_to_display_dec_coin(d.amount.clone().into())?; + let record = DelegationRecord { amount: amount.clone(), block_height: d.block_height, @@ -193,14 +203,16 @@ pub async fn get_all_mix_delegations( let entry = map .entry(d.node_identity.clone()) .or_insert(DelegationWithHistory { - delegation: d.try_into()?, + delegation: Delegation::from_mixnet_contract(d, reg)?, history: vec![], - amount_sum: MajorCurrencyAmount::zero(&amount.denom), + 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 = entry.amount_sum.clone() + amount; + entry.amount_sum.amount += amount.amount; entry.uses_vesting_contract_tokens = entry.uses_vesting_contract_tokens || entry.delegation.proxy.is_some(); } @@ -229,25 +241,25 @@ pub async fn get_all_mix_delegations( .iter() .find(|m| m.mix_node.identity_key == node_identity); - let pledge_amount: Option = - mixnode.and_then(|m| m.pledge_amount.clone().try_into().ok()); + let pledge_amount = mixnode + .map(|m| guard.attempt_convert_to_display_dec_coin(m.pledge_amount.clone().into())) + .transpose()?; - let total_delegation: Option = - mixnode.and_then(|m| m.total_delegation.clone().try_into().ok()); + let total_delegation = mixnode + .map(|m| guard.attempt_convert_to_display_dec_coin(m.total_delegation.clone().into())) + .transpose()?; let profit_margin_percent: Option = mixnode.map(|m| m.mix_node.profit_margin_percent); log::trace!(" >>> Get accumulated rewards: address = {}", address); - let accumulated_rewards = match nymd_client!(state) - .get_delegator_rewards(address.clone(), node_identity.clone(), proxy.clone()) + let accumulated_rewards = match client + .nymd + .get_delegator_rewards(address.to_string(), node_identity.clone(), proxy.clone()) .await { Ok(rewards) => { - let reward = CosmWasmCoin { - denom: denom_minor.to_string(), - amount: rewards, - }; - let amount = MajorCurrencyAmount::from(reward); + 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); Some(amount) } @@ -338,40 +350,52 @@ pub async fn get_delegator_rewards( address: String, mix_identity: IdentityKey, proxy: Option, - state: tauri::State<'_, Arc>>, -) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); + state: tauri::State<'_, WalletState>, +) -> Result { log::info!( ">>> Get delegator rewards: mix_identity = {}, proxy = {:?}", mix_identity, proxy ); - let res = nymd_client!(state) + let guard = state.read().await; + let network = guard.current_network(); + let denom = network.base_mix_denom(); + let reward_amount = guard + .current_client()? + .nymd .get_delegator_rewards(address, mix_identity, proxy) .await?; - let coin = CosmWasmCoin::new(res.u128(), denom_minor.as_ref()); - let amount = coin.into(); - log::info!(">>> res = {}, amount = {}", res, amount); - Ok(amount) + let base_coin = Coin::new(reward_amount.u128(), denom); + let display_coin: DecCoin = guard.attempt_convert_to_display_dec_coin(base_coin.clone())?; + + log::info!( + "<<< rewards_base = {}, rewards_display = {}", + base_coin, + display_coin + ); + Ok(display_coin) } #[tauri::command] pub async fn get_delegation_summary( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Get delegation summary"); - let denom_minor = state.read().await.current_network().base_mix_denom(); - let denom: CurrencyDenom = denom_minor.clone().try_into()?; + let guard = state.read().await; + let network = guard.current_network(); + let display_mix_denom = network.display_mix_denom(); let delegations = get_all_mix_delegations(state.clone()).await?; - let mut total_delegations = MajorCurrencyAmount::zero(&denom); - let mut total_rewards = MajorCurrencyAmount::zero(&denom); + let mut total_delegations = DecCoin::zero(display_mix_denom); + let mut total_rewards = DecCoin::zero(display_mix_denom); - for d in delegations.clone() { - total_delegations = total_delegations + d.amount; - if let Some(rewards) = d.accumulated_rewards { - total_rewards = total_rewards + rewards; + 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 { + debug_assert_eq!(rewards.denom, display_mix_denom); + total_rewards.amount += rewards.amount; } } log::info!( @@ -391,7 +415,7 @@ pub async fn get_delegation_summary( #[tauri::command] pub async fn get_all_pending_delegation_events( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::info!(">>> Get all pending delegation events"); diff --git a/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs b/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs index 5203e5bc62..2ca8fe0c05 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/epoch.rs @@ -1,13 +1,11 @@ use crate::error::BackendError; use crate::nymd_client; -use crate::state::State; +use crate::state::WalletState; use nym_wallet_types::epoch::Epoch; -use std::sync::Arc; -use tokio::sync::RwLock; #[tauri::command] pub async fn get_current_epoch( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Get curren epoch"); let interval = nymd_client!(state).get_current_epoch().await?; diff --git a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs index c335ed0624..983c870c61 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs @@ -1,48 +1,50 @@ use crate::error::BackendError; use crate::nymd_client; -use crate::state::State; +use crate::state::WalletState; use crate::vesting::rewards::{vesting_claim_delegator_reward, vesting_compound_delegator_reward}; use mixnet_contract_common::IdentityKey; use nym_types::transaction::TransactionExecuteResult; -use std::sync::Arc; -use tokio::sync::RwLock; use validator_client::nymd::Fee; #[tauri::command] pub async fn claim_operator_reward( fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { // TODO: handle operator bonding with vesting contract log::info!(">>> Claim operator reward"); - let denom_minor = state.read().await.current_network().base_mix_denom(); - let res = nymd_client!(state) + 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, - denom_minor.as_ref(), + res, fee_amount, )?) } #[tauri::command] pub async fn compound_operator_reward( fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { // TODO: handle operator bonding with vesting contract log::info!(">>> Compound operator reward"); - let denom_minor = state.read().await.current_network().base_mix_denom(); - let res = nymd_client!(state) + 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) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } @@ -50,21 +52,23 @@ pub async fn compound_operator_reward( pub async fn claim_delegator_reward( mix_identity: IdentityKey, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!( ">>> Claim delegator reward: identity_key = {}", mix_identity ); - let denom_minor = state.read().await.current_network().base_mix_denom(); - let res = nymd_client!(state) + 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, - denom_minor.as_ref(), + res, fee_amount, )?) } @@ -72,21 +76,23 @@ pub async fn claim_delegator_reward( pub async fn compound_delegator_reward( mix_identity: IdentityKey, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!( ">>> Compound delegator reward: identity_key = {}", mix_identity ); - let denom_minor = state.read().await.current_network().base_mix_denom(); - let res = nymd_client!(state) + 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) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } @@ -94,7 +100,7 @@ pub async fn compound_delegator_reward( pub async fn claim_locked_and_unlocked_delegator_reward( mix_identity: IdentityKey, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::info!( ">>> Claim delegator reward (locked and unlocked): identity_key = {}", @@ -140,7 +146,7 @@ pub async fn claim_locked_and_unlocked_delegator_reward( pub async fn compound_locked_and_unlocked_delegator_reward( mix_identity: IdentityKey, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::info!( ">>> Compound delegator reward (locked and unlocked): identity_key = {}", diff --git a/nym-wallet/src-tauri/src/operations/mixnet/send.rs b/nym-wallet/src-tauri/src/operations/mixnet/send.rs index f29d4b834c..0e4cbdd6a3 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/send.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/send.rs @@ -1,46 +1,43 @@ use crate::error::BackendError; -use crate::nymd_client; -use crate::state::State; -use nym_types::currency::MajorCurrencyAmount; +use crate::state::WalletState; +use nym_types::currency::DecCoin; use nym_types::transaction::{SendTxResult, TransactionDetails}; use std::str::FromStr; -use std::sync::Arc; -use tokio::sync::RwLock; use validator_client::nymd::{AccountId, Fee}; #[tauri::command] pub async fn send( address: &str, - amount: MajorCurrencyAmount, + amount: DecCoin, memo: String, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); - let address = AccountId::from_str(address)?; - let from_address = nymd_client!(state).address().to_string(); - let amount2 = amount.clone().into(); + let guard = state.read().await; + let amount_base = guard.attempt_convert_to_base_coin(amount.clone())?; + + let to_address = AccountId::from_str(address)?; + let from_address = guard.current_client()?.nymd.address().to_string(); + let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( - ">>> Send: amount = {}, minor_amount = {:?}, from = {}, to = {}, fee = {:?}", + ">>> Send: display_amount = {}, base_amount = {}, from = {}, to = {}, fee = {:?}", amount, - amount2, + amount_base, from_address, - address.as_ref(), + to_address, fee, ); - let raw_res = nymd_client!(state) - .send(&address, vec![amount2], memo, fee) + let raw_res = guard + .current_client()? + .nymd + .send(&to_address, vec![amount_base], memo, fee) .await?; log::info!("<<< tx hash = {}", raw_res.hash.to_string()); let res = SendTxResult::new( raw_res, - TransactionDetails { - from_address, - to_address: address.to_string(), - amount, - }, - denom_minor.as_ref(), - )?; + TransactionDetails::new(amount, from_address, to_address.to_string()), + fee_amount, + ); log::trace!("<<< {:?}", res); Ok(res) } diff --git a/nym-wallet/src-tauri/src/operations/simulate/admin.rs b/nym-wallet/src-tauri/src/operations/simulate/admin.rs index 053f61b35c..50a559105b 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/admin.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/admin.rs @@ -3,17 +3,14 @@ use crate::error::BackendError; use crate::operations::simulate::FeeDetails; -use crate::simulate::detailed_fee; -use crate::State; +use crate::WalletState; use mixnet_contract_common::{ContractStateParams, ExecuteMsg}; use nym_wallet_types::admin::TauriContractStateParams; -use std::sync::Arc; -use tokio::sync::RwLock; #[tauri::command] pub async fn simulate_update_contract_settings( params: TauriContractStateParams, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; let mixnet_contract_settings_params: ContractStateParams = params.try_into()?; @@ -28,5 +25,5 @@ pub async fn simulate_update_contract_settings( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } diff --git a/nym-wallet/src-tauri/src/operations/simulate/cosmos.rs b/nym-wallet/src-tauri/src/operations/simulate/cosmos.rs index 282ff5b0fb..2796e02097 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/cosmos.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/cosmos.rs @@ -3,24 +3,22 @@ use crate::error::BackendError; use crate::operations::simulate::FeeDetails; -use crate::simulate::detailed_fee; -use crate::state::State; -use nym_types::currency::MajorCurrencyAmount; +use crate::state::WalletState; +use nym_types::currency::DecCoin; use std::str::FromStr; -use std::sync::Arc; -use tokio::sync::RwLock; use validator_client::nymd::{AccountId, MsgSend}; #[tauri::command] pub async fn simulate_send( address: &str, - amount: MajorCurrencyAmount, - state: tauri::State<'_, Arc>>, + amount: DecCoin, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; + let amount_base = guard.attempt_convert_to_base_coin(amount.clone())?; let to_address = AccountId::from_str(address)?; - let amount = vec![amount.into()]; + let amount = vec![amount_base.into()]; let client = guard.current_client()?; let from_address = client.nymd.address().clone(); @@ -33,5 +31,5 @@ pub async fn simulate_send( }; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index d00d8c68be..794a1f09fe 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -3,23 +3,20 @@ use crate::error::BackendError; use crate::operations::simulate::FeeDetails; -use crate::simulate::detailed_fee; -use crate::State; +use crate::WalletState; use mixnet_contract_common::IdentityKey; use mixnet_contract_common::{ExecuteMsg, Gateway, MixNode}; -use nym_types::currency::MajorCurrencyAmount; -use std::sync::Arc; -use tokio::sync::RwLock; +use nym_types::currency::DecCoin; #[tauri::command] pub async fn simulate_bond_gateway( gateway: Gateway, - pledge: MajorCurrencyAmount, + pledge: DecCoin, owner_signature: String, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let pledge = pledge.into(); + let pledge = guard.attempt_convert_to_base_coin(pledge)?; let client = guard.current_client()?; let mixnet_contract = client.nymd.mixnet_contract_address(); @@ -35,12 +32,12 @@ pub async fn simulate_bond_gateway( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_unbond_gateway( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; let client = guard.current_client()?; @@ -53,18 +50,18 @@ pub async fn simulate_unbond_gateway( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_bond_mixnode( mixnode: MixNode, owner_signature: String, - pledge: MajorCurrencyAmount, - state: tauri::State<'_, Arc>>, + pledge: DecCoin, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let pledge = pledge.into(); + let pledge = guard.attempt_convert_to_base_coin(pledge)?; let client = guard.current_client()?; let mixnet_contract = client.nymd.mixnet_contract_address(); @@ -79,12 +76,12 @@ pub async fn simulate_bond_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_unbond_mixnode( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; let client = guard.current_client()?; @@ -97,13 +94,13 @@ pub async fn simulate_unbond_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_update_mixnode( profit_margin_percent: u8, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; let client = guard.current_client()?; @@ -118,17 +115,17 @@ pub async fn simulate_update_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_delegate_to_mixnode( identity: &str, - amount: MajorCurrencyAmount, - state: tauri::State<'_, Arc>>, + amount: DecCoin, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let delegation = amount.into(); + let delegation = guard.attempt_convert_to_base_coin(amount)?; let client = guard.current_client()?; let mixnet_contract = client.nymd.mixnet_contract_address(); @@ -142,15 +139,14 @@ pub async fn simulate_delegate_to_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_undelegate_from_mixnode( identity: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - println!("Called"); let guard = state.read().await; let client = guard.current_client()?; let mixnet_contract = client.nymd.mixnet_contract_address(); @@ -164,53 +160,57 @@ pub async fn simulate_undelegate_from_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_claim_operator_reward( - state: tauri::State<'_, Arc>>, + 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?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_compound_operator_reward( - state: tauri::State<'_, Arc>>, + 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?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_claim_delegator_reward( mix_identity: IdentityKey, - state: tauri::State<'_, Arc>>, + 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?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_compound_delegator_reward( mix_identity: IdentityKey, - state: tauri::State<'_, Arc>>, + 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?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } diff --git a/nym-wallet/src-tauri/src/operations/simulate/mod.rs b/nym-wallet/src-tauri/src/operations/simulate/mod.rs index 61e4179687..256f6f9a84 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mod.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mod.rs @@ -3,29 +3,15 @@ use cosmrs::tx; use cosmrs::tx::Gas; -use nym_types::currency::MajorCurrencyAmount; use nym_types::fees::FeeDetails; -use validator_client::nymd::cosmwasm_client::types::{GasInfo, SimulateResponse}; -use validator_client::nymd::{ - CosmosCoin, Fee, GasAdjustable, GasAdjustment, GasPrice, SigningNymdClient, -}; -use validator_client::Client; +use validator_client::nymd::cosmwasm_client::types::GasInfo; +use validator_client::nymd::{CosmosCoin, Fee, GasAdjustable, GasAdjustment, GasPrice}; pub mod admin; pub mod cosmos; pub mod mixnet; pub mod vesting; -pub(crate) fn detailed_fee( - client: &Client, - simulate_response: SimulateResponse, -) -> FeeDetails { - let gas_price = client.nymd.gas_price().clone(); - let gas_adjustment = client.nymd.gas_adjustment(); - - SimulateResult::new(simulate_response.gas_info, gas_price, gas_adjustment).detailed_fee() -} - // technically we could have also exposed a result: Option field from the SimulateResponse, // but in the context of the wallet it's really irrelevant and useless for the time being pub(crate) struct SimulateResult { @@ -50,24 +36,16 @@ impl SimulateResult { } } - pub fn detailed_fee(&self) -> FeeDetails { - let amount = self.to_fee_amount().map(MajorCurrencyAmount::from); - FeeDetails { - amount, - fee: self.to_fee(), - } - } - - fn adjusted_gas(&self) -> Option { + pub(crate) fn adjusted_gas(&self) -> Option { self.gas_info .map(|gas_info| gas_info.gas_used.adjust_gas(self.gas_adjustment)) } - fn to_fee_amount(&self) -> Option { + pub(crate) fn to_fee_amount(&self) -> Option { self.adjusted_gas().map(|gas| &self.gas_price * gas) } - fn to_fee(&self) -> Fee { + pub(crate) fn to_fee(&self) -> Fee { self.adjusted_gas() .map(|gas| { let fee_amount = &self.gas_price * gas; diff --git a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs index fc9779ddf0..d8850d202c 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -3,24 +3,21 @@ use crate::error::BackendError; use crate::operations::simulate::FeeDetails; -use crate::simulate::detailed_fee; -use crate::State; +use crate::WalletState; use mixnet_contract_common::IdentityKey; use mixnet_contract_common::{Gateway, MixNode}; -use nym_types::currency::MajorCurrencyAmount; -use std::sync::Arc; -use tokio::sync::RwLock; +use nym_types::currency::DecCoin; use vesting_contract_common::ExecuteMsg; #[tauri::command] pub async fn simulate_vesting_bond_gateway( gateway: Gateway, - pledge: MajorCurrencyAmount, + pledge: DecCoin, owner_signature: String, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let pledge = pledge.into(); + let pledge = guard.attempt_convert_to_base_coin(pledge)?; let client = guard.current_client()?; let vesting_contract = client.nymd.vesting_contract_address(); @@ -30,18 +27,18 @@ pub async fn simulate_vesting_bond_gateway( &ExecuteMsg::BondGateway { gateway, owner_signature, - amount: pledge, + amount: pledge.into(), }, vec![], )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_vesting_unbond_gateway( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; @@ -55,18 +52,18 @@ pub async fn simulate_vesting_unbond_gateway( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_vesting_bond_mixnode( mixnode: MixNode, owner_signature: String, - pledge: MajorCurrencyAmount, - state: tauri::State<'_, Arc>>, + pledge: DecCoin, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let pledge = pledge.into(); + let pledge = guard.attempt_convert_to_base_coin(pledge)?; let client = guard.current_client()?; let vesting_contract = client.nymd.vesting_contract_address(); @@ -76,18 +73,18 @@ pub async fn simulate_vesting_bond_mixnode( &ExecuteMsg::BondMixnode { mix_node: mixnode, owner_signature, - amount: pledge, + amount: pledge.into(), }, vec![], )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_vesting_unbond_mixnode( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; @@ -101,13 +98,13 @@ pub async fn simulate_vesting_unbond_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_vesting_update_mixnode( profit_margin_percent: u8, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; @@ -123,17 +120,17 @@ pub async fn simulate_vesting_update_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_vesting_delegate_to_mixnode( identity: &str, - amount: MajorCurrencyAmount, - state: tauri::State<'_, Arc>>, + amount: DecCoin, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let amount = amount.into(); + let amount = guard.attempt_convert_to_base_coin(amount)?.into(); let client = guard.current_client()?; let vesting_contract = client.nymd.vesting_contract_address(); @@ -148,13 +145,13 @@ pub async fn simulate_vesting_delegate_to_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_vesting_undelegate_from_mixnode( identity: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; @@ -170,16 +167,16 @@ pub async fn simulate_vesting_undelegate_from_mixnode( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_withdraw_vested_coins( - amount: MajorCurrencyAmount, - state: tauri::State<'_, Arc>>, + amount: DecCoin, + state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; - let amount = amount.into(); + let amount = guard.attempt_convert_to_base_coin(amount)?.into(); let client = guard.current_client()?; let vesting_contract = client.nymd.vesting_contract_address(); @@ -191,59 +188,63 @@ pub async fn simulate_withdraw_vested_coins( )?; let result = client.nymd.simulate(vec![msg]).await?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_vesting_claim_operator_reward( - state: tauri::State<'_, Arc>>, + 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?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_vesting_compound_operator_reward( - state: tauri::State<'_, Arc>>, + 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?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_vesting_claim_delegator_reward( mix_identity: IdentityKey, - state: tauri::State<'_, Arc>>, + 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?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } #[tauri::command] pub async fn simulate_vesting_compound_delegator_reward( mix_identity: IdentityKey, - state: tauri::State<'_, Arc>>, + 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?; - Ok(detailed_fee(client, result)) + guard.create_detailed_fee(result) } 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 721c39bf82..7ff60d3e6b 100644 --- a/nym-wallet/src-tauri/src/operations/validator_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/validator_api/status.rs @@ -3,9 +3,7 @@ use crate::api_client; use crate::error::BackendError; -use crate::state::State; -use std::sync::Arc; -use tokio::sync::RwLock; +use crate::state::WalletState; use validator_client::models::{ CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, @@ -15,7 +13,7 @@ use validator_client::models::{ pub async fn mixnode_core_node_status( identity: &str, since: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) .get_mixnode_core_status_count(identity, since) @@ -26,7 +24,7 @@ pub async fn mixnode_core_node_status( pub async fn gateway_core_node_status( identity: &str, since: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) .get_gateway_core_status_count(identity, since) @@ -36,7 +34,7 @@ pub async fn gateway_core_node_status( #[tauri::command] pub async fn mixnode_status( identity: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state).get_mixnode_status(identity).await?) } @@ -44,7 +42,7 @@ pub async fn mixnode_status( #[tauri::command] pub async fn mixnode_reward_estimation( identity: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) .get_mixnode_reward_estimation(identity) @@ -54,7 +52,7 @@ pub async fn mixnode_reward_estimation( #[tauri::command] pub async fn mixnode_stake_saturation( identity: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) .get_mixnode_stake_saturation(identity) @@ -64,7 +62,7 @@ pub async fn mixnode_stake_saturation( #[tauri::command] pub async fn mixnode_inclusion_probability( identity: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) .get_mixnode_inclusion_probability(identity) diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index b76a442708..c567e4e908 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -1,48 +1,50 @@ use crate::error::BackendError; use crate::nymd_client; -use crate::state::State; +use crate::state::WalletState; use crate::{Gateway, MixNode}; -use nym_types::currency::MajorCurrencyAmount; +use nym_types::currency::DecCoin; use nym_types::transaction::TransactionExecuteResult; -use std::sync::Arc; -use tokio::sync::RwLock; use validator_client::nymd::{Fee, VestingSigningClient}; #[tauri::command] pub async fn vesting_bond_gateway( gateway: Gateway, - pledge: MajorCurrencyAmount, + pledge: DecCoin, owner_signature: String, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); - let pledge_minor = pledge.clone().into(); + let guard = state.read().await; + let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( - ">>> Bond gateway with locked tokens: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", - gateway.identity_key, - pledge, - pledge_minor, - fee, - ); - let res = nymd_client!(state) - .vesting_bond_gateway(gateway, &owner_signature, pledge_minor, fee) + ">>> Bond gateway with locked tokens: identity_key = {}, pledge_display = {}, pledge_base = {}, fee = {:?}", + gateway.identity_key, + pledge, + pledge_base, + fee, + ); + let res = guard + .current_client()? + .nymd + .vesting_bond_gateway(gateway, &owner_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } #[tauri::command] pub async fn vesting_unbond_gateway( fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( ">>> Unbond gateway bonded with locked tokens, fee = {:?}", fee @@ -51,8 +53,7 @@ pub async fn vesting_unbond_gateway( log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } @@ -60,71 +61,81 @@ pub async fn vesting_unbond_gateway( pub async fn vesting_bond_mixnode( mixnode: MixNode, owner_signature: String, - pledge: MajorCurrencyAmount, + pledge: DecCoin, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); - let pledge_minor = pledge.clone().into(); + let guard = state.read().await; + let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( - ">>> Bond mixnode with locked tokens: identity_key = {}, pledge = {}, pledge_minor = {}, fee = {:?}", + ">>> Bond mixnode with locked tokens: identity_key = {}, pledge_display = {}, pledge_base = {}, fee = {:?}", mixnode.identity_key, pledge, - pledge_minor, + pledge_base, fee ); - let res = nymd_client!(state) - .vesting_bond_mixnode(mixnode, &owner_signature, pledge_minor, fee) + let res = guard + .current_client()? + .nymd + .vesting_bond_mixnode(mixnode, &owner_signature, pledge_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } #[tauri::command] pub async fn vesting_unbond_mixnode( fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( ">>> Unbond mixnode bonded with locked tokens, fee = {:?}", fee ); - let res = nymd_client!(state).vesting_unbond_mixnode(fee).await?; + let res = guard + .current_client()? + .nymd + .vesting_unbond_mixnode(fee) + .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } #[tauri::command] pub async fn withdraw_vested_coins( - amount: MajorCurrencyAmount, + amount: DecCoin, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); - let amount_minor = amount.clone().into(); + let guard = state.read().await; + let amount_base = guard.attempt_convert_to_base_coin(amount.clone())?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( - ">>> Withdraw vested liquid coins: amount = {}, amount_minor = {}, fee = {:?}", + ">>> Withdraw vested liquid coins: amount_base = {}, amount_base = {}, fee = {:?}", amount, - amount_minor, + amount_base, fee ); - let res = nymd_client!(state) - .withdraw_vested_coins(amount_minor, fee) + let res = guard + .current_client()? + .nymd + .withdraw_vested_coins(amount_base, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } @@ -132,21 +143,23 @@ pub async fn withdraw_vested_coins( pub async fn vesting_update_mixnode( profit_margin_percent: u8, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!( ">>> Update mixnode bonded with locked tokens: profit_margin_percent = {}, fee = {:?}", profit_margin_percent, fee, ); - let res = nymd_client!(state) + let res = guard + .current_client()? + .nymd .vesting_update_mixnode_config(profit_margin_percent, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs index ab6fba6a59..9ddde467a8 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs @@ -1,23 +1,18 @@ -use std::sync::Arc; - -use tokio::sync::RwLock; - -use nym_types::currency::MajorCurrencyAmount; -use nym_types::delegation::{from_contract_delegation_events, DelegationEvent}; +use crate::error::BackendError; +use crate::state::WalletState; +use nym_types::currency::DecCoin; +use nym_types::delegation::DelegationEvent; use nym_types::transaction::TransactionExecuteResult; use validator_client::nymd::{Fee, VestingSigningClient}; -use crate::error::BackendError; -use crate::nymd_client; -use crate::state::State; - #[tauri::command] pub async fn get_pending_vesting_delegation_events( - state: tauri::State<'_, Arc>>, + 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(); @@ -31,36 +26,39 @@ pub async fn get_pending_vesting_delegation_events( log::info!("<<< {} events", events.len()); log::trace!("<<< {:?}", events); - match from_contract_delegation_events(events) { - Ok(res) => Ok(res), - Err(e) => Err(e.into()), - } + Ok(events + .into_iter() + .map(|event| DelegationEvent::from_mixnet_contract(event, reg)) + .collect::>()?) } #[tauri::command] pub async fn vesting_delegate_to_mixnode( identity: &str, - amount: MajorCurrencyAmount, + amount: DecCoin, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); - let delegation = amount.clone().into(); + let guard = state.read().await; + let delegation = guard.attempt_convert_to_base_coin(amount.clone())?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!( - ">>> Delegate to mixnode with locked tokens: identity_key = {}, amount = {}, minor_amount = {}, fee = {:?}", + ">>> Delegate to mixnode with locked tokens: identity_key = {}, amount_display = {}, amount_base = {}, fee = {:?}", identity, amount, delegation, fee ); - let res = nymd_client!(state) + let res = guard + .current_client()? + .nymd .vesting_delegate_to_mixnode(identity, delegation, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } @@ -68,21 +66,23 @@ pub async fn vesting_delegate_to_mixnode( pub async fn vesting_undelegate_from_mixnode( identity: &str, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { - let denom_minor = state.read().await.current_network().base_mix_denom(); + 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, fee, ); - let res = nymd_client!(state) + let res = guard + .current_client()? + .nymd .vesting_undelegate_from_mixnode(identity, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/queries.rs b/nym-wallet/src-tauri/src/operations/vesting/queries.rs index 72c85f69d7..7d0d726a1c 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/queries.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/queries.rs @@ -1,92 +1,106 @@ -use std::sync::Arc; - +use crate::error::BackendError; +use crate::nymd_client; +use crate::state::WalletState; use cosmwasm_std::Timestamp; -use tokio::sync::RwLock; - -use nym_types::currency::MajorCurrencyAmount; +use nym_types::currency::DecCoin; use nym_types::vesting::VestingAccountInfo; use nym_types::vesting::{OriginalVestingResponse, PledgeData}; use validator_client::nymd::VestingQueryClient; use vesting_contract_common::Period; -use crate::error::BackendError; -use crate::nymd_client; -use crate::state::State; - #[tauri::command] pub async fn locked_coins( block_time: Option, - state: tauri::State<'_, Arc>>, -) -> Result { + state: tauri::State<'_, WalletState>, +) -> Result { log::info!(">>> Query locked coins"); - let res = nymd_client!(state) + let guard = state.read().await; + let client = guard.current_client()?; + + let res = client + .nymd .locked_coins( - nymd_client!(state).address().as_ref(), + client.nymd.address().as_ref(), block_time.map(Timestamp::from_seconds), ) - .await? - .into(); - log::info!("<<< locked coins = {}", res); - Ok(res) + .await?; + let display = guard.attempt_convert_to_display_dec_coin(res)?; + log::info!("<<< locked coins = {}", display); + Ok(display) } #[tauri::command] pub async fn spendable_coins( block_time: Option, - state: tauri::State<'_, Arc>>, -) -> Result { + state: tauri::State<'_, WalletState>, +) -> Result { log::info!(">>> Query spendable coins"); - let res = nymd_client!(state) + let guard = state.read().await; + let client = guard.current_client()?; + + let res = client + .nymd .spendable_coins( - nymd_client!(state).address().as_ref(), + client.nymd.address().as_ref(), block_time.map(Timestamp::from_seconds), ) - .await? - .into(); - log::info!("<<< spendable coins = {}", res); - Ok(res) + .await?; + + let display = guard.attempt_convert_to_display_dec_coin(res)?; + log::info!("<<< spendable coins = {}", display); + Ok(display) } #[tauri::command] pub async fn vested_coins( vesting_account_address: &str, block_time: Option, - state: tauri::State<'_, Arc>>, -) -> Result { + state: tauri::State<'_, WalletState>, +) -> Result { log::info!(">>> Query vested coins"); - let res = nymd_client!(state) + let guard = state.read().await; + + let res = guard + .current_client()? + .nymd .vested_coins( vesting_account_address, block_time.map(Timestamp::from_seconds), ) - .await? - .into(); - log::info!("<<< vested coins = {}", res); - Ok(res) + .await?; + + let display = guard.attempt_convert_to_display_dec_coin(res)?; + log::info!("<<< vested coins = {}", display); + Ok(display) } #[tauri::command] pub async fn vesting_coins( vesting_account_address: &str, block_time: Option, - state: tauri::State<'_, Arc>>, -) -> Result { + state: tauri::State<'_, WalletState>, +) -> Result { log::info!(">>> Query vesting coins"); - let res = nymd_client!(state) + let guard = state.read().await; + + let res = guard + .current_client()? + .nymd .vesting_coins( vesting_account_address, block_time.map(Timestamp::from_seconds), ) - .await? - .into(); - log::info!("<<< vesting coins = {}", res); - Ok(res) + .await?; + + let display = guard.attempt_convert_to_display_dec_coin(res)?; + log::info!("<<< vesting coins = {}", display); + Ok(display) } #[tauri::command] pub async fn vesting_start_time( vesting_account_address: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Query vesting start time"); let res = nymd_client!(state) @@ -100,7 +114,7 @@ pub async fn vesting_start_time( #[tauri::command] pub async fn vesting_end_time( vesting_account_address: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Query vesting end time"); let res = nymd_client!(state) @@ -114,13 +128,19 @@ pub async fn vesting_end_time( #[tauri::command] pub async fn original_vesting( vesting_account_address: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Query original vesting"); - let res = nymd_client!(state) + let guard = state.read().await; + let reg = guard.registered_coins()?; + + let res = guard + .current_client()? + .nymd .original_vesting(vesting_account_address) - .await? - .try_into()?; + .await?; + + let res = OriginalVestingResponse::from_vesting_contract(res, reg)?; log::info!("<<< {:?}", res); Ok(res) } @@ -129,18 +149,23 @@ pub async fn original_vesting( pub async fn delegated_free( vesting_account_address: &str, block_time: Option, - state: tauri::State<'_, Arc>>, -) -> Result { + state: tauri::State<'_, WalletState>, +) -> Result { log::info!(">>> Query delegated free"); - let res = nymd_client!(state) + let guard = state.read().await; + + let res = guard + .current_client()? + .nymd .delegated_free( vesting_account_address, block_time.map(Timestamp::from_seconds), ) - .await? - .into(); - log::info!("<<< delegated free = {}", res); - Ok(res) + .await?; + + let display = guard.attempt_convert_to_display_dec_coin(res)?; + log::info!("<<< delegated free = {}", display); + Ok(display) } /// Returns the total amount of delegated tokens that have vested @@ -148,30 +173,42 @@ pub async fn delegated_free( pub async fn delegated_vesting( block_time: Option, vesting_account_address: &str, - state: tauri::State<'_, Arc>>, -) -> Result { + state: tauri::State<'_, WalletState>, +) -> Result { log::info!(">>> Query delegated vesting"); - let res = nymd_client!(state) + let guard = state.read().await; + + let res = guard + .current_client()? + .nymd .delegated_vesting( vesting_account_address, block_time.map(Timestamp::from_seconds), ) - .await? - .into(); - log::info!("<<< delegated_vesting = {}", res); - Ok(res) + .await?; + + let display = guard.attempt_convert_to_display_dec_coin(res)?; + log::info!("<<< delegated_vesting = {}", display); + Ok(display) } #[tauri::command] pub async fn vesting_get_mixnode_pledge( address: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::info!(">>> Query vesting get mixnode pledge"); - let res = nymd_client!(state) + let guard = state.read().await; + let reg = guard.registered_coins()?; + + let res = guard + .current_client()? + .nymd .get_mixnode_pledge(address) .await? - .and_then(PledgeData::and_then); + .map(|pledge| PledgeData::from_vesting_contract(pledge, reg)) + .transpose()?; + log::info!("<<< {:?}", res); Ok(res) } @@ -179,13 +216,20 @@ pub async fn vesting_get_mixnode_pledge( #[tauri::command] pub async fn vesting_get_gateway_pledge( address: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { log::info!(">>> Query vesting get gateway pledge"); - let res = nymd_client!(state) + let guard = state.read().await; + let reg = guard.registered_coins()?; + + let res = guard + .current_client()? + .nymd .get_gateway_pledge(address) .await? - .and_then(PledgeData::and_then); + .map(|pledge| PledgeData::from_vesting_contract(pledge, reg)) + .transpose()?; + log::info!("<<< {:?}", res); Ok(res) } @@ -193,7 +237,7 @@ pub async fn vesting_get_gateway_pledge( #[tauri::command] pub async fn get_current_vesting_period( address: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Query current vesting period"); let res = nymd_client!(state) @@ -206,10 +250,15 @@ pub async fn get_current_vesting_period( #[tauri::command] pub async fn get_account_info( address: &str, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Query account info"); - let res = nymd_client!(state).get_account(address).await?.try_into()?; + let guard = state.read().await; + let res = guard.registered_coins()?; + + let vesting_account = guard.current_client()?.nymd.get_account(address).await?; + let res = VestingAccountInfo::from_vesting_contract(vesting_account, res)?; + log::info!("<<< {:?}", res); Ok(res) } diff --git a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs index 5912c95f2e..b3a190c1ff 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs @@ -1,44 +1,46 @@ use crate::error::BackendError; -use crate::nymd_client; -use crate::state::State; +use crate::state::WalletState; use mixnet_contract_common::IdentityKey; use nym_types::transaction::TransactionExecuteResult; -use std::sync::Arc; -use tokio::sync::RwLock; use validator_client::nymd::Fee; #[tauri::command] pub async fn vesting_claim_operator_reward( - state: tauri::State<'_, Arc>>, + fee: Option, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Vesting account: claim operator reward"); - let denom_minor = state.read().await.current_network().base_mix_denom(); - let res = nymd_client!(state) + let guard = state.read().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + 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, - denom_minor.as_ref(), + res, fee_amount, )?) } #[tauri::command] pub async fn vesting_compound_operator_reward( fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Vesting account: compound operator reward"); - let denom_minor = state.read().await.current_network().base_mix_denom(); - let res = nymd_client!(state) + 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) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } @@ -46,21 +48,23 @@ pub async fn vesting_compound_operator_reward( pub async fn vesting_claim_delegator_reward( mix_identity: IdentityKey, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!( ">>> Vesting account: claim delegator reward: identity_key = {}", mix_identity ); - let denom_minor = state.read().await.current_network().base_mix_denom(); - let res = nymd_client!(state) + 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, - denom_minor.as_ref(), + res, fee_amount, )?) } @@ -68,20 +72,22 @@ pub async fn vesting_claim_delegator_reward( pub async fn vesting_compound_delegator_reward( mix_identity: IdentityKey, fee: Option, - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result { log::info!( ">>> Vesting account: compound delegator reward: identity_key = {}", mix_identity ); - let denom_minor = state.read().await.current_network().base_mix_denom(); - let res = nymd_client!(state) + 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) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( - res, - denom_minor.as_ref(), + res, fee_amount, )?) } diff --git a/nym-wallet/src-tauri/src/state.rs b/nym-wallet/src-tauri/src/state.rs index 1095d872b6..3abcb29324 100644 --- a/nym-wallet/src-tauri/src/state.rs +++ b/nym-wallet/src-tauri/src/state.rs @@ -1,20 +1,22 @@ use crate::config; use crate::error::BackendError; +use crate::simulate::SimulateResult; +use itertools::Itertools; +use log::warn; +use nym_types::currency::{DecCoin, RegisteredCoins}; +use nym_types::fees::FeeDetails; use nym_wallet_types::network::Network; use nym_wallet_types::network_config; - -use strum::IntoEnumIterator; -use validator_client::nymd::{AccountId as CosmosAccountId, SigningNymdClient}; -use validator_client::Client; - -use itertools::Itertools; use once_cell::sync::Lazy; -use tokio::sync::RwLock; -use url::Url; - use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; +use strum::IntoEnumIterator; +use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use url::Url; +use validator_client::nymd::cosmwasm_client::types::SimulateResponse; +use validator_client::nymd::{AccountId as CosmosAccountId, Coin, Fee, SigningNymdClient}; +use validator_client::Client; // Some hardcoded metadata overrides static METADATA_OVERRIDES: Lazy> = Lazy::new(|| { @@ -28,7 +30,7 @@ static METADATA_OVERRIDES: Lazy> = Lazy::new(|| { #[tauri::command] pub async fn load_config_from_files( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result<(), BackendError> { state.write().await.load_config_files(); Ok(()) @@ -36,13 +38,30 @@ pub async fn load_config_from_files( #[tauri::command] pub async fn save_config_to_files( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, ) -> Result<(), BackendError> { state.read().await.save_config_files() } +#[derive(Default, Clone)] +pub struct WalletState { + inner: Arc>, +} + +impl WalletState { + // not the best API, but those are exposed here for backwards compatibility with the existing + // state type assumptions so that we wouldn't need to fix it up everywhere at once + pub(crate) async fn read(&self) -> RwLockReadGuard<'_, WalletStateInner> { + self.inner.read().await + } + + pub(crate) async fn write(&self) -> RwLockWriteGuard<'_, WalletStateInner> { + self.inner.write().await + } +} + #[derive(Default)] -pub struct State { +pub struct WalletStateInner { config: config::Config, signing_clients: HashMap>, current_network: Network, @@ -56,6 +75,7 @@ pub struct State { /// We fetch (and cache) some metadata, such as names, when available validator_metadata: HashMap, + registered_coins: HashMap, } pub(crate) struct WalletAccountIds { @@ -65,7 +85,70 @@ pub(crate) struct WalletAccountIds { pub addresses: HashMap, } -impl State { +impl WalletStateInner { + // note that `Coin` is ALWAYS the base coin + pub fn attempt_convert_to_base_coin(&self, coin: DecCoin) -> Result { + let registered_coins = self + .registered_coins + .get(&self.current_network) + .ok_or_else(|| BackendError::UnknownCoinDenom(coin.denom.clone()))?; + + Ok(registered_coins.attempt_convert_to_base_coin(coin)?) + } + + pub fn attempt_convert_to_display_dec_coin(&self, coin: Coin) -> Result { + let registered_coins = self + .registered_coins + .get(&self.current_network) + .ok_or_else(|| BackendError::UnknownCoinDenom(coin.denom.clone()))?; + + Ok(registered_coins.attempt_convert_to_display_dec_coin(coin)?) + } + + pub(crate) fn registered_coins(&self) -> Result<&RegisteredCoins, BackendError> { + self.registered_coins + .get(&self.current_network) + .ok_or(BackendError::NoCoinsRegistered { + network: self.current_network, + }) + } + + pub(crate) fn convert_tx_fee(&self, fee: Option<&Fee>) -> Option { + let mut fee_amount = fee?.try_get_manual_amount()?; + if fee_amount.len() > 1 { + warn!( + "our tx fee contained more than a single denomination. using the first one for display" + ) + } + if fee_amount.is_empty() { + warn!("our tx has had an unknown fee set"); + None + } else { + self.attempt_convert_to_display_dec_coin(fee_amount.pop().unwrap()) + .ok() + } + } + + // this one is rather gnarly and I'm not 100% sure how to feel about existence of it + pub(crate) fn create_detailed_fee( + &self, + simulate_response: SimulateResponse, + ) -> Result { + // this MUST succeed as we just used it before + let client = self.current_client()?; + let gas_price = client.nymd.gas_price().clone(); + let gas_adjustment = client.nymd.gas_adjustment(); + + let res = SimulateResult::new(simulate_response.gas_info, gas_price, gas_adjustment); + + let amount = res + .to_fee_amount() + .map(|amount| self.attempt_convert_to_display_dec_coin(amount.into())) + .transpose()?; + + Ok(FeeDetails::new(amount, res.to_fee())) + } + pub fn client(&self, network: Network) -> Result<&Client, BackendError> { self.signing_clients .get(&network) @@ -112,6 +195,11 @@ impl State { self.signing_clients.insert(network, client); } + pub fn register_default_denoms(&mut self, network: Network) { + self.registered_coins + .insert(network, RegisteredCoins::default_denoms(network.into())); + } + pub fn set_network(&mut self, network: Network) { self.current_network = network; } @@ -390,7 +478,7 @@ mod tests { #[test] fn adding_validators_urls_prepends() { - let mut state = State::default(); + let mut state = WalletStateInner::default(); let _api_urls = state.get_api_urls(Network::MAINNET).collect::>(); state.add_validator_url( diff --git a/nym-wallet/src-tauri/src/utils.rs b/nym-wallet/src-tauri/src/utils.rs index ff8f6c2c85..304004a6ef 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -1,11 +1,9 @@ use crate::error::BackendError; use crate::nymd_client; -use crate::state::State; -use nym_types::currency::MajorCurrencyAmount; +use crate::state::WalletState; +use nym_types::currency::DecCoin; use nym_wallet_types::app::AppEnv; use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use tokio::sync::RwLock; use validator_client::nymd::{tx, Coin, CosmosCoin, Gas, GasPrice}; fn get_env_as_option(key: &str) -> Option { @@ -25,9 +23,7 @@ pub fn get_env() -> AppEnv { } #[tauri::command] -pub async fn owns_mixnode( - state: tauri::State<'_, Arc>>, -) -> Result { +pub async fn owns_mixnode(state: tauri::State<'_, WalletState>) -> Result { Ok(nymd_client!(state) .owns_mixnode(nymd_client!(state).address()) .await? @@ -35,9 +31,7 @@ pub async fn owns_mixnode( } #[tauri::command] -pub async fn owns_gateway( - state: tauri::State<'_, Arc>>, -) -> Result { +pub async fn owns_gateway(state: tauri::State<'_, WalletState>) -> Result { Ok(nymd_client!(state) .owns_gateway(nymd_client!(state).address()) .await? @@ -145,13 +139,14 @@ impl Operation { #[tauri::command] pub async fn get_old_and_incorrect_hardcoded_fee( - state: tauri::State<'_, Arc>>, + state: tauri::State<'_, WalletState>, operation: Operation, -) -> Result { - let mut approximate_fee = operation.default_fee(nymd_client!(state).gas_price()); +) -> Result { + let guard = state.read().await; + let mut approximate_fee = operation.default_fee(guard.current_client()?.nymd.gas_price()); // on all our chains it should only ever contain a single type of currency assert_eq!(approximate_fee.amount.len(), 1); let coin: Coin = approximate_fee.amount.pop().unwrap().into(); log::info!("hardcoded fee for {:?} is {:?}", operation, coin); - Ok(coin.into()) + guard.attempt_convert_to_display_dec_coin(coin) } diff --git a/nym-wallet/src-tauri/tauri.conf.json b/nym-wallet/src-tauri/tauri.conf.json index d2b8c6c5f7..238f534a1a 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.7" + "version": "1.0.8" }, "build": { "distDir": "../dist", diff --git a/nym-wallet/src/components/Accounts/AccountAvatar.tsx b/nym-wallet/src/components/Accounts/AccountAvatar.tsx index 5f71df533e..9335c1a743 100644 --- a/nym-wallet/src/components/Accounts/AccountAvatar.tsx +++ b/nym-wallet/src/components/Accounts/AccountAvatar.tsx @@ -1,8 +1,12 @@ import React from 'react'; -import { Avatar } from '@mui/material'; +import { Avatar, Typography } from '@mui/material'; import stc from 'string-to-color'; import { TAccount } from 'src/types'; -export const AccountAvatar = ({ name }: Pick) => ( - {name?.split('')[0]} +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 d2c8dd9125..5df9cdf2ed 100644 --- a/nym-wallet/src/components/Accounts/AccountOverview.tsx +++ b/nym-wallet/src/components/Accounts/AccountOverview.tsx @@ -5,10 +5,10 @@ import { AccountAvatar } from './AccountAvatar'; export const AccountOverview = ({ account, onClick }: { account: AccountEntry; onClick: () => void }) => ( diff --git a/nym-wallet/src/components/Accounts/MultiAccountHowTo.tsx b/nym-wallet/src/components/Accounts/MultiAccountHowTo.tsx index 0ca8524543..aca5552b2e 100644 --- a/nym-wallet/src/components/Accounts/MultiAccountHowTo.tsx +++ b/nym-wallet/src/components/Accounts/MultiAccountHowTo.tsx @@ -20,7 +20,7 @@ export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handle - t.palette.nym.text.muted }}> + theme.palette.nym.text.muted }}> How to set up multiple accounts @@ -29,7 +29,7 @@ export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handle (t.palette.mode === 'dark' ? { bgcolor: (t) => t.palette.background.paper } : {})} + sx={(t) => (t.palette.mode === 'dark' ? { bgcolor: (theme) => theme.palette.background.paper } : {})} > In order to create multiple accounts your wallet needs a password. Follow steps below to create password. diff --git a/nym-wallet/src/components/Accounts/modals/AccountsModal.tsx b/nym-wallet/src/components/Accounts/modals/AccountsModal.tsx index 92988c6f9e..cb1fe51c1d 100644 --- a/nym-wallet/src/components/Accounts/modals/AccountsModal.tsx +++ b/nym-wallet/src/components/Accounts/modals/AccountsModal.tsx @@ -9,6 +9,7 @@ import { DialogTitle, IconButton, Typography, + Divider, } from '@mui/material'; import { Add, ArrowDownwardSharp, Close } from '@mui/icons-material'; import { AccountsContext } from 'src/context'; @@ -51,7 +52,7 @@ export const AccountsModal = () => { - theme.palette.text.disabled }}> + Switch between accounts @@ -69,6 +70,7 @@ export const AccountsModal = () => { /> ))} + @@ -50,10 +50,12 @@ const ImportMnemonic = ({ value, onChange, onNext, + onBack, }: { value: string; onChange: (value: string) => void; onNext: () => void; + onBack: () => void; }) => { const [error, setError] = useState(); @@ -77,7 +79,8 @@ const ImportMnemonic = ({ }} /> - + + - - - - ); -}; diff --git a/nym-wallet/src/components/ActionsMenu.tsx b/nym-wallet/src/components/ActionsMenu.tsx new file mode 100644 index 0000000000..63e8193833 --- /dev/null +++ b/nym-wallet/src/components/ActionsMenu.tsx @@ -0,0 +1,42 @@ +import React, { useRef } from 'react'; +import { MoreVertSharp } from '@mui/icons-material'; +import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material'; + +export const ActionsMenu: React.FC<{ open: boolean; onOpen: () => void; onClose: () => void }> = ({ + children, + open, + onOpen, + onClose, +}) => { + const anchorEl: any = useRef(); + + return ( + <> + + + + + {children} + + + ); +}; + +export const ActionsMenuItem = ({ + title, + description, + onClick, + Icon, + disabled, +}: { + title: string; + description?: string; + onClick?: () => void; + Icon?: React.ReactNode; + disabled?: boolean; +}) => ( + + {Icon} + + +); diff --git a/nym-wallet/src/components/AppBar.tsx b/nym-wallet/src/components/AppBar.tsx index 74ed63fbf3..9aedf14d1a 100644 --- a/nym-wallet/src/components/AppBar.tsx +++ b/nym-wallet/src/components/AppBar.tsx @@ -1,6 +1,5 @@ import React, { useContext } from 'react'; -import { styled } from '@mui/material/styles'; -import { AppBar as MuiAppBar, Grid, IconButton, Toolbar, FormGroup, FormControlLabel, Switch } from '@mui/material'; +import { AppBar as MuiAppBar, Grid, IconButton, Toolbar } from '@mui/material'; import { useNavigate } from 'react-router-dom'; import { Logout } from '@mui/icons-material'; import TerminalIcon from '@mui/icons-material/Terminal'; @@ -8,13 +7,11 @@ import ModeNightOutlinedIcon from '@mui/icons-material/ModeNightOutlined'; import LightModeOutlinedIcon from '@mui/icons-material/LightModeOutlined'; import { AppContext } from '../context/main'; import { NetworkSelector } from './NetworkSelector'; -import { Node as NodeIcon } from '../svg-icons/node'; import { MultiAccounts } from './Accounts'; import { config } from '../config'; export const AppBar = () => { - const { logOut, handleShowTerminal, appEnv, handleShowSettings, showSettings, mode, handleSwitchMode } = - useContext(AppContext); + const { logOut, handleShowTerminal, appEnv, mode, handleSwitchMode } = useContext(AppContext); const navigate = useNavigate(); return ( @@ -32,7 +29,7 @@ export const AppBar = () => { - {mode === 'light' ? ( + {mode === 'dark' ? ( ) : ( @@ -46,15 +43,6 @@ export const AppBar = () => { )} - - - - - { - const { showSettings, handleShowTerminal, appEnv, handleShowSettings, logOut, mode, handleSwitchMode } = - useContext(AppContext); - - return ( - - - - - - - - - - - - - - - {mode === 'light' ? ( - - ) : ( - - )} - - - {(appEnv?.SHOW_TERMINAL || config.IS_DEV_MODE) && ( - - - - - - )} - - - - - - - - - - - - - - - ); -}; diff --git a/nym-wallet/src/components/AppBar/index.ts b/nym-wallet/src/components/AppBar/index.ts deleted file mode 100644 index 60f2f0e90b..0000000000 --- a/nym-wallet/src/components/AppBar/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './AppBar'; diff --git a/nym-wallet/src/components/Bonding/Bond.tsx b/nym-wallet/src/components/Bonding/Bond.tsx new file mode 100644 index 0000000000..3392cde43f --- /dev/null +++ b/nym-wallet/src/components/Bonding/Bond.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Box, Button, Typography } from '@mui/material'; +import { NymCard } from '../NymCard'; + +export const Bond = ({ + onBond, + disabled, +}: { + onBond: () => void; + + disabled: boolean; +}) => ( + + + Bond a mixnode or a gateway + + + + + +); diff --git a/nym-wallet/src/components/Bonding/BondedGateway.tsx b/nym-wallet/src/components/Bonding/BondedGateway.tsx new file mode 100644 index 0000000000..2ae9b26608 --- /dev/null +++ b/nym-wallet/src/components/Bonding/BondedGateway.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { Stack, Typography } from '@mui/material'; +import { Link } from '@nymproject/react/link/Link'; +import { TBondedGateway, urls } from 'src/context'; +import { NymCard } from 'src/components'; +import { Network } from 'src/types'; +import { IdentityKey } from 'src/components/IdentityKey'; +import { Cell, Header, NodeTable } from './NodeTable'; +import { BondedGatewayActions, TBondedGatwayActions } from './BondedGatewayAction'; + +const headers: Header[] = [ + { + header: 'IP', + id: 'ip', + sx: { pl: 0 }, + }, + { + header: 'Bond', + id: 'bond', + }, + { + id: 'menu-button', + sx: { width: 34, maxWidth: 34 }, + }, +]; + +export const BondedGateway = ({ + gateway, + network, + onActionSelect, +}: { + gateway: TBondedGateway; + network?: Network; + onActionSelect: (action: TBondedGatwayActions) => void; +}) => { + const { name, bond, ip, identityKey } = gateway; + const cells: Cell[] = [ + { + cell: ip, + id: 'stake-saturation-cell', + }, + { + cell: `${bond.amount} ${bond.denom}`, + id: 'stake-cell', + sx: { pl: 0 }, + }, + + { + cell: , + id: 'actions-cell', + align: 'right', + }, + ]; + + return ( + + + Gateway + + + {name && ( + + {name} + + )} + + + } + > + + {network && ( + + Check more stats of your node on the{' '} + + explorer + + + )} + + ); +}; diff --git a/nym-wallet/src/components/Bonding/BondedGatewayAction.tsx b/nym-wallet/src/components/Bonding/BondedGatewayAction.tsx new file mode 100644 index 0000000000..c511d07bf4 --- /dev/null +++ b/nym-wallet/src/components/Bonding/BondedGatewayAction.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; +import { ActionsMenu, ActionsMenuItem } from 'src/components/ActionsMenu'; +import { Unbond as UnbondIcon } from '../../svg-icons'; + +export type TBondedGatwayActions = 'unbond'; + +export const BondedGatewayActions = ({ + onActionSelect, +}: { + onActionSelect: (action: TBondedGatwayActions) => void; +}) => { + const [isOpen, setIsOpen] = useState(false); + + const handleOpen = () => setIsOpen(true); + const handleClose = () => setIsOpen(false); + + const handleActionClick = (action: TBondedGatwayActions) => { + onActionSelect(action); + handleClose(); + }; + + return ( + + } + onClick={() => handleActionClick('unbond')} + /> + + ); +}; diff --git a/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx new file mode 100644 index 0000000000..2e9a233d85 --- /dev/null +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -0,0 +1,138 @@ +import React from 'react'; +import { Box, Button, Stack, Typography } from '@mui/material'; +import { Link } from '@nymproject/react/link/Link'; +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'; + +const headers: Header[] = [ + { + header: 'Stake', + id: 'stake', + sx: { pl: 0 }, + }, + { + header: 'Bond', + id: 'bond', + }, + { + header: 'Stake saturation', + id: 'stake-saturation', + }, + { + header: 'PM', + id: 'profit-margin', + 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: 'Operator rewards', + id: 'operator-rewards', + tooltipText: + 'This is your (operator) new rewards including the PM and cost. You can compound your rewards manually every epoch or unbond your node to redeem them.', + }, + { + header: 'No. delegators', + id: 'delegators', + }, + { + id: 'menu-button', + sx: { width: 34, maxWidth: 34 }, + }, +]; + +export const BondedMixnode = ({ + mixnode, + network, + onActionSelect, +}: { + mixnode: TBondedMixnode; + network?: Network; + onActionSelect: (action: TBondedMixnodeActions) => void; +}) => { + const { name, stake, bond, stakeSaturation, profitMargin, operatorRewards, delegators, status, identityKey } = + mixnode; + const cells: Cell[] = [ + { + cell: `${stake.amount} ${stake.denom}`, + id: 'stake-cell', + }, + { + cell: `${bond.amount} ${bond.denom}`, + id: 'bond-cell', + }, + { + cell: `${stakeSaturation}%`, + id: 'stake-saturation-cell', + }, + { + cell: `${profitMargin}%`, + id: 'pm-cell', + }, + { + cell: operatorRewards ? `${operatorRewards.amount} ${operatorRewards.denom}` : '-', + id: 'operator-rewards-cell', + }, + { + cell: delegators, + id: 'delegators-cell', + }, + { + cell: ( + + ), + id: 'actions-cell', + align: 'right', + }, + ]; + + return ( + + + + Mix node + + + + {name && ( + + {name} + + )} + + + } + Action={ + + } + > + + {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 new file mode 100644 index 0000000000..83cc2a0198 --- /dev/null +++ b/nym-wallet/src/components/Bonding/BondedMixnodeActions.tsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; +import { Typography } from '@mui/material'; +import { ActionsMenu, ActionsMenuItem } from 'src/components/ActionsMenu'; +import { Unbond as UnbondIcon } from '../../svg-icons'; + +export type TBondedMixnodeActions = 'nodeSettings' | 'bondMore' | 'unbond' | 'redeem' | 'compound'; + +export const BondedMixnodeActions = ({ + onActionSelect, + disabledRedeemAndCompound, +}: { + onActionSelect: (action: TBondedMixnodeActions) => void; + disabledRedeemAndCompound: boolean; +}) => { + const [isOpen, setIsOpen] = useState(false); + + const handleOpen = () => setIsOpen(true); + const handleClose = () => setIsOpen(false); + + const handleActionClick = (action: TBondedMixnodeActions) => { + onActionSelect(action); + handleClose(); + }; + + return ( + + } + 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/NodeTable.tsx b/nym-wallet/src/components/Bonding/NodeTable.tsx new file mode 100644 index 0000000000..d1e620a1ce --- /dev/null +++ b/nym-wallet/src/components/Bonding/NodeTable.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { + Stack, + SxProps, + Table, + TableBody, + TableCell, + TableCellProps, + TableContainer, + TableHead, + TableRow, + Typography, +} from '@mui/material'; +import { InfoTooltip } from '../InfoToolTip'; + +export type Header = { header?: string; id: string; tooltipText?: string; sx?: SxProps }; +export type Cell = { cell: string | React.ReactNode; id: string; align?: TableCellProps['align']; sx?: SxProps }; + +export interface TableProps { + headers: Header[]; + cells: Cell[]; +} + +export const NodeTable = ({ headers, cells }: TableProps) => ( + +
+ + + {headers.map(({ header, id, tooltipText }) => ( + + + {tooltipText && } + {header} + + + ))} + + + + + {cells.map(({ cell, id, align }) => ( + + {cell} + + ))} + + +
+
+); diff --git a/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx b/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx new file mode 100644 index 0000000000..c4f827dcd5 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx @@ -0,0 +1,214 @@ +import React, { useEffect, useState } from 'react'; +import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; +import { Box, Checkbox, FormControlLabel, Stack, TextField } from '@mui/material'; +import { useForm } from 'react-hook-form'; +import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { NodeTypeSelector, TokenPoolSelector } from 'src/components'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from 'src/utils'; +import { CurrencyDenom, TNodeType } from '@nymproject/types'; +import { GatewayAmount, GatewayData } from 'src/pages/bonding/types'; +import { gatewayValidationSchema, amountSchema } from './gatewayValidationSchema'; + +const NodeFormData = ({ gatewayData, onNext }: { gatewayData: GatewayData; onNext: (data: GatewayData) => void }) => { + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + + const { + register, + formState: { errors }, + handleSubmit, + setValue, + } = useForm({ resolver: yupResolver(gatewayValidationSchema), defaultValues: gatewayData }); + + const handleRequestValidation = (event: { detail: { step: number } }) => { + if (event.detail.step === 1) { + handleSubmit(onNext)(); + } + }; + + useEffect(() => { + window.addEventListener('validate_bond_gateway_step' as any, handleRequestValidation); + return () => window.removeEventListener('validate_bond_gateway_step' as any, handleRequestValidation); + }, []); + + return ( + + setValue('identityKey', value)} + /> + + + + + + + + setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />} + label="Show advanced options" + /> + {showAdvancedOptions && ( + + + + + )} + + ); +}; + +const AmountFormData = ({ + denom, + amountData, + hasVestingTokens, + onNext, +}: { + denom: CurrencyDenom; + amountData: GatewayAmount; + hasVestingTokens: boolean; + onNext: (data: any) => void; +}) => { + const { + formState: { errors }, + handleSubmit, + setValue, + getValues, + setError, + } = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData }); + + const handleRequestValidation = async (event: { detail: { step: number } }) => { + let hasSufficientTokens = true; + const values = getValues(); + + if (values.tokenPool === 'balance') { + hasSufficientTokens = await checkHasEnoughFunds(values.amount.amount); + } + + if (values.tokenPool === 'locked') { + hasSufficientTokens = await checkHasEnoughLockedTokens(values.amount.amount); + } + + if (event.detail.step === 2 && hasSufficientTokens) { + handleSubmit(onNext)(); + } else { + setError('amount.amount', { message: 'Not enough tokens' }); + } + }; + + useEffect(() => { + window.addEventListener('validate_bond_gateway_step' as any, handleRequestValidation); + return () => window.removeEventListener('validate_bond_gateway_step' as any, handleRequestValidation); + }, []); + + return ( + + + {hasVestingTokens && setValue('tokenPool', pool)} />} + setValue('amount', newValue, { shouldValidate: true })} + validationError={errors.amount?.amount?.message} + denom={denom} + initialValue={amountData.amount.amount} + /> + + + ); +}; + +export const BondGatewayForm = ({ + step, + denom, + gatewayData, + amountData, + hasVestingTokens, + onValidateGatewayData, + onValidateAmountData, + onSelectNodeType, +}: { + step: 1 | 2 | 3; + gatewayData: GatewayData; + amountData: GatewayAmount; + denom: CurrencyDenom; + hasVestingTokens: boolean; + onValidateGatewayData: (data: GatewayData) => void; + onValidateAmountData: (data: GatewayAmount) => Promise; + onSelectNodeType: (nodeType: TNodeType) => void; +}) => ( + <> + {step === 1 && ( + <> + + + + + + )} + {step === 2 && ( + + )} + +); diff --git a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx new file mode 100644 index 0000000000..99be47d96b --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx @@ -0,0 +1,223 @@ +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 { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; +import { CurrencyDenom, TNodeType } from '@nymproject/types'; +import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from 'src/utils'; +import { NodeTypeSelector, TokenPoolSelector } from 'src/components'; +import { MixnodeAmount, MixnodeData } from 'src/pages/bonding/types'; +import { amountSchema, mixnodeValidationSchema } from './mixnodeValidationSchema'; + +const NodeFormData = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNext: (data: any) => void }) => { + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + + const { + register, + formState: { errors }, + handleSubmit, + setValue, + } = useForm({ resolver: yupResolver(mixnodeValidationSchema), defaultValues: mixnodeData }); + + const handleRequestValidation = (event: { detail: { step: number } }) => { + if (event.detail.step === 1) { + handleSubmit(onNext)(); + } + }; + + useEffect(() => { + window.addEventListener('validate_bond_mixnode_step' as any, handleRequestValidation); + return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleRequestValidation); + }, []); + + return ( + + setValue('identityKey', value)} + /> + + + + + + + setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />} + label="Show advanced options" + /> + {showAdvancedOptions && ( + + + + + + )} + + ); +}; + +const AmountFormData = ({ + amountData, + hasVestingTokens, + denom, + onNext, +}: { + amountData: MixnodeAmount; + hasVestingTokens: boolean; + denom: CurrencyDenom; + onNext: (data: MixnodeAmount) => void; +}) => { + const { + register, + formState: { errors }, + handleSubmit, + setValue, + getValues, + setError, + } = useForm({ resolver: yupResolver(amountSchema), defaultValues: amountData }); + + const handleRequestValidation = async (event: { detail: { step: number } }) => { + let hasSufficientTokens = true; + const values = getValues(); + + if (values.tokenPool === 'balance') { + hasSufficientTokens = await checkHasEnoughFunds(values.amount.amount); + } + + if (values.tokenPool === 'locked') { + hasSufficientTokens = await checkHasEnoughLockedTokens(values.amount.amount); + } + + if (event.detail.step === 2 && hasSufficientTokens) { + handleSubmit(onNext)(); + } else { + setError('amount.amount', { message: 'Not enough tokens' }); + } + }; + + useEffect(() => { + window.addEventListener('validate_bond_mixnode_step' as any, handleRequestValidation); + return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleRequestValidation); + }, []); + + return ( + + + {hasVestingTokens && setValue('tokenPool', pool)} />} + { + setValue('amount', newValue, { shouldValidate: true }); + }} + validationError={errors.amount?.amount?.message} + denom={denom} + initialValue={amountData.amount.amount} + /> + + + + ); +}; + +export const BondMixnodeForm = ({ + step, + denom, + mixnodeData, + amountData, + hasVestingTokens, + onValidateMixnodeData, + onValidateAmountData, + onSelectNodeType, +}: { + step: 1 | 2 | 3; + mixnodeData: MixnodeData; + amountData: MixnodeAmount; + denom: CurrencyDenom; + hasVestingTokens: boolean; + onValidateMixnodeData: (data: MixnodeData) => void; + onValidateAmountData: (data: MixnodeAmount) => Promise; + onSelectNodeType: (nodeType: TNodeType) => void; +}) => ( + <> + {step === 1 && ( + <> + + + + + + )} + {step === 2 && ( + + )} + +); diff --git a/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts new file mode 100644 index 0000000000..fb1625adc1 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts @@ -0,0 +1,59 @@ +import * as Yup from 'yup'; +import { + isValidHostname, + validateAmount, + validateKey, + validateLocation, + validateRawPort, + validateVersion, +} from 'src/utils'; + +export const gatewayValidationSchema = Yup.object().shape({ + identityKey: Yup.string() + .required('An indentity key is required') + .test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)), + + sphinxKey: Yup.string() + .required('A sphinx key is required') + .test('valid-sphinx-key', 'A valid sphinx key is required', (value) => validateKey(value || '', 32)), + + ownerSignature: Yup.string() + .required('Signature is required') + .test('valid-signature', 'A valid signature is required', (value) => validateKey(value || '', 64)), + + 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)), + + location: Yup.string() + .required('A location is required') + .test('valid-location', 'A valid version is required', (locationValueTest) => + locationValueTest ? validateLocation(locationValueTest) : false, + ), + + mixPort: Yup.number() + .required('A mixport is required') + .test('valid-mixport', 'A valid mixport is required', (value) => (value ? validateRawPort(value) : false)), + + clientsPort: Yup.number() + .required('A clients port is required') + .test('valid-clients', 'A valid clients port is required', (value) => (value ? validateRawPort(value) : false)), +}); + +export const amountSchema = Yup.object().shape({ + amount: Yup.object().shape({ + amount: Yup.string() + .required('An amount is required') + .test('valid-amount', 'Pledge error', async function isValidAmount(this, value) { + const isValid = await validateAmount(value || '', '100'); + if (!isValid) { + return this.createError({ message: 'A valid amount is required (min 100)' }); + } + return true; + }), + }), +}); diff --git a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts new file mode 100644 index 0000000000..aa9e66db1c --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts @@ -0,0 +1,51 @@ +import * as Yup from 'yup'; +import { isValidHostname, validateAmount, validateKey, validateRawPort, validateVersion } from 'src/utils'; + +export const mixnodeValidationSchema = Yup.object().shape({ + identityKey: Yup.string() + .required('An identity key is required') + .test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)), + + sphinxKey: Yup.string() + .required('A sphinx key is required') + .test('valid-sphinx-key', 'A valid sphinx key is required', (value) => validateKey(value || '', 32)), + + ownerSignature: Yup.string() + .required('Signature is required') + .test('valid-signature', 'A valid signature is required', (value) => validateKey(value || '', 64)), + + 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 amountSchema = Yup.object().shape({ + amount: Yup.object().shape({ + amount: Yup.string() + .required('An amount is required') + .test('valid-amount', 'Pledge error', async function isValidAmount(this, value) { + const isValid = await validateAmount(value || '', '100'); + if (!isValid) { + return this.createError({ message: 'A valid amount is required (min 100)' }); + } + return true; + }), + }), + profitMargin: Yup.number().required('Profit Percentage is required').min(0).max(100), +}); diff --git a/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx b/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx new file mode 100644 index 0000000000..d54f75ce24 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx @@ -0,0 +1,161 @@ +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'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { TPoolOption } from 'src/components/TokenPoolSelector'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { GatewayAmount, GatewayData } from 'src/pages/bonding/types'; +import { simulateBondGateway, simulateVestingBondGateway } from 'src/requests'; +import { TBondGatewayArgs } from 'src/types'; +import { BondGatewayForm } from '../forms/BondGatewayForm'; + +const defaultMixnodeValues: GatewayData = { + identityKey: '', + sphinxKey: '', + ownerSignature: '', + location: '', + host: '', + version: '', + mixPort: 1789, + clientsPort: 1790, +}; + +const defaultAmountValues = (denom: CurrencyDenom) => ({ + amount: { amount: '100', denom }, + tokenPool: 'balance', +}); + +export const BondGatewayModal = ({ + denom, + hasVestingTokens, + onBondGateway, + onSelectNodeType, + onClose, + onError, +}: { + denom: CurrencyDenom; + hasVestingTokens: boolean; + onBondGateway: (data: TBondGatewayArgs, tokenPool: TPoolOption) => void; + onSelectNodeType: (type: TNodeType) => void; + onClose: () => void; + onError: (e: string) => void; +}) => { + const [step, setStep] = useState<1 | 2 | 3>(1); + const [gatewayData, setGatewayData] = useState(defaultMixnodeValues); + const [amountData, setAmountData] = useState(defaultAmountValues(denom)); + + const { fee, getFee, resetFeeState, feeError } = useGetFee(); + + useEffect(() => { + if (feeError) { + onError(feeError); + } + }, [feeError]); + + const validateStep = async (s: number) => { + const event = new CustomEvent('validate_bond_gateway_step', { detail: { step: s } }); + window.dispatchEvent(event); + }; + + const handleBack = () => { + setStep(1); + }; + + const handleUpdateGatwayData = (data: GatewayData) => { + setGatewayData(data); + setStep(2); + }; + + const handleUpdateAmountData = async (data: GatewayAmount) => { + setAmountData(data); + const payload = { + pledge: data.amount, + ownerSignature: gatewayData.ownerSignature, + gateway: { + ...gatewayData, + host: gatewayData.host, + version: gatewayData.version, + mix_port: gatewayData.mixPort, + clients_port: gatewayData.clientsPort, + sphinx_key: gatewayData.sphinxKey, + identity_key: gatewayData.identityKey, + location: gatewayData.location, + }, + }; + + if (data.tokenPool === 'balance') { + await getFee(simulateBondGateway, payload); + } else { + await getFee(simulateVestingBondGateway, payload); + } + }; + + const handleConfirm = async () => { + await onBondGateway( + { + pledge: amountData.amount, + ownerSignature: gatewayData.ownerSignature, + gateway: { + ...gatewayData, + host: gatewayData.host, + version: gatewayData.version, + mix_port: gatewayData.mixPort, + clients_port: gatewayData.clientsPort, + sphinx_key: gatewayData.sphinxKey, + identity_key: gatewayData.identityKey, + location: gatewayData.location, + }, + }, + amountData.tokenPool as TPoolOption, + ); + }; + + if (fee) { + return ( + + + + + ); + } + + return ( + { + await validateStep(step); + }} + onBack={step === 2 ? handleBack : undefined} + onClose={onClose} + header="Bond gateway" + subHeader={`Step ${step}/2`} + okLabel="Next" + > + + + + + ); +}; diff --git a/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx b/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx new file mode 100644 index 0000000000..fd0de36051 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx @@ -0,0 +1,160 @@ +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'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { TPoolOption } from 'src/components/TokenPoolSelector'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { MixnodeAmount, MixnodeData } from 'src/pages/bonding/types'; +import { simulateBondMixnode, simulateVestingBondMixnode } from 'src/requests'; +import { TBondMixNodeArgs } from 'src/types'; +import { BondMixnodeForm } from '../forms/BondMixnodeForm'; + +const defaultMixnodeValues: MixnodeData = { + identityKey: '', + sphinxKey: '', + ownerSignature: '', + host: '', + version: '', + mixPort: 1789, + verlocPort: 1790, + httpApiPort: 8000, +}; + +const defaultAmountValues = (denom: CurrencyDenom) => ({ + amount: { amount: '100', denom }, + profitMargin: 10, + tokenPool: 'balance', +}); + +export const BondMixnodeModal = ({ + denom, + hasVestingTokens, + onBondMixnode, + onSelectNodeType, + onClose, + onError, +}: { + denom: CurrencyDenom; + hasVestingTokens: boolean; + onBondMixnode: (data: TBondMixNodeArgs, tokenPool: TPoolOption) => void; + onSelectNodeType: (type: TNodeType) => void; + onClose: () => void; + onError: (e: string) => void; +}) => { + const [step, setStep] = useState<1 | 2 | 3>(1); + const [mixnodeData, setMixnodeData] = useState(defaultMixnodeValues); + const [amountData, setAmountData] = useState(defaultAmountValues(denom)); + + const { fee, getFee, resetFeeState, feeError } = useGetFee(); + + useEffect(() => { + if (feeError) { + onError(feeError); + } + }, [feeError]); + + const validateStep = async (s: number) => { + const event = new CustomEvent('validate_bond_mixnode_step', { detail: { step: s } }); + window.dispatchEvent(event); + }; + + const handleBack = () => { + setStep(1); + }; + + const handleUpdateMixnodeData = (data: MixnodeData) => { + setMixnodeData(data); + setStep(2); + }; + + const handleUpdateAmountData = async (data: MixnodeAmount) => { + setAmountData(data); + const payload = { + pledge: data.amount, + ownerSignature: mixnodeData.ownerSignature, + mixnode: { + ...mixnodeData, + mix_port: mixnodeData.mixPort, + http_api_port: mixnodeData.httpApiPort, + verloc_port: mixnodeData.verlocPort, + sphinx_key: mixnodeData.sphinxKey, + identity_key: mixnodeData.identityKey, + profit_margin_percent: data.profitMargin, + }, + }; + + if (data.tokenPool === 'balance') { + await getFee(simulateBondMixnode, payload); + } else { + await getFee(simulateVestingBondMixnode, payload); + } + }; + + const handleConfirm = async () => { + await onBondMixnode( + { + pledge: amountData.amount, + ownerSignature: mixnodeData.ownerSignature, + mixnode: { + ...mixnodeData, + mix_port: mixnodeData.mixPort, + http_api_port: mixnodeData.httpApiPort, + verloc_port: mixnodeData.verlocPort, + sphinx_key: mixnodeData.sphinxKey, + identity_key: mixnodeData.identityKey, + profit_margin_percent: amountData.profitMargin, + }, + }, + amountData.tokenPool as TPoolOption, + ); + }; + + if (fee) { + return ( + + + + + ); + } + + return ( + { + await validateStep(step); + }} + onBack={step === 2 ? handleBack : undefined} + onClose={onClose} + header="Bond mixnode" + 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 new file mode 100644 index 0000000000..235795e77a --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/BondMoreModal.tsx @@ -0,0 +1,115 @@ +import React, { useEffect, useState } from 'react'; +import { Box, FormHelperText, Stack, TextField } from '@mui/material'; +import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { ModalListItem } from 'src/components/Modals/ModalListItem'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { DecCoin } from '@nymproject/types'; +import { TokenPoolSelector, TPoolOption } from 'src/components/TokenPoolSelector'; +import { ConfirmTx } from 'src/components/ConfirmTX'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { validateAmount, validateKey } from 'src/utils'; + +export const BondMoreModal = ({ + currentBond, + userBalance, + hasVestingTokens, + onConfirm, + onClose, +}: { + currentBond: DecCoin; + userBalance?: string; + hasVestingTokens: boolean; + onConfirm: (args: { additionalBond: DecCoin; signature: string; tokenPool: TPoolOption }) => Promise; + onClose: () => void; +}) => { + const { fee, resetFeeState } = useGetFee(); + const [additionalBond, setAdditionalBond] = useState({ amount: '0', denom: currentBond.denom }); + const [signature, setSignature] = useState(''); + const [tokenPool, setTokenPool] = useState('balance'); + const [errorAmount, setErrorAmount] = useState(false); + const [errorSignature, setErrorSignature] = useState(false); + + const handleOnOk = async () => { + const errors = { + amount: false, + signature: false, + }; + + if (!validateKey(signature || '', 64)) { + errors.signature = true; + } + + if (!additionalBond?.amount) { + errors.amount = true; + } + + if (additionalBond && !(await validateAmount(additionalBond.amount, '1'))) { + errors.amount = true; + } + + if (!errors.amount && !errors.signature) { + onConfirm({ additionalBond, signature, tokenPool }); + } else { + setErrorAmount(errors.amount); + setErrorSignature(errors.signature); + } + }; + + useEffect(() => { + setErrorAmount(false); + }, [additionalBond]); + + if (fee) + return ( + onConfirm({ additionalBond, signature, tokenPool })} + onPrev={resetFeeState} + > + + + + ); + + return ( + + + + {hasVestingTokens && setTokenPool(pool)} />} + { + setAdditionalBond(value); + setErrorSignature(false); + }} + fullWidth + validationError={errorAmount ? 'Please enter a valid amount' : undefined} + /> + + + + setSignature(e.target.value)} /> + {errorSignature && Invalid signature} + + + + + + + + + + ); +}; diff --git a/nym-wallet/src/components/Bonding/modals/CompoundRewardsModal.tsx b/nym-wallet/src/components/Bonding/modals/CompoundRewardsModal.tsx new file mode 100644 index 0000000000..17b3a5b2e7 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/CompoundRewardsModal.tsx @@ -0,0 +1,55 @@ +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/ConfirmationModal.tsx b/nym-wallet/src/components/Bonding/modals/ConfirmationModal.tsx new file mode 100644 index 0000000000..9a3c683b55 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/ConfirmationModal.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { Stack, Typography, SxProps } from '@mui/material'; +import { Link } from '@nymproject/react/link/Link'; +import { ConfirmationModal } from 'src/components/Modals/ConfirmationModal'; +import { ErrorModal } from 'src/components/Modals/ErrorModal'; + +export type ConfirmationDetailProps = { + status: 'success' | 'error'; + title: string; + subtitle?: string; + txUrl?: string; +}; + +export const ConfirmationDetailsModal = ({ + title, + subtitle, + txUrl, + status, + onClose, + sx, + backdropProps, +}: ConfirmationDetailProps & { + onClose: () => void; + sx?: SxProps; + backdropProps?: object; +}) => { + if (status === 'error') { + ; + } + + return ( + + + + {title} + + {subtitle} + {txUrl && } + + + ); +}; diff --git a/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx b/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx new file mode 100644 index 0000000000..1a1903d205 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx @@ -0,0 +1,128 @@ +import React, { useEffect, useState } from 'react'; +import { Box, Button, FormHelperText, TextField, Typography } from '@mui/material'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +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 { useGetFee } from 'src/hooks/useGetFee'; +import { ConfirmTx } from 'src/components/ConfirmTX'; +import { simulateUpdateMixnode, simulateVestingUpdateMixnode } from 'src/requests'; +import { LoadingModal } from 'src/components/Modals/LoadingModal'; +import { FeeDetails } from '@nymproject/types'; + +export const NodeSettings = ({ + currentPm, + isVesting, + onConfirm, + onClose, + onError, +}: { + currentPm: TBondedMixnode['profitMargin']; + isVesting: boolean; + onConfirm: (profitMargin: number, fee?: FeeDetails) => Promise; + onClose: () => void; + onError: (err: string) => void; +}) => { + const [pm, setPm] = useState(currentPm.toString()); + const [error, setError] = useState(false); + + const { fee, getFee, resetFeeState, isFeeLoading, feeError } = useGetFee(); + + const handleValidate = async () => { + let isValid = true; + const pmAsNumber = Number(pm); + + if (!pmAsNumber) { + isValid = false; + } + if (isDecimal(pmAsNumber)) { + isValid = false; + } + if (pmAsNumber > 100) { + isValid = false; + } + if (pmAsNumber < 0) { + isValid = false; + } + + if (!isValid) { + setError(true); + return; + } + + if (isVesting) { + await getFee(simulateVestingUpdateMixnode, { profitMarginPercent: pmAsNumber }); + } else { + await getFee(simulateUpdateMixnode, { profitMarginPercent: pmAsNumber }); + } + }; + + useEffect(() => { + setError(false); + }, [pm]); + + useEffect(() => { + if (feeError) { + onError(feeError); + } + }, [feeError]); + + if (isFeeLoading) return ; + + if (fee) + return ( + onConfirm(Number(pm), fee)} + > + + + + ); + + return ( + + + + Node Settings + +
+ } + okLabel="Next" + onClose={onClose} + > + + + + Set profit margin + + + setPm(e.target.value)} fullWidth /> + {error && ( + + Profit margin should be a whole number between 0 and 100 + + )} + Your new profit margin will be applied in the next epoch + + + + + + + + ); +}; diff --git a/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx b/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx new file mode 100644 index 0000000000..76b9192df9 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx @@ -0,0 +1,55 @@ +import React, { useEffect } from 'react'; +import { FeeDetails } from '@nymproject/types'; +import { ModalListItem } from 'src/components/Modals/ModalListItem'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { ModalFee } from 'src/components/Modals/ModalFee'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { simulateClaimOperatorReward, simulateVestingClaimOperatorReward } from 'src/requests'; +import { TBondedMixnode } from 'src/context'; + +export const RedeemRewardsModal = ({ + node, + onConfirm, + onError, + onClose, +}: { + node: TBondedMixnode; + onConfirm: (fee?: FeeDetails) => Promise; + onError: (err: string) => void; + onClose: () => void; +}) => { + const { fee, getFee, isFeeLoading, feeError } = useGetFee(); + + useEffect(() => { + if (feeError) onError(feeError); + }, [feeError]); + + useEffect(() => { + if (node.proxy) getFee(simulateVestingClaimOperatorReward, {}); + else getFee(simulateClaimOperatorReward, {}); + }, []); + + const handleOnOK = async () => onConfirm(fee); + + return ( + + + + + + ); +}; diff --git a/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx new file mode 100644 index 0000000000..9617e68431 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +import { Typography } from '@mui/material'; +import { useEffect } from 'react'; +import { TBondedGateway, TBondedMixnode } from 'src/context'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { isGateway, isMixnode } from 'src/types'; +import { ModalFee } from '../../Modals/ModalFee'; +import { ModalListItem } from '../../Modals/ModalListItem'; +import { SimpleModal } from '../../Modals/SimpleModal'; +import { + simulateUnbondGateway, + simulateUnbondMixnode, + simulateVestingUnbondGateway, + simulateVestingUnbondMixnode, +} from '../../../requests'; + +interface Props { + node: TBondedMixnode | TBondedGateway; + onConfirm: () => Promise; + onClose: () => void; + onError: (e: string) => void; +} + +export const UnbondModal = ({ node, onConfirm, onClose, onError }: Props) => { + const { fee, isFeeLoading, getFee, feeError } = useGetFee(); + + useEffect(() => { + if (feeError) { + onError(feeError); + } + }, [feeError]); + + useEffect(() => { + if (isMixnode(node) && !node.proxy) { + getFee(simulateUnbondMixnode, {}); + } + + if (isMixnode(node) && node.proxy) { + getFee(simulateVestingUnbondMixnode, {}); + } + + if (isGateway(node) && !node.proxy) { + getFee(simulateUnbondGateway, {}); + } + + if (isGateway(node) && node.proxy) { + getFee(simulateVestingUnbondGateway, {}); + } + }, [node]); + + return ( + + + {isMixnode(node) && ( + + )} + + Tokens will be transferred to the account you are logged in with now + + ); +}; diff --git a/nym-wallet/src/components/ConfirmPassword.tsx b/nym-wallet/src/components/ConfirmPassword.tsx index 43a82be0f3..ba025fa1e6 100644 --- a/nym-wallet/src/components/ConfirmPassword.tsx +++ b/nym-wallet/src/components/ConfirmPassword.tsx @@ -2,16 +2,19 @@ import React, { useEffect, useState } from 'react'; import { Button, CircularProgress, DialogActions, DialogContent, Typography } from '@mui/material'; import { useKeyPress } from 'src/hooks/useKeyPress'; import { PasswordInput } from './textfields'; +import { StyledBackButton } from './StyledBackButton'; export const ConfirmPassword = ({ error, isLoading, onConfirm, + onCancel, buttonTitle, }: { error?: string; isLoading?: boolean; buttonTitle: string; + onCancel?: () => void; onConfirm: (password: string) => void; }) => { const [value, setValue] = useState(''); @@ -39,7 +42,8 @@ export const ConfirmPassword = ({ disabled={isLoading} /> - + + {onCancel && } - - + + {children} + ); } - transactions && - transactions.map((transaction) => console.log('action', action, 'status', status, 'key', transaction.hash)); - return ( - - - theme.palette.success.main} mb={1}> - {actionToHeader(action)} - - - {message} - - {recipient && ( - theme.palette.text.secondary}> - Recipient: {recipient} - + transactions?.map((transaction) => Console.log('action', action, 'status', status, 'key', transaction.hash)); + + return ( + {})} + title={actionToHeader(action)} + confirmButton="Done" + > + + {message && {message}} + {transactions?.length === 1 && ( + )} - {balanceVested ? ( - <> - theme.palette.text.secondary}> - Your current balance: {balance} - - theme.palette.text.secondary}> - ({balanceVested} is unlocked in your vesting account) - - - ) : ( - theme.palette.text.secondary}> - Your current balance: {balance} - - )} - {transactions && ( - theme.palette.text.secondary}> - Check the transaction {transactions.length > 1 ? 'hashes' : 'hash'}: - {transactions.map((transaction) => ( - + {transactions && transactions.length > 1 && ( + + View the transactions on blockchain: + {transactions.map(({ url, hash }) => ( + ))} - + )} - {children} - - - + + ); }; diff --git a/nym-wallet/src/components/Delegation/Modals.stories.tsx b/nym-wallet/src/components/Delegation/Modals.stories.tsx index 0207965e4c..0ca52166b7 100644 --- a/nym-wallet/src/components/Delegation/Modals.stories.tsx +++ b/nym-wallet/src/components/Delegation/Modals.stories.tsx @@ -61,7 +61,7 @@ export const Delegate = () => { open={open} onClose={() => setOpen(false)} onOk={async () => setOpen(false)} - currency="NYM" + denom="nym" estimatedReward={50.423} accountBalance="425.2345053" nodeUptimePercentage={99.28394} @@ -84,7 +84,7 @@ export const DelegateBelowMinimum = () => { open={open} onClose={() => setOpen(false)} onOk={async () => setOpen(false)} - currency="NYM" + denom="nym" estimatedReward={425.2345053} nodeUptimePercentage={99.28394} profitMarginPercentage={11.12334234} @@ -109,7 +109,7 @@ export const DelegateMore = () => { onOk={async () => setOpen(false)} header="Delegate more" buttonText="Delegate more" - currency="NYM" + denom="nym" estimatedReward={50.423} accountBalance="425.2345053" nodeUptimePercentage={99.28394} @@ -132,7 +132,7 @@ export const Undelegate = () => { open={open} onClose={() => setOpen(false)} onOk={async () => setOpen(false)} - currency="NYM" + currency="nym" amount={150} identityKey="AA6RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujyxx" usesVestingContractTokens={false} diff --git a/nym-wallet/src/components/Delegation/PendingEvents.tsx b/nym-wallet/src/components/Delegation/PendingEvents.tsx deleted file mode 100644 index 6e9263b03f..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}`} - - {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 1c05390090..2b21632162 100644 --- a/nym-wallet/src/components/Delegation/UndelegateModal.tsx +++ b/nym-wallet/src/components/Delegation/UndelegateModal.tsx @@ -40,7 +40,6 @@ export const UndelegateModal: React.FC<{ onClose={onClose} onOk={handleOk} header="Undelegate" - subHeader="Undelegate from mixnode" okLabel="Undelegate stake" okDisabled={!fee} sx={sx} @@ -55,14 +54,10 @@ export const UndelegateModal: React.FC<{ /> - + + + - - - Tokens will be transferred to account you are logged in with now - - - ); }; diff --git a/nym-wallet/src/components/Fee.tsx b/nym-wallet/src/components/Fee.tsx deleted file mode 100644 index 950cbf9e20..0000000000 --- a/nym-wallet/src/components/Fee.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useState, useEffect, useContext } from 'react'; -import { Typography } from '@mui/material'; -import { Operation } from '@nymproject/types'; -import { getGasFee } from '../requests'; -import { AppContext } from '../context/main'; - -export const Fee = ({ feeType }: { feeType: Operation }) => { - const [fee, setFee] = useState(); - const { clientDetails } = useContext(AppContext); - - const getFee = async () => { - const res = await getGasFee(feeType); - setFee(res.amount); - }; - - useEffect(() => { - getFee(); - }, []); - - if (fee) { - return ( - - Est.fee for this transaction: {`${fee} ${clientDetails?.denom}`}{' '} - - ); - } - - return null; -}; diff --git a/nym-wallet/src/components/IdentityKey.tsx b/nym-wallet/src/components/IdentityKey.tsx new file mode 100644 index 0000000000..bca08ab68c --- /dev/null +++ b/nym-wallet/src/components/IdentityKey.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { Stack, Typography } from '@mui/material'; +import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard'; +import { splice } from 'src/utils'; + +export const IdentityKey = ({ identityKey }: { identityKey: string }) => ( + + + {splice(6, identityKey)} + + + +); diff --git a/nym-wallet/src/components/Modals/ConfirmationModal.tsx b/nym-wallet/src/components/Modals/ConfirmationModal.tsx index d76c9a0c13..d1ff7b6528 100644 --- a/nym-wallet/src/components/Modals/ConfirmationModal.tsx +++ b/nym-wallet/src/components/Modals/ConfirmationModal.tsx @@ -41,7 +41,7 @@ export const ConfirmationModal = ({ backdropProps, }: ConfirmationModalProps) => { const Title = ( - + {title} {subTitle && (typeof subTitle === 'string' ? ( diff --git a/nym-wallet/src/components/Modals/ErrorModal.tsx b/nym-wallet/src/components/Modals/ErrorModal.tsx new file mode 100644 index 0000000000..3ea54368d2 --- /dev/null +++ b/nym-wallet/src/components/Modals/ErrorModal.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Box, Button, Modal, SxProps, Typography } from '@mui/material'; +import { modalStyle } from './styles'; + +export const ErrorModal: React.FC<{ + open: boolean; + title?: string; + message?: string; + sx?: SxProps; + backdropProps?: object; + onClose: () => void; +}> = ({ children, open, title, message, sx, backdropProps, onClose }) => ( + + + theme.palette.error.main} mb={1}> + {title || 'Oh no! Something went wrong...'} + + + {message} + + {children} + + + +); diff --git a/nym-wallet/src/components/Modals/LoadingModal.tsx b/nym-wallet/src/components/Modals/LoadingModal.tsx index 8422390e3b..a50bec77eb 100644 --- a/nym-wallet/src/components/Modals/LoadingModal.tsx +++ b/nym-wallet/src/components/Modals/LoadingModal.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import { Box, CircularProgress, Modal, Stack, Typography } from '@mui/material'; -import { SxProps } from '@mui/system'; +import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material'; const modalStyle: SxProps = { position: 'absolute', diff --git a/nym-wallet/src/components/Modals/ModalFee.tsx b/nym-wallet/src/components/Modals/ModalFee.tsx index c64fa0286e..f60f6690ac 100644 --- a/nym-wallet/src/components/Modals/ModalFee.tsx +++ b/nym-wallet/src/components/Modals/ModalFee.tsx @@ -2,16 +2,20 @@ import React from 'react'; import { FeeDetails } from '@nymproject/types'; import { CircularProgress } from '@mui/material'; import { ModalListItem } from './ModalListItem'; +import { ModalDivider } from './ModalDivider'; -type TFeeProps = { fee?: FeeDetails; isLoading: boolean; error?: string }; +type TFeeProps = { fee?: FeeDetails; isLoading: boolean; error?: string; divider?: boolean }; const getValue = ({ fee, isLoading, error }: TFeeProps) => { if (isLoading) return ; if (error && !isLoading) return 'n/a'; - if (fee) return `${fee.amount?.amount} ${fee.amount?.denom}`; + if (fee) return `${fee.amount?.amount} ${fee.amount?.denom.toUpperCase()}`; return '-'; }; -export const ModalFee = ({ fee, isLoading, error }: TFeeProps) => ( - +export const ModalFee = ({ fee, isLoading, error, divider }: TFeeProps) => ( + <> + + {divider && } + ); diff --git a/nym-wallet/src/components/Modals/ModalListItem.tsx b/nym-wallet/src/components/Modals/ModalListItem.tsx index 3eab5f1a80..2c1524dec8 100644 --- a/nym-wallet/src/components/Modals/ModalListItem.tsx +++ b/nym-wallet/src/components/Modals/ModalListItem.tsx @@ -1,22 +1,25 @@ import React from 'react'; -import { Box, Stack, Typography } from '@mui/material'; +import { Box, Stack, Typography, TypographyProps } from '@mui/material'; import { ModalDivider } from './ModalDivider'; export const ModalListItem: React.FC<{ label: string; divider?: boolean; hidden?: boolean; - strong?: boolean; - value: React.ReactNode; -}> = ({ label, value, hidden, divider, strong }) => ( + fontWeight?: TypographyProps['fontWeight']; + light?: boolean; + value?: React.ReactNode; +}> = ({ label, value, hidden, fontWeight, divider }) => ( diff --git a/nym-wallet/src/components/Modals/SimpleModal.tsx b/nym-wallet/src/components/Modals/SimpleModal.tsx index 46adf6d4c1..cfc3c10d79 100644 --- a/nym-wallet/src/components/Modals/SimpleModal.tsx +++ b/nym-wallet/src/components/Modals/SimpleModal.tsx @@ -14,7 +14,7 @@ export const SimpleModal: React.FC<{ onClose?: () => void; onOk?: () => Promise; onBack?: () => void; - header: string; + header: string | React.ReactNode; subHeader?: string; okLabel: string; okDisabled?: boolean; @@ -41,31 +41,38 @@ export const SimpleModal: React.FC<{ {displayErrorIcon && } - - {header} - + {typeof header === 'string' ? ( + + {header} + + ) : ( + header + )} {!hideCloseIcon && } - {subHeader && ( - theme.palette.text.secondary} - sx={{ color: (theme) => theme.palette.nym.nymWallet.text.muted, ...subHeaderStyles }} - > - {subHeader} - - )} + + theme.palette.text.secondary} + sx={{ color: (theme) => theme.palette.nym.nymWallet.text.muted, ...subHeaderStyles }} + > + {subHeader} + {children} - - {onBack && } - - + {(onOk || onBack) && ( + + {onBack && } + {onOk && ( + + )} + + )} ); diff --git a/nym-wallet/src/components/Nav.tsx b/nym-wallet/src/components/Nav.tsx index 4c91e29ad4..ccb1dcd10d 100644 --- a/nym-wallet/src/components/Nav.tsx +++ b/nym-wallet/src/components/Nav.tsx @@ -3,13 +3,13 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material'; import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material'; import { AppContext } from '../context/main'; -import { Bond, Delegate, Unbond } from '../svg-icons'; +import { Delegate, Bonding } from '../svg-icons'; export const Nav = () => { const location = useLocation(); const navigate = useNavigate(); - const { isAdminAddress, handleShowSendModal } = useContext(AppContext); + const { isAdminAddress, handleShowSendModal, handleShowReceiveModal } = useContext(AppContext); const [routesSchema] = useState([ { @@ -25,21 +25,14 @@ export const Nav = () => { }, { label: 'Receive', - route: '/receive', Icon: ArrowBack, - onClick: () => navigate('/receive'), + onClick: handleShowReceiveModal, }, { - label: 'Bond', - route: '/bond', - Icon: Bond, - onClick: () => navigate('/bond'), - }, - { - label: 'Unbond', - route: '/unbond', - Icon: Unbond, - onClick: () => navigate('/unbond'), + label: 'Bonding', + route: '/bonding', + Icon: Bonding, + onClick: () => navigate('/bonding'), }, { label: 'Delegation', @@ -69,9 +62,16 @@ export const Nav = () => { display: 'flex', alignItems: 'center', justifyContent: 'flex-start', + marginLeft: 12, + marginRight: 12, }} > - + {routesSchema .filter(({ mode }) => { if (!mode) { @@ -87,17 +87,35 @@ export const Nav = () => { } }) .map(({ Icon, onClick, label, route }) => ( - + theme.palette.nym.nymWallet.hover.background }, + }} + > - + { - - - ); -}; diff --git a/nym-wallet/src/pages/delegate/SuccessView.tsx b/nym-wallet/src/pages/delegate/SuccessView.tsx deleted file mode 100644 index 3942cfc252..0000000000 --- a/nym-wallet/src/pages/delegate/SuccessView.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { useContext } from 'react'; -import { Box, Stack, Typography } from '@mui/material'; -import { TransactionExecuteResult } from '@nymproject/types'; -import { SuccessReponse, TransactionDetails } from '../../components'; -import { AppContext } from '../../context/main'; - -export const SuccessView: React.FC<{ details?: { amount: string; result: TransactionExecuteResult } }> = ({ - details, -}) => { - const { userBalance, clientDetails } = useContext(AppContext); - return ( - <> - - Successfully requested delegation to node - - Note it may take up to one hour to take effect - - - } - caption={`Your current balance is: ${userBalance.balance?.printable_balance}`} - /> - {details && ( - - - - )} - - ); -}; diff --git a/nym-wallet/src/pages/delegate/index.tsx b/nym-wallet/src/pages/delegate/index.tsx deleted file mode 100644 index 6fbe7dbf63..0000000000 --- a/nym-wallet/src/pages/delegate/index.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useContext, useState } from 'react'; -import { Alert, AlertTitle, Box, Button, Typography } from '@mui/material'; -import { TransactionExecuteResult } from '@nymproject/types'; -import { Link } from '@nymproject/react/link/Link'; -import { DelegateForm } from './DelegateForm'; -import { NymCard } from '../../components'; -import { EnumRequestStatus, RequestStatus } from '../../components/RequestStatus'; -import { SuccessView } from './SuccessView'; -import { AppContext, urls } from '../../context/main'; -import { PageLayout } from '../../layouts'; - -export const Delegate = () => { - const [status, setStatus] = useState(EnumRequestStatus.initial); - const [error, setError] = useState(); - const [successDetails, setSuccessDetails] = useState<{ amount: string; result: TransactionExecuteResult }>(); - - const { network } = useContext(AppContext); - - return ( - - - <> - {status === EnumRequestStatus.initial && ( - - Always ensure you leave yourself enough funds to UNDELEGATE - - )} - {status === EnumRequestStatus.initial && ( - { - setStatus(EnumRequestStatus.error); - setError(message); - }} - onSuccess={(details) => { - setStatus(EnumRequestStatus.success); - setSuccessDetails(details); - }} - /> - )} - {status !== EnumRequestStatus.initial && ( - <> - - Delegation failed - An error occurred with the request: - {error} - - } - Success={} - /> - - - - - )} - - - - Checkout the{' '} - {' '} - for uptime and performances to help make delegation decisions - - - ); -}; diff --git a/nym-wallet/src/pages/delegate/validationSchema.ts b/nym-wallet/src/pages/delegate/validationSchema.ts deleted file mode 100644 index 6f97bda05e..0000000000 --- a/nym-wallet/src/pages/delegate/validationSchema.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as Yup from 'yup'; -import { checkHasEnoughFunds, checkHasEnoughLockedTokens, validateAmount, validateKey } from '../../utils'; - -export const validationSchema = Yup.object().shape({ - identity: Yup.string() - .required() - .test( - 'valid-id-key', - 'A valid identity key is required e.g. 824WyExLUWvLE2mpSHBatN4AoByuLzfnHFeHWiBYzg4z', - (value) => (value ? validateKey(value, 32) : false), - ), - - amount: Yup.object().shape({ - amount: Yup.string() - .required() - .test('valid-amount', 'A valid amount is required', async function isValidAmount(value) { - const isValid = await validateAmount(value || '', '0'); - - if (!isValid) { - return this.createError({ message: 'A valid amount is required' }); - } - - const hasEnoughBalance = await checkHasEnoughFunds(value || ''); - const hasEnoughLocked = await checkHasEnoughLockedTokens(value || ''); - - if (this.parent.tokenPool === 'balance' && !hasEnoughBalance) { - return this.createError({ message: 'Not enough funds in wallet' }); - } - - if (this.parent.tokenPool === 'locked' && !hasEnoughLocked) { - return this.createError({ message: 'Not enough locked tokens' }); - } - - return true; - }), - denom: Yup.string().required(), - }), -}); diff --git a/nym-wallet/src/pages/delegation/index.tsx b/nym-wallet/src/pages/delegation/index.tsx index 8110c742c5..15cbdbe75f 100644 --- a/nym-wallet/src/pages/delegation/index.tsx +++ b/nym-wallet/src/pages/delegation/index.tsx @@ -1,11 +1,10 @@ import React, { FC, useContext, useEffect, useState } from 'react'; import { Box, Button, Paper, Stack, Typography } from '@mui/material'; import { useTheme, Theme } from '@mui/material/styles'; -import { DelegationWithEverything, FeeDetails, MajorCurrencyAmount } from '@nymproject/types'; +import { DelegationWithEverything, FeeDetails, DecCoin } from '@nymproject/types'; import { Link } from '@nymproject/react/link/Link'; import { AppContext, urls } from 'src/context/main'; import { DelegationList } from 'src/components/Delegation/DelegationList'; -import { PendingEvents } from 'src/components/Delegation/PendingEvents'; import { TPoolOption } from 'src/components'; import { Console } from 'src/utils/console'; import { CompoundModal } from 'src/components/Rewards/CompoundModal'; @@ -22,7 +21,7 @@ import { DelegationModal, DelegationModalProps } from '../../components/Delegati import { backDropStyles, modalStyles } from '../../../.storybook/storiesStyles'; const storybookStyles = (theme: Theme, isStorybook?: boolean, backdropProps?: object) => - !!isStorybook + isStorybook ? { backdropProps: { ...backDropStyles(theme), ...backdropProps }, sx: modalStyles(theme), @@ -49,7 +48,6 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { const { delegations, - pendingDelegations, totalDelegations, totalRewards, isLoading, @@ -64,7 +62,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { const getAllBalances = async () => { const resBalance = (await userBalance()).printable_balance; - let resVesting: MajorCurrencyAmount | undefined; + let resVesting: DecCoin | undefined; try { resVesting = await getSpendableCoins(); } catch (e) { @@ -85,7 +83,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { useEffect(() => { refresh(); - }, [network, clientDetails, confirmationModalProps]); + }, [clientDetails, confirmationModalProps]); const handleDelegationItemActionClick = (item: DelegationWithEverything, action: DelegationListItemActions) => { if ((action === 'delegate' || action === 'compound') && item.stake_saturation && item.stake_saturation > 1) { @@ -113,7 +111,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { const handleNewDelegation = async ( identityKey: string, - amount: MajorCurrencyAmount, + amount: DecCoin, tokenPool: TPoolOption, fee?: FeeDetails, ) => { @@ -138,7 +136,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { setConfirmationModalProps({ status: 'success', action: 'delegate', - message: 'Delegations can take up to one hour to process', + message: 'This operation can take up to one hour to process', ...balances, transactions: [ { url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, hash: tx.transaction_hash }, @@ -154,12 +152,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { } }; - const handleDelegateMore = async ( - identityKey: string, - amount: MajorCurrencyAmount, - tokenPool: TPoolOption, - fee?: FeeDetails, - ) => { + const handleDelegateMore = async (identityKey: string, amount: DecCoin, tokenPool: TPoolOption, fee?: FeeDetails) => { if (currentDelegationListActionItem?.node_identity !== identityKey || !clientDetails) { setConfirmationModalProps({ status: 'error', @@ -245,11 +238,9 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { try { const txs = await claimRewards(identityKey, fee); - const bal = await userBalance(); setConfirmationModalProps({ status: 'success', action: 'redeem', - balance: bal?.printable_balance || '-', transactions: txs.map((tx) => ({ url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, hash: tx.transaction_hash, @@ -275,11 +266,9 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { try { const txs = await compoundRewards(identityKey, fee); - const bal = await userBalance(); setConfirmationModalProps({ status: 'success', action: 'compound', - balance: bal?.printable_balance || '-', transactions: txs.map((tx) => ({ url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, hash: tx.transaction_hash, @@ -297,7 +286,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { return ( <> - + Delegations @@ -311,7 +300,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { noIcon /> - + - )} - - - - - ); -}; diff --git a/nym-wallet/src/pages/send/index.tsx b/nym-wallet/src/pages/send/index.tsx deleted file mode 100644 index 386f4876d1..0000000000 --- a/nym-wallet/src/pages/send/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React, { useContext } from 'react'; -import { NymCard } from '../../components'; -import { SendWizard } from './SendWizard'; -import { AppContext } from '../../context/main'; -import { PageLayout } from '../../layouts'; - -export const Send = () => { - const { clientDetails } = useContext(AppContext); - return ( - - - - - - ); -}; diff --git a/nym-wallet/src/pages/send/validationSchema.ts b/nym-wallet/src/pages/send/validationSchema.ts deleted file mode 100644 index ba15abdc44..0000000000 --- a/nym-wallet/src/pages/send/validationSchema.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as Yup from 'yup'; -import { validateAmount } from '../../utils'; - -export const validationSchema = Yup.object().shape({ - to: Yup.string().strict().trim('Cannot have leading space').required(), - amount: Yup.object().shape({ - amount: Yup.string() - .required('Amount is required') - .test('valid-amount', 'A valid amount is required', (amount) => validateAmount(amount || '0', '0')), - denom: Yup.string(), - }), -}); diff --git a/nym-wallet/src/pages/settings/index.tsx b/nym-wallet/src/pages/settings/index.tsx deleted file mode 100644 index 15f4e3efa5..0000000000 --- a/nym-wallet/src/pages/settings/index.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { useContext, useEffect, useState } from 'react'; -import { Alert, Box, Dialog } from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; -import { NymCard } from '../../components'; -import { AppContext } from '../../context/main'; -import { Tabs } from './tabs'; -import { Profile } from './profile'; -import { SystemVariables } from './system-variables'; -import { NodeStats } from './node-stats'; -import { useSettingsState } from './useSettingsState'; -import { NodeStatus } from '../../components/NodeStatus'; -import { Node as NodeIcon } from '../../svg-icons/node'; - -const tabs = ['Profile', 'System variables', 'Node stats']; - -export const Settings = () => { - const [selectedTab, setSelectedTab] = useState(0); - - const { mixnodeDetails, showSettings, getBondDetails, handleShowSettings } = useContext(AppContext); - const { status, saturation, rewardEstimation, inclusionProbability, updateAllMixnodeStats } = useSettingsState(); - - const handleTabChange = (_: React.SyntheticEvent, newTab: number) => setSelectedTab(newTab); - - useEffect(() => { - getBondDetails(); - if (mixnodeDetails) { - updateAllMixnodeStats(mixnodeDetails.mix_node.identity_key); - } - }, [showSettings, selectedTab]); - - return showSettings ? ( - - - - - Node Settings - - - - } - Action={} - dataTestid="node-settings" - noPadding - > - <> - - {!mixnodeDetails && ( - - You do not currently have a node running - - )} - {selectedTab === 0 && } - {selectedTab === 1 && ( - - )} - {selectedTab === 2 && mixnodeDetails && } - - - - ) : null; -}; diff --git a/nym-wallet/src/pages/settings/system-variables.tsx b/nym-wallet/src/pages/settings/system-variables.tsx index c075cd8561..b43c404127 100644 --- a/nym-wallet/src/pages/settings/system-variables.tsx +++ b/nym-wallet/src/pages/settings/system-variables.tsx @@ -6,7 +6,7 @@ import { PercentOutlined } from '@mui/icons-material'; import { yupResolver } from '@hookform/resolvers/yup'; import { InclusionProbabilityResponse, SelectionChance } from '@nymproject/types'; import { validationSchema } from './validationSchema'; -import { Fee, InfoTooltip } from '../../components'; +import { InfoTooltip } from '../../components'; import { useCheckOwnership } from '../../hooks/useCheckOwnership'; import { updateMixnode, vestingUpdateMixnode } from '../../requests'; import { AppContext } from '../../context/main'; @@ -30,18 +30,14 @@ const DataField = ({ title, info, Indicator }: { title: string; info: string; In ); const colorMap: { [key in SelectionChance]: string } = { - VeryLow: 'error.main', Low: 'error.main', Moderate: 'warning.main', - High: 'success.main', VeryHigh: 'success.main', }; const textMap: { [key in SelectionChance]: string } = { - VeryLow: 'Very low', Low: 'Low', Moderate: 'Moderate', - High: 'High', VeryHigh: 'Very high', }; @@ -162,7 +158,6 @@ export const SystemVariables = ({ {nodeUpdateResponse === 'failed' && ( Node update failed )} - {!nodeUpdateResponse && } - - - ); -}; diff --git a/nym-wallet/src/pages/undelegate/index.tsx b/nym-wallet/src/pages/undelegate/index.tsx deleted file mode 100644 index 98f51b49e9..0000000000 --- a/nym-wallet/src/pages/undelegate/index.tsx +++ /dev/null @@ -1,170 +0,0 @@ -import React, { useContext, useEffect, useState } from 'react'; -import { Alert, AlertTitle, Box, Button, CircularProgress, Grid, IconButton } from '@mui/material'; -import { ArrowDropDown, ArrowDropUp } from '@mui/icons-material'; -import { DelegationResult, PendingUndelegate, TPagedDelegations } from '@nymproject/types'; -import { Epoch } from 'src/types'; -import { UndelegateForm } from './UndelegateForm'; -import { AppContext } from '../../context/main'; -import { EnumRequestStatus, NymCard, RequestStatus } from '../../components'; -import { getCurrentEpoch, getReverseMixDelegations } from '../../requests'; -import { PageLayout } from '../../layouts'; -import { removeObjectDuplicates } from '../../utils'; -import { PendingEvents } from './PendingEvents'; - -export const Undelegate = () => { - const [message, setMessage] = useState(); - const [status, setStatus] = useState(EnumRequestStatus.initial); - const [isLoading, setIsLoading] = useState(true); - const [pagedDelegations, setPagesDelegations] = useState(); - const [pendingUndelegations /* , setPendingndelegations */] = useState(); - const [pendingDelegations /* , setPendingDelegations */] = useState(); - const [currentEndEpoch, setCurrentEndEpoch] = useState(); - const [showPendingDelegations, setShowPendingDelegations] = useState(false); - - const { clientDetails } = useContext(AppContext); - - const refresh = async () => { - const mixnodeDelegations = await getReverseMixDelegations(); - - // Temporarily comment out to fix breaking type change - - // const pendingEvents = await getPendingDelegations(); - // const pendingVestingEvents = await getPendingVestingDelegations(); - // const pendingUndelegationEvents = [...pendingEvents, ...pendingVestingEvents] - // .filter((evt): evt is { Undelegate: PendingUndelegate } => 'Undelegate' in evt) - // .map((e) => ({ ...e.Undelegate })); - // const pendingDelegationEvents = [...pendingEvents, ...pendingVestingEvents] - // .filter((evt): evt is { Delegate: DelegationResult } => 'Delegate' in evt) - // .map((e) => ({ ...e.Delegate })); - const epoch = await getCurrentEpoch(); - - setCurrentEndEpoch(epoch.end); - - // Temporarily comment out to fix breaking type change - // setPendingUndelegations(pendingUndelegationEvents); - // setPendingDelegations(pendingDelegationEvents); - setPagesDelegations({ - ...mixnodeDelegations, - delegations: removeObjectDuplicates(mixnodeDelegations.delegations, 'node_identity'), - }); - }; - - const initialize = async () => { - setStatus(EnumRequestStatus.initial); - setIsLoading(true); - - try { - await refresh(); - } catch (e) { - setStatus(EnumRequestStatus.error); - setMessage(e as string); - } - - setIsLoading(false); - }; - - useEffect(() => { - initialize(); - }, [clientDetails]); - - return ( - - - - - {isLoading && ( - - - - )} - <> - {status === EnumRequestStatus.initial && pagedDelegations && ( - { - setMessage(m); - setStatus(EnumRequestStatus.error); - refresh(); - }} - onSuccess={(m) => { - setMessage(m); - setStatus(EnumRequestStatus.success); - refresh(); - }} - /> - )} - {status !== EnumRequestStatus.initial && ( - <> - - An error occurred with the request: {message} - - } - Success={ - - Undelegation request complete - {message} - - } - /> - - - - - )} - - - - {pendingDelegations?.length && ( - - setShowPendingDelegations((show) => !show)}> - {!showPendingDelegations ? : } - - } - > - {pendingDelegations ? ( - - ) : ( -
No pending delegations
- )} -
-
- )} -
-
- ); -}; diff --git a/nym-wallet/src/pages/undelegate/validationSchema.ts b/nym-wallet/src/pages/undelegate/validationSchema.ts deleted file mode 100644 index 54a7cc7cde..0000000000 --- a/nym-wallet/src/pages/undelegate/validationSchema.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as Yup from 'yup'; -import { validateKey } from '../../utils'; - -export const validationSchema = Yup.object().shape({ - identity: Yup.string() - .required() - .test('valid-id-key', 'A valid identity key is required', (value) => validateKey(value || '', 32)), -}); diff --git a/nym-wallet/src/requests/actions.ts b/nym-wallet/src/requests/actions.ts index 7b0ebbbf79..74d5f35cb3 100644 --- a/nym-wallet/src/requests/actions.ts +++ b/nym-wallet/src/requests/actions.ts @@ -1,4 +1,4 @@ -import { Fee, MajorCurrencyAmount, SendTxResult, TransactionExecuteResult } from '@nymproject/types'; +import { Fee, DecCoin, SendTxResult, TransactionExecuteResult } from '@nymproject/types'; import { EnumNodeType, TBondGatewayArgs, TBondMixNodeArgs } from '../types'; import { invokeWrapper } from './wrapper'; @@ -12,10 +12,10 @@ export const bondMixNode = async (args: TBondMixNodeArgs) => export const unbondMixNode = async (fee?: Fee) => invokeWrapper('unbond_mixnode', { fee }); -export const updateMixnode = async (profitMarginPercent: number) => - invokeWrapper('update_mixnode', { profitMarginPercent }); +export const updateMixnode = async (profitMarginPercent: number, fee?: Fee) => + invokeWrapper('update_mixnode', { profitMarginPercent, fee }); -export const send = async (args: { amount: MajorCurrencyAmount; address: string; memo: string; fee?: Fee }) => +export const send = async (args: { amount: DecCoin; address: string; memo: string; fee?: Fee }) => invokeWrapper('send', args); export const unbond = async (type: EnumNodeType) => { diff --git a/nym-wallet/src/requests/delegation.ts b/nym-wallet/src/requests/delegation.ts index e26f8a6360..826e5e8f60 100644 --- a/nym-wallet/src/requests/delegation.ts +++ b/nym-wallet/src/requests/delegation.ts @@ -2,7 +2,7 @@ import { DelegationWithEverything, DelegationsSummaryResponse, TransactionExecuteResult, - MajorCurrencyAmount, + DecCoin, FeeDetails, } from '@nymproject/types'; import { invokeWrapper } from './wrapper'; @@ -26,5 +26,5 @@ export const undelegateAllFromMixnode = async ( fee: fee?.fee, }); -export const delegateToMixnode = async ({ identity, amount }: { identity: string; amount: MajorCurrencyAmount }) => +export const delegateToMixnode = async ({ identity, amount }: { identity: string; amount: DecCoin }) => invokeWrapper('delegate_to_mixnode', { identity, amount }); diff --git a/nym-wallet/src/requests/queries.ts b/nym-wallet/src/requests/queries.ts index dd161015fe..34ce2f1a24 100644 --- a/nym-wallet/src/requests/queries.ts +++ b/nym-wallet/src/requests/queries.ts @@ -4,11 +4,11 @@ import { StakeSaturationResponse, MixnodeStatusResponse, InclusionProbabilityResponse, - MajorCurrencyAmount, + DecCoin, MixNodeBond, - Operation, + GatewayBond, } from '@nymproject/types'; -import { Epoch } from 'src/types'; +import { Epoch, TNodeDescription } from 'src/types'; import { invokeWrapper } from './wrapper'; export const getReverseMixDelegations = async () => @@ -26,9 +26,10 @@ export const getAllPendingDelegations = async () => invokeWrapper('get_all_pending_delegation_events'); export const getMixnodeBondDetails = async () => invokeWrapper('mixnode_bond_details'); +export const getGatewayBondDetails = async () => invokeWrapper('gateway_bond_details'); export const getOperatorRewards = async (address: string) => - invokeWrapper('get_operator_rewards', { address }); + invokeWrapper('get_operator_rewards', { address }); export const getMixnodeStakeSaturation = async (identity: string) => invokeWrapper('mixnode_stake_saturation', { identity }); @@ -43,11 +44,13 @@ export const checkMixnodeOwnership = async () => invokeWrapper('owns_mi export const checkGatewayOwnership = async () => invokeWrapper('owns_gateway'); -// TODO: remove this method -export const getGasFee = async (operation: Operation): Promise => - invokeWrapper('get_old_and_incorrect_hardcoded_fee', { operation }); - export const getInclusionProbability = async (identity: string) => invokeWrapper('mixnode_inclusion_probability', { identity }); export const getCurrentEpoch = async () => invokeWrapper('get_current_epoch'); + +export const getNumberOfMixnodeDelegators = async (identity: string) => + invokeWrapper('get_number_of_mixnode_delegators', { identity }); + +export const getNodeDescription = async (host: string, port: number) => + invokeWrapper('get_mix_node_description', { host, port }); diff --git a/nym-wallet/src/requests/rewards.ts b/nym-wallet/src/requests/rewards.ts index 1254ebee83..df9e28825d 100644 --- a/nym-wallet/src/requests/rewards.ts +++ b/nym-wallet/src/requests/rewards.ts @@ -1,10 +1,11 @@ -import { FeeDetails, TransactionExecuteResult } from '@nymproject/types'; +import { Fee, FeeDetails, TransactionExecuteResult } from '@nymproject/types'; import { invokeWrapper } from './wrapper'; -export const claimOperatorRewards = async () => invokeWrapper('claim_operator_reward'); +export const claimOperatorReward = async (fee?: Fee) => + invokeWrapper('claim_operator_reward', { fee }); -export const compoundOperatorRewards = async () => - invokeWrapper('compound_operator_reward'); +export const compoundOperatorReward = async (fee?: Fee) => + invokeWrapper('compound_operator_reward', { fee }); export const claimDelegatorRewards = async (mixIdentity: string, fee?: FeeDetails) => invokeWrapper('claim_locked_and_unlocked_delegator_reward', { diff --git a/nym-wallet/src/requests/simulate.ts b/nym-wallet/src/requests/simulate.ts index b574a55ba5..ccde2bb9e6 100644 --- a/nym-wallet/src/requests/simulate.ts +++ b/nym-wallet/src/requests/simulate.ts @@ -1,4 +1,4 @@ -import { FeeDetails, MajorCurrencyAmount, Gateway, MixNode } from '@nymproject/types'; +import { FeeDetails, DecCoin, Gateway, MixNode } from '@nymproject/types'; import { TBondGatewayArgs, TBondMixNodeArgs } from 'src/types'; import { invokeWrapper } from './wrapper'; @@ -12,9 +12,10 @@ export const simulateBondMixnode = async (args: TBondMixNodeArgs) => export const simulateUnbondMixnode = async (args: any) => invokeWrapper('simulate_unbond_mixnode', args); -export const simulateUpdateMixnode = async (args: any) => invokeWrapper('simulate_update_mixnode', args); +export const simulateUpdateMixnode = async (args: { profitMarginPercent: number }) => + invokeWrapper('simulate_update_mixnode', args); -export const simulateDelegateToMixnode = async (args: { identity: string; amount: MajorCurrencyAmount }) => +export const simulateDelegateToMixnode = async (args: { identity: string; amount: DecCoin }) => invokeWrapper('simulate_delegate_to_mixnode', args); export const simulateUndelegateFromMixnode = async (identity: string) => @@ -35,11 +36,8 @@ export const simulateVestingCompoundDelgatorReward = async (identity: string) => export const simulateVestingUndelegateFromMixnode = async (args: any) => invokeWrapper('simulate_vesting_undelegate_from_mixnode', args); -export const simulateVestingBondGateway = async (args: { - gateway: Gateway; - pledge: MajorCurrencyAmount; - ownerSignature: string; -}) => invokeWrapper('simulate_vesting_bond_gateway', args); +export const simulateVestingBondGateway = async (args: { gateway: Gateway; pledge: DecCoin; ownerSignature: string }) => + invokeWrapper('simulate_vesting_bond_gateway', args); export const simulateVestingUnbondGateway = async (args: any) => invokeWrapper('simulate_vesting_unbond_gateway', args); @@ -47,19 +45,27 @@ export const simulateVestingUnbondGateway = async (args: any) => export const simulateVestingDelegateToMixnode = async (args: { identity: string }) => invokeWrapper('simulate_vesting_delegate_to_mixnode', args); -export const simulateVestingBondMixnode = async (args: { - mixnode: MixNode; - pledge: MajorCurrencyAmount; - ownerSignature: string; -}) => invokeWrapper('simulate_vesting_bond_mixnode', args); +export const simulateVestingBondMixnode = async (args: { mixnode: MixNode; pledge: DecCoin; ownerSignature: string }) => + invokeWrapper('simulate_vesting_bond_mixnode', args); export const simulateVestingUnbondMixnode = async () => invokeWrapper('simulate_vesting_unbond_mixnode'); -export const simulateVestingUpdateMixnode = async (args: any) => +export const simulateVestingUpdateMixnode = async (args: { profitMarginPercent: number }) => invokeWrapper('simulate_vesting_update_mixnode', args); export const simulateWithdrawVestedCoins = async (args: any) => invokeWrapper('simulate_withdraw_vested_coins', args); -export const simulateSend = async ({ address, amount }: { address: string; amount: MajorCurrencyAmount }) => +export const simulateSend = async ({ address, amount }: { address: string; amount: DecCoin }) => invokeWrapper('simulate_send', { address, amount }); + +export const simulateClaimOperatorReward = async () => invokeWrapper('simulate_claim_operator_reward'); + +export const simulateVestingClaimOperatorReward = async () => + invokeWrapper('simulate_vesting_claim_operator_reward'); + +export const simulateCompoundOperatorReward = async () => + invokeWrapper('simulate_compound_operator_reward'); + +export const simulateVestingCompoundOperatorReward = async () => + invokeWrapper('simulate_vesting_compound_operator_reward'); diff --git a/nym-wallet/src/requests/vesting.ts b/nym-wallet/src/requests/vesting.ts index c9640bccf0..ac0d9b8a94 100644 --- a/nym-wallet/src/requests/vesting.ts +++ b/nym-wallet/src/requests/vesting.ts @@ -2,7 +2,7 @@ import { TNodeType, FeeDetails, Gateway, - MajorCurrencyAmount, + DecCoin, MixNode, OriginalVestingResponse, Period, @@ -13,17 +13,15 @@ import { import { Fee } from '@nymproject/types/dist/types/rust/Fee'; import { invokeWrapper } from './wrapper'; -export const getLockedCoins = async (): Promise => - invokeWrapper('locked_coins'); +export const getLockedCoins = async (): Promise => invokeWrapper('locked_coins'); -export const getSpendableCoins = async (): Promise => - invokeWrapper('spendable_coins'); +export const getSpendableCoins = async (): Promise => invokeWrapper('spendable_coins'); -export const getVestingCoins = async (vestingAccountAddress: string): Promise => - invokeWrapper('vesting_coins', { vestingAccountAddress }); +export const getVestingCoins = async (vestingAccountAddress: string): Promise => + invokeWrapper('vesting_coins', { vestingAccountAddress }); -export const getVestedCoins = async (vestingAccountAddress: string): Promise => - invokeWrapper('vested_coins', { vestingAccountAddress }); +export const getVestedCoins = async (vestingAccountAddress: string): Promise => + invokeWrapper('vested_coins', { vestingAccountAddress }); export const getOriginalVesting = async (vestingAccountAddress: string): Promise => { const res = await invokeWrapper('original_vesting', { vestingAccountAddress }); @@ -39,7 +37,7 @@ export const vestingBondGateway = async ({ ownerSignature, }: { gateway: Gateway; - pledge: MajorCurrencyAmount; + pledge: DecCoin; ownerSignature: string; }) => invokeWrapper('vesting_bond_gateway', { gateway, ownerSignature, pledge }); @@ -52,18 +50,18 @@ export const vestingBondMixNode = async ({ ownerSignature, }: { mixnode: MixNode; - pledge: MajorCurrencyAmount; + pledge: DecCoin; ownerSignature: string; }) => invokeWrapper('vesting_bond_mixnode', { mixnode, ownerSignature, pledge }); export const vestingUnbondMixnode = async (fee?: Fee) => invokeWrapper('vesting_unbond_mixnode', { fee }); -export const withdrawVestedCoins = async (amount: MajorCurrencyAmount) => - invokeWrapper('withdraw_vested_coins', { amount }); +export const withdrawVestedCoins = async (amount: DecCoin, fee?: Fee) => + invokeWrapper('withdraw_vested_coins', { amount, fee }); -export const vestingUpdateMixnode = async (profitMarginPercent: number) => - invokeWrapper('vesting_update_mixnode', { profitMarginPercent }); +export const vestingUpdateMixnode = async (profitMarginPercent: number, fee?: Fee) => + invokeWrapper('vesting_update_mixnode', { profitMarginPercent, fee }); export const vestingDelegateToMixnode = async ({ identity, @@ -71,7 +69,7 @@ export const vestingDelegateToMixnode = async ({ fee, }: { identity: string; - amount: MajorCurrencyAmount; + amount: DecCoin; fee?: FeeDetails; }) => invokeWrapper('vesting_delegate_to_mixnode', { identity, amount, fee: fee?.fee }); @@ -96,18 +94,18 @@ export const getVestingPledgeInfo = async ({ }; export const vestingDelegatedFree = async (vestingAccountAddress: string) => - invokeWrapper('delegated_free', { vestingAccountAddress }); + invokeWrapper('delegated_free', { vestingAccountAddress }); export const vestingUnbond = async (type: TNodeType) => { if (type === 'mixnode') return vestingUnbondMixnode(); return vestingUnbondGateway(); }; -export const vestingClaimOperatorRewards = async () => - invokeWrapper('vesting_claim_operator_reward'); +export const vestingClaimOperatorReward = async (fee?: Fee) => + invokeWrapper('vesting_claim_operator_reward', { fee }); -export const vestingCompoundOperatorRewards = async () => - invokeWrapper('vesting_compound_operator_reward'); +export const vestingCompoundOperatorReward = async (fee?: Fee) => + invokeWrapper('vesting_compound_operator_reward', { fee }); export const vestingClaimDelegatorRewards = async (mixIdentity: string) => invokeWrapper('vesting_claim_delegator_reward', { mixIdentity }); diff --git a/nym-wallet/src/routes/app.tsx b/nym-wallet/src/routes/app.tsx index 19f4c769e4..f7cafe8b89 100644 --- a/nym-wallet/src/routes/app.tsx +++ b/nym-wallet/src/routes/app.tsx @@ -3,17 +3,17 @@ import { Route, Routes } from 'react-router-dom'; import { ApplicationLayout } from 'src/layouts'; import { Terminal } from 'src/pages/terminal'; import { Send } from 'src/components/Send'; -import { Bond, Balance, InternalDocs, Receive, Unbond, DelegationPage, Admin, Settings } from '../pages'; +import { Receive } from '../components/Receive'; +import { Balance, InternalDocs, Unbond, DelegationPage, Admin, BondingPage } from '../pages'; export const AppRoutes = () => ( - + } /> - } /> - } /> + } /> } /> } /> } /> diff --git a/nym-wallet/src/svg-icons/bonding.tsx b/nym-wallet/src/svg-icons/bonding.tsx new file mode 100644 index 0000000000..32d7a2fd14 --- /dev/null +++ b/nym-wallet/src/svg-icons/bonding.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { SvgIcon, SvgIconProps } from '@mui/material'; + +export const Bonding = (props: SvgIconProps) => ( + + + + + +); diff --git a/nym-wallet/src/svg-icons/index.ts b/nym-wallet/src/svg-icons/index.ts index 2671250474..ec0d4b9c49 100644 --- a/nym-wallet/src/svg-icons/index.ts +++ b/nym-wallet/src/svg-icons/index.ts @@ -2,3 +2,4 @@ export * from './delegate'; export * from './undelegate'; export * from './bond'; export * from './unbond'; +export * from './bonding'; diff --git a/nym-wallet/src/theme/mui-theme.d.ts b/nym-wallet/src/theme/mui-theme.d.ts index 21096a1827..4015963ec3 100644 --- a/nym-wallet/src/theme/mui-theme.d.ts +++ b/nym-wallet/src/theme/mui-theme.d.ts @@ -64,6 +64,9 @@ declare module '@mui/material/styles' { nav: { background: string; }; + hover: { + background: string; + }; } /** @@ -82,7 +85,7 @@ declare module '@mui/material/styles' { /** * Add anything not palette related to the theme here */ - interface NymTheme { } + interface NymTheme {} /** * This augments the definitions of the MUI Theme with the Nym theme, as well as @@ -90,8 +93,8 @@ declare module '@mui/material/styles' { * * IMPORTANT: only add extensions to the interfaces above, do not modify the lines below */ - interface Theme extends NymTheme { } - interface ThemeOptions extends Partial { } - interface Palette extends NymPaletteAndNymWalletPalette { } - interface PaletteOptions extends NymPaletteAndNymWalletPaletteOptions { } + interface Theme extends NymTheme {} + interface ThemeOptions extends Partial {} + interface Palette extends NymPaletteAndNymWalletPalette {} + interface PaletteOptions extends NymPaletteAndNymWalletPaletteOptions {} } diff --git a/nym-wallet/src/theme/theme.tsx b/nym-wallet/src/theme/theme.tsx index 6749d3d864..b686c8a833 100644 --- a/nym-wallet/src/theme/theme.tsx +++ b/nym-wallet/src/theme/theme.tsx @@ -56,12 +56,15 @@ const darkMode: NymPaletteVariant = { nav: { background: '#292E34', }, + hover: { + background: '#36393E', + }, }; const lightMode: NymPaletteVariant = { mode: 'light', background: { - main: '#E5E5E5', + main: '#F4F6F8', paper: '#FFFFFF', warn: '#FFE600', grey: '#F5F5F5', @@ -80,6 +83,9 @@ const lightMode: NymPaletteVariant = { nav: { background: '#FFFFFF', }, + hover: { + background: '#F9F9F9', + }, }; /** @@ -223,13 +229,6 @@ export const getDesignTokens = (mode: PaletteMode): ThemeOptions => { }, }, components: { - MuiTypography: { - styleOverrides: { - root: { - fontSize: 14, - }, - }, - }, MuiButton: { styleOverrides: { root: { @@ -273,6 +272,13 @@ export const getDesignTokens = (mode: PaletteMode): ThemeOptions => { underline: 'none', }, }, + MuiDialogTitle: { + styleOverrides: { + root: { + fontWeight: 600, + }, + }, + }, }, palette, }; diff --git a/nym-wallet/src/types/global.ts b/nym-wallet/src/types/global.ts index ae5f46bedd..b76a3b5d7e 100644 --- a/nym-wallet/src/types/global.ts +++ b/nym-wallet/src/types/global.ts @@ -1,5 +1,6 @@ -import { Gateway, MajorCurrencyAmount, MixNode, PledgeData } from '@nymproject/types'; +import { Gateway, DecCoin, MixNode, PledgeData } from '@nymproject/types'; import { Fee } from '@nymproject/types/dist/types/rust/Fee'; +import { TBondedGateway, TBondedMixnode } from 'src/context'; export enum EnumNodeType { mixnode = 'mixnode', @@ -19,7 +20,7 @@ export type TPendingDelegation = { export type TDelegation = { owner: string; node_identity: string; - amount: MajorCurrencyAmount; + amount: DecCoin; block_height: number; proxy: string; // proxy address used to delegate the funds on behalf of another address pending?: TPendingDelegation; @@ -27,21 +28,28 @@ export type TDelegation = { export type TBondGatewayArgs = { gateway: Gateway; - pledge: MajorCurrencyAmount; + pledge: DecCoin; ownerSignature: string; fee?: Fee; }; export type TBondMixNodeArgs = { mixnode: MixNode; - pledge: MajorCurrencyAmount; + pledge: DecCoin; ownerSignature: string; fee?: Fee; }; +export type TNodeDescription = { + name: string; + description: string; + link: string; + location: string; +}; + export type TDelegateArgs = { identity: string; - amount: MajorCurrencyAmount; + amount: DecCoin; }; export type Period = 'Before' | { In: number } | 'After'; @@ -51,3 +59,8 @@ export type TAccount = { address: string; mnemonic: string; }; + +export const isMixnode = (node: TBondedMixnode | TBondedGateway): node is TBondedMixnode => + (node as TBondedMixnode).profitMargin !== undefined; + +export const isGateway = (node: TBondedMixnode | TBondedGateway): node is TBondedGateway => !isMixnode(node); diff --git a/nym-wallet/src/utils/index.ts b/nym-wallet/src/utils/index.ts index 82c3f97f0a..c0336da670 100644 --- a/nym-wallet/src/utils/index.ts +++ b/nym-wallet/src/utils/index.ts @@ -1,7 +1,7 @@ import { appWindow } from '@tauri-apps/api/window'; import bs58 from 'bs58'; import { valid } from 'semver'; -import { isValidRawCoin, MajorAmountString } from '@nymproject/types'; +import { isValidRawCoin, DecCoin } from '@nymproject/types'; import { TPoolOption } from 'src/components'; import { getLockedCoins, getSpendableCoins, userBalance } from '../requests'; import { Console } from './console'; @@ -19,8 +19,8 @@ export const validateKey = (key: string, bytesLength: number): boolean => { }; export const validateAmount = async ( - majorAmountAsString: MajorAmountString, - minimumAmountAsString: MajorAmountString, + majorAmountAsString: DecCoin['amount'], + minimumAmountAsString: DecCoin['amount'], ): Promise => { // tests basic coin value requirements, like no more than 6 decimal places, value lower than total supply, etc if (!Number(majorAmountAsString)) { @@ -121,3 +121,5 @@ export const checkTokenBalance = async (tokenPool: TPoolOption, amount: string) return hasEnoughFunds; }; + +export const isDecimal = (value: number) => value - Math.floor(value) !== 0; diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index 983bfe999d..4d30d20755 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-network-requester" -version = "1.0.1" +version = "1.0.2" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2021" diff --git a/service-providers/network-requester/src/allowed_hosts.rs b/service-providers/network-requester/src/allowed_hosts.rs index 294a75edab..3746d9a706 100644 --- a/service-providers/network-requester/src/allowed_hosts.rs +++ b/service-providers/network-requester/src/allowed_hosts.rs @@ -109,9 +109,14 @@ impl OutboundRequestFilter { } /// Attempts to get the root domain, shorn of subdomains, using publicsuffix. + /// If the domain is itself a suffix, then just use the full address as root. fn get_domain_root(&self, host: &str) -> Option { match self.domain_list.parse_domain(host) { - Ok(d) => d.root().map(|root| root.to_string()), + Ok(d) => Some( + d.root() + .map(|root| root.to_string()) + .unwrap_or_else(|| d.full().to_string()), + ), Err(_) => { log::warn!("Error parsing domain: {:?}", host); None // domain couldn't be parsed @@ -348,9 +353,12 @@ mod tests { } #[test] - fn returns_none_on_nonsense_domains() { + fn returns_full_on_suffix_domains() { let filter = setup(); - assert_eq!(None, filter.get_domain_root("flappappa")); + assert_eq!( + Some("s3.amazonaws.com".to_string()), + filter.get_domain_root("s3.amazonaws.com") + ); } } diff --git a/service-providers/network-requester/src/statistics/collector.rs b/service-providers/network-requester/src/statistics/collector.rs index 6df70c3e39..96e7292d82 100644 --- a/service-providers/network-requester/src/statistics/collector.rs +++ b/service-providers/network-requester/src/statistics/collector.rs @@ -12,7 +12,6 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; -use network_defaults::DEFAULT_NETWORK; use nymsphinx::addressing::clients::Recipient; use ordered_buffer::OrderedMessageSender; use socks5_requests::{ConnectionId, Message as Socks5Message, RemoteAddress, Request}; @@ -59,6 +58,7 @@ pub struct StatsProviderConfigEntry { #[derive(Clone, Debug, Deserialize)] #[serde(deny_unknown_fields)] +#[allow(dead_code)] pub struct OptionalStatsProviderConfig { mainnet: Option, sandbox: Option, @@ -67,15 +67,7 @@ pub struct OptionalStatsProviderConfig { impl OptionalStatsProviderConfig { pub fn stats_client_address(&self) -> Option { - let entry_config = match DEFAULT_NETWORK { - network_defaults::all::Network::MAINNET => self.mainnet.clone(), - network_defaults::all::Network::SANDBOX => self.sandbox.clone(), - network_defaults::all::Network::QA => self.qa.clone(), - network_defaults::all::Network::CUSTOM { .. } => { - panic!("custom network is not supported") - } - }; - entry_config.map(|e| e.stats_client_address) + self.mainnet.clone().map(|e| e.stats_client_address) } } @@ -202,17 +194,10 @@ impl StatisticsCollector for ServiceStatisticsCollector { } async fn reset_stats(&mut self) { - self.request_stats_data - .write() - .await - .client_processed_bytes - .iter_mut() - .for_each(|(_, b)| *b = 0); + self.request_stats_data.write().await.client_processed_bytes = HashMap::new(); self.response_stats_data .write() .await - .client_processed_bytes - .iter_mut() - .for_each(|(_, b)| *b = 0); + .client_processed_bytes = HashMap::new(); } } diff --git a/service-providers/network-statistics/Cargo.toml b/service-providers/network-statistics/Cargo.toml index d310b31fc4..9277486291 100644 --- a/service-providers/network-statistics/Cargo.toml +++ b/service-providers/network-statistics/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-network-statistics" -version = "0.1.0" +version = "1.0.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/service-providers/network-statistics/migrations/20220512120000_mixnet_statistics.sql b/service-providers/network-statistics/migrations/20220512120000_mixnet_statistics.sql index 5e6b2aa348..da13b953ca 100644 --- a/service-providers/network-statistics/migrations/20220512120000_mixnet_statistics.sql +++ b/service-providers/network-statistics/migrations/20220512120000_mixnet_statistics.sql @@ -16,6 +16,7 @@ CREATE TABLE service_statistics CREATE TABLE gateway_statistics ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + gateway_id VARCHAR NOT NULL, inbox_count INTEGER NOT NULL, timestamp DATETIME NOT NULL ); \ No newline at end of file diff --git a/service-providers/network-statistics/src/api/routes.rs b/service-providers/network-statistics/src/api/routes.rs index 7316d2c375..0a1c558128 100644 --- a/service-providers/network-statistics/src/api/routes.rs +++ b/service-providers/network-statistics/src/api/routes.rs @@ -35,6 +35,7 @@ pub struct ServiceStatistic { #[derive(Clone, Serialize, Deserialize, Debug)] pub struct GatewayStatistic { + pub gateway_id: String, pub inbox_count: u32, pub timestamp: String, } @@ -70,6 +71,7 @@ pub(crate) async fn post_all_statistics( .into_iter() .map(|data| { GenericStatistic::Gateway(GatewayStatistic { + gateway_id: data.gateway_id, inbox_count: data.inbox_count as u32, timestamp: data.timestamp.to_string(), }) diff --git a/service-providers/network-statistics/src/storage/manager.rs b/service-providers/network-statistics/src/storage/manager.rs index 07e0cc751b..f77a5aace4 100644 --- a/service-providers/network-statistics/src/storage/manager.rs +++ b/service-providers/network-statistics/src/storage/manager.rs @@ -52,11 +52,13 @@ impl StorageManager { /// * `timestamp`: The moment in time when the data started being collected. pub(super) async fn insert_gateway_statistics( &self, + gateway_id: String, inbox_count: u32, timestamp: DateTime, ) -> Result<(), sqlx::Error> { sqlx::query!( - "INSERT INTO gateway_statistics(inbox_count, timestamp) VALUES (?, ?)", + "INSERT INTO gateway_statistics(gateway_id, inbox_count, timestamp) VALUES (?, ?, ?)", + gateway_id, inbox_count, timestamp, ) diff --git a/service-providers/network-statistics/src/storage/mod.rs b/service-providers/network-statistics/src/storage/mod.rs index 6ae4e08646..ea468c8606 100644 --- a/service-providers/network-statistics/src/storage/mod.rs +++ b/service-providers/network-statistics/src/storage/mod.rs @@ -71,7 +71,11 @@ impl NetworkStatisticsStorage { } statistics_common::StatsData::Gateway(gateway_data) => { self.manager - .insert_gateway_statistics(gateway_data.inbox_count, timestamp) + .insert_gateway_statistics( + gateway_data.gateway_id, + gateway_data.inbox_count, + timestamp, + ) .await? } } diff --git a/service-providers/network-statistics/src/storage/models.rs b/service-providers/network-statistics/src/storage/models.rs index f04a333495..d179914540 100644 --- a/service-providers/network-statistics/src/storage/models.rs +++ b/service-providers/network-statistics/src/storage/models.rs @@ -17,6 +17,7 @@ pub(crate) struct ServiceStatistics { pub(crate) struct GatewayStatistics { #[allow(dead_code)] pub(crate) id: i64, + pub(crate) gateway_id: String, pub(crate) inbox_count: i64, pub(crate) timestamp: NaiveDateTime, } diff --git a/tools/ts-rs-cli/src/main.rs b/tools/ts-rs-cli/src/main.rs index b5f06dd38f..70a442cbae 100644 --- a/tools/ts-rs-cli/src/main.rs +++ b/tools/ts-rs-cli/src/main.rs @@ -4,7 +4,7 @@ use walkdir::WalkDir; use mixnet_contract_common::mixnode::RewardedSetNodeStatus; use nym_types::account::{Account, AccountEntry, AccountWithMnemonic, Balance}; -use nym_types::currency::{CurrencyDenom, MajorAmountString, MajorCurrencyAmount}; +use nym_types::currency::{CurrencyDenom, DecCoin}; use nym_types::delegation::{ Delegation, DelegationEvent, DelegationEventKind, DelegationRecord, DelegationResult, DelegationWithEverything, DelegationsSummaryResponse, PendingUndelegate, @@ -59,7 +59,6 @@ fn main() { do_export!(AccountEntry); do_export!(AccountWithMnemonic); do_export!(Balance); - do_export!(CurrencyDenom); do_export!(Delegation); do_export!(DelegationEvent); do_export!(DelegationEventKind); @@ -77,8 +76,8 @@ fn main() { do_export!(GasInfo); do_export!(Gateway); do_export!(GatewayBond); - do_export!(MajorAmountString); - do_export!(MajorCurrencyAmount); + do_export!(CurrencyDenom); + do_export!(DecCoin); do_export!(MixNode); do_export!(MixNodeBond); do_export!(OriginalVestingResponse); diff --git a/ts-packages/react-components/src/components/currency/Currency.stories.tsx b/ts-packages/react-components/src/components/currency/Currency.stories.tsx index b5a9be21bc..6a7c16c9df 100644 --- a/ts-packages/react-components/src/components/currency/Currency.stories.tsx +++ b/ts-packages/react-components/src/components/currency/Currency.stories.tsx @@ -11,24 +11,24 @@ export default { export const Mainnet = () => ( - - - - + + + + {amounts.map((amount) => ( - + ))} ); export const Testnet = () => ( - - - - + + + + {amounts.map((amount) => ( - + ))} ); @@ -40,7 +40,7 @@ export const WithSX = () => ( {amounts.map((amount) => ( @@ -48,7 +48,7 @@ export const WithSX = () => ( {amounts.map((amount) => ( ))} diff --git a/ts-packages/react-components/src/components/currency/Currency.tsx b/ts-packages/react-components/src/components/currency/Currency.tsx index 1168ff6b24..6f740a3916 100644 --- a/ts-packages/react-components/src/components/currency/Currency.tsx +++ b/ts-packages/react-components/src/components/currency/Currency.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; -import type { MajorCurrencyAmount } from '@nymproject/types'; +import type { DecCoin } from '@nymproject/types'; import { Stack, SxProps, Typography } from '@mui/material'; import { CurrencyWithCoinMark } from './CurrencyWithCoinMark'; import { CURRENCY_AMOUNT_SPACING, CurrencyAmount } from './CurrencyAmount'; export const Currency: React.FC<{ - majorAmount?: MajorCurrencyAmount; + majorAmount?: DecCoin; showDenom?: boolean; showCoinMark?: boolean; coinMarkPrefix?: boolean; diff --git a/ts-packages/react-components/src/components/currency/CurrencyAmount.stories.tsx b/ts-packages/react-components/src/components/currency/CurrencyAmount.stories.tsx index d971d03207..0c6743022f 100644 --- a/ts-packages/react-components/src/components/currency/CurrencyAmount.stories.tsx +++ b/ts-packages/react-components/src/components/currency/CurrencyAmount.stories.tsx @@ -34,7 +34,7 @@ export const amounts = [ export const WithSeparators = () => ( {amounts.map((amount) => ( - + ))} ); @@ -42,20 +42,20 @@ export const WithSeparators = () => ( export const NoSeparators = () => ( {amounts.map((amount) => ( - + ))} ); -export const MaxRange = () => ; +export const MaxRange = () => ; export const Weird = () => ( - - - - - + + + + + ); @@ -66,7 +66,7 @@ export const NoSeparatorsWithSX = () => ( {amounts.map((amount) => ( @@ -79,7 +79,7 @@ export const WithSX = () => ( {amounts.map((amount) => ( ))} diff --git a/ts-packages/react-components/src/components/currency/CurrencyAmount.tsx b/ts-packages/react-components/src/components/currency/CurrencyAmount.tsx index 44726eacfc..534e79bf7c 100644 --- a/ts-packages/react-components/src/components/currency/CurrencyAmount.tsx +++ b/ts-packages/react-components/src/components/currency/CurrencyAmount.tsx @@ -1,6 +1,6 @@ /* eslint-disable react/no-array-index-key */ import * as React from 'react'; -import type { MajorCurrencyAmount } from '@nymproject/types'; +import type { DecCoin } from '@nymproject/types'; import { Stack, SxProps, Typography } from '@mui/material'; export const CURRENCY_AMOUNT_SPACING = 0.35; @@ -94,7 +94,7 @@ export const CurrencyAmountString: React.FC<{ }; export const CurrencyAmount: React.FC<{ - majorAmount?: MajorCurrencyAmount; + majorAmount?: DecCoin; showSeparators?: boolean; sx?: SxProps; }> = ({ majorAmount, ...props }) => ; diff --git a/ts-packages/react-components/src/components/currency/CurrencyFormField.tsx b/ts-packages/react-components/src/components/currency/CurrencyFormField.tsx index bc949ba4b2..34fb118c34 100644 --- a/ts-packages/react-components/src/components/currency/CurrencyFormField.tsx +++ b/ts-packages/react-components/src/components/currency/CurrencyFormField.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { ChangeEvent } from 'react'; import { InputAdornment, TextField } from '@mui/material'; import { SxProps } from '@mui/system'; -import { CurrencyDenom, MajorCurrencyAmount } from '@nymproject/types'; +import { CurrencyDenom, DecCoin } from '@nymproject/types'; import { CoinMark } from '../coins/CoinMark'; const MAX_VALUE = 1_000_000_000_000_000; @@ -19,7 +19,7 @@ export const CurrencyFormField: React.FC<{ placeholder?: string; label?: string; denom?: CurrencyDenom; - onChanged?: (newValue: MajorCurrencyAmount) => void; + onChanged?: (newValue: DecCoin) => void; onValidate?: (newValue: string | undefined, isValid: boolean, error?: string) => void; sx?: SxProps; }> = ({ @@ -35,7 +35,7 @@ export const CurrencyFormField: React.FC<{ onValidate, sx, showCoinMark = true, - denom = 'NYM', + denom = 'nym', }) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const [value, setValue] = React.useState(initialValue); @@ -111,7 +111,7 @@ export const CurrencyFormField: React.FC<{ doValidation(newValue); if (onChanged) { - const newMajorCurrencyAmount: MajorCurrencyAmount = { + const newMajorCurrencyAmount: DecCoin = { amount: newValue, denom, }; @@ -132,8 +132,8 @@ export const CurrencyFormField: React.FC<{ required, endAdornment: showCoinMark && ( - {denom === 'NYM' && } - {denom !== 'NYM' && NYMT} + {denom === 'nym' && } + {denom !== 'nym' && NYMT} ), ...{ diff --git a/ts-packages/react-components/src/components/currency/CurrencyWithCoinMark.tsx b/ts-packages/react-components/src/components/currency/CurrencyWithCoinMark.tsx index 0fef6e3a75..3431a975be 100644 --- a/ts-packages/react-components/src/components/currency/CurrencyWithCoinMark.tsx +++ b/ts-packages/react-components/src/components/currency/CurrencyWithCoinMark.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { MajorCurrencyAmount } from '@nymproject/types'; +import { DecCoin } from '@nymproject/types'; import { Stack, SxProps } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { CoinMark } from '../coins/CoinMark'; @@ -7,7 +7,7 @@ import { CoinMarkTestnet } from '../coins/CoinMarkTestnet'; import { CurrencyAmount } from './CurrencyAmount'; export const CurrencyWithCoinMark: React.FC<{ - majorAmount?: MajorCurrencyAmount; + majorAmount?: DecCoin; fontSize?: number; prefix?: boolean; showSeparators?: boolean; @@ -18,7 +18,7 @@ export const CurrencyWithCoinMark: React.FC<{ if (!majorAmount) { return -; } - const DenomMark = majorAmount.denom === 'NYMT' ? CoinMarkTestnet : CoinMark; + const DenomMark = majorAmount.denom === 'nymt' ? CoinMarkTestnet : CoinMark; return ( {prefix ? ( diff --git a/ts-packages/react-components/src/components/mixnodes/IdentityKeyFormField.tsx b/ts-packages/react-components/src/components/mixnodes/IdentityKeyFormField.tsx index 89a3957a2e..b3f0de2857 100644 --- a/ts-packages/react-components/src/components/mixnodes/IdentityKeyFormField.tsx +++ b/ts-packages/react-components/src/components/mixnodes/IdentityKeyFormField.tsx @@ -13,16 +13,21 @@ export const IdentityKeyFormField: React.FC<{ readOnly?: boolean; initialValue?: string; placeholder?: string; + label?: string; + helperText?: string; onChanged?: (newValue: string) => void; onValidate?: (isValid: boolean, error?: string) => void; textFieldProps?: TextFieldProps; + errorText?: string; sx?: SxProps; }> = ({ required, fullWidth, placeholder, + label, readOnly, initialValue, + errorText, sx, onChanged, onValidate, @@ -55,7 +60,11 @@ export const IdentityKeyFormField: React.FC<{ if (initialValue) { doValidation(initialValue); } - }, [initialValue]); + + if (errorText) { + setValidationError(errorText); + } + }, [initialValue, errorText]); const handleChange = (event: ChangeEvent) => { const newValue = event.target.value; @@ -85,6 +94,7 @@ export const IdentityKeyFormField: React.FC<{ ), }} placeholder={placeholder} + label={label} sx={sx} {...textFieldProps} aria-readonly={readOnly} diff --git a/ts-packages/types/src/types/global.ts b/ts-packages/types/src/types/global.ts index 98ec025a81..3c020800e2 100644 --- a/ts-packages/types/src/types/global.ts +++ b/ts-packages/types/src/types/global.ts @@ -1,4 +1,4 @@ -import { MixNode, MajorCurrencyAmount } from './rust'; +import { MixNode, DecCoin, CurrencyDenom } from './rust'; export type TNodeType = 'mixnode' | 'gateway'; @@ -10,7 +10,7 @@ export type TNodeOwnership = { export type TDelegation = { owner: string; node_identity: string; - amount: MajorCurrencyAmount; + amount: DecCoin; block_height: number; proxy: string; // proxy address used to delegate the funds on behalf of anouther address }; @@ -21,8 +21,8 @@ export type TPagedDelegations = { }; export type TMixnodeBondDetails = { - pledge_amount: MajorCurrencyAmount; - total_delegation: MajorCurrencyAmount; + pledge_amount: DecCoin; + total_delegation: DecCoin; owner: string; layer: string; block_height: number; diff --git a/ts-packages/types/src/types/rust/Account.ts b/ts-packages/types/src/types/rust/Account.ts index 1f3c8f470a..ea0586828f 100644 --- a/ts-packages/types/src/types/rust/Account.ts +++ b/ts-packages/types/src/types/rust/Account.ts @@ -1,7 +1,7 @@ import type { CurrencyDenom } from './CurrencyDenom'; export interface Account { - contract_address: string; client_address: string; - denom: CurrencyDenom; + base_mix_denom: string; + display_mix_denom: CurrencyDenom; } diff --git a/ts-packages/types/src/types/rust/Balance.ts b/ts-packages/types/src/types/rust/Balance.ts index 80ebb917a1..ba6be26651 100644 --- a/ts-packages/types/src/types/rust/Balance.ts +++ b/ts-packages/types/src/types/rust/Balance.ts @@ -1,6 +1,6 @@ -import type { MajorCurrencyAmount } from './Currency'; +import type { DecCoin } from './DecCoin'; export interface Balance { - amount: MajorCurrencyAmount; + amount: DecCoin; printable_balance: string; } diff --git a/ts-packages/types/src/types/rust/Coin.ts b/ts-packages/types/src/types/rust/Coin.ts index e02141439d..dda00291d7 100644 --- a/ts-packages/types/src/types/rust/Coin.ts +++ b/ts-packages/types/src/types/rust/Coin.ts @@ -1,2 +1,4 @@ - -export interface Coin { denom: string, amount: bigint, } \ No newline at end of file +export interface Coin { + denom: string; + amount: bigint; +} diff --git a/ts-packages/types/src/types/rust/CosmosFee.ts b/ts-packages/types/src/types/rust/CosmosFee.ts index c4220b1658..54c0f689bc 100644 --- a/ts-packages/types/src/types/rust/CosmosFee.ts +++ b/ts-packages/types/src/types/rust/CosmosFee.ts @@ -1,3 +1,8 @@ -import type { Coin } from "./Coin"; +import type { Coin } from './Coin'; -export interface CosmosFee { amount: Array, gas_limit: bigint, payer: string | null, granter: string | null, } \ No newline at end of file +export interface CosmosFee { + amount: Array; + gas_limit: bigint; + payer: string | null; + granter: string | null; +} diff --git a/ts-packages/types/src/types/rust/Currency.ts b/ts-packages/types/src/types/rust/Currency.ts deleted file mode 100644 index f0c6d57fbb..0000000000 --- a/ts-packages/types/src/types/rust/Currency.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { CurrencyDenom } from './CurrencyDenom'; -import type { MajorAmountString } from './CurrencyStringMajorAmount'; - -export interface MajorCurrencyAmount { - amount: MajorAmountString; - denom: CurrencyDenom; -} diff --git a/ts-packages/types/src/types/rust/CurrencyDenom.ts b/ts-packages/types/src/types/rust/CurrencyDenom.ts index d6a45dc3d2..04760b4fd7 100644 --- a/ts-packages/types/src/types/rust/CurrencyDenom.ts +++ b/ts-packages/types/src/types/rust/CurrencyDenom.ts @@ -1 +1 @@ -export type CurrencyDenom = 'NYM' | 'NYMT' | 'NYX' | 'NYXT'; +export type CurrencyDenom = 'unknown' | 'nym' | 'nymt' | 'nyx' | 'nyxt'; diff --git a/ts-packages/types/src/types/rust/CurrencyStringMajorAmount.ts b/ts-packages/types/src/types/rust/CurrencyStringMajorAmount.ts deleted file mode 100644 index 9863028330..0000000000 --- a/ts-packages/types/src/types/rust/CurrencyStringMajorAmount.ts +++ /dev/null @@ -1 +0,0 @@ -export type MajorAmountString = string; diff --git a/ts-packages/types/src/types/rust/DecCoin.ts b/ts-packages/types/src/types/rust/DecCoin.ts new file mode 100644 index 0000000000..d2b1b4dd69 --- /dev/null +++ b/ts-packages/types/src/types/rust/DecCoin.ts @@ -0,0 +1,3 @@ +import type { CurrencyDenom } from './CurrencyDenom'; + +export type DecCoin = { denom: CurrencyDenom; amount: string }; diff --git a/ts-packages/types/src/types/rust/Delegation.ts b/ts-packages/types/src/types/rust/Delegation.ts index 72e2ce17c7..c6f857254d 100644 --- a/ts-packages/types/src/types/rust/Delegation.ts +++ b/ts-packages/types/src/types/rust/Delegation.ts @@ -1,9 +1,9 @@ -import type { MajorCurrencyAmount } from './Currency'; +import type { DecCoin } from './DecCoin'; export interface Delegation { owner: string; node_identity: string; - amount: MajorCurrencyAmount; + amount: DecCoin; block_height: bigint; proxy: string | null; } diff --git a/ts-packages/types/src/types/rust/DelegationEvent.ts b/ts-packages/types/src/types/rust/DelegationEvent.ts index ec44a8fda7..a7fbca2737 100644 --- a/ts-packages/types/src/types/rust/DelegationEvent.ts +++ b/ts-packages/types/src/types/rust/DelegationEvent.ts @@ -1,11 +1,11 @@ +import type { DecCoin } from './DecCoin'; import type { DelegationEventKind } from './DelegationEventKind'; -import type { MajorCurrencyAmount } from './Currency'; export interface DelegationEvent { kind: DelegationEventKind; node_identity: string; address: string; - amount: MajorCurrencyAmount | null; + amount: DecCoin | null; block_height: bigint; proxy: string | null; } diff --git a/ts-packages/types/src/types/rust/DelegationRecord.ts b/ts-packages/types/src/types/rust/DelegationRecord.ts index 38ec024a35..7f1d6aa7a3 100644 --- a/ts-packages/types/src/types/rust/DelegationRecord.ts +++ b/ts-packages/types/src/types/rust/DelegationRecord.ts @@ -1,7 +1,7 @@ -import type { MajorCurrencyAmount } from './Currency'; +import type { DecCoin } from './DecCoin'; export interface DelegationRecord { - amount: MajorCurrencyAmount; + amount: DecCoin; block_height: bigint; delegated_on_iso_datetime: string; uses_vesting_contract_tokens: boolean; diff --git a/ts-packages/types/src/types/rust/DelegationResult.ts b/ts-packages/types/src/types/rust/DelegationResult.ts index 33c6dba091..f6d29d9435 100644 --- a/ts-packages/types/src/types/rust/DelegationResult.ts +++ b/ts-packages/types/src/types/rust/DelegationResult.ts @@ -1,7 +1,7 @@ -import type { MajorCurrencyAmount } from './Currency'; +import type { DecCoin } from './DecCoin'; export interface DelegationResult { source_address: string; target_address: string; - amount: MajorCurrencyAmount | null; + amount: DecCoin | null; } diff --git a/ts-packages/types/src/types/rust/DelegationSummaryResponse.ts b/ts-packages/types/src/types/rust/DelegationSummaryResponse.ts index a801d2865e..0cb9346c59 100644 --- a/ts-packages/types/src/types/rust/DelegationSummaryResponse.ts +++ b/ts-packages/types/src/types/rust/DelegationSummaryResponse.ts @@ -1,8 +1,8 @@ +import type { DecCoin } from './DecCoin'; import type { DelegationWithEverything } from './DelegationWithEverything'; -import type { MajorCurrencyAmount } from './Currency'; export interface DelegationsSummaryResponse { delegations: Array; - total_delegations: MajorCurrencyAmount; - total_rewards: MajorCurrencyAmount; + total_delegations: DecCoin; + total_rewards: DecCoin; } diff --git a/ts-packages/types/src/types/rust/DelegationWithEverything.ts b/ts-packages/types/src/types/rust/DelegationWithEverything.ts index 6bf6896e0b..8c5f824e5e 100644 --- a/ts-packages/types/src/types/rust/DelegationWithEverything.ts +++ b/ts-packages/types/src/types/rust/DelegationWithEverything.ts @@ -1,20 +1,20 @@ +import type { DecCoin } from './DecCoin'; import type { DelegationEvent } from './DelegationEvent'; import type { DelegationRecord } from './DelegationRecord'; -import type { MajorCurrencyAmount } from './Currency'; export interface DelegationWithEverything { owner: string; node_identity: string; - amount: MajorCurrencyAmount; - total_delegation: MajorCurrencyAmount | null; - pledge_amount: MajorCurrencyAmount | null; + amount: DecCoin; + total_delegation: DecCoin | null; + pledge_amount: DecCoin | null; block_height: bigint; delegated_on_iso_datetime: string; profit_margin_percent: number | null; avg_uptime_percent: number | null; stake_saturation: number | null; uses_vesting_contract_tokens: boolean; - accumulated_rewards: MajorCurrencyAmount | null; + accumulated_rewards: DecCoin | null; pending_events: Array; history: Array; } diff --git a/ts-packages/types/src/types/rust/FeeDetails.ts b/ts-packages/types/src/types/rust/FeeDetails.ts index 17d9631bc9..60bcd4cae5 100644 --- a/ts-packages/types/src/types/rust/FeeDetails.ts +++ b/ts-packages/types/src/types/rust/FeeDetails.ts @@ -1,4 +1,4 @@ +import type { DecCoin } from './DecCoin'; import type { Fee } from './Fee'; -import type { MajorCurrencyAmount } from './Currency'; -export type FeeDetails = { amount: MajorCurrencyAmount | null; fee: Fee }; +export type FeeDetails = { amount: DecCoin | null; fee: Fee }; diff --git a/ts-packages/types/src/types/rust/GasInfo.ts b/ts-packages/types/src/types/rust/GasInfo.ts index 3fbaaac7ba..fd5b21e582 100644 --- a/ts-packages/types/src/types/rust/GasInfo.ts +++ b/ts-packages/types/src/types/rust/GasInfo.ts @@ -1,7 +1,6 @@ -import type { MajorCurrencyAmount } from './Currency'; +import type { Gas } from './Gas'; export interface GasInfo { - gas_wanted: bigint; - gas_used: bigint; - fee: MajorCurrencyAmount; + gas_wanted: Gas; + gas_used: Gas; } diff --git a/ts-packages/types/src/types/rust/GatewayBond.ts b/ts-packages/types/src/types/rust/GatewayBond.ts index 4ffe275186..277994efac 100644 --- a/ts-packages/types/src/types/rust/GatewayBond.ts +++ b/ts-packages/types/src/types/rust/GatewayBond.ts @@ -1,8 +1,8 @@ +import type { DecCoin } from './DecCoin'; import type { Gateway } from './Gateway'; -import type { MajorCurrencyAmount } from './Currency'; export interface GatewayBond { - pledge_amount: MajorCurrencyAmount; + pledge_amount: DecCoin; owner: string; block_height: bigint; gateway: Gateway; diff --git a/ts-packages/types/src/types/rust/MixNodeBond.ts b/ts-packages/types/src/types/rust/MixNodeBond.ts index 06e8cc884f..bfcb737290 100644 --- a/ts-packages/types/src/types/rust/MixNodeBond.ts +++ b/ts-packages/types/src/types/rust/MixNodeBond.ts @@ -1,13 +1,13 @@ -import type { MajorCurrencyAmount } from './Currency'; +import type { DecCoin } from './DecCoin'; import type { MixNode } from './Mixnode'; export interface MixNodeBond { - pledge_amount: MajorCurrencyAmount; - total_delegation: MajorCurrencyAmount; + pledge_amount: DecCoin; + total_delegation: DecCoin; owner: string; layer: string; block_height: bigint; mix_node: MixNode; proxy: string | null; - accumulated_rewards: MajorCurrencyAmount | null; + accumulated_rewards: DecCoin | null; } diff --git a/ts-packages/types/src/types/rust/Operation.ts b/ts-packages/types/src/types/rust/Operation.ts deleted file mode 100644 index 405b5dbb0b..0000000000 --- a/ts-packages/types/src/types/rust/Operation.ts +++ /dev/null @@ -1,34 +0,0 @@ -export type Operation = - | 'Upload' - | 'Init' - | 'Migrate' - | 'ChangeAdmin' - | 'Send' - | 'BondMixnode' - | 'BondMixnodeOnBehalf' - | 'UnbondMixnode' - | 'UnbondMixnodeOnBehalf' - | 'UpdateMixnodeConfig' - | 'DelegateToMixnode' - | 'DelegateToMixnodeOnBehalf' - | 'UndelegateFromMixnode' - | 'UndelegateFromMixnodeOnBehalf' - | 'BondGateway' - | 'BondGatewayOnBehalf' - | 'UnbondGateway' - | 'UnbondGatewayOnBehalf' - | 'UpdateContractSettings' - | 'BeginMixnodeRewarding' - | 'FinishMixnodeRewarding' - | 'TrackUnbondGateway' - | 'TrackUnbondMixnode' - | 'WithdrawVestedCoins' - | 'TrackUndelegation' - | 'CreatePeriodicVestingAccount' - | 'AdvanceCurrentInterval' - | 'AdvanceCurrentEpoch' - | 'WriteRewardedSet' - | 'ClearRewardedSet' - | 'UpdateMixnetAddress' - | 'CheckpointMixnodes' - | 'ReconcileDelegations'; diff --git a/ts-packages/types/src/types/rust/OriginalVestingResponse.ts b/ts-packages/types/src/types/rust/OriginalVestingResponse.ts index 2ebbd40bb2..8d781d5ad0 100644 --- a/ts-packages/types/src/types/rust/OriginalVestingResponse.ts +++ b/ts-packages/types/src/types/rust/OriginalVestingResponse.ts @@ -1,7 +1,7 @@ -import type { MajorCurrencyAmount } from './Currency'; +import type { DecCoin } from './DecCoin'; export interface OriginalVestingResponse { - amount: MajorCurrencyAmount; + amount: DecCoin; number_of_periods: number; period_duration: bigint; } diff --git a/ts-packages/types/src/types/rust/PledgeData.ts b/ts-packages/types/src/types/rust/PledgeData.ts index 2f82758842..9fb19f9aaf 100644 --- a/ts-packages/types/src/types/rust/PledgeData.ts +++ b/ts-packages/types/src/types/rust/PledgeData.ts @@ -1,6 +1,6 @@ -import type { MajorCurrencyAmount } from './Currency'; +import type { DecCoin } from './DecCoin'; export interface PledgeData { - amount: MajorCurrencyAmount; + amount: DecCoin; block_time: bigint; } diff --git a/ts-packages/types/src/types/rust/RpcTransactionResponse.ts b/ts-packages/types/src/types/rust/RpcTransactionResponse.ts index d2938e149e..2463e3dd20 100644 --- a/ts-packages/types/src/types/rust/RpcTransactionResponse.ts +++ b/ts-packages/types/src/types/rust/RpcTransactionResponse.ts @@ -1,9 +1,12 @@ -import type { GasInfo } from './GasInfo'; +import type { DecCoin } from './DecCoin'; +import type { Gas } from './Gas'; export interface RpcTransactionResponse { index: number; tx_result_json: string; block_height: bigint; transaction_hash: string; - gas_info: GasInfo; + gas_used: Gas; + gas_wanted: Gas; + fee: DecCoin | null; } diff --git a/ts-packages/types/src/types/rust/SelectionChance.ts b/ts-packages/types/src/types/rust/SelectionChance.ts index a9e7e4797b..04f182aa79 100644 --- a/ts-packages/types/src/types/rust/SelectionChance.ts +++ b/ts-packages/types/src/types/rust/SelectionChance.ts @@ -1 +1 @@ -export type SelectionChance = 'VeryHigh' | 'High' | 'Moderate' | 'Low' | 'VeryLow'; +export type SelectionChance = 'VeryHigh' | 'Moderate' | 'Low'; diff --git a/ts-packages/types/src/types/rust/SendTxResult.ts b/ts-packages/types/src/types/rust/SendTxResult.ts index 1e6f5ac366..b44de3324d 100644 --- a/ts-packages/types/src/types/rust/SendTxResult.ts +++ b/ts-packages/types/src/types/rust/SendTxResult.ts @@ -1,10 +1,13 @@ +import type { DecCoin } from './DecCoin'; +import type { Gas } from './Gas'; import type { TransactionDetails } from './TransactionDetails'; export interface SendTxResult { block_height: bigint; code: number; details: TransactionDetails; - gas_used: bigint; - gas_wanted: bigint; + gas_used: Gas; + gas_wanted: Gas; tx_hash: string; + fee: DecCoin | null; } diff --git a/ts-packages/types/src/types/rust/TauriTxResult.ts b/ts-packages/types/src/types/rust/TauriTxResult.ts deleted file mode 100644 index 93c47da931..0000000000 --- a/ts-packages/types/src/types/rust/TauriTxResult.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { TransactionDetails } from './TransactionDetails'; - -export interface TauriTxResult { - block_height: bigint; - code: number; - details: TransactionDetails; - gas_used: bigint; - gas_wanted: bigint; - tx_hash: string; -} diff --git a/ts-packages/types/src/types/rust/TransactionDetails.ts b/ts-packages/types/src/types/rust/TransactionDetails.ts index 8e26480c19..8de1cf9ec3 100644 --- a/ts-packages/types/src/types/rust/TransactionDetails.ts +++ b/ts-packages/types/src/types/rust/TransactionDetails.ts @@ -1,7 +1,7 @@ -import type { MajorCurrencyAmount } from './Currency'; +import type { DecCoin } from './DecCoin'; export interface TransactionDetails { - amount: MajorCurrencyAmount; + amount: DecCoin; from_address: string; to_address: string; } diff --git a/ts-packages/types/src/types/rust/TransactionExecuteResult.ts b/ts-packages/types/src/types/rust/TransactionExecuteResult.ts index fd67980be6..e6d93e8448 100644 --- a/ts-packages/types/src/types/rust/TransactionExecuteResult.ts +++ b/ts-packages/types/src/types/rust/TransactionExecuteResult.ts @@ -1,10 +1,10 @@ +import type { DecCoin } from './DecCoin'; import type { GasInfo } from './GasInfo'; -import type { MajorCurrencyAmount } from './Currency'; export interface TransactionExecuteResult { logs_json: string; data_json: string; transaction_hash: string; gas_info: GasInfo; - fee: MajorCurrencyAmount; + fee: DecCoin | null; } diff --git a/ts-packages/types/src/types/rust/VestingAccountInfo.ts b/ts-packages/types/src/types/rust/VestingAccountInfo.ts index 793431c215..d317d045d1 100644 --- a/ts-packages/types/src/types/rust/VestingAccountInfo.ts +++ b/ts-packages/types/src/types/rust/VestingAccountInfo.ts @@ -1,4 +1,4 @@ -import type { MajorCurrencyAmount } from './Currency'; +import type { DecCoin } from './DecCoin'; import type { VestingPeriod } from './VestingPeriod'; export interface VestingAccountInfo { @@ -6,5 +6,5 @@ export interface VestingAccountInfo { staking_address: string | null; start_time: bigint; periods: Array; - amount: MajorCurrencyAmount; + amount: DecCoin; } diff --git a/ts-packages/types/src/types/rust/index.ts b/ts-packages/types/src/types/rust/index.ts index 1be7a06002..031ecf14fc 100644 --- a/ts-packages/types/src/types/rust/index.ts +++ b/ts-packages/types/src/types/rust/index.ts @@ -3,14 +3,13 @@ export * from './AccountEntry'; export * from './AccountWithMnemonic'; export * from './Balance'; export * from './CoreNodeStatusResponse'; -export * from './Currency'; export * from './CurrencyDenom'; -export * from './CurrencyStringMajorAmount'; export * from './Delegation'; export * from './DelegationEvent'; export * from './DelegationEventKind'; export * from './DelegationRecord'; export * from './DelegationResult'; +export * from './DecCoin'; export * from './DelegationSummaryResponse'; export * from './DelegationWithEverything'; export * from './Fee'; @@ -24,7 +23,6 @@ export * from './Mixnode'; export * from './MixNodeBond'; export * from './MixnodeStatus'; export * from './MixnodeStatusResponse'; -export * from './Operation'; export * from './OriginalVestingResponse'; export * from './PendingUndelegate'; export * from './Period'; diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index e4cb7fc8e4..ce09411419 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-validator-api" -version = "1.0.1" +version = "1.0.2" authors = [ "Dave Hrycyszyn ", "Jędrzej Stuczyński ", @@ -58,6 +58,8 @@ gateway-client = { path="../common/client-libs/gateway-client" } mixnet-contract-common = { path= "../common/cosmwasm-smart-contracts/mixnet-contract" } multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" } nymsphinx = { path="../common/nymsphinx" } +nymcoconut = { path = "../common/nymcoconut", optional = true } +task = { path = "../common/task" } topology = { path="../common/topology" } validator-api-requests = { path = "validator-api-requests" } validator-client = { path="../common/client-libs/validator-client", features = ["nymd-client"] } @@ -70,7 +72,7 @@ console-subscriber = { version = "0.1.1", optional = true} cfg-if = "1.0" [features] -coconut = ["coconut-interface", "credentials", "gateway-client/coconut", "credentials/coconut", "validator-api-requests/coconut"] +coconut = ["coconut-interface", "credentials", "gateway-client/coconut", "credentials/coconut", "validator-api-requests/coconut", "nymcoconut"] no-reward = [] generate-ts = [] @@ -81,4 +83,5 @@ vergen = { version = "5", default-features = false, features = ["build", "git", [dev-dependencies] attohttpc = {version = "0.18.0", features = ["json"]} -nymcoconut = { path = "../common/nymcoconut" } +cw3 = "0.13.2" +cw-utils = "0.13.2" diff --git a/validator-api/src/coconut/client.rs b/validator-api/src/coconut/client.rs index 612efc20c1..626a4b775b 100644 --- a/validator-api/src/coconut/client.rs +++ b/validator-api/src/coconut/client.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::coconut::error::Result; +use coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; use multisig_contract_common::msg::ProposalResponse; use validator_client::nymd::{AccountId, Fee, TxResponse}; @@ -10,13 +11,10 @@ pub trait Client { async fn address(&self) -> AccountId; async fn get_tx(&self, tx_hash: &str) -> Result; async fn get_proposal(&self, proposal_id: u64) -> Result; - async fn propose_release_funds( + async fn get_spent_credential( &self, - title: String, blinded_serial_number: String, - voucher_value: u128, - fee: Option, - ) -> Result; + ) -> Result; async fn vote_proposal(&self, proposal_id: u64, vote_yes: bool, fee: Option) -> Result<()>; } diff --git a/validator-api/src/coconut/comm.rs b/validator-api/src/coconut/comm.rs new file mode 100644 index 0000000000..e6f7b8a1ac --- /dev/null +++ b/validator-api/src/coconut/comm.rs @@ -0,0 +1,29 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::error::Result; +use coconut_interface::VerificationKey; +use credentials::obtain_aggregate_verification_key; +use url::Url; + +#[async_trait] +pub trait APICommunicationChannel { + async fn aggregated_verification_key(&self) -> Result; +} + +pub struct QueryCommunicationChannel { + validator_apis: Vec, +} + +impl QueryCommunicationChannel { + pub fn new(validator_apis: Vec) -> Self { + QueryCommunicationChannel { validator_apis } + } +} + +#[async_trait] +impl APICommunicationChannel for QueryCommunicationChannel { + async fn aggregated_verification_key(&self) -> Result { + Ok(obtain_aggregate_verification_key(&self.validator_apis).await?) + } +} diff --git a/validator-api/src/coconut/error.rs b/validator-api/src/coconut/error.rs index 78f9b08e43..7dfdb843c2 100644 --- a/validator-api/src/coconut/error.rs +++ b/validator-api/src/coconut/error.rs @@ -31,6 +31,9 @@ pub enum CoconutError { #[error("Nymd error - {0}")] NymdError(#[from] NymdError), + #[error("Coconut internal error - {0}")] + CoconutInternalError(#[from] nymcoconut::CoconutError), + #[error("Could not find a deposit event in the transaction provided")] DepositEventNotFound, @@ -63,22 +66,17 @@ pub enum CoconutError { #[error("Error in coconut interface - {0}")] CoconutInterfaceError(#[from] coconut_interface::error::CoconutInterfaceError), - #[error("Could not create proposal for spending credential")] - CreateProposalError, - #[error("Storage error - {0}")] StorageError(#[from] ValidatorApiStorageError), #[error("Credentials error - {0}")] CredentialsError(#[from] credentials::error::Error), - #[error( - "Incorrect credential proposal description. Expected blinded serial number in base 58" - )] - IncorrectProposal, + #[error("Incorrect credential proposal description: {reason}")] + IncorrectProposal { reason: String }, - #[error("Internal error: {0}")] - InternalError(String), + #[error("Invalid status of credential: {status}")] + InvalidCredentialStatus { status: String }, } impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError { diff --git a/validator-api/src/coconut/mod.rs b/validator-api/src/coconut/mod.rs index 2eba8eed0d..efd1728a7b 100644 --- a/validator-api/src/coconut/mod.rs +++ b/validator-api/src/coconut/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub(crate) mod client; +pub(crate) mod comm; mod deposit; pub(crate) mod error; #[cfg(test)] @@ -12,6 +13,9 @@ use crate::coconut::deposit::extract_encryption_key; use crate::coconut::error::{CoconutError, Result}; use crate::ValidatorApiStorage; +use coconut_bandwidth_contract_common::spend_credential::{ + funds_from_cosmos_msgs, SpendCredentialStatus, +}; use coconut_interface::{ Attribute, BlindSignRequest, BlindedSignature, KeyPair, Parameters, VerificationKey, }; @@ -19,16 +23,14 @@ use config::defaults::VALIDATOR_API_VERSION; use credentials::coconut::params::{ ValidatorApiCredentialEncryptionAlgorithm, ValidatorApiCredentialHkdfAlgorithm, }; -use credentials::obtain_aggregate_verification_key; use crypto::asymmetric::encryption; use crypto::shared_key::new_ephemeral_shared_key; use crypto::symmetric::stream_cipher; use validator_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, - ProposeReleaseFundsRequestBody, ProposeReleaseFundsResponse, VerificationKeyResponse, + BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, VerifyCredentialBody, VerifyCredentialResponse, }; -use validator_client::nymd::Fee; +use validator_client::nymd::{Coin, Fee}; use validator_client::validator_api::routes::{BANDWIDTH, COCONUT_ROUTES}; use getset::{CopyGetters, Getters}; @@ -38,32 +40,38 @@ use rocket::serde::json::Json; use rocket::State as RocketState; use std::sync::Arc; use tokio::sync::Mutex; -use url::Url; + +use self::comm::APICommunicationChannel; pub struct State { client: Arc, + mix_denom: String, key_pair: KeyPair, - validator_apis: Vec, + comm_channel: Arc, storage: ValidatorApiStorage, rng: Arc>, } impl State { - pub(crate) fn new( + pub(crate) fn new( client: C, + mix_denom: String, key_pair: KeyPair, - validator_apis: Vec, + comm_channel: D, storage: ValidatorApiStorage, ) -> Self where C: LocalClient + Send + Sync + 'static, + D: APICommunicationChannel + Send + Sync + 'static, { let client = Arc::new(client); + let comm_channel = Arc::new(comm_channel); let rng = Arc::new(Mutex::new(OsRng)); Self { client, + mix_denom, key_pair, - validator_apis, + comm_channel, storage, rng, } @@ -125,7 +133,7 @@ impl State { } pub async fn verification_key(&self) -> Result { - Ok(obtain_aggregate_verification_key(&self.validator_apis).await?) + self.comm_channel.aggregated_verification_key().await } } @@ -153,16 +161,18 @@ impl InternalSignRequest { } } - pub fn stage( + pub fn stage( client: C, + mix_denom: String, key_pair: KeyPair, - validator_apis: Vec, + comm_channel: D, storage: ValidatorApiStorage, ) -> AdHoc where C: LocalClient + Send + Sync + 'static, + D: APICommunicationChannel + Send + Sync + 'static, { - let state = State::new(client, key_pair, validator_apis, storage); + let state = State::new(client, mix_denom, key_pair, comm_channel, storage); AdHoc::on_ignite("Internal Sign Request Stage", |rocket| async { rocket.manage(state).mount( // this format! is so ugly... @@ -175,23 +185,21 @@ impl InternalSignRequest { get_verification_key, get_cosmos_address, post_partial_bandwidth_credential, - verify_bandwidth_credential, - post_propose_release_funds + verify_bandwidth_credential ], ) }) } } -fn blind_sign(request: InternalSignRequest, key_pair: &KeyPair) -> BlindedSignature { - let params = Parameters::new(request.total_params()).unwrap(); - coconut_interface::blind_sign( +fn blind_sign(request: InternalSignRequest, key_pair: &KeyPair) -> Result { + let params = Parameters::new(request.total_params())?; + Ok(coconut_interface::blind_sign( ¶ms, &key_pair.secret_key(), request.blind_sign_request(), request.public_attributes(), - ) - .unwrap() + )?) } #[post("/blind-sign", data = "")] @@ -217,7 +225,7 @@ pub async fn post_blind_sign( blind_sign_request_body.public_attributes(), blind_sign_request_body.blind_sign_request().clone(), ); - let blinded_signature = blind_sign(internal_request, &state.key_pair); + let blinded_signature = blind_sign(internal_request, &state.key_pair)?; let response = state .encrypt_and_store( @@ -263,69 +271,60 @@ pub async fn verify_bandwidth_credential( verify_credential_body: Json, state: &RocketState, ) -> Result> { - let proposal_id = *verify_credential_body.0.proposal_id(); + let proposal_id = *verify_credential_body.proposal_id(); let proposal = state.client.get_proposal(proposal_id).await?; // Proposal description is the blinded serial number if !verify_credential_body - .0 .credential() .has_blinded_serial_number(&proposal.description)? { - return Err(CoconutError::IncorrectProposal); + return Err(CoconutError::IncorrectProposal { + reason: String::from("incorrect blinded serial number in description"), + }); + } + let proposed_release_funds = + funds_from_cosmos_msgs(proposal.msgs).ok_or(CoconutError::IncorrectProposal { + reason: String::from("action is not to release funds"), + })?; + // Credential has not been spent before, and is on its way of being spent + let credential_status = state + .client + .get_spent_credential(verify_credential_body.credential().blinded_serial_number()) + .await? + .spend_credential + .ok_or(CoconutError::InvalidCredentialStatus { + status: String::from("Inexistent"), + })? + .status(); + if credential_status != SpendCredentialStatus::InProgress { + return Err(CoconutError::InvalidCredentialStatus { + status: format!("{:?}", credential_status), + }); } let verification_key = state.verification_key().await?; - let verification_result = verify_credential_body - .0 + let mut vote_yes = verify_credential_body .credential() .verify(&verification_key); + vote_yes &= Coin::from(proposed_release_funds) + == Coin::new( + verify_credential_body.credential().voucher_value() as u128, + state.mix_denom.clone(), + ); + // Vote yes or no on the proposal based on the verification result state .client .vote_proposal( proposal_id, - verification_result, - Some(Fee::PayerGranterAuto( + vote_yes, + Some(Fee::new_payer_granter_auto( None, None, - Some(verify_credential_body.0.gateway_cosmos_addr().to_owned()), + Some(verify_credential_body.gateway_cosmos_addr().to_owned()), )), ) .await?; - Ok(Json(VerifyCredentialResponse::new(verification_result))) -} - -#[post("/propose-release-funds", data = "")] -pub async fn post_propose_release_funds( - propose_release_funds: Json, - state: &RocketState, -) -> Result> { - let verification_key = state.verification_key().await?; - if !propose_release_funds - .0 - .credential() - .verify(&verification_key) - { - return Err(CoconutError::CreateProposalError); - } - - let title = String::from("Create proposal to spend a coconut credential"); - let blinded_serial_number = propose_release_funds.0.credential().blinded_serial_number(); - let voucher_value = propose_release_funds.0.credential().voucher_value() as u128; - let proposal_id = state - .client - .propose_release_funds( - title, - blinded_serial_number, - voucher_value, - Some(Fee::PayerGranterAuto( - None, - None, - Some(propose_release_funds.0.gateway_cosmos_addr().to_owned()), - )), - ) - .await?; - - Ok(Json(ProposeReleaseFundsResponse::new(proposal_id))) + Ok(Json(VerifyCredentialResponse::new(vote_yes))) } diff --git a/validator-api/src/coconut/tests.rs b/validator-api/src/coconut/tests.rs index 1fd66b9160..757eb9aa4a 100644 --- a/validator-api/src/coconut/tests.rs +++ b/validator-api/src/coconut/tests.rs @@ -7,7 +7,12 @@ use coconut_bandwidth_contract_common::events::{ DEPOSITED_FUNDS_EVENT_TYPE, DEPOSIT_ENCRYPTION_KEY, DEPOSIT_IDENTITY_KEY, DEPOSIT_INFO, DEPOSIT_VALUE, }; +use coconut_bandwidth_contract_common::spend_credential::{ + SpendCredential, SpendCredentialResponse, +}; +use coconut_interface::{hash_to_scalar, Credential, VerificationKey}; use config::defaults::VOUCHER_INFO; +use cosmwasm_std::{to_binary, Addr, CosmosMsg, Decimal, WasmMsg}; use credentials::coconut::bandwidth::BandwidthVoucher; use credentials::coconut::params::{ ValidatorApiCredentialEncryptionAlgorithm, ValidatorApiCredentialHkdfAlgorithm, @@ -15,16 +20,20 @@ use credentials::coconut::params::{ use crypto::shared_key::recompute_shared_key; use crypto::symmetric::stream_cipher; use multisig_contract_common::msg::ProposalResponse; +use nymcoconut::tests::helpers::theta_from_keys_and_attributes; use nymcoconut::{ prepare_blind_sign, ttp_keygen, Base58, BlindSignRequest, BlindedSignature, KeyPair, Parameters, }; use validator_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse, + BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, + VerifyCredentialBody, VerifyCredentialResponse, }; +use validator_client::nymd::Coin; use validator_client::nymd::{tx::Hash, AccountId, DeliverTx, Event, Fee, Tag, TxResponse}; use validator_client::validator_api::routes::{ - API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_PARTIAL_BANDWIDTH_CREDENTIAL, - COCONUT_ROUTES, COCONUT_VERIFICATION_KEY, + API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_COSMOS_ADDRESS, + COCONUT_PARTIAL_BANDWIDTH_CREDENTIAL, COCONUT_ROUTES, COCONUT_VERIFICATION_KEY, + COCONUT_VERIFY_BANDWIDTH_CREDENTIAL, }; use crate::coconut::State; @@ -38,25 +47,44 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::{Arc, RwLock}; +const TEST_COIN_DENOM: &str = "unym"; +const TEST_REWARDING_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; + +#[derive(Clone, Debug)] struct DummyClient { - db: Arc>>, + validator_address: AccountId, + tx_db: Arc>>, + proposal_db: Arc>>, + spent_credential_db: Arc>>, } impl DummyClient { - pub fn new(db: &Arc>>) -> Self { - let db = Arc::clone(db); - Self { db } + pub fn new( + validator_address: AccountId, + tx_db: &Arc>>, + proposal_db: &Arc>>, + spent_credential_db: &Arc>>, + ) -> Self { + let tx_db = Arc::clone(tx_db); + let proposal_db = Arc::clone(proposal_db); + let spent_credential_db = Arc::clone(spent_credential_db); + Self { + validator_address, + tx_db, + proposal_db, + spent_credential_db, + } } } #[async_trait] impl super::client::Client for DummyClient { async fn address(&self) -> AccountId { - todo!() + self.validator_address.clone() } async fn get_tx(&self, tx_hash: &str) -> Result { - self.db + self.tx_db .read() .unwrap() .get(tx_hash) @@ -64,27 +92,65 @@ impl super::client::Client for DummyClient { .ok_or(CoconutError::TxHashParseError) } - async fn get_proposal(&self, _proposal_id: u64) -> Result { - todo!() + async fn get_proposal(&self, proposal_id: u64) -> Result { + self.proposal_db + .read() + .unwrap() + .get(&proposal_id) + .cloned() + .ok_or(CoconutError::IncorrectProposal { + reason: String::from("proposal not found"), + }) } - async fn propose_release_funds( + async fn get_spent_credential( &self, - _title: String, - _blinded_serial_number: String, - _voucher_value: u128, - _fee: Option, - ) -> Result { - todo!() + blinded_serial_number: String, + ) -> Result { + self.spent_credential_db + .read() + .unwrap() + .get(&blinded_serial_number) + .cloned() + .ok_or(CoconutError::InvalidCredentialStatus { + status: String::from("spent credential not found"), + }) } async fn vote_proposal( &self, - _proposal_id: u64, - _vote_yes: bool, + proposal_id: u64, + vote_yes: bool, _fee: Option, ) -> Result<()> { - todo!() + if let Some(proposal) = self.proposal_db.write().unwrap().get_mut(&proposal_id) { + if vote_yes { + proposal.status = cw3::Status::Passed; + } else { + proposal.status = cw3::Status::Rejected; + } + } + Ok(()) + } +} + +#[derive(Clone, Debug)] +pub struct DummyCommunicationChannel { + aggregated_verification_key: VerificationKey, +} + +impl DummyCommunicationChannel { + pub fn new(aggregated_verification_key: VerificationKey) -> Self { + DummyCommunicationChannel { + aggregated_verification_key, + } + } +} + +#[async_trait] +impl super::comm::APICommunicationChannel for DummyCommunicationChannel { + async fn aggregated_verification_key(&self) -> Result { + Ok(self.aggregated_verification_key.clone()) } } @@ -114,13 +180,19 @@ async fn check_signer_verif_key(key_pair: KeyPair) { let mut db_dir = std::env::temp_dir(); db_dir.push(&verification_key.to_bs58()[..8]); let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); - let nymd_db = Arc::new(RwLock::new(HashMap::new())); - let nymd_client = DummyClient::new(&nymd_db); + let nymd_client = DummyClient::new( + AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), + &Arc::new(RwLock::new(HashMap::new())), + &Arc::new(RwLock::new(HashMap::new())), + &Arc::new(RwLock::new(HashMap::new())), + ); + let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client, + TEST_COIN_DENOM.to_string(), key_pair, - vec![], + comm_channel, storage, )); @@ -197,17 +269,24 @@ async fn signed_before() { let mut db_dir = std::env::temp_dir(); db_dir.push(&key_pair.verification_key().to_bs58()[..8]); let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); - let nymd_db = Arc::new(RwLock::new(HashMap::new())); - nymd_db + let tx_db = Arc::new(RwLock::new(HashMap::new())); + tx_db .write() .unwrap() .insert(tx_hash.to_string(), tx_entry.clone()); - let nymd_client = DummyClient::new(&nymd_db); + let nymd_client = DummyClient::new( + AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), + &tx_db, + &Arc::new(RwLock::new(HashMap::new())), + &Arc::new(RwLock::new(HashMap::new())), + ); + let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client, + TEST_COIN_DENOM.to_string(), key_pair, - vec![], + comm_channel, storage.clone(), )); let client = Client::tracked(rocket) @@ -260,14 +339,25 @@ async fn signed_before() { #[tokio::test] async fn state_functions() { - let nymd_db = Arc::new(RwLock::new(HashMap::new())); - let nymd_client = DummyClient::new(&nymd_db); + let nymd_client = DummyClient::new( + AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), + &Arc::new(RwLock::new(HashMap::new())), + &Arc::new(RwLock::new(HashMap::new())), + &Arc::new(RwLock::new(HashMap::new())), + ); let params = Parameters::new(4).unwrap(); let key_pair = ttp_keygen(¶ms, 1, 1).unwrap().remove(0); let mut db_dir = std::env::temp_dir(); db_dir.push(&key_pair.verification_key().to_bs58()[..8]); let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); - let state = State::new(nymd_client, key_pair, vec![], storage.clone()); + let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let state = State::new( + nymd_client, + TEST_COIN_DENOM.to_string(), + key_pair, + comm_channel, + storage.clone(), + ); let tx_hash = String::from("6B27412050B823E58BB38447D7870BBC8CBE3C51C905BEA89D459ACCDA80A00E"); assert!(state.signed_before(&tx_hash).await.unwrap().is_none()); @@ -391,7 +481,7 @@ async fn blind_sign_correct() { let mut db_dir = std::env::temp_dir(); db_dir.push(&key_pair.verification_key().to_bs58()[..8]); let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); - let nymd_db = Arc::new(RwLock::new(HashMap::new())); + let tx_db = Arc::new(RwLock::new(HashMap::new())); let mut tx_entry = tx_entry_fixture(&tx_hash.to_string()); tx_entry.tx_result.events.push(Event { @@ -420,16 +510,23 @@ async fn blind_sign_correct() { .unwrap(), }, ]; - nymd_db + tx_db .write() .unwrap() .insert(tx_hash.to_string(), tx_entry.clone()); - let nymd_client = DummyClient::new(&nymd_db); + let nymd_client = DummyClient::new( + AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), + &tx_db, + &Arc::new(RwLock::new(HashMap::new())), + &Arc::new(RwLock::new(HashMap::new())), + ); + let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client, + TEST_COIN_DENOM.to_string(), key_pair, - vec![], + comm_channel, storage.clone(), )); let client = Client::tracked(rocket) @@ -496,13 +593,19 @@ async fn signature_test() { let mut db_dir = std::env::temp_dir(); db_dir.push(&key_pair.verification_key().to_bs58()[..8]); let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); - let nymd_db = Arc::new(RwLock::new(HashMap::new())); - let nymd_client = DummyClient::new(&nymd_db); + let nymd_client = DummyClient::new( + AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), + &Arc::new(RwLock::new(HashMap::new())), + &Arc::new(RwLock::new(HashMap::new())), + &Arc::new(RwLock::new(HashMap::new())), + ); + let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client, + TEST_COIN_DENOM.to_string(), key_pair, - vec![], + comm_channel, storage.clone(), )); let client = Client::tracked(rocket) @@ -552,3 +655,360 @@ async fn signature_test() { expected_response.to_bytes() ); } + +#[tokio::test] +async fn get_cosmos_address() { + let validator_address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(); + let nymd_client = DummyClient::new( + validator_address.clone(), + &Arc::new(RwLock::new(HashMap::new())), + &Arc::new(RwLock::new(HashMap::new())), + &Arc::new(RwLock::new(HashMap::new())), + ); + let mut db_dir = std::env::temp_dir(); + let key_pair = ttp_keygen(&Parameters::new(4).unwrap(), 1, 1) + .unwrap() + .remove(0); + db_dir.push(&key_pair.verification_key().to_bs58()[..8]); + let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); + let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let rocket = rocket::build().attach(InternalSignRequest::stage( + nymd_client, + TEST_COIN_DENOM.to_string(), + key_pair, + comm_channel, + storage.clone(), + )); + let client = Client::tracked(rocket) + .await + .expect("valid rocket instance"); + + let response = client + .get(format!( + "/{}/{}/{}/{}", + API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_COSMOS_ADDRESS + )) + .dispatch() + .await; + assert_eq!(response.status(), Status::Ok); + let cosmos_addr_response = + serde_json::from_str::(&response.into_string().await.unwrap()) + .unwrap(); + assert_eq!(validator_address, cosmos_addr_response.addr); +} + +#[tokio::test] +async fn verification_of_bandwidth_credential() { + // Setup variables + let validator_address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(); + let proposal_db = Arc::new(RwLock::new(HashMap::new())); + let spent_credential_db = Arc::new(RwLock::new(HashMap::new())); + let nymd_client = DummyClient::new( + validator_address.clone(), + &Arc::new(RwLock::new(HashMap::new())), + &proposal_db, + &spent_credential_db, + ); + let mut db_dir = std::env::temp_dir(); + let params = Parameters::new(4).unwrap(); + let mut key_pairs = ttp_keygen(¶ms, 1, 1).unwrap(); + let voucher_value = 1234u64; + let voucher_info = "voucher info"; + let public_attributes = vec![ + hash_to_scalar(voucher_value.to_string()), + hash_to_scalar(voucher_info), + ]; + let theta = theta_from_keys_and_attributes(¶ms, &key_pairs, &public_attributes).unwrap(); + let key_pair = key_pairs.remove(0); + db_dir.push(&key_pair.verification_key().to_bs58()[..8]); + let storage1 = ValidatorApiStorage::init(db_dir).await.unwrap(); + let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let rocket = rocket::build().attach(InternalSignRequest::stage( + nymd_client.clone(), + TEST_COIN_DENOM.to_string(), + key_pair, + comm_channel.clone(), + storage1.clone(), + )); + + let client = Client::tracked(rocket) + .await + .expect("valid rocket instance"); + + let credential = Credential::new(4, theta.clone(), voucher_value, voucher_info.to_string()); + let proposal_id = 42; + // The address is not used, so we can use a duplicate + let gateway_cosmos_addr = validator_address.clone(); + let req = + VerifyCredentialBody::new(credential.clone(), proposal_id, gateway_cosmos_addr.clone()); + + // Test endpoint with not proposal for the proposal id + let response = client + .post(format!( + "/{}/{}/{}/{}", + API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL + )) + .json(&req) + .dispatch() + .await; + assert_eq!(response.status(), Status::BadRequest); + assert_eq!( + response.into_string().await.unwrap(), + CoconutError::IncorrectProposal { + reason: "proposal not found".to_string() + } + .to_string() + ); + + let mut proposal = ProposalResponse { + id: proposal_id, + title: String::new(), + description: String::from("25mnnoCcUfeizfC85avvroFg2prpEZBgJbJM2SLtkgyyUkoAU3cqJiqWmg8cMHEPjfFf5sQF92SMAM2vbEoLZvUjenvXhadTLdA4TqMYArJpihyqirW2AhGoNehtcdcK5gnH"), + msgs: vec![], + status: cw3::Status::Open, + expires: cw_utils::Expiration::Never {}, + threshold: cw_utils::ThresholdResponse::AbsolutePercentage { + percentage: Decimal::from_ratio(2u32, 3u32), + total_weight: 100, + }, + }; + + // Test the endpoint with a different blinded serial number in the description + proposal_db + .write() + .unwrap() + .insert(proposal_id, proposal.clone()); + let response = client + .post(format!( + "/{}/{}/{}/{}", + API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL + )) + .json(&req) + .dispatch() + .await; + assert_eq!(response.status(), Status::BadRequest); + assert_eq!( + response.into_string().await.unwrap(), + CoconutError::IncorrectProposal { + reason: "incorrect blinded serial number in description".to_string() + } + .to_string() + ); + + // Test the endpoint with no msg in the proposal action + proposal.description = credential.blinded_serial_number(); + proposal_db + .write() + .unwrap() + .insert(proposal_id, proposal.clone()); + let response = client + .post(format!( + "/{}/{}/{}/{}", + API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL + )) + .json(&req) + .dispatch() + .await; + assert_eq!(response.status(), Status::BadRequest); + assert_eq!( + response.into_string().await.unwrap(), + CoconutError::IncorrectProposal { + reason: "action is not to release funds".to_string() + } + .to_string() + ); + + // Test the endpoint without any credential recorded in the Coconut Bandwidth Contract + let funds = Coin::new(voucher_value as u128, TEST_COIN_DENOM); + let msg = coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds { + funds: funds.clone().into(), + }; + let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: String::new(), + msg: to_binary(&msg).unwrap(), + funds: vec![], + }); + proposal.msgs = vec![cosmos_msg]; + proposal_db + .write() + .unwrap() + .insert(proposal_id, proposal.clone()); + let response = client + .post(format!( + "/{}/{}/{}/{}", + API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL + )) + .json(&req) + .dispatch() + .await; + assert_eq!(response.status(), Status::BadRequest); + assert_eq!( + response.into_string().await.unwrap(), + CoconutError::InvalidCredentialStatus { + status: "spent credential not found".to_string() + } + .to_string() + ); + + spent_credential_db.write().unwrap().insert( + credential.blinded_serial_number(), + SpendCredentialResponse::new(None), + ); + let response = client + .post(format!( + "/{}/{}/{}/{}", + API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL + )) + .json(&req) + .dispatch() + .await; + assert_eq!(response.status(), Status::BadRequest); + assert_eq!( + response.into_string().await.unwrap(), + CoconutError::InvalidCredentialStatus { + status: "Inexistent".to_string() + } + .to_string() + ); + + // Test the endpoint with a credential that doesn't verify correctly + let mut spent_credential = SpendCredential::new( + funds.clone().into(), + credential.blinded_serial_number(), + Addr::unchecked("unimportant"), + ); + spent_credential_db.write().unwrap().insert( + credential.blinded_serial_number(), + SpendCredentialResponse::new(Some(spent_credential.clone())), + ); + let bad_credential = Credential::new( + 4, + theta.clone(), + voucher_value, + String::from("bad voucher info"), + ); + let bad_req = + VerifyCredentialBody::new(bad_credential, proposal_id, gateway_cosmos_addr.clone()); + let response = client + .post(format!( + "/{}/{}/{}/{}", + API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL + )) + .json(&bad_req) + .dispatch() + .await; + assert_eq!(response.status(), Status::Ok); + let verify_credential_response = + serde_json::from_str::(&response.into_string().await.unwrap()) + .unwrap(); + assert!(!verify_credential_response.verification_result); + assert_eq!( + cw3::Status::Rejected, + proposal_db + .read() + .unwrap() + .get(&proposal_id) + .unwrap() + .status + ); + + // Test the endpoint with a proposal that has a different value for the funds to be released + // then what's in the credential + let funds = Coin::new((voucher_value + 10) as u128, TEST_COIN_DENOM); + let msg = coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds { + funds: funds.clone().into(), + }; + let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: String::new(), + msg: to_binary(&msg).unwrap(), + funds: vec![], + }); + proposal.msgs = vec![cosmos_msg]; + proposal_db + .write() + .unwrap() + .insert(proposal_id, proposal.clone()); + let response = client + .post(format!( + "/{}/{}/{}/{}", + API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL + )) + .json(&req) + .dispatch() + .await; + assert_eq!(response.status(), Status::Ok); + let verify_credential_response = + serde_json::from_str::(&response.into_string().await.unwrap()) + .unwrap(); + assert!(!verify_credential_response.verification_result); + assert_eq!( + cw3::Status::Rejected, + proposal_db + .read() + .unwrap() + .get(&proposal_id) + .unwrap() + .status + ); + + // Test the endpoint with every dependency met + let funds = Coin::new(voucher_value as u128, TEST_COIN_DENOM); + let msg = coconut_bandwidth_contract_common::msg::ExecuteMsg::ReleaseFunds { + funds: funds.clone().into(), + }; + let cosmos_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: String::new(), + msg: to_binary(&msg).unwrap(), + funds: vec![], + }); + proposal.msgs = vec![cosmos_msg]; + proposal_db + .write() + .unwrap() + .insert(proposal_id, proposal.clone()); + let response = client + .post(format!( + "/{}/{}/{}/{}", + API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL + )) + .json(&req) + .dispatch() + .await; + assert_eq!(response.status(), Status::Ok); + let verify_credential_response = + serde_json::from_str::(&response.into_string().await.unwrap()) + .unwrap(); + assert!(verify_credential_response.verification_result); + assert_eq!( + cw3::Status::Passed, + proposal_db + .read() + .unwrap() + .get(&proposal_id) + .unwrap() + .status + ); + + // Test the endpoint with the credential marked as Spent in the Coconut Bandwidth Contract + spent_credential.mark_as_spent(); + spent_credential_db.write().unwrap().insert( + credential.blinded_serial_number(), + SpendCredentialResponse::new(Some(spent_credential)), + ); + let response = client + .post(format!( + "/{}/{}/{}/{}", + API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL + )) + .json(&req) + .dispatch() + .await; + assert_eq!(response.status(), Status::BadRequest); + assert_eq!( + response.into_string().await.unwrap(), + CoconutError::InvalidCredentialStatus { + status: "Spent".to_string() + } + .to_string() + ); +} diff --git a/validator-api/src/config/mod.rs b/validator-api/src/config/mod.rs index cc34dbd92e..5eb67d3c4b 100644 --- a/validator-api/src/config/mod.rs +++ b/validator-api/src/config/mod.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::template::config_template; -use config::defaults::DEFAULT_NETWORK; use config::NymConfig; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -108,9 +107,7 @@ impl Default for Base { local_validator: DEFAULT_LOCAL_VALIDATOR .parse() .expect("default local validator is malformed!"), - mixnet_contract_address: DEFAULT_NETWORK - .mixnet_contract_address() - .expect("mixnet contract address is unavailable"), + mixnet_contract_address: String::default(), mnemonic: "exact antique hybrid width raise anchor puzzle degree fee quit long crack net vague hip despair write put useless civil mechanic broom music day".to_string(), } } @@ -279,7 +276,7 @@ impl Default for Rewarding { } } -#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +#[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] pub struct CoconutSigner { /// Specifies whether rewarding service is enabled in this process. @@ -294,16 +291,6 @@ pub struct CoconutSigner { all_validator_apis: Vec, } -impl Default for CoconutSigner { - fn default() -> Self { - CoconutSigner { - enabled: false, - keypair_path: PathBuf::default(), - all_validator_apis: config::defaults::default_api_endpoints(), - } - } -} - impl Config { pub fn new() -> Self { Config::default() diff --git a/validator-api/src/contract_cache/mod.rs b/validator-api/src/contract_cache/mod.rs index d46b6f0e15..593fe63b88 100644 --- a/validator-api/src/contract_cache/mod.rs +++ b/validator-api/src/contract_cache/mod.rs @@ -14,6 +14,7 @@ use okapi::openapi3::OpenApi; use rocket::Route; use rocket_okapi::openapi_get_routes_spec; use rocket_okapi::settings::OpenApiSettings; +use task::ShutdownListener; use rocket::fairing::AdHoc; use serde::Serialize; @@ -252,20 +253,26 @@ impl ValidatorCacheRefresher { Ok(()) } - pub(crate) async fn run(&self) + pub(crate) async fn run(&self, mut shutdown: ShutdownListener) where C: CosmWasmClient + Sync, { let mut interval = time::interval(self.caching_interval); - loop { - interval.tick().await; - if let Err(err) = self.refresh_cache().await { - error!("Failed to refresh validator cache - {}", err); - } else { - // relaxed memory ordering is fine here. worst case scenario network monitor - // will just have to wait for an additional backoff to see the change. - // And so this will not really incur any performance penalties by setting it every loop iteration - self.cache.initialised.store(true, Ordering::Relaxed) + while !shutdown.is_shutdown() { + tokio::select! { + _ = interval.tick() => { + if let Err(err) = self.refresh_cache().await { + error!("Failed to refresh validator cache - {}", err); + } else { + // relaxed memory ordering is fine here. worst case scenario network monitor + // will just have to wait for an additional backoff to see the change. + // And so this will not really incur any performance penalties by setting it every loop iteration + self.cache.initialised.store(true, Ordering::Relaxed) + } + } + _ = shutdown.recv() => { + trace!("UpdateHandler: Received shutdown"); + } } } } diff --git a/validator-api/src/main.rs b/validator-api/src/main.rs index 51c4bccc6e..6fe0ca07ae 100644 --- a/validator-api/src/main.rs +++ b/validator-api/src/main.rs @@ -10,7 +10,10 @@ use crate::network_monitor::NetworkMonitorBuilder; use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; use crate::nymd_client::Client; use crate::storage::ValidatorApiStorage; -use ::config::defaults::DEFAULT_NETWORK; +use ::config::defaults::setup_env; +#[cfg(feature = "coconut")] +use ::config::defaults::var_names::API_VALIDATOR; +use ::config::defaults::var_names::{CONFIGURED, MIXNET_CONTRACT_ADDRESS, MIX_DENOM}; use ::config::NymConfig; use anyhow::Result; use clap::{crate_version, App, Arg, ArgMatches}; @@ -23,16 +26,19 @@ use rocket::{Ignite, Rocket}; use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors}; use rocket_okapi::mount_endpoints_and_merged_docs; use rocket_okapi::swagger_ui::make_swagger_ui; +use std::path::PathBuf; +use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use std::{fs, process}; +use task::ShutdownNotifier; use tokio::sync::Notify; // use validator_client::nymd::SigningNymdClient; // use validator_client::ValidatorClientError; use crate::rewarded_set_updater::RewardedSetUpdater; #[cfg(feature = "coconut")] -use coconut::InternalSignRequest; +use coconut::{comm::QueryCommunicationChannel, InternalSignRequest}; #[cfg(feature = "coconut")] use coconut_interface::{Base58, KeyPair}; use validator_client::nymd::SigningNymdClient; @@ -50,6 +56,7 @@ mod swagger; mod coconut; const ID: &str = "id"; +const CONFIG_ENV_FILE: &str = "config-env-file"; const MONITORING_ENABLED: &str = "enable-monitor"; const REWARDING_ENABLED: &str = "enable-rewarding"; const MIXNET_CONTRACT_ARG: &str = "mixnet-contract"; @@ -75,18 +82,6 @@ const REWARDING_MONITOR_THRESHOLD_ARG: &str = "monitor-threshold"; const MIN_MIXNODE_RELIABILITY_ARG: &str = "min_mixnode_reliability"; const MIN_GATEWAY_RELIABILITY_ARG: &str = "min_gateway_reliability"; -#[cfg(feature = "coconut")] -fn parse_validators(raw: &str) -> Vec { - raw.split(',') - .map(|raw_validator| { - raw_validator - .trim() - .parse() - .expect("one of the provided validator api urls is invalid") - }) - .collect() -} - fn long_version() -> String { format!( r#" @@ -98,7 +93,6 @@ fn long_version() -> String { {:<20}{} {:<20}{} {:<20}{} -{:<20}{} "#, "Build Timestamp:", env!("VERGEN_BUILD_TIMESTAMP"), @@ -115,9 +109,7 @@ fn long_version() -> String { "rustc Channel:", env!("VERGEN_RUSTC_CHANNEL"), "cargo Profile:", - env!("VERGEN_CARGO_PROFILE"), - "Network:", - DEFAULT_NETWORK + env!("VERGEN_CARGO_PROFILE") ) } @@ -127,6 +119,12 @@ fn parse_args<'a>() -> ArgMatches<'a> { .version(crate_version!()) .long_version(&*build_details) .author("Nymtech") + .arg( + Arg::with_name(CONFIG_ENV_FILE) + .help("Path pointing to an env file that configures the validator API") + .long(CONFIG_ENV_FILE) + .takes_value(true) + ) .arg( Arg::with_name(ID) .help("Id of the validator-api we want to run") @@ -217,14 +215,44 @@ fn parse_args<'a>() -> ArgMatches<'a> { base_app.get_matches() } -async fn wait_for_interrupt() { - if let Err(e) = tokio::signal::ctrl_c().await { - error!( - "There was an error while capturing SIGINT - {:?}. We will terminate regardless", - e - ); +async fn wait_for_interrupt(mut shutdown: ShutdownNotifier) { + wait_for_signal().await; + + log::info!("Sending shutdown"); + 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 validator API"); +} + +#[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"); + }, } - println!("Received SIGINT - the network monitor will terminate now"); } fn setup_logging() { @@ -272,7 +300,10 @@ fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> Config { #[cfg(feature = "coconut")] if let Some(raw_validators) = matches.value_of(API_VALIDATORS_ARG) { - config = config.with_custom_validator_apis(parse_validators(raw_validators)); + config = config.with_custom_validator_apis(::config::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(::config::parse_validators(&raw_validators)) } if let Some(raw_validator) = matches.value_of(NYMD_VALIDATOR_ARG) { @@ -288,6 +319,10 @@ fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> Config { if let Some(mixnet_contract) = matches.value_of(MIXNET_CONTRACT_ARG) { config = config.with_custom_mixnet_contract(mixnet_contract) + } else if std::env::var(CONFIGURED).is_ok() { + let mixnet_contract = + std::env::var(MIXNET_CONTRACT_ADDRESS).expect("mixnet contract not set"); + config = config.with_custom_mixnet_contract(mixnet_contract) } if let Some(mnemonic) = matches.value_of(MNEMONIC_ARG) { @@ -413,6 +448,7 @@ fn expected_monitor_test_runs(config: &Config, interval_length: Duration) -> usi async fn setup_rocket( config: &Config, + _mix_denom: String, liftoff_notify: Arc, _nymd_client: Client, ) -> Result> { @@ -452,8 +488,9 @@ async fn setup_rocket( let keypair = KeyPair::try_from_bs58(keypair_bs58)?; rocket.attach(InternalSignRequest::stage( _nymd_client, + _mix_denom, keypair, - config.get_all_validator_api_endpoints(), + QueryCommunicationChannel::new(config.get_all_validator_api_endpoints()), storage.clone().unwrap(), )) } else { @@ -524,14 +561,17 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { if matches.is_present(WRITE_CONFIG_ARG) { return Ok(()); } + let mix_denom = std::env::var(MIX_DENOM).expect("mix denom not set"); let signing_nymd_client = Client::new_signing(&config); let liftoff_notify = Arc::new(Notify::new()); + let shutdown = ShutdownNotifier::default(); // let's build our rocket! let rocket = setup_rocket( &config, + mix_denom, Arc::clone(&liftoff_notify), signing_nymd_client.clone(), ) @@ -549,7 +589,8 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { // setup our daily uptime updater. Note that if network monitor is disabled, then we have // no data for the updates and hence we don't need to start it up let uptime_updater = HistoricalUptimeUpdater::new(storage.clone()); - tokio::spawn(async move { uptime_updater.run().await }); + let shutdown_listener = shutdown.subscribe(); + tokio::spawn(async move { uptime_updater.run(shutdown_listener).await }); // spawn the cache refresher let validator_cache_refresher = ValidatorCacheRefresher::new( @@ -558,7 +599,8 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { validator_cache.clone(), Some(storage.clone()), ); - tokio::spawn(async move { validator_cache_refresher.run().await }); + let shutdown_listener = shutdown.subscribe(); + tokio::spawn(async move { validator_cache_refresher.run(shutdown_listener).await }); // spawn rewarded set updater let mut rewarded_set_updater = @@ -573,11 +615,15 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { None, ); + let shutdown_listener = shutdown.subscribe(); // spawn our cacher - tokio::spawn(async move { validator_cache_refresher.run().await }); + tokio::spawn(async move { validator_cache_refresher.run(shutdown_listener).await }); } // launch the rocket! + // Rocket handles shutdown on it's own, but its shutdown handling should be incorporated + // with that of the rest of the tasks. + // Currently it's runtime is forcefully terminated once the validator-api exits. let shutdown_handle = rocket.shutdown(); tokio::spawn(rocket.launch()); @@ -590,12 +636,12 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { // we're ready to go! spawn the network monitor! let runnables = monitor_builder.build().await; - runnables.spawn_tasks(); + runnables.spawn_tasks(&shutdown); } else { info!("Network monitoring is disabled."); } - wait_for_interrupt().await; + wait_for_interrupt(shutdown).await; shutdown_handle.notify(); Ok(()) @@ -605,8 +651,6 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { async fn main() -> Result<()> { println!("Starting validator api..."); - let _ = dotenv::dotenv(); - cfg_if::cfg_if! {if #[cfg(feature = "console-subscriber")] { // instriment tokio console subscriber needs RUSTFLAGS="--cfg tokio_unstable" at build time console_subscriber::init(); @@ -614,5 +658,9 @@ async fn main() -> Result<()> { setup_logging(); let args = parse_args(); + let config_env_file = args + .value_of(CONFIG_ENV_FILE) + .map(|s| PathBuf::from_str(s).expect("invalid env config file")); + setup_env(config_env_file); run_validator_api(args).await } diff --git a/validator-api/src/network_monitor/mod.rs b/validator-api/src/network_monitor/mod.rs index 0a15b87053..49d90deccb 100644 --- a/validator-api/src/network_monitor/mod.rs +++ b/validator-api/src/network_monitor/mod.rs @@ -6,6 +6,7 @@ use crypto::asymmetric::{encryption, identity}; use futures::channel::mpsc; use gateway_client::bandwidth::BandwidthController; use std::sync::Arc; +use task::ShutdownNotifier; use crate::config::Config; use crate::contract_cache::ValidatorCache; @@ -131,11 +132,13 @@ impl NetworkMonitorRunnables { // TODO: note, that is not exactly doing what we want, because when // `ReceivedProcessor` is constructed, it already spawns a future // this needs to be refactored! - pub(crate) fn spawn_tasks(self) { + pub(crate) fn spawn_tasks(self, shutdown: &ShutdownNotifier) { let mut packet_receiver = self.packet_receiver; let mut monitor = self.monitor; - tokio::spawn(async move { packet_receiver.run().await }); - tokio::spawn(async move { monitor.run().await }); + let shutdown_listener = shutdown.subscribe(); + tokio::spawn(async move { packet_receiver.run(shutdown_listener).await }); + let shutdown_listener = shutdown.subscribe(); + tokio::spawn(async move { monitor.run(shutdown_listener).await }); } } diff --git a/validator-api/src/network_monitor/monitor/mod.rs b/validator-api/src/network_monitor/monitor/mod.rs index f6fd3fe881..2cd1950f96 100644 --- a/validator-api/src/network_monitor/monitor/mod.rs +++ b/validator-api/src/network_monitor/monitor/mod.rs @@ -12,6 +12,7 @@ use crate::storage::ValidatorApiStorage; use log::{debug, error, info}; use std::collections::{HashMap, HashSet}; use std::process; +use task::ShutdownListener; use tokio::time::{sleep, Duration, Instant}; pub(crate) mod gateway_clients_cache; @@ -296,7 +297,7 @@ impl Monitor { self.test_nonce += 1; } - pub(crate) async fn run(&mut self) { + pub(crate) async fn run(&mut self, mut shutdown: ShutdownListener) { self.received_processor.start_receiving(); // wait for validator cache to be ready @@ -308,9 +309,13 @@ impl Monitor { .spawn_gateways_pinger(self.gateway_ping_interval); let mut run_interval = tokio::time::interval(self.run_interval); - loop { - run_interval.tick().await; - self.test_run().await; + while !shutdown.is_shutdown() { + tokio::select! { + _ = run_interval.tick() => self.test_run().await, + _ = shutdown.recv() => { + trace!("UpdateHandler: Received shutdown"); + } + } } } } diff --git a/validator-api/src/network_monitor/monitor/receiver.rs b/validator-api/src/network_monitor/monitor/receiver.rs index f837cb9f80..36913241db 100644 --- a/validator-api/src/network_monitor/monitor/receiver.rs +++ b/validator-api/src/network_monitor/monitor/receiver.rs @@ -7,6 +7,7 @@ use crypto::asymmetric::identity; use futures::channel::mpsc; use futures::StreamExt; use gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver}; +use task::ShutdownListener; pub(crate) type GatewayClientUpdateSender = mpsc::UnboundedSender; pub(crate) type GatewayClientUpdateReceiver = mpsc::UnboundedReceiver; @@ -55,8 +56,8 @@ impl PacketReceiver { .expect("packet processor seems to have crashed!"); } - pub(crate) async fn run(&mut self) { - loop { + pub(crate) async fn run(&mut self, mut shutdown: ShutdownListener) { + while !shutdown.is_shutdown() { tokio::select! { // unwrap here is fine as it can only return a `None` if the PacketSender has died // and if that was the case, then the entire monitor is already in an undefined state @@ -67,6 +68,9 @@ impl PacketReceiver { Some((_gateway_id, message)) = self.gateways_reader.stream_map().next() => { self.process_gateway_messages(message) } + _ = shutdown.recv() => { + trace!("UpdateHandler: Received shutdown"); + } } } } diff --git a/validator-api/src/node_status_api/uptime_updater.rs b/validator-api/src/node_status_api/uptime_updater.rs index 417a9a30b2..56e7655aae 100644 --- a/validator-api/src/node_status_api/uptime_updater.rs +++ b/validator-api/src/node_status_api/uptime_updater.rs @@ -7,6 +7,7 @@ use crate::node_status_api::models::{ use crate::node_status_api::ONE_DAY; use crate::storage::ValidatorApiStorage; use log::error; +use task::ShutdownListener; use time::OffsetDateTime; use tokio::time::sleep; @@ -67,18 +68,23 @@ impl HistoricalUptimeUpdater { Ok(()) } - pub(crate) async fn run(&self) { - loop { - // start any updates a day after starting the task so that we would have complete data - sleep(ONE_DAY).await; - if let Err(err) = self.update_uptimes().await { - // normally that would have been a warning rather than an error, - // however, in this case it implies some underlying issues with our database - // that might affect the entire program - error!( - "We failed to update daily uptimes of active nodes - {}", - err - ) + pub(crate) async fn run(&self, mut shutdown: ShutdownListener) { + while !shutdown.is_shutdown() { + tokio::select! { + _ = sleep(ONE_DAY) => { + if let Err(err) = self.update_uptimes().await { + // normally that would have been a warning rather than an error, + // however, in this case it implies some underlying issues with our database + // that might affect the entire program + error!( + "We failed to update daily uptimes of active nodes - {}", + err + ) + } + } + _ = shutdown.recv() => { + trace!("UpdateHandler: Received shutdown"); + } } } } diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 6977e73c1b..eedaebe547 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -3,32 +3,31 @@ #[cfg(feature = "coconut")] use async_trait::async_trait; -use serde::Serialize; #[cfg(feature = "coconut")] -use std::str::FromStr; +use coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; +use serde::Serialize; use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; use tokio::time::sleep; -use config::defaults::{DEFAULT_NETWORK, DEFAULT_VALIDATOR_API_PORT}; +use config::defaults::{NymNetworkDetails, DEFAULT_VALIDATOR_API_PORT}; use mixnet_contract_common::{ reward_params::EpochRewardParams, ContractStateParams, Delegation, ExecuteMsg, GatewayBond, IdentityKey, Interval, MixNodeBond, MixnodeRewardingStatusResponse, RewardedSetNodeStatus, }; #[cfg(feature = "coconut")] use multisig_contract_common::msg::ProposalResponse; -#[cfg(feature = "coconut")] -use validator_client::nymd::{ - cosmwasm_client::logs::find_attribute, - traits::{MultisigSigningClient, QueryClient}, - AccountId, -}; use validator_client::nymd::{ hash::{Hash, SHA256_HASH_SIZE}, Coin, CosmWasmClient, Fee, QueryNymdClient, SigningCosmWasmClient, SigningNymdClient, TendermintTime, }; +#[cfg(feature = "coconut")] +use validator_client::nymd::{ + traits::{CoconutBandwidthQueryClient, MultisigQueryClient, MultisigSigningClient}, + AccountId, +}; use validator_client::ValidatorClientError; #[cfg(feature = "coconut")] @@ -52,17 +51,16 @@ impl Client { .parse() .unwrap(); let nymd_url = config.get_nymd_validator_url(); - let network = DEFAULT_NETWORK; - let details = network - .details() + + let details = NymNetworkDetails::new_from_env() .with_mixnet_contract(Some(config.get_mixnet_contract_address())); let client_config = validator_client::Config::try_from_nym_network_details(&details) .expect("failed to construct valid validator client config with the provided network") .with_urls(nymd_url, api_url); - let inner = validator_client::Client::new_query(client_config, network) - .expect("Failed to connect to nymd!"); + let inner = + validator_client::Client::new_query(client_config).expect("Failed to connect to nymd!"); Client(Arc::new(RwLock::new(inner))) } @@ -77,9 +75,7 @@ impl Client { .unwrap(); let nymd_url = config.get_nymd_validator_url(); - let network = DEFAULT_NETWORK; - let details = network - .details() + let details = NymNetworkDetails::new_from_env() .with_mixnet_contract(Some(config.get_mixnet_contract_address())); let client_config = validator_client::Config::try_from_nym_network_details(&details) @@ -91,7 +87,7 @@ impl Client { .parse() .expect("the mnemonic is invalid!"); - let inner = validator_client::Client::new_signing(client_config, network, mnemonic) + let inner = validator_client::Client::new_signing(client_config, mnemonic) .expect("Failed to connect to nymd!"); Client(Arc::new(RwLock::new(inner))) @@ -368,15 +364,14 @@ impl Client { let memo = "Performing epoch operations".to_string(); - self.execute_multiple_with_retry(msgs, Default::default(), memo) - .await?; + self.execute_multiple_with_retry(msgs, None, memo).await?; Ok(()) } async fn execute_multiple_with_retry( &self, msgs: Vec<(M, Vec)>, - fee: Fee, + fee: Option, memo: String, ) -> Result<(), RewardingError> where @@ -455,32 +450,17 @@ where Ok(self.0.read().await.nymd.get_proposal(proposal_id).await?) } - async fn propose_release_funds( + async fn get_spent_credential( &self, - title: String, blinded_serial_number: String, - voucher_value: u128, - fee: Option, - ) -> Result { - let res = self + ) -> crate::coconut::error::Result { + Ok(self .0 .read() .await .nymd - .propose_release_funds(title, blinded_serial_number, voucher_value, fee) - .await?; - let proposal_id = u64::from_str( - &find_attribute(&res.logs, "wasm", "proposal_id") - .ok_or_else(|| { - CoconutError::InternalError("No attribute with proposal_id as key".to_string()) - })? - .value, - ) - .map_err(|_| { - CoconutError::InternalError("proposal_id could not be parsed to u64".to_string()) - })?; - - Ok(proposal_id) + .get_spent_credential(blinded_serial_number) + .await?) } async fn vote_proposal( diff --git a/validator-api/src/rewarded_set_updater/mod.rs b/validator-api/src/rewarded_set_updater/mod.rs index d6006d2617..627452d673 100644 --- a/validator-api/src/rewarded_set_updater/mod.rs +++ b/validator-api/src/rewarded_set_updater/mod.rs @@ -22,6 +22,7 @@ use mixnet_contract_common::{IdentityKey, Interval, MixNodeBond}; use rand::prelude::SliceRandom; use rand::rngs::OsRng; use std::collections::HashSet; +use std::ops::Deref; use std::time::Duration; use time::OffsetDateTime; use tokio::time::sleep; @@ -58,7 +59,44 @@ impl MixnodeToReward { } // Epoch has all the same semantics as interval, but has a lower set duration -type Epoch = Interval; +pub struct Epoch(Interval); + +impl Epoch { + pub(crate) async fn update_to_latest( + &mut self, + rewarded_set_updater: &RewardedSetUpdater, + ) -> Result<(), RewardingError> { + let mut wait_time = 2; + let mut ret = Ok(()); + for _ in 0..3 { + match rewarded_set_updater.nymd_client.get_current_epoch().await { + Ok(epoch) => { + *self = Epoch(epoch); + return Ok(()); + } + Err(e) => { + warn!( + "Could not update epoch: {:?}. Retrying again in {} seconds", + e, wait_time + ); + ret = Err(e); + } + }; + sleep(Duration::from_secs(wait_time)).await; + wait_time *= wait_time; + } + error!("Failed to update epoch. Exiting now"); + Ok(ret?) + } +} + +impl Deref for Epoch { + type Target = Interval; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} pub struct RewardedSetUpdater { nymd_client: Client, @@ -68,7 +106,7 @@ pub struct RewardedSetUpdater { impl RewardedSetUpdater { pub(crate) async fn epoch(&self) -> Result { - Ok(self.nymd_client.get_current_epoch().await?) + Ok(Epoch(self.nymd_client.get_current_epoch().await?)) } pub(crate) async fn new( @@ -120,9 +158,10 @@ impl RewardedSetUpdater { async fn reward_current_rewarded_set( &self, + epoch: &mut Epoch, ) -> Result)>, RewardingError> { - let to_reward = self.nodes_to_reward().await?; - let epoch = self.epoch().await?; + let to_reward = self.nodes_to_reward(epoch).await?; + epoch.update_to_latest(self).await?; // self.storage.insert_started_epoch_rewarding(epoch).await?; @@ -157,8 +196,11 @@ impl RewardedSetUpdater { } } - async fn nodes_to_reward(&self) -> Result, RewardingError> { - let epoch = self.epoch().await?; + async fn nodes_to_reward( + &self, + epoch: &mut Epoch, + ) -> Result, RewardingError> { + epoch.update_to_latest(self).await?; let active_set = self .validator_cache .active_set_detailed() @@ -200,8 +242,8 @@ impl RewardedSetUpdater { } // This is where the epoch gets advanced, and all epoch related transactions originate - async fn update(&self) -> Result<(), RewardingError> { - let epoch = self.epoch().await?; + async fn update(&self, epoch: &mut Epoch) -> Result<(), RewardingError> { + epoch.update_to_latest(self).await?; log::info!("Starting rewarded set update"); // we know the entries are not stale, as a matter of fact they were JUST updated, since we got notified let all_nodes = self.validator_cache.mixnodes().await; @@ -218,7 +260,7 @@ impl RewardedSetUpdater { // log::info!("Rewarded current rewarded set... SUCCESS"); // } - let reward_msgs = self.reward_current_rewarded_set().await?; + let reward_msgs = self.reward_current_rewarded_set(epoch).await?; let rewarded_set_size = epoch_reward_params.rewarded_set_size() as u32; let active_set_size = epoch_reward_params.active_set_size() as u32; @@ -288,11 +330,12 @@ impl RewardedSetUpdater { pub(crate) async fn run(&mut self) -> Result<(), RewardingError> { self.validator_cache.wait_for_initial_values().await; + let mut epoch = self.epoch().await?; loop { // wait until the cache refresher determined its time to update the rewarded/active sets let time = OffsetDateTime::now_utc().unix_timestamp(); - let epoch = self.epoch().await?; + epoch.update_to_latest(self).await?; let time_to_epoch_change = epoch.end_unix_timestamp() - time; if time_to_epoch_change <= 0 { self.update_blacklist(&epoch).await?; @@ -300,7 +343,7 @@ impl RewardedSetUpdater { "Time to epoch change is {}, updating rewarded set", time_to_epoch_change ); - self.update().await?; + self.update(&mut epoch).await?; } else { log::info!( "Waiting for epoch change, time to epoch change is {}", diff --git a/validator-api/tests/README.md b/validator-api/tests/README.md new file mode 100644 index 0000000000..914e55bdcd --- /dev/null +++ b/validator-api/tests/README.md @@ -0,0 +1,30 @@ + +## validator-api-test suite + +A Typescript test framework utilising Jest and Node to perform tests against the NYM validator-apis. + + +## Getting Started + +### Installation +1. Having `yarn` install on your machine +1. Install all the packages - `yarn install` +2. Peeping the package.json will show you the commands to run for the tests + + + +## Usage + +1. Run the testsuite - currently the configuration hasn't fully been set up to switch envs + ``` + yarn test + ``` + +## ToDo +1. Finish happy and negative test scenarios +2. Full reporting +3. Fully functioniting env switching +4. Run in CI +5. Docker? + +Kudos to jmfiola21@gmail.com for the baseline of this framework diff --git a/validator-api/tests/functional_test/validator-api/status/status-mixnode.test.ts b/validator-api/tests/functional_test/validator-api/status/status-mixnode.test.ts new file mode 100644 index 0000000000..58542fafae --- /dev/null +++ b/validator-api/tests/functional_test/validator-api/status/status-mixnode.test.ts @@ -0,0 +1,123 @@ +import Status from "../../../src/endpoints/Status"; +import ConfigHandler from "../../../src/config/configHandler"; + +let status: Status; +let config: ConfigHandler; + +describe("Get mixnode data", (): void => { + beforeAll(async (): Promise => { + status = new Status(); + config = ConfigHandler.getInstance(); + }); + + it("Get a mixnode stake saturation", async (): Promise => { + const identity_key = config.environmnetConfig.mixnode_identity; + const response = await status.getMixnodeStakeSaturation(identity_key); + + console.log(response.as_at); + console.log(response.saturation); + + expect(typeof response.as_at).toBe("number"); + expect(typeof response.saturation).toBe("number"); + }); + + it("Get a mixnode average uptime", async (): Promise => { + const identity_key = config.environmnetConfig.mixnode_identity; + const response = await status.getMixnodeAverageUptime(identity_key); + + console.log(response.avg_uptime); + console.log(response.identity); + + expect(identity_key).toStrictEqual(response.identity); + expect(typeof response.avg_uptime).toBe("number"); + }); + + it("Get a mixnode history", async (): Promise => { + const identity_key = config.environmnetConfig.mixnode_identity; + const response = await status.getMixnodeHistory(identity_key); + + response.history.forEach((x) => { + console.log(x.date); + console.log(x.uptime); + }); + console.log(response.identity); + console.log(response.owner); + + expect(identity_key).toStrictEqual(response.identity); + expect(typeof response.owner).toBe("string"); + }); + + it("Get a gateway history", async (): Promise => { + const identity_key = config.environmnetConfig.gateway_identity; + const response = await status.getGatewayHistory(identity_key); + + response.history.forEach((x) => { + console.log(x.date); + console.log(x.uptime); + }); + console.log(response.identity); + console.log(response.owner); + + expect(identity_key).toStrictEqual(response.identity); + expect(typeof response.owner).toBe("string"); + }); + + it("Get a gateway history", async (): Promise => { + const identity_key = config.environmnetConfig.gateway_identity; + const response = await status.getGatewayCoreCount(identity_key); + + console.log(response.count); + console.log(response.identity); + + expect(identity_key).toStrictEqual(response.identity); + expect(typeof response.count).toBe("number"); + }); + + it("Get a gateway history", async (): Promise => { + const identity_key = config.environmnetConfig.mixnode_identity; + const response = await status.getMixnodeCoreCount(identity_key); + + console.log(response.count); + console.log(response.identity); + + expect(identity_key).toStrictEqual(response.identity); + expect(typeof response.count).toBe("number"); + }); + + it("Get a mixnode status", async (): Promise => { + const identity_key = config.environmnetConfig.mixnode_identity; + const response = await status.getMixnodeStatus(identity_key); + + console.log(response.status); + + expect(response.status).toStrictEqual("active"); + }); + + it("Get a mixnode reward estimation", async (): Promise => { + const identity_key = config.environmnetConfig.mixnode_identity; + const response = await status.getMixnodeRewardComputation(identity_key); + + console.log(response.estimated_delegators_reward); + console.log(response.estimated_node_profit); + console.log(response.estimated_operator_cost); + console.log(response.estimated_operator_reward); + console.log(response.estimated_total_node_reward); + console.log(response.reward_params); + console.log(response.as_at); + console.log(response); + + //assertions to come + //expect(response).toStrictEqual('something'); + }); + + it("Get a mixnode inclusion probability", async (): Promise => { + const identity_key = config.environmnetConfig.mixnode_identity; + const response = await status.getMixnodeInclusionProbability(identity_key); + + console.log(response.in_active); + console.log(response.in_reserve); + + //assertions to come + //expect(response).toStrictEqual('something'); + }); +}); diff --git a/validator-api/tests/jest.config.js b/validator-api/tests/jest.config.js new file mode 100644 index 0000000000..250724bda1 --- /dev/null +++ b/validator-api/tests/jest.config.js @@ -0,0 +1,6 @@ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + reporters: ["default", "jest-junit"], + collectCoverageFrom: ["src/**/*.ts"] +}; diff --git a/validator-api/tests/package.json b/validator-api/tests/package.json new file mode 100644 index 0000000000..deb5ee46b1 --- /dev/null +++ b/validator-api/tests/package.json @@ -0,0 +1,49 @@ +{ + "name": "validator-api-test-suite", + "version": "1.0.0", + "description": "a basic validator-api suite to test the validator-api", + "main": "dist/index.js", + "directories": { + "test": "test" + }, + "scripts": { + "test": "jest --forceExit --detectOpenHandles --passWithNoTests", + "build": "tsc", + "lint": "eslint --ext .js,.ts,.tsx .", + "lint:fix": "eslint --fix", + "cleanup": "rm -rf node_modules; rm -rf dist; yarn install" + }, + "author": "Nymtech", + "license": "MIT", + "files": [ + "dist" + ], + "engines": { + "node": "18.1.0", + "npm": "8.x" + }, + "dependencies": { + "axios": "^0.27.2", + "eslint": "^8.21.0", + "form-data": "4.0.0", + "json-stringify-safe": "5.0.1", + "tslog": "3.3.3", + "uuid": "8.3.2", + "yaml": "^2.1.1" + }, + "devDependencies": { + "@types/jest": "28.1.6", + "@typescript-eslint/eslint-plugin": "^5.12.1", + "@typescript-eslint/parser": "^5.33.0", + "axios-mock-adapter": "^1.20.0", + "eslint-config-prettier": "^8.4.0", + "eslint-plugin-prettier": "^4.0.0", + "jest": "^28.1.3", + "jest-junit": "^14.0.0", + "prettier": "2.7.1", + "process": "0.11.10", + "ts-jest": "28.0.7", + "typescript": "^4.7.4", + "uuidv4": "^6.2.12" + } +} diff --git a/validator-api/tests/src/config/config.yaml b/validator-api/tests/src/config/config.yaml new file mode 100644 index 0000000000..a2271032bf --- /dev/null +++ b/validator-api/tests/src/config/config.yaml @@ -0,0 +1,15 @@ +common: + request_headers: + Accept: application/json + Content-Type: application/json +qa: + api_base_url: https://qa-validator-api.nymtech.net/api/v1 + mixnode_identity: DLdMKLPywEy1vnu3yPrtXvzY7fw1puiiHpA9n9UQatiQ + gateway_identity: CgQrYP8etksSBf4nALNqp93SHPpgFwEUyTsjBNNLj5WM + log_level: debug +prod: + api_base_url: https://qa-validator-api.nymtech.net/api/v1 + mixnode_identity: DLdMKLPywEy1vnu3yPrtXvzY7fw1puiiHpA9n9UQatiQ + gateway_identity: CgQrYP8etksSBf4nALNqp93SHPpgFwEUyTsjBNNLj5WM + log_level: debug + time_zone: utc diff --git a/validator-api/tests/src/config/configHandler.ts b/validator-api/tests/src/config/configHandler.ts new file mode 100644 index 0000000000..be37be979d --- /dev/null +++ b/validator-api/tests/src/config/configHandler.ts @@ -0,0 +1,63 @@ +import { readFileSync } from "fs"; +import { TLogLevelName } from "tslog"; + +import YAML from "yaml"; + +class ConfigHandler { + private static instance: ConfigHandler; + + private validEnvironments = ["qa", "prod"]; + + public commonConfig: { request_headers: object }; + + public environment: string; + + public environmnetConfig: { + log_level: TLogLevelName; + time_zone: string; + api_base_url: string; + mixnode_identity: string; + gateway_identity: string; + }; + + private constructor() { + this.setCommonConfig(); + this.setEnvironmentConfig(process.env.TEST_ENV || "prod"); + } + + public static getInstance(): ConfigHandler { + if (!ConfigHandler.instance) { + ConfigHandler.instance = new ConfigHandler(); + } + return ConfigHandler.instance; + } + + private setCommonConfig(): void { + try { + this.commonConfig = YAML.parse( + readFileSync("src/config/config.yaml", "utf8") + ).common; + } catch (error) { + throw Error(`Error reading common config: (${error})`); + } + } + + private setEnvironmentConfig(environment: string): void { + this.ensureEnvironmentIsValid(environment); + try { + this.environmnetConfig = YAML.parse( + readFileSync("src/config/config.yaml", "utf8") + )[environment]; + } catch (error) { + throw Error(`Error reading environment config: (${error})`); + } + } + + private ensureEnvironmentIsValid(environment: string): void { + if (this.validEnvironments.indexOf(environment) === -1) { + throw Error(`Config environment is not valid: "${environment}"`); + } + } +} + +export default ConfigHandler; diff --git a/validator-api/tests/src/endpoints/Status.ts b/validator-api/tests/src/endpoints/Status.ts new file mode 100644 index 0000000000..8c0576f524 --- /dev/null +++ b/validator-api/tests/src/endpoints/Status.ts @@ -0,0 +1,177 @@ +import { AxiosResponse } from "axios"; +import { + ActiveStatus, + AvgUptime, + CoreCount, + EstimatedReward, + InclusionProbability, + NodeHistory, + Report, + StakeSaturation, +} from "../../src/interfaces/StatusInterfaces"; +import { APIClient } from "./abstracts/APIClient"; + +export default class Status extends APIClient { + constructor() { + super("/status"); + } + + public async getMixnodeStatusReport(identity_key: string): Promise { + const response = await this.restClient.sendGet({ + route: `/mixnode/${identity_key}/report`, + }); + + return { + identity: response.data.identity, + owner: response.data.owner, + most_recent: response.data.most_recent, + last_hour: response.data.last_hour, + last_day: response.data.last_day, + }; + } + + public async getGatewayStatusReport(identity_key: string): Promise { + const response = await this.restClient.sendGet({ + route: `/gateway/${identity_key}/report`, + }); + + return { + identity: response.data.identity, + owner: response.data.owner, + most_recent: response.data.most_recent, + last_hour: response.data.last_hour, + last_day: response.data.last_day, + }; + } + + public async getGatewayHistory(identity_key: string): Promise { + const response = await this.restClient.sendGet({ + route: `/gateway/${identity_key}/history`, + }); + + return { + identity: response.data.identity, + owner: response.data.owner, + history: response.data.history, + }; + } + + public async getMixnodeStakeSaturation( + identity_key: string + ): Promise { + const response = await this.restClient.sendGet({ + route: `/mixnode/${identity_key}/stake-saturation`, + }); + + return { + as_at: response.data.as_at, + saturation: response.data.saturation, + }; + } + + public async getMixnodeCoreCount(identity_key: string): Promise { + const response = await this.restClient.sendGet({ + route: `/mixnode/${identity_key}/core-status-count`, + }); + + return { + identity: response.data.identity, + count: response.data.count, + }; + } + + public async getGatewayCoreCount(identity_key: string): Promise { + const response = await this.restClient.sendGet({ + route: `/gateway/${identity_key}/core-status-count`, + }); + + return { + identity: response.data.identity, + count: response.data.count, + }; + } + + public async getMixnodeRewardComputation( + identity_key: string + ): Promise { + const response = await this.restClient.sendGet({ + route: `/mixnode/${identity_key}/reward-estimation`, + }); + + return { + estimated_total_node_reward: response.data.estimated_total_node_reward, + estimated_operator_reward: response.data.estimated_operator_reward, + estimated_delegators_reward: response.data.estimated_delegators_reward, + estimated_node_profit: response.data.estimated_node_profit, + estimated_operator_cost: response.data.estimated_operator_cost, + reward_params: response.data.reward_params, + as_at: response.data.as_at, + }; + } + + public async getMixnodeRewardEstimatedComputation( + identity_key: string + ): Promise { + const response = await this.restClient.sendPost({ + route: `/mixnode/${identity_key}/compute-reward-estimation`, + }); + + return { + estimated_total_node_reward: response.data.estimated_total_node_reward, + estimated_operator_reward: response.data.estimated_operator_reward, + estimated_delegators_reward: response.data.estimated_delegators_reward, + estimated_node_profit: response.data.estimated_node_profit, + estimated_operator_cost: response.data.estimated_operator_cost, + reward_params: response.data.reward_params, + as_at: response.data.as_at, + }; + } + + public async getMixnodeHistory(identity_key: string): Promise { + const response = await this.restClient.sendGet({ + route: `/mixnode/${identity_key}/history`, + }); + + return { + identity: response.data.identity, + owner: response.data.owner, + history: response.data.history, + }; + } + + public async getMixnodeAverageUptime( + identity_key: string + ): Promise { + const response = await this.restClient.sendGet({ + route: `/mixnode/${identity_key}/avg_uptime`, + }); + + return { + identity: response.data.identity, + avg_uptime: response.data.avg_uptime, + }; + } + + public async getMixnodeInclusionProbability( + identity_key: string + ): Promise { + const response = await this.restClient.sendGet({ + route: `/mixnode/${identity_key}/inclusion-probability`, + }); + + return { + in_active: response.data.in_active, + in_reserve: response.data.in_reserve, + }; + } + + public async getMixnodeStatus(identity_key: string): Promise { + const response = await this.restClient.sendGet({ + route: `/mixnode/${identity_key}/status`, + }); + + return { + status: response.data.status, + }; + } +} diff --git a/validator-api/tests/src/endpoints/abstracts/APIClient.ts b/validator-api/tests/src/endpoints/abstracts/APIClient.ts new file mode 100644 index 0000000000..93249de2ff --- /dev/null +++ b/validator-api/tests/src/endpoints/abstracts/APIClient.ts @@ -0,0 +1,30 @@ +import { Logger } from "tslog"; +import ConfigHandler from "../../config/configHandler"; +import { RestClient } from "../../restClient/RestClient"; + +export abstract class APIClient { + protected constructor(serviceUrl: string) { + const baseUrl: string = this.config.environmnetConfig.api_base_url; + this.url = baseUrl + serviceUrl; + this.restClient = new RestClient(this.url); + this.serviceName = this.constructor.toString().match(/\w+/g)[1]; + this.log.info(`The Service URL for ${this.serviceName} is ${this.url}`); + } + + public createdItemIds: Set = new Set(); + + protected config = ConfigHandler.getInstance(); + + protected log: Logger = new Logger({ + minLevel: this.config.environmnetConfig.log_level, + dateTimeTimezone: + this.config.environmnetConfig.time_zone || + Intl.DateTimeFormat().resolvedOptions().timeZone, + }); + + protected url: string; + + public restClient: RestClient; + + protected serviceName: string; +} diff --git a/validator-api/tests/src/interfaces/StatusInterfaces.ts b/validator-api/tests/src/interfaces/StatusInterfaces.ts new file mode 100644 index 0000000000..d6a7d91498 --- /dev/null +++ b/validator-api/tests/src/interfaces/StatusInterfaces.ts @@ -0,0 +1,72 @@ +export type Epoch = { + epoch_reward_pool: string; + rewarded_set_size: string; + active_set_size: string; + staking_supply: string; + sybil_resistance_percent: number; + active_set_work_factor: number; +}; + +export type Node = { + reward_blockstamp: number; + uptime: string; + in_active_set: boolean; +}; + +export type RewardParams = { + epoch: Epoch; + node: Node; +}; + +export type EstimatedReward = { + estimated_total_node_reward: number; + estimated_operator_reward: number; + estimated_delegators_reward: number; + estimated_node_profit: number; + estimated_operator_cost: number; + reward_params: RewardParams; + as_at: number; +}; + +export type StakeSaturation = { + saturation: number; + as_at: number; +}; + +export type AvgUptime = { + identity: string; + avg_uptime: number; +}; + +export type InclusionProbability = { + in_active: string; + in_reserve: string; +}; + +export type Report = { + identity: string; + owner: string; + most_recent: number; + last_hour: number; + last_day: number; +}; + +export type History = { + date: string; + uptime: number; +}; + +export type NodeHistory = { + identity: string; + owner: string; + history: History[]; +}; + +export type CoreCount = { + identity: string; + count: number; +}; + +export type ActiveStatus = { + status: string; +}; diff --git a/validator-api/tests/src/restClient/RestClient.ts b/validator-api/tests/src/restClient/RestClient.ts new file mode 100644 index 0000000000..7992d7afa7 --- /dev/null +++ b/validator-api/tests/src/restClient/RestClient.ts @@ -0,0 +1,243 @@ +import axios, { + AxiosInstance, + AxiosRequestConfig, + AxiosResponse, + Method, +} from "axios"; +import { Logger } from "tslog"; +import { stringify } from "yaml"; + +import https from "https"; + +import ConfigHandler from "../config/configHandler"; + +const config = ConfigHandler.getInstance(); +const log = new Logger({ + minLevel: config.environmnetConfig.log_level, + dateTimeTimezone: + config.environmnetConfig.time_zone || + Intl.DateTimeFormat().resolvedOptions().timeZone, +}); + +function isSet(property): boolean { + return property !== undefined && property !== null; +} + +export class RestClient { + private static authToken: string; + + private axiosInstance: AxiosInstance; + + constructor(baseUrl: string) { + this.axiosInstance = axios.create({ baseURL: baseUrl }); + } + + private httpsAgent = new https.Agent({ + rejectUnauthorized: false, + }); + + // Not returning an actual auth token for this example project. + // Just showing how it can be done! + static async getToken(requestHeaders: object) { + requestHeaders["Authorization"] = `asdf`; + } + + public async callEndpoint({ + route, + method, + authToken, + headers, + data, + additionalConfigs, + params, + }: IAxiosCallEndpointArgs): Promise { + let response; + let responseLog = "Response: "; + let requestHeaders = headers; + + // if headers are not passed in, use the default headers + if (requestHeaders == undefined) { + requestHeaders = { ...config.commonConfig.request_headers }; + } + + // if authToken is passed in, add it to the request headers + if (authToken !== undefined) { + requestHeaders = { + ...requestHeaders, + ...{ + Authorization: `Bearer ${authToken}`, + }, + }; + } + + // if we have not set the auth headers yet, set them + else if (!requestHeaders.Authorization) { + await RestClient.getToken(requestHeaders); + } + + log.debug( + RestClient.prepareLogRecord({ + route, + method, + headers: requestHeaders, + data, + additionalConfigs, + params, + }) + ); + + await this.axiosInstance + .request({ + url: route, + method, + data, + headers: requestHeaders, + httpsAgent: this.httpsAgent, + params, + ...additionalConfigs, + }) + .then((res) => { + response = res; + responseLog = ` Status = ${res.status} ${res.statusText}`; + }) + .catch((error) => { + response = error.response; + if (response === undefined) + responseLog = ` Something wrong happened, did not get proper error from server! (${error.message})`; + else + responseLog = ` Status = ${response.status} ${response.statusText}, ${error.message}`; + }); + log.debug(responseLog); + return response; + } + + public async sendPost({ + route, + authToken, + data, + params, + headers, + additionalConfigs, + }: IAxiosHttpRequestArgs): Promise { + return this.callEndpoint({ + route, + method: "POST", + authToken, + data, + params, + headers, + additionalConfigs, + }); + } + + public async sendGet({ + route, + authToken, + params, + headers, + additionalConfigs, + }: IAxiosHttpRequestArgs): Promise { + return this.callEndpoint({ + route, + method: "GET", + authToken, + params, + headers, + additionalConfigs, + }); + } + + public async sendDelete({ + route, + authToken, + params, + headers, + additionalConfigs, + }: IAxiosHttpRequestArgs): Promise { + return this.callEndpoint({ + route, + method: "DELETE", + authToken, + params, + headers, + additionalConfigs, + }); + } + + public async sendPatch({ + route, + authToken, + data, + headers, + additionalConfigs, + }: IAxiosHttpRequestArgs): Promise { + return this.callEndpoint({ + route, + method: "PATCH", + authToken, + data, + headers, + additionalConfigs, + }); + } + + public async sendPut({ + route, + authToken, + data, + headers, + additionalConfigs, + }: IAxiosHttpRequestArgs): Promise { + return this.callEndpoint({ + route, + method: "PUT", + authToken, + data, + headers, + additionalConfigs, + }); + } + + private static prepareLogRecord({ + route, + method, + headers, + data, + additionalConfigs, + params, + }: IAxiosCallEndpointArgs): string { + let logRecord = `Request: ${method} ${route}`; + if (isSet(headers)) + logRecord = `${logRecord}\nHeaders: ${stringify(headers)}`; + + if (isSet(params)) logRecord = `${logRecord}\nParams: ${stringify(params)}`; + + if (isSet(additionalConfigs)) { + logRecord = `${logRecord}\nAdditional Configuration: ${stringify( + additionalConfigs + )}`; + } + + if (isSet(data)) { + const jsonData = stringify(data); + // We don't want to log anything that isn't json data + logRecord = `${logRecord}\nData: ${ + jsonData === undefined ? "Some data, not JSON!" : jsonData + }`; + } + return logRecord; + } +} + +export interface IAxiosHttpRequestArgs { + route: string; + authToken?: string; + data?: object; + params?: object; + headers?: any; + additionalConfigs?: AxiosRequestConfig; +} + +export interface IAxiosCallEndpointArgs extends IAxiosHttpRequestArgs { + method: Method; +} diff --git a/validator-api/tests/tsconfig.eslint.json b/validator-api/tests/tsconfig.eslint.json new file mode 100644 index 0000000000..a43751dc4e --- /dev/null +++ b/validator-api/tests/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*", "functional_test/**/*", "unit_test/**/*", "*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/validator-api/tests/tsconfig.json b/validator-api/tests/tsconfig.json new file mode 100644 index 0000000000..c25d4218fa --- /dev/null +++ b/validator-api/tests/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "removeComments": true, + "allowJs": true, + "preserveConstEnums": true, + "module": "commonjs", + "target": "ES2019", + "declaration": true, + "esModuleInterop": true, + "sourceMap": true, + "lib": ["esnext"], + "outDir": "dist", + "resolveJsonModule": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "paths": { + "*": ["types/*"] + }, + "baseUrl": "./", + "typeRoots": ["node_modules/@types"], + "alwaysStrict": true + }, + "include": ["src/**/*", "functional_test/**/*"], + "exclude": ["unit_test/**/*"] +} diff --git a/validator-api/tests/yarn.lock b/validator-api/tests/yarn.lock new file mode 100644 index 0000000000..e13dae8701 --- /dev/null +++ b/validator-api/tests/yarn.lock @@ -0,0 +1,2931 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/compat-data@^7.18.8": + version "7.18.8" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.18.8.tgz" + integrity sha512-HSmX4WZPPK3FUxYp7g2T6EyO8j96HlZJlxmKPSh6KAcqwyDrfx7hKjXpAW/0FhFfTJsR0Yt4lAjLI2coMptIHQ== + +"@babel/core@^7.11.6", "@babel/core@^7.12.3": + version "7.18.10" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.18.10.tgz" + integrity sha512-JQM6k6ENcBFKVtWvLavlvi/mPcpYZ3+R+2EySDEMSMbp7Mn4FexlbbJVrx2R7Ijhr01T8gyqrOaABWIOgxeUyw== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.10" + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-module-transforms" "^7.18.9" + "@babel/helpers" "^7.18.9" + "@babel/parser" "^7.18.10" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.18.10" + "@babel/types" "^7.18.10" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/generator@^7.18.10", "@babel/generator@^7.7.2": + version "7.18.12" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.18.12.tgz" + integrity sha512-dfQ8ebCN98SvyL7IxNMCUtZQSq5R7kxgN+r8qYTGDmmSion1hX2C0zq2yo1bsCDhXixokv1SAWTZUMYbO/V5zg== + dependencies: + "@babel/types" "^7.18.10" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.18.9": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.18.9.tgz" + integrity sha512-tzLCyVmqUiFlcFoAPLA/gL9TeYrF61VLNtb+hvkuVaB5SUjW7jcfrglBIX1vUIoT7CLP3bBlIMeyEsIl2eFQNg== + dependencies: + "@babel/compat-data" "^7.18.8" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.20.2" + semver "^6.3.0" + +"@babel/helper-environment-visitor@^7.18.9": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz" + integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== + +"@babel/helper-function-name@^7.18.9": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.18.9.tgz" + integrity sha512-fJgWlZt7nxGksJS9a0XdSaI4XvpExnNIgRP+rVefWh5U7BL8pPuir6SJUmFKRfjWQ51OtWSzwOxhaH/EBWWc0A== + dependencies: + "@babel/template" "^7.18.6" + "@babel/types" "^7.18.9" + +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-imports@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-transforms@^7.18.9": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.18.9.tgz" + integrity sha512-KYNqY0ICwfv19b31XzvmI/mfcylOzbLtowkw+mfvGPAQ3kfCnMLYbED3YecL5tPd8nAYFQFAd6JHp2LxZk/J1g== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.18.6" + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.9" + "@babel/types" "^7.18.9" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.8.0": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.18.9.tgz" + integrity sha512-aBXPT3bmtLryXaoJLyYPXPlSD4p1ld9aYeR+sJNOZjJJGiOpb+fKfh3NkcCu7J54nUJwCERPBExCCpyCOHnu/w== + +"@babel/helper-simple-access@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.18.6.tgz" + integrity sha512-iNpIgTgyAvDQpDj76POqg+YEt8fPxx3yaNBg3S30dxNKm2SWfYhD0TGrK/Eu9wHpUW63VQU894TsTg+GLbUa1g== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-string-parser@^7.18.10": + version "7.18.10" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.18.10.tgz" + integrity sha512-XtIfWmeNY3i4t7t4D2t02q50HvqHybPqW2ki1kosnvWCwuCMeo81Jf0gwr85jy/neUdg5XDdeFE/80DXiO+njw== + +"@babel/helper-validator-identifier@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz" + integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== + +"@babel/helper-validator-option@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz" + integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== + +"@babel/helpers@^7.18.9": + version "7.18.9" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.18.9.tgz" + integrity sha512-Jf5a+rbrLoR4eNdUmnFu8cN5eNJT6qdTdOg5IHIzq87WwyRw9PwguLFOWYgktN/60IP4fgDUawJvs7PjQIzELQ== + dependencies: + "@babel/template" "^7.18.6" + "@babel/traverse" "^7.18.9" + "@babel/types" "^7.18.9" + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.18.10", "@babel/parser@^7.18.11": + version "7.18.11" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz" + integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.7.2": + version "7.18.6" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz" + integrity sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/template@^7.18.10", "@babel/template@^7.18.6", "@babel/template@^7.3.3": + version "7.18.10" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz" + integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.18.10" + "@babel/types" "^7.18.10" + +"@babel/traverse@^7.18.10", "@babel/traverse@^7.18.9", "@babel/traverse@^7.7.2": + version "7.18.11" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.18.11.tgz" + integrity sha512-TG9PiM2R/cWCAy6BPJKeHzNbu4lPzOSZpeMfeNErskGpTJx6trEvFaVCbDvpcxwy49BKWmEPwiW8mrysNiDvIQ== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.18.10" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.18.11" + "@babel/types" "^7.18.10" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.3.0", "@babel/types@^7.3.3": + version "7.18.10" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.18.10.tgz" + integrity sha512-MJvnbEiiNkpjo+LknnmRrqbY1GPUUggjv+wQVjetM/AONoupqRALB7I6jGqNUAZsKcRIEu2J6FRFvsczljjsaQ== + dependencies: + "@babel/helper-string-parser" "^7.18.10" + "@babel/helper-validator-identifier" "^7.18.6" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@eslint/eslintrc@^1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.3.0.tgz#29f92c30bb3e771e4a2048c95fa6855392dfac4f" + integrity sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.3.2" + globals "^13.15.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@humanwhocodes/config-array@^0.10.4": + version "0.10.4" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.4.tgz#01e7366e57d2ad104feea63e72248f22015c520c" + integrity sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/gitignore-to-minimatch@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz#316b0a63b91c10e53f242efb4ace5c3b34e8728d" + integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA== + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/console/-/console-28.1.3.tgz" + integrity sha512-QPAkP5EwKdK/bxIr6C1I4Vs0rm2nHiANzj/Z5X2JQkrZo6IqvC4ldZ9K95tF0HdidhA8Bo6egxSzUFPYKcEXLw== + dependencies: + "@jest/types" "^28.1.3" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^28.1.3" + jest-util "^28.1.3" + slash "^3.0.0" + +"@jest/core@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/core/-/core-28.1.3.tgz" + integrity sha512-CIKBrlaKOzA7YG19BEqCw3SLIsEwjZkeJzf5bdooVnW4bH5cktqe3JX+G2YV1aK5vP8N9na1IGWFzYaTp6k6NA== + dependencies: + "@jest/console" "^28.1.3" + "@jest/reporters" "^28.1.3" + "@jest/test-result" "^28.1.3" + "@jest/transform" "^28.1.3" + "@jest/types" "^28.1.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + ci-info "^3.2.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + jest-changed-files "^28.1.3" + jest-config "^28.1.3" + jest-haste-map "^28.1.3" + jest-message-util "^28.1.3" + jest-regex-util "^28.0.2" + jest-resolve "^28.1.3" + jest-resolve-dependencies "^28.1.3" + jest-runner "^28.1.3" + jest-runtime "^28.1.3" + jest-snapshot "^28.1.3" + jest-util "^28.1.3" + jest-validate "^28.1.3" + jest-watcher "^28.1.3" + micromatch "^4.0.4" + pretty-format "^28.1.3" + rimraf "^3.0.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-28.1.3.tgz" + integrity sha512-1bf40cMFTEkKyEf585R9Iz1WayDjHoHqvts0XFYEqyKM3cFWDpeMoqKKTAF9LSYQModPUlh8FKptoM2YcMWAXA== + dependencies: + "@jest/fake-timers" "^28.1.3" + "@jest/types" "^28.1.3" + "@types/node" "*" + jest-mock "^28.1.3" + +"@jest/expect-utils@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-28.1.3.tgz" + integrity sha512-wvbi9LUrHJLn3NlDW6wF2hvIMtd4JUl2QNVrjq+IBSHirgfrR3o9RnVtxzdEGO2n9JyIWwHnLfby5KzqBGg2YA== + dependencies: + jest-get-type "^28.0.2" + +"@jest/expect@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/expect/-/expect-28.1.3.tgz" + integrity sha512-lzc8CpUbSoE4dqT0U+g1qODQjBRHPpCPXissXD4mS9+sWQdmmpeJ9zSH1rS1HEkrsMN0fb7nKrJ9giAR1d3wBw== + dependencies: + expect "^28.1.3" + jest-snapshot "^28.1.3" + +"@jest/fake-timers@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-28.1.3.tgz" + integrity sha512-D/wOkL2POHv52h+ok5Oj/1gOG9HSywdoPtFsRCUmlCILXNn5eIWmcnd3DIiWlJnpGvQtmajqBP95Ei0EimxfLw== + dependencies: + "@jest/types" "^28.1.3" + "@sinonjs/fake-timers" "^9.1.2" + "@types/node" "*" + jest-message-util "^28.1.3" + jest-mock "^28.1.3" + jest-util "^28.1.3" + +"@jest/globals@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-28.1.3.tgz" + integrity sha512-XFU4P4phyryCXu1pbcqMO0GSQcYe1IsalYCDzRNyhetyeyxMcIxa11qPNDpVNLeretItNqEmYYQn1UYz/5x1NA== + dependencies: + "@jest/environment" "^28.1.3" + "@jest/expect" "^28.1.3" + "@jest/types" "^28.1.3" + +"@jest/reporters@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-28.1.3.tgz" + integrity sha512-JuAy7wkxQZVNU/V6g9xKzCGC5LVXx9FDcABKsSXp5MiKPEE2144a/vXTEDoyzjUpZKfVwp08Wqg5A4WfTMAzjg== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^28.1.3" + "@jest/test-result" "^28.1.3" + "@jest/transform" "^28.1.3" + "@jest/types" "^28.1.3" + "@jridgewell/trace-mapping" "^0.3.13" + "@types/node" "*" + chalk "^4.0.0" + collect-v8-coverage "^1.0.0" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-message-util "^28.1.3" + jest-util "^28.1.3" + jest-worker "^28.1.3" + slash "^3.0.0" + string-length "^4.0.1" + strip-ansi "^6.0.0" + terminal-link "^2.0.0" + v8-to-istanbul "^9.0.1" + +"@jest/schemas@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/schemas/-/schemas-28.1.3.tgz" + integrity sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg== + dependencies: + "@sinclair/typebox" "^0.24.1" + +"@jest/source-map@^28.1.2": + version "28.1.2" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-28.1.2.tgz" + integrity sha512-cV8Lx3BeStJb8ipPHnqVw/IM2VCMWO3crWZzYodSIkxXnRcXJipCdx1JCK0K5MsJJouZQTH73mzf4vgxRaH9ww== + dependencies: + "@jridgewell/trace-mapping" "^0.3.13" + callsites "^3.0.0" + graceful-fs "^4.2.9" + +"@jest/test-result@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-28.1.3.tgz" + integrity sha512-kZAkxnSE+FqE8YjW8gNuoVkkC9I7S1qmenl8sGcDOLropASP+BkcGKwhXoyqQuGOGeYY0y/ixjrd/iERpEXHNg== + dependencies: + "@jest/console" "^28.1.3" + "@jest/types" "^28.1.3" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-28.1.3.tgz" + integrity sha512-NIMPEqqa59MWnDi1kvXXpYbqsfQmSJsIbnd85mdVGkiDfQ9WQQTXOLsvISUfonmnBT+w85WEgneCigEEdHDFxw== + dependencies: + "@jest/test-result" "^28.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^28.1.3" + slash "^3.0.0" + +"@jest/transform@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-28.1.3.tgz" + integrity sha512-u5dT5di+oFI6hfcLOHGTAfmUxFRrjK+vnaP0kkVow9Md/M7V/MxqQMOz/VV25UZO8pzeA9PjfTpOu6BDuwSPQA== + dependencies: + "@babel/core" "^7.11.6" + "@jest/types" "^28.1.3" + "@jridgewell/trace-mapping" "^0.3.13" + babel-plugin-istanbul "^6.1.1" + chalk "^4.0.0" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^28.1.3" + jest-regex-util "^28.0.2" + jest-util "^28.1.3" + micromatch "^4.0.4" + pirates "^4.0.4" + slash "^3.0.0" + write-file-atomic "^4.0.1" + +"@jest/types@^28.1.3": + version "28.1.3" + resolved "https://registry.npmjs.org/@jest/types/-/types-28.1.3.tgz" + integrity sha512-RyjiyMUZrKz/c+zlMFO1pm70DcIlST8AeWTkoUdZevew44wcNZQHsEVOiCVtgVnlFFD82FPaXycys58cf2muVQ== + dependencies: + "@jest/schemas" "^28.1.3" + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^17.0.8" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.2": + 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" + +"@jridgewell/resolve-uri@^3.0.3": + 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@^1.0.0", "@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/sourcemap-codec@^1.4.10": + 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@^0.3.12", "@jridgewell/trace-mapping@^0.3.13", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.14" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz" + integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@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" + +"@sinclair/typebox@^0.24.1": + version "0.24.27" + resolved "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.27.tgz" + integrity sha512-K7C7IlQ3zLePEZleUN21ceBA2aLcMnLHTLph8QWk1JK37L90obdpY+QGY8bXMKxf1ht1Z0MNewvXxWv0oGDYFg== + +"@sinonjs/commons@^1.7.0": + version "1.8.3" + resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz" + integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^9.1.2": + version "9.1.2" + resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz" + integrity sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@types/babel__core@^7.1.14": + version "7.1.19" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz" + integrity sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.4" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.1" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.18.0" + resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.0.tgz" + integrity sha512-v4Vwdko+pgymgS+A2UIaJru93zQd85vIGWObM5ekZNdXCKtDYqATlEYnWgfo86Q6I1Lh0oXnksDnMU1cwmlPDw== + dependencies: + "@babel/types" "^7.3.0" + +"@types/graceful-fs@^4.1.3": + version "4.1.5" + resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz" + integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== + dependencies: + "@types/node" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.4" + resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@28.1.6": + version "28.1.6" + resolved "https://registry.npmjs.org/@types/jest/-/jest-28.1.6.tgz" + integrity sha512-0RbGAFMfcBJKOmqRazM8L98uokwuwD5F8rHrv/ZMbrZBwVOWZUyPG6VFNscjYr/vjM3Vu4fRrCPbOs42AfemaQ== + dependencies: + jest-matcher-utils "^28.0.0" + pretty-format "^28.0.0" + +"@types/json-schema@^7.0.9": + version "7.0.11" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/node@*": + version "18.7.1" + resolved "https://registry.npmjs.org/@types/node/-/node-18.7.1.tgz" + integrity sha512-GKX1Qnqxo4S+Z/+Z8KKPLpH282LD7jLHWJcVryOflnsnH+BtSDfieR6ObwBMwpnNws0bUK8GI7z0unQf9bARNQ== + +"@types/prettier@^2.1.5": + version "2.7.0" + resolved "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.0.tgz" + integrity sha512-RI1L7N4JnW5gQw2spvL7Sllfuf1SaHdrZpCHiBlCXjIlufi1SMNnbu2teze3/QE67Fg2tBlH7W+mi4hVNk4p0A== + +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + +"@types/uuid@8.3.4": + version "8.3.4" + resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz" + integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw== + +"@types/yargs-parser@*": + version "21.0.0" + resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz" + integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== + +"@types/yargs@^17.0.8": + version "17.0.11" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.11.tgz" + integrity sha512-aB4y9UDUXTSMxmM4MH+YnuR0g5Cph3FLQBoWoMB21DSvFVAxRVEHEMx3TLh+zUZYMCQtKiqazz0Q4Rre31f/OA== + dependencies: + "@types/yargs-parser" "*" + +"@typescript-eslint/eslint-plugin@^5.12.1": + version "5.33.0" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.33.0.tgz" + integrity sha512-jHvZNSW2WZ31OPJ3enhLrEKvAZNyAFWZ6rx9tUwaessTc4sx9KmgMNhVcqVAl1ETnT5rU5fpXTLmY9YvC1DCNg== + dependencies: + "@typescript-eslint/scope-manager" "5.33.0" + "@typescript-eslint/type-utils" "5.33.0" + "@typescript-eslint/utils" "5.33.0" + debug "^4.3.4" + functional-red-black-tree "^1.0.1" + ignore "^5.2.0" + regexpp "^3.2.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.33.0": + version "5.33.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.33.0.tgz#26ec3235b74f0667414613727cb98f9b69dc5383" + integrity sha512-cgM5cJrWmrDV2KpvlcSkelTBASAs1mgqq+IUGKJvFxWrapHpaRy5EXPQz9YaKF3nZ8KY18ILTiVpUtbIac86/w== + dependencies: + "@typescript-eslint/scope-manager" "5.33.0" + "@typescript-eslint/types" "5.33.0" + "@typescript-eslint/typescript-estree" "5.33.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@5.33.0": + version "5.33.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.33.0.tgz" + integrity sha512-/Jta8yMNpXYpRDl8EwF/M8It2A9sFJTubDo0ATZefGXmOqlaBffEw0ZbkbQ7TNDK6q55NPHFshGBPAZvZkE8Pw== + dependencies: + "@typescript-eslint/types" "5.33.0" + "@typescript-eslint/visitor-keys" "5.33.0" + +"@typescript-eslint/type-utils@5.33.0": + version "5.33.0" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.33.0.tgz" + integrity sha512-2zB8uEn7hEH2pBeyk3NpzX1p3lF9dKrEbnXq1F7YkpZ6hlyqb2yZujqgRGqXgRBTHWIUG3NGx/WeZk224UKlIA== + dependencies: + "@typescript-eslint/utils" "5.33.0" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.33.0": + version "5.33.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.33.0.tgz" + integrity sha512-nIMt96JngB4MYFYXpZ/3ZNU4GWPNdBbcB5w2rDOCpXOVUkhtNlG2mmm8uXhubhidRZdwMaMBap7Uk8SZMU/ppw== + +"@typescript-eslint/typescript-estree@5.33.0": + version "5.33.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.33.0.tgz" + integrity sha512-tqq3MRLlggkJKJUrzM6wltk8NckKyyorCSGMq4eVkyL5sDYzJJcMgZATqmF8fLdsWrW7OjjIZ1m9v81vKcaqwQ== + dependencies: + "@typescript-eslint/types" "5.33.0" + "@typescript-eslint/visitor-keys" "5.33.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.33.0": + version "5.33.0" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.33.0.tgz" + integrity sha512-JxOAnXt9oZjXLIiXb5ZIcZXiwVHCkqZgof0O8KPgz7C7y0HS42gi75PdPlqh1Tf109M0fyUw45Ao6JLo7S5AHw== + dependencies: + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.33.0" + "@typescript-eslint/types" "5.33.0" + "@typescript-eslint/typescript-estree" "5.33.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/visitor-keys@5.33.0": + version "5.33.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.33.0.tgz" + integrity sha512-/XsqCzD4t+Y9p5wd9HZiptuGKBlaZO5showwqODii5C0nZawxWLF+Q6k5wYHBrQv96h6GYKyqqMHCSTqta8Kiw== + dependencies: + "@typescript-eslint/types" "5.33.0" + eslint-visitor-keys "^3.3.0" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.8.0: + 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@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + 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" + +ansi-escapes@^4.2.1: + version "4.3.2" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +anymatch@^3.0.3: + 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" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios-mock-adapter@^1.20.0: + version "1.21.2" + resolved "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.21.2.tgz" + integrity sha512-jzyNxU3JzB2XVhplZboUcF0YDs7xuExzoRSHXPHr+UQajaGmcTqvkkUADgkVI2WkGlpZ1zZlMVdcTMU0ejV8zQ== + dependencies: + fast-deep-equal "^3.1.3" + is-buffer "^2.0.5" + +axios@^0.27.2: + version "0.27.2" + resolved "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz" + integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ== + dependencies: + follow-redirects "^1.14.9" + form-data "^4.0.0" + +babel-jest@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-28.1.3.tgz" + integrity sha512-epUaPOEWMk3cWX0M/sPvCHHCe9fMFAa/9hXEgKP8nFfNl/jlGkE9ucq9NqkZGXLDduCJYS0UvSlPUwC0S+rH6Q== + dependencies: + "@jest/transform" "^28.1.3" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^28.1.3" + chalk "^4.0.0" + graceful-fs "^4.2.9" + slash "^3.0.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.3.tgz" + integrity sha512-Ys3tUKAmfnkRUpPdpa98eYrAR0nV+sSFUZZEGuQ2EbFd1y4SOLtD5QDNHAq+bb9a+bbXvYQC4b+ID/THIMcU6Q== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.1.14" + "@types/babel__traverse" "^7.0.6" + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-28.1.3.tgz" + integrity sha512-L+fupJvlWAHbQfn74coNX3zf60LXMJsezNvvx8eIh7iOR1luJ1poxYgQk1F8PYtNq/6QODDHCqsSnTFSWC491A== + dependencies: + babel-plugin-jest-hoist "^28.1.3" + babel-preset-current-node-syntax "^1.0.0" + +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== + +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: + version "3.0.2" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.20.2: + version "4.21.3" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz" + integrity sha512-898rgRXLAyRkM1GryrrBHGkqA5hlpkV5MhtZwg9QXeiyLUYs2k00Un05aX5l2/yJIOObYKOpS2JNo8nJDE7fWQ== + dependencies: + caniuse-lite "^1.0.30001370" + electron-to-chromium "^1.4.202" + node-releases "^2.0.6" + update-browserslist-db "^1.0.5" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +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== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0: + version "6.3.0" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-lite@^1.0.30001370: + version "1.0.30001375" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001375.tgz" + integrity sha512-kWIMkNzLYxSvnjy0hL8w1NOaWNr2rn39RTAVyIwcw8juu60bZDWiF1/loOYANzjtJmy6qPgNmn38ro5Pygagdw== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +ci-info@^3.2.0: + version "3.3.2" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.3.2.tgz" + integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg== + +cjs-module-lexer@^1.0.0: + version "1.2.2" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz" + integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" + integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== + +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +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== + +convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + +cross-spawn@^7.0.2, 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@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz" + integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +diff-sequences@^28.1.1: + version "28.1.1" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz" + integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== + +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" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +electron-to-chromium@^1.4.202: + version "1.4.215" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.215.tgz" + integrity sha512-vqZxT8C5mlDZ//hQFhneHmOLnj1LhbzxV0+I1yqHV8SB1Oo4Y5Ne9+qQhwHl7O1s9s9cRuo2l5CoLEHdhMTwZg== + +emittery@^0.10.2: + version "0.10.2" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz" + integrity sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-prettier@^8.4.0: + version "8.5.0" + resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz" + integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== + +eslint-plugin-prettier@^4.0.0: + version "4.2.1" + resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz" + integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== + dependencies: + prettier-linter-helpers "^1.0.0" + +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" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@^8.21.0: + version "8.21.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.21.0.tgz#1940a68d7e0573cef6f50037addee295ff9be9ef" + integrity sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA== + dependencies: + "@eslint/eslintrc" "^1.3.0" + "@humanwhocodes/config-array" "^0.10.4" + "@humanwhocodes/gitignore-to-minimatch" "^1.0.2" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.3.3" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + functional-red-black-tree "^1.0.1" + glob-parent "^6.0.1" + globals "^13.15.0" + globby "^11.1.0" + grapheme-splitter "^1.0.4" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^9.3.2, espree@^9.3.3: + version "9.3.3" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.3.tgz#2dd37c4162bb05f433ad3c1a52ddf8a49dc08e9d" + integrity sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng== + dependencies: + acorn "^8.8.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.3.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +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.1.0, 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== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +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" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" + integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== + +expect@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/expect/-/expect-28.1.3.tgz" + integrity sha512-eEh0xn8HlsuOBxFgIss+2mX85VAS4Qy3OSkjV7rlBWljtA4oWH37glVGyOZSZvErDT/yBywZdPGwCXuTvSG85g== + dependencies: + "@jest/expect-utils" "^28.1.3" + jest-get-type "^28.0.2" + jest-matcher-utils "^28.1.3" + jest-message-util "^28.1.3" + jest-util "^28.1.3" + +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-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz" + integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + +fast-glob@^3.2.9: + version "3.2.11" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + 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.x, 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== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +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" + +fb-watchman@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + dependencies: + bser "2.1.1" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +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" + +find-up@^4.0.0, find-up@^4.1.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" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.6" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2" + integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ== + +follow-redirects@^1.14.9: + version "1.15.1" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz" + integrity sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA== + +form-data@4.0.0, form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +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.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + 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== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" + integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +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: + 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.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3, glob@^7.1.4: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + 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" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.15.0: + version "13.17.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.17.0.tgz#902eb1e680a41da93945adbdcb5a9f361ba69bd4" + integrity sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw== + dependencies: + type-fest "^0.20.2" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +grapheme-splitter@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" + integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +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@^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" + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +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== + +ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +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" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +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: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-buffer@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz" + integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== + +is-core-module@^2.9.0: + version "2.10.0" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz" + integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== + dependencies: + has "^1.0.3" + +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-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + 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-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== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.2.0" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.0.tgz" + integrity sha512-6Lthe1hqXHBNsqvgDzGO6l03XNeu3CrG4RqQ1KM9+l5+jNGpEJfIELx1NS3SEHmJQA8np/u+E4EPRKRiu6m19A== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.5" + resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz" + integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jest-changed-files@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-28.1.3.tgz" + integrity sha512-esaOfUWJXk2nfZt9SPyC8gA1kNfdKLkQWyzsMlqq8msYSlNKfmZxfRgZn4Cd4MGVUF+7v6dBs0d5TOAKa7iIiA== + dependencies: + execa "^5.0.0" + p-limit "^3.1.0" + +jest-circus@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-28.1.3.tgz" + integrity sha512-cZ+eS5zc79MBwt+IhQhiEp0OeBddpc1n8MBo1nMB8A7oPMKEO+Sre+wHaLJexQUj9Ya/8NOBY0RESUgYjB6fow== + dependencies: + "@jest/environment" "^28.1.3" + "@jest/expect" "^28.1.3" + "@jest/test-result" "^28.1.3" + "@jest/types" "^28.1.3" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + is-generator-fn "^2.0.0" + jest-each "^28.1.3" + jest-matcher-utils "^28.1.3" + jest-message-util "^28.1.3" + jest-runtime "^28.1.3" + jest-snapshot "^28.1.3" + jest-util "^28.1.3" + p-limit "^3.1.0" + pretty-format "^28.1.3" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-cli@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-28.1.3.tgz" + integrity sha512-roY3kvrv57Azn1yPgdTebPAXvdR2xfezaKKYzVxZ6It/5NCxzJym6tUI5P1zkdWhfUYkxEI9uZWcQdaFLo8mJQ== + dependencies: + "@jest/core" "^28.1.3" + "@jest/test-result" "^28.1.3" + "@jest/types" "^28.1.3" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.9" + import-local "^3.0.2" + jest-config "^28.1.3" + jest-util "^28.1.3" + jest-validate "^28.1.3" + prompts "^2.0.1" + yargs "^17.3.1" + +jest-config@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-28.1.3.tgz" + integrity sha512-MG3INjByJ0J4AsNBm7T3hsuxKQqFIiRo/AUqb1q9LRKI5UU6Aar9JHbr9Ivn1TVwfUD9KirRoM/T6u8XlcQPHQ== + dependencies: + "@babel/core" "^7.11.6" + "@jest/test-sequencer" "^28.1.3" + "@jest/types" "^28.1.3" + babel-jest "^28.1.3" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-circus "^28.1.3" + jest-environment-node "^28.1.3" + jest-get-type "^28.0.2" + jest-regex-util "^28.0.2" + jest-resolve "^28.1.3" + jest-runner "^28.1.3" + jest-util "^28.1.3" + jest-validate "^28.1.3" + micromatch "^4.0.4" + parse-json "^5.2.0" + pretty-format "^28.1.3" + slash "^3.0.0" + strip-json-comments "^3.1.1" + +jest-diff@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz" + integrity sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw== + dependencies: + chalk "^4.0.0" + diff-sequences "^28.1.1" + jest-get-type "^28.0.2" + pretty-format "^28.1.3" + +jest-docblock@^28.1.1: + version "28.1.1" + resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-28.1.1.tgz" + integrity sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA== + dependencies: + detect-newline "^3.0.0" + +jest-each@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-28.1.3.tgz" + integrity sha512-arT1z4sg2yABU5uogObVPvSlSMQlDA48owx07BDPAiasW0yYpYHYOo4HHLz9q0BVzDVU4hILFjzJw0So9aCL/g== + dependencies: + "@jest/types" "^28.1.3" + chalk "^4.0.0" + jest-get-type "^28.0.2" + jest-util "^28.1.3" + pretty-format "^28.1.3" + +jest-environment-node@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-28.1.3.tgz" + integrity sha512-ugP6XOhEpjAEhGYvp5Xj989ns5cB1K6ZdjBYuS30umT4CQEETaxSiPcZ/E1kFktX4GkrcM4qu07IIlDYX1gp+A== + dependencies: + "@jest/environment" "^28.1.3" + "@jest/fake-timers" "^28.1.3" + "@jest/types" "^28.1.3" + "@types/node" "*" + jest-mock "^28.1.3" + jest-util "^28.1.3" + +jest-get-type@^28.0.2: + version "28.0.2" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz" + integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA== + +jest-haste-map@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-28.1.3.tgz" + integrity sha512-3S+RQWDXccXDKSWnkHa/dPwt+2qwA8CJzR61w3FoYCvoo3Pn8tvGcysmMF0Bj0EX5RYvAI2EIvC57OmotfdtKA== + dependencies: + "@jest/types" "^28.1.3" + "@types/graceful-fs" "^4.1.3" + "@types/node" "*" + anymatch "^3.0.3" + fb-watchman "^2.0.0" + graceful-fs "^4.2.9" + jest-regex-util "^28.0.2" + jest-util "^28.1.3" + jest-worker "^28.1.3" + micromatch "^4.0.4" + walker "^1.0.8" + optionalDependencies: + fsevents "^2.3.2" + +jest-junit@^14.0.0: + version "14.0.0" + resolved "https://registry.npmjs.org/jest-junit/-/jest-junit-14.0.0.tgz" + integrity sha512-kALvBDegstTROfDGXH71UGD7k5g7593Y1wuX1wpWT+QTYcBbmtuGOA8UlAt56zo/B2eMIOcaOVEON3j0VXVa4g== + dependencies: + mkdirp "^1.0.4" + strip-ansi "^6.0.1" + uuid "^8.3.2" + xml "^1.0.1" + +jest-leak-detector@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-28.1.3.tgz" + integrity sha512-WFVJhnQsiKtDEo5lG2mM0v40QWnBM+zMdHHyJs8AWZ7J0QZJS59MsyKeJHWhpBZBH32S48FOVvGyOFT1h0DlqA== + dependencies: + jest-get-type "^28.0.2" + pretty-format "^28.1.3" + +jest-matcher-utils@^28.0.0, jest-matcher-utils@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz" + integrity sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw== + dependencies: + chalk "^4.0.0" + jest-diff "^28.1.3" + jest-get-type "^28.0.2" + pretty-format "^28.1.3" + +jest-message-util@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-28.1.3.tgz" + integrity sha512-PFdn9Iewbt575zKPf1286Ht9EPoJmYT7P0kY+RibeYZ2XtOr53pDLEFoTWXbd1h4JiGiWpTBC84fc8xMXQMb7g== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^28.1.3" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.9" + micromatch "^4.0.4" + pretty-format "^28.1.3" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-28.1.3.tgz" + integrity sha512-o3J2jr6dMMWYVH4Lh/NKmDXdosrsJgi4AviS8oXLujcjpCMBb1FMsblDnOXKZKfSiHLxYub1eS0IHuRXsio9eA== + dependencies: + "@jest/types" "^28.1.3" + "@types/node" "*" + +jest-pnp-resolver@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz" + integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== + +jest-regex-util@^28.0.2: + version "28.0.2" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-28.0.2.tgz" + integrity sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw== + +jest-resolve-dependencies@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.3.tgz" + integrity sha512-qa0QO2Q0XzQoNPouMbCc7Bvtsem8eQgVPNkwn9LnS+R2n8DaVDPL/U1gngC0LTl1RYXJU0uJa2BMC2DbTfFrHA== + dependencies: + jest-regex-util "^28.0.2" + jest-snapshot "^28.1.3" + +jest-resolve@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-28.1.3.tgz" + integrity sha512-Z1W3tTjE6QaNI90qo/BJpfnvpxtaFTFw5CDgwpyE/Kz8U/06N1Hjf4ia9quUhCh39qIGWF1ZuxFiBiJQwSEYKQ== + dependencies: + chalk "^4.0.0" + graceful-fs "^4.2.9" + jest-haste-map "^28.1.3" + jest-pnp-resolver "^1.2.2" + jest-util "^28.1.3" + jest-validate "^28.1.3" + resolve "^1.20.0" + resolve.exports "^1.1.0" + slash "^3.0.0" + +jest-runner@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-28.1.3.tgz" + integrity sha512-GkMw4D/0USd62OVO0oEgjn23TM+YJa2U2Wu5zz9xsQB1MxWKDOlrnykPxnMsN0tnJllfLPinHTka61u0QhaxBA== + dependencies: + "@jest/console" "^28.1.3" + "@jest/environment" "^28.1.3" + "@jest/test-result" "^28.1.3" + "@jest/transform" "^28.1.3" + "@jest/types" "^28.1.3" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.10.2" + graceful-fs "^4.2.9" + jest-docblock "^28.1.1" + jest-environment-node "^28.1.3" + jest-haste-map "^28.1.3" + jest-leak-detector "^28.1.3" + jest-message-util "^28.1.3" + jest-resolve "^28.1.3" + jest-runtime "^28.1.3" + jest-util "^28.1.3" + jest-watcher "^28.1.3" + jest-worker "^28.1.3" + p-limit "^3.1.0" + source-map-support "0.5.13" + +jest-runtime@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-28.1.3.tgz" + integrity sha512-NU+881ScBQQLc1JHG5eJGU7Ui3kLKrmwCPPtYsJtBykixrM2OhVQlpMmFWJjMyDfdkGgBMNjXCGB/ebzsgNGQw== + dependencies: + "@jest/environment" "^28.1.3" + "@jest/fake-timers" "^28.1.3" + "@jest/globals" "^28.1.3" + "@jest/source-map" "^28.1.2" + "@jest/test-result" "^28.1.3" + "@jest/transform" "^28.1.3" + "@jest/types" "^28.1.3" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + execa "^5.0.0" + glob "^7.1.3" + graceful-fs "^4.2.9" + jest-haste-map "^28.1.3" + jest-message-util "^28.1.3" + jest-mock "^28.1.3" + jest-regex-util "^28.0.2" + jest-resolve "^28.1.3" + jest-snapshot "^28.1.3" + jest-util "^28.1.3" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-snapshot@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-28.1.3.tgz" + integrity sha512-4lzMgtiNlc3DU/8lZfmqxN3AYD6GGLbl+72rdBpXvcV+whX7mDrREzkPdp2RnmfIiWBg1YbuFSkXduF2JcafJg== + dependencies: + "@babel/core" "^7.11.6" + "@babel/generator" "^7.7.2" + "@babel/plugin-syntax-typescript" "^7.7.2" + "@babel/traverse" "^7.7.2" + "@babel/types" "^7.3.3" + "@jest/expect-utils" "^28.1.3" + "@jest/transform" "^28.1.3" + "@jest/types" "^28.1.3" + "@types/babel__traverse" "^7.0.6" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^28.1.3" + graceful-fs "^4.2.9" + jest-diff "^28.1.3" + jest-get-type "^28.0.2" + jest-haste-map "^28.1.3" + jest-matcher-utils "^28.1.3" + jest-message-util "^28.1.3" + jest-util "^28.1.3" + natural-compare "^1.4.0" + pretty-format "^28.1.3" + semver "^7.3.5" + +jest-util@^28.0.0, jest-util@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-28.1.3.tgz" + integrity sha512-XdqfpHwpcSRko/C35uLYFM2emRAltIIKZiJ9eAmhjsj0CqZMa0p1ib0R5fWIqGhn1a103DebTbpqIaP1qCQ6tQ== + dependencies: + "@jest/types" "^28.1.3" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.9" + picomatch "^2.2.3" + +jest-validate@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-28.1.3.tgz" + integrity sha512-SZbOGBWEsaTxBGCOpsRWlXlvNkvTkY0XxRfh7zYmvd8uL5Qzyg0CHAXiXKROflh801quA6+/DsT4ODDthOC/OA== + dependencies: + "@jest/types" "^28.1.3" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^28.0.2" + leven "^3.1.0" + pretty-format "^28.1.3" + +jest-watcher@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-28.1.3.tgz" + integrity sha512-t4qcqj9hze+jviFPUN3YAtAEeFnr/azITXQEMARf5cMwKY2SMBRnCQTXLixTl20OR6mLh9KLMrgVJgJISym+1g== + dependencies: + "@jest/test-result" "^28.1.3" + "@jest/types" "^28.1.3" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + emittery "^0.10.2" + jest-util "^28.1.3" + string-length "^4.0.1" + +jest-worker@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz" + integrity sha512-CqRA220YV/6jCo8VWvAt1KKx6eek1VIHMPeLEbpcfSfkEeWyBNppynM/o6q+Wmw+sOhos2ml34wZbSX3G13//g== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz" + integrity sha512-N4GT5on8UkZgH0O5LUavMRV1EDEhNTL0KEfRmDIeZHSV7p2XgLoY9t9VDUgL6o+yfdgYHVxuz81G8oB9VG5uyA== + dependencies: + "@jest/core" "^28.1.3" + "@jest/types" "^28.1.3" + import-local "^3.0.2" + jest-cli "^28.1.3" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json-stringify-safe@5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +json5@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz" + integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +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" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-error@1.x: + version "1.3.6" + resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +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== + +micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +mime-db@1.52.0: + 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.12: + 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" + +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== + +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +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== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" + integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== + +node-releases@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz" + integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + +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" + +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" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +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-limit@^3.0.2, p-limit@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.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-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +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== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +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-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.3, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pirates@^4.0.4: + version "4.0.5" + resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz" + integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== + +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" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@2.7.1: + version "2.7.1" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz" + integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== + +pretty-format@^28.0.0, pretty-format@^28.1.3: + version "28.1.3" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz" + integrity sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q== + dependencies: + "@jest/schemas" "^28.1.3" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +process@0.11.10: + version "0.11.10" + resolved "https://registry.npmjs.org/process/-/process-0.11.10.tgz" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +prompts@^2.0.1: + version "2.4.2" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +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== + +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +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@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +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.exports@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz" + integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== + +resolve@^1.20.0: + version "1.22.1" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz" + 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" + +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.0, 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.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +semver@7.x, semver@^7.3.5, semver@^7.3.7: + version "7.3.7" + resolved "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz" + integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + dependencies: + lru-cache "^6.0.0" + +semver@^6.0.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +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== + +signal-exit@^3.0.3, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-support@0.5.13: + version "0.5.13" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-support@^0.5.21: + 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, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +stack-utils@^2.0.3: + version "2.0.5" + resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz" + integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== + dependencies: + escape-string-regexp "^2.0.0" + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +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== + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +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-hyperlinks@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz" + integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== + dependencies: + has-flag "^4.0.0" + supports-color "^7.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== + +terminal-link@^2.0.0: + version "2.1.1" + resolved "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +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" + +ts-jest@28.0.7: + version "28.0.7" + resolved "https://registry.npmjs.org/ts-jest/-/ts-jest-28.0.7.tgz" + integrity sha512-wWXCSmTwBVmdvWrOpYhal79bDpioDy4rTT+0vyUnE3ZzM7LOAAGG9NXwzkEL/a516rQEgnMmS/WKP9jBPCVJyA== + dependencies: + bs-logger "0.x" + fast-json-stable-stringify "2.x" + jest-util "^28.0.0" + json5 "^2.2.1" + lodash.memoize "4.x" + make-error "1.x" + semver "7.x" + yargs-parser "^21.0.1" + +tslib@^1.8.1: + version "1.14.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslog@3.3.3: + version "3.3.3" + resolved "https://registry.npmjs.org/tslog/-/tslog-3.3.3.tgz" + integrity sha512-lGrkndwpAohZ9ntQpT+xtUw5k9YFV1DjsksiWQlBSf82TTqsSAWBARPRD9juI730r8o3Awpkjp2aXy9k+6vr+g== + dependencies: + source-map-support "^0.5.21" + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-detect@4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +typescript@^4.7.4: + version "4.7.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" + integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + +update-browserslist-db@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.5.tgz" + integrity sha512-dteFFpCyvuDdr9S/ff1ISkKt/9YZxKjI9WlRR99c180GaztJtRa/fn18FdxGVKVsnPY7/a/FDN68mcvUmP4U7Q== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +uuid@8.3.2, uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +uuidv4@^6.2.12: + version "6.2.13" + resolved "https://registry.npmjs.org/uuidv4/-/uuidv4-6.2.13.tgz" + integrity sha512-AXyzMjazYB3ovL3q051VLH06Ixj//Knx7QnUSi1T//Ie3io6CpsPu9nVMOx5MoLWh6xV0B9J0hIaxungxXUbPQ== + dependencies: + "@types/uuid" "8.3.4" + uuid "8.3.2" + +v8-compile-cache@^2.0.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + +v8-to-istanbul@^9.0.1: + version "9.0.1" + resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz" + integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.12" + "@types/istanbul-lib-coverage" "^2.0.1" + convert-source-map "^1.6.0" + +walker@^1.0.8: + version "1.0.8" + resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +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" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +write-file-atomic@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.1.tgz" + integrity sha512-nSKUxgAbyioruk6hU87QzVbY279oYT6uiwgDoujth2ju4mJ+TZau7SQBhtbTmUyuNYTuXnSyRn66FV0+eCgcrQ== + dependencies: + imurmurhash "^0.1.4" + signal-exit "^3.0.7" + +xml@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz" + integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/yaml/-/yaml-2.1.1.tgz" + integrity sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw== + +yargs-parser@^21.0.0, yargs-parser@^21.0.1: + version "21.1.1" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs@^17.3.1: + version "17.5.1" + resolved "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz" + integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.0.0" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/validator-api/validator-api-requests/src/coconut.rs b/validator-api/validator-api-requests/src/coconut.rs index 917934dd52..18afdd81e9 100644 --- a/validator-api/validator-api-requests/src/coconut.rs +++ b/validator-api/validator-api-requests/src/coconut.rs @@ -156,31 +156,3 @@ impl CosmosAddressResponse { CosmosAddressResponse { addr } } } - -#[derive(Serialize, Deserialize, Getters, CopyGetters)] -pub struct ProposeReleaseFundsRequestBody { - #[getset(get = "pub")] - credential: Credential, - #[getset(get = "pub")] - gateway_cosmos_addr: AccountId, -} - -impl ProposeReleaseFundsRequestBody { - pub fn new(credential: Credential, gateway_cosmos_addr: AccountId) -> Self { - ProposeReleaseFundsRequestBody { - credential, - gateway_cosmos_addr, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ProposeReleaseFundsResponse { - pub proposal_id: u64, -} - -impl ProposeReleaseFundsResponse { - pub fn new(proposal_id: u64) -> Self { - ProposeReleaseFundsResponse { proposal_id } - } -} diff --git a/validator-api/validator-api-requests/src/models.rs b/validator-api/validator-api-requests/src/models.rs index faf384fdb2..3018d2b77a 100644 --- a/validator-api/validator-api-requests/src/models.rs +++ b/validator-api/validator-api-requests/src/models.rs @@ -101,20 +101,16 @@ pub type StakeSaturation = f32; )] pub enum SelectionChance { VeryHigh, - High, Moderate, Low, - VeryLow, } impl From for SelectionChance { fn from(p: f64) -> SelectionChance { match p { - p if p >= 1. => SelectionChance::VeryHigh, - p if p > 0.9 => SelectionChance::High, - p if p > 0.7 => SelectionChance::Moderate, - p if p > 0.5 => SelectionChance::Low, - _ => SelectionChance::VeryLow, + p if p > 0.15 => SelectionChance::VeryHigh, + p if p >= 0.05 => SelectionChance::Moderate, + _ => SelectionChance::Low, } } } @@ -123,10 +119,8 @@ impl fmt::Display for SelectionChance { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { SelectionChance::VeryHigh => write!(f, "VeryHigh"), - SelectionChance::High => write!(f, "High"), SelectionChance::Moderate => write!(f, "Moderate"), SelectionChance::Low => write!(f, "Low"), - SelectionChance::VeryLow => write!(f, "VeryLow"), } } } diff --git a/yarn.lock b/yarn.lock index dc79575c76..4b1a89b105 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1162,7 +1162,7 @@ dependencies: regenerator-runtime "^0.13.2" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.17.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.17.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== @@ -1183,6 +1183,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.15.4": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.9.tgz#b4fcfce55db3d2e5e080d2490f608a3b9f407f4a" + integrity sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/runtime@~7.5.4": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132" @@ -5381,44 +5388,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= -"@types/keyv@*": - version "3.1.4" - resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" - integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== - dependencies: - "@types/node" "*" - -"@types/lodash.flattendeep@^4.4.6": - version "4.4.7" - resolved "https://registry.yarnpkg.com/@types/lodash.flattendeep/-/lodash.flattendeep-4.4.7.tgz#0ce3dccbe006826d58e9824b27df4b00ed3e90e6" - integrity sha512-1h6GW/AeZw/Wej6uxrqgmdTDZX1yFS39lRsXYkg+3kWvOWWrlGCI6H7lXxlUHOzxDT4QeYGmgPpQ3BX9XevzOg== - dependencies: - "@types/lodash" "*" - -"@types/lodash.pickby@^4.6.6": - version "4.6.7" - resolved "https://registry.yarnpkg.com/@types/lodash.pickby/-/lodash.pickby-4.6.7.tgz#fd089a5a7f8cbe7294ae5c90ea5ecd9f4cae4d2c" - integrity sha512-4ebXRusuLflfscbD0PUX4eVknDHD9Yf+uMtBIvA/hrnTqeAzbuHuDjvnYriLjUrI9YrhCPVKUf4wkRSXJQ6gig== - dependencies: - "@types/lodash" "*" - -"@types/lodash.union@^4.6.6": - version "4.6.7" - resolved "https://registry.yarnpkg.com/@types/lodash.union/-/lodash.union-4.6.7.tgz#ceace5ed9f3610652ba4a72e0e0afb2a0eec7a4d" - integrity sha512-6HXM6tsnHJzKgJE0gA/LhTGf/7AbjUk759WZ1MziVm+OBNAATHhdgj+a3KVE8g76GCLAnN4ZEQQG1EGgtBIABA== - dependencies: - "@types/lodash" "*" - -"@types/lodash@*", "@types/lodash@^4.14.167": +"@types/lodash@^4.14.167", "@types/lodash@^4.14.175": version "4.14.182" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.182.tgz#05301a4d5e62963227eaafe0ce04dd77c54ea5c2" integrity sha512-/THyiqyQAP9AfARo4pF+aCGcyiQ94tX/Is2I7HofNRqoYLgN1PBoOWu2/zTA5zMxzP5EFutMtWtGAFRKUe961Q== -"@types/lodash@^4.14.175": - version "4.14.179" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.179.tgz#490ec3288088c91295780237d2497a3aa9dfb5c5" - integrity sha512-uwc1x90yCKqGcIOAT6DwOSuxnrAbpkdPsUOZtwrXb4D/6wZs+6qG7QnIawDuZWg0sWpxl+ltIKCaLoMlna678w== - "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -11635,18 +11609,6 @@ glob@^8.0.3: minimatch "^5.0.1" once "^1.3.0" -glob@~7.1.1: - version "7.1.7" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - global@^4.4.0, global@~4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/global/-/global-4.4.0.tgz#3e7b105179006a323ed71aafca3e9c57a5cc6406" @@ -14538,11 +14500,6 @@ marked@^4.0.16: resolved "https://registry.yarnpkg.com/marked/-/marked-4.0.18.tgz#cd0ac54b2e5610cfb90e8fd46ccaa8292c9ed569" integrity sha512-wbLDJ7Zh0sqA0Vdg6aqlbT+yPxqLblpAZh1mK2+AO2twQkPywvvqQNfEPVwSSRjZ7dZcdeVBIAgiO7MMp3Dszw== -marky@^1.2.2: - version "1.2.5" - resolved "https://registry.yarnpkg.com/marky/-/marky-1.2.5.tgz#55796b688cbd72390d2d399eaaf1832c9413e3c0" - integrity sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q== - md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -14846,20 +14803,13 @@ minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.0, minimatch@^5.0.1, minimatch@^5.1.0: +minimatch@^5.0.1, minimatch@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== dependencies: brace-expansion "^2.0.1" -minimatch@~3.0.2: - version "3.0.8" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" - integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== - dependencies: - brace-expansion "^1.1.7" - minimist-options@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619"