Compare commits

...

171 Commits

Author SHA1 Message Date
Jędrzej Stuczyński d6a81d9213 initial validator API-networking related things
adapted from DKG impl
2022-05-23 15:34:48 +01:00
Jędrzej Stuczyński a6db5fe704 Added STATE_DENOM network specific constant 2022-05-23 13:37:53 +01:00
Jędrzej Stuczyński fe57d08f3e actually calling dotenv at validator API startup 2022-05-23 13:36:05 +01:00
Jędrzej Stuczyński ae29b2300c optional serde support for x25519 keys 2022-05-23 13:34:28 +01:00
Jędrzej Stuczyński 7b98d62f96 optional serde support for ed25519 keys 2022-05-23 12:17:58 +01:00
Jędrzej Stuczyński 6abe95ed61 Added abci::Data field to ExecuteResult 2022-05-23 12:12:03 +01:00
Raphaël Walther db743578a9 Add an extra clean step in nightly build 2022-05-23 11:13:07 +02:00
Bogdan-Ștefan Neacșu ba7f535cb7 Update stats provider mainnet client address 2022-05-20 13:23:58 +03:00
Bogdan-Ștefan Neacşu fd4f5b319c Add serviced client data to stats (#1278)
* Add serviced client data to stats

* Put stats service address in network defaults

* Update CHANGELOG.md
2022-05-19 15:54:51 +03:00
Mark Sinclair 6045f57612 Merge pull request #1279 from nymtech/change-text
change text
2022-05-19 12:50:20 +01:00
Mark Sinclair 878cb3f0e5 Merge pull request #1277 from nymtech/av-uptime-nodes-list
adding percentage symbol and make column wider
2022-05-19 12:50:06 +01:00
gala1234 686c0de5eb change text 2022-05-19 13:42:51 +02:00
Drazen Urch 5ce2e0c8bb Optimize fetch and loop (#1271)
* Optimize fetch and loop

* Better operator fetch

* cargo fmt
2022-05-19 11:33:11 +02:00
Drazen Urch 68e120dbf4 Replace checked with saturating sub (#1275)
* Replace checked with saturating sub

* CHANGELOG
2022-05-18 13:33:19 +02:00
gala1234 1362fcdbfa adding percentage symbol and make column wider 2022-05-18 09:41:56 +02:00
Jon Häggblad 3281f10443 wallet: multi account support in wallet-recovery-cli (#1276)
* wallet-recovery-cli: support multi accounts

* wallet-recovery-cli: simplify tests
2022-05-17 22:48:13 +02:00
Jon Häggblad 2409f85e31 Merge pull request #1273 from nymtech/fix/wallet-store-ids-in-state
wallet: store ids in state and require password to switch account
2022-05-17 22:21:01 +02:00
Jon Häggblad 37c06f338f wallet: just print account id in log statement 2022-05-17 21:56:19 +02:00
fmtabbara 8a37df64b5 update methods 2022-05-17 15:43:37 +01:00
fmtabbara 20828d0d28 rework account selection functions 2022-05-17 15:43:22 +01:00
fmtabbara 1e3c8ed3e0 confirm password modal 2022-05-17 15:41:25 +01:00
fmtabbara 830dbcecfc extend component props 2022-05-17 15:41:06 +01:00
fmtabbara a0d3144837 extend component props 2022-05-17 15:40:18 +01:00
fmtabbara a4988e3547 tidy up 2022-05-17 15:39:56 +01:00
fmtabbara 0b585a15b7 use confirm password modal on account selection 2022-05-17 15:39:45 +01:00
fmtabbara 86a93a71e9 relocate modals to their own directory 2022-05-17 15:39:17 +01:00
fmtabbara 967692bf88 create confirm password modal 2022-05-17 15:38:06 +01:00
Jędrzej Stuczyński 662b4d2fff Modified docker setup to start 2 validators in local consensus (#1270)
it was mostly for my own needs, but figured I might create separate PR for that
2022-05-17 09:52:32 +01:00
Jon Häggblad add4747e99 wallet: more test files and regression tests (#1274)
* wallet: add test file for 1.0.4

* wallet: add test file for 1.0.5

* wallet: add test for multi account wallet file

* rustfmt
2022-05-17 10:20:39 +02:00
Bogdan-Ștefan Neacșu 242733f144 Add release configuration for network requester rocket 2022-05-17 11:19:47 +03:00
Jon Häggblad 0cc1926636 Merge branch 'develop' into fix/wallet-store-ids-in-state 2022-05-17 07:22:30 +02:00
Jon Häggblad 0f54c073a7 wallet: simple cli tool for decrypting the wallet (#1272)
* wallet: simple cli tool for decrypting the wallet

* changelog: add note about wallet file recovery tool
2022-05-17 07:18:50 +02:00
Jon Häggblad 3bfce128a6 wallet: store id instead of mnemonic in state 2022-05-17 00:33:23 +02:00
Raphaël Walther 1b3e79e84d Add a way to manually trigger build 2022-05-16 16:43:42 +02:00
Gala e94f99211f Merge pull request #1269 from nymtech/av-uptime-nodes-list
adding average uptime on mix nodes table
2022-05-16 16:04:10 +02:00
gala1234 003ea095cc remove not needed variable, PR request 2022-05-16 14:12:31 +02:00
gala1234 74d93df74b adding av uptime on mix nodes table 2022-05-16 13:41:10 +02:00
Bogdan-Ștefan Neacşu 0c66cc7393 Feature/service stats (#1267)
* Send message from service provider to stats service

* Put some actual data in stats

* Put stats sender on its own thread and send response data too

* Use SQLite for storing stats

* Add the data interval and timestamp

* Fix clippy

* Set description at boot

* Guard stats service functionality under a feature for now

* Make stats service address data into consts

* Add README to network requester

* Retrieve sql data in interval

* Expose sql data via rocket rest api

* Add entry to changelog
2022-05-16 12:06:15 +03:00
Fouad 82abfa5c5c Merge pull request #1265 from nymtech/sprint/wallet-multiple-accounts
Support multiple accounts in the wallet
2022-05-13 12:46:01 +01:00
fmtabbara f7486f0490 add field validation for mnemonic and account name 2022-05-13 12:22:19 +01:00
Jon Häggblad ee5f0f5808 cargo: update lock files 2022-05-13 13:16:03 +02:00
Jon Häggblad 6cb25cf1fb wallet: dont use default wallet when removing 2022-05-13 11:53:27 +02:00
Jon Häggblad db01c245d9 wallet: update wallet error enum cases 2022-05-13 11:53:22 +02:00
Jon Häggblad 53347bec67 changelog: added note about multiple accounts 2022-05-13 11:23:50 +02:00
Jon Häggblad 252385688d wallet: remove unnecessary blank lines 2022-05-13 09:35:27 +02:00
Jon Häggblad cbcef9fbcd wallet: switch from PathBuf to Path 2022-05-13 09:18:21 +02:00
Jon Häggblad e27fc82524 wallet: use login_id and account_id instead of id and inner_id 2022-05-13 09:00:09 +02:00
Jon Häggblad f661bf0446 wallet: write_to_file function 2022-05-12 22:15:38 +02:00
Jon Häggblad 1d22f35c82 wallet: pass by ref 2022-05-12 22:05:40 +02:00
Jon Häggblad fa41fe62c4 wallet: simplify naming 2022-05-12 21:48:23 +02:00
fmtabbara 94a006a725 Merge branch 'develop' into sprint/wallet-multiple-accounts 2022-05-12 15:13:12 +01:00
Jon Häggblad 98cbd2509c wallet: more moving things around 2022-05-12 15:24:38 +02:00
Jon Häggblad d2d99ca5c9 wallet: additional tidy 2022-05-12 14:43:22 +02:00
Jon Häggblad 9e4904ff37 wallet: tweak names of wallet_storage functions 2022-05-12 14:43:22 +02:00
Jon Häggblad bf9db4128d wallet: qualify with wallet_storage 2022-05-12 14:43:22 +02:00
Jon Häggblad bce86235c7 wallet: create LoginId type to distinguish 2022-05-12 11:56:00 +02:00
Tommy Verrall 423f6bfb55 Merge pull request #1266 from nymtech/feature/update-qa
Update QA network defaults
2022-05-12 10:22:12 +01:00
tommy 38fcbb7f2e fmt 2022-05-12 10:21:37 +01:00
tommy 1785b10d91 clean up 2022-05-12 10:20:52 +01:00
Jon Häggblad 7771c5d3d9 wallet: create password with multi-account by default 2022-05-12 10:28:45 +02:00
Jon Häggblad d6206a04bd wallet: restore test in account 2022-05-12 08:37:54 +02:00
Jon Häggblad 83a7f6577b wallet: logout before re-connecting 2022-05-11 22:26:54 +02:00
Jon Häggblad 97c6567139 wallet: restore unit tests 2022-05-11 22:26:48 +02:00
tommy a1534d23af add correct bech32 address as the network-explorer-api was complaining 2022-05-11 18:17:49 +01:00
tommy bcbda85477 re-map coin type for qa 2022-05-11 16:01:04 +01:00
Jon Häggblad 54e95d795e wallet: revert back to old default account name for the time being 2022-05-11 16:48:10 +02:00
tommy 11c0e79725 fmt 2022-05-11 15:03:06 +01:00
tommy 4525be9871 Update QA vars 2022-05-11 14:56:07 +01:00
fmtabbara b83d4ca1a3 reset to initial step after add account 2022-05-11 13:47:41 +01:00
fmtabbara bfb868bfc7 move textfield components to global components 2022-05-11 13:39:01 +01:00
fmtabbara 7b00282b27 reset add account step on completion 2022-05-11 13:21:22 +01:00
Jon Häggblad 54194c03e1 wallet: fix default inner account name 2022-05-11 12:17:08 +02:00
fmtabbara 87c2a317d5 merge develop 2022-05-11 10:54:25 +01:00
Jon Häggblad be92171fec common/config: fix clippy (#1264)
* common/config: fix clippy

* common/config: use cfg_if
2022-05-10 13:32:18 +02:00
Jon Häggblad 04eef83c15 wallet: add backend support for validator name (#1262)
* wallet: add support for validator nymd name

* changelog: add entry for wallt validator name

* rustfmt

* wallet: keep nymd_name entirely on wallet side

* wallet: lint fixes
2022-05-10 11:26:49 +02:00
fmtabbara 25cc7dbebf content update 2022-05-10 10:23:08 +01:00
Jon Häggblad beeb67e9c2 wallet: change default name of first account 2022-05-10 10:25:40 +02:00
Fouad ec19de6fa3 Merge pull request #1227 from nymtech/feature/multi-accounts-integrate-rust
Multiple accounts
2022-05-09 14:40:59 +01:00
fmtabbara 575845af38 fix lint errors 2022-05-09 13:38:38 +01:00
fmtabbara b6b757436e remove edit button until feature delivered 2022-05-09 13:36:38 +01:00
fmtabbara eda69447de add how-to modal for creating multi accounts 2022-05-09 13:31:01 +01:00
Jędrzej Stuczyński 0f6f47c5ac Fixed overflow in determining reconnection backoff (#1260)
* Fixed overflow in determining connection backoff

* Updated changelog
2022-05-09 13:23:14 +01:00
fmtabbara b57c17e5af fix storybook 2022-05-09 10:34:14 +01:00
Drazen Urch c237b37ea6 Use saturating_sub for extra safety (#1258)
* Use saturating_sub for extra safety

* More saturating_sub

* Save some lines

* Add change log
2022-05-09 09:47:03 +02:00
fmtabbara f2fa221489 big tidy and some refactoring of high level component hierachy 2022-05-07 17:53:46 +01:00
Gala d7e82b075f Network Explorer: add economic dynamics (#1223)
* adding delegators number info on mixnode details

* add PM, Delegators and Avg. Uptime fields to the node list hardcoded

* make delegations number dynamic

* fixing bg color bug

* wip node info statistics

* adding basic tooltip new section and some ui

* tooltip customisation

* progress bar styles

* remove not used import

* fix info icons color

* remove discord icon

* Economic dynamics stats endpoint on the explorer API with dummy fixture data

* fetching economic-dynamics-stats

* Populating the endpoint with real data aggregated from validator api

* Introduced new cache functionalities

* using explorer-api data

* adding marging profit

* adding average update

* Update network-explorer.yml

* adding more info on mix nodes page

* display only part of wallet and node id

* typo

* remove log

* adding new values on node response and fix a typo

* remove delegators number column

* Endpoints for average mixnode uptime

* remove TODO

* Clippy

* some ui fixes for percentage linear progress

* GitHub Actions: build storybook for the Network Explorer and add to notification

* Fix file extension to `.ts`

* Fix up formatting and types

* Add storybook

* Add story for mix node details economics

* Fix unused warning

* adding percentage symbol on uptime in mix nodes

* Change eslint config

* some refactor

* progress bar story

* wip refactoring

* more refactor

* adding empty state to the story

* change default values for empty state

* refactor naming and progress bar contrast

* adding hardcoded selection chance and update the storybook

* adding selection chance stories

* adding the progress bar back

* tooltip button padding fix

* Endpoints for average mixnode uptime

* Fix unused warning

* Rustfmt

* moking selection chance response and new colors

* remove log

* fix camelCases issue

* remove hardcoded code

* remove avg_uptime at mixnodes table

* Add jsonchema to uptimeresponse struct

- add the route for avg_uptimes

* adding space between words

* update selection chance colours

* adding the 2 missing tooltips

* fix up uptimeresponse

* fix duplicate entry

* fmt

* validator-client: use statement

* explorer: PR requests

Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
Co-authored-by: Fouad <fmtabbara@hotmail.co.uk>
Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com>
Co-authored-by: Mark Sinclair <mmsinclair@gmail.com>
Co-authored-by: tommy <tommyvez@protonmail.com>
2022-05-06 20:50:26 +01:00
fmtabbara 0e4787f078 minor refactors 2022-05-06 15:54:38 +01:00
Jędrzej Stuczyński f684664472 Removed an 'expect' in mixnet contract query (#1257)
* Removed an 'expect' in mixnet contract query

* Updated changelog
2022-05-06 14:28:57 +01:00
Jędrzej Stuczyński cd5fff92ad Added network information to --version (or --help) command (#1256)
* Added network information to --version (or --help) command

* Updated changelog
2022-05-06 10:46:17 +01:00
fmtabbara 9b0b961d43 content updates 2022-05-06 09:56:13 +01:00
Raphaël Walther 4b95e71adb Merge pull request #1254 from nymtech/cargo_manifest_path
Fixed nightly build mainifest path
2022-05-06 10:39:30 +02:00
Raphaël Walther cde5b66306 Fixed nightly build mainifest path 2022-05-06 09:44:55 +02:00
Jon Häggblad ac72e20447 wallet: additional test for decrypting stored wallet file 2022-05-06 09:41:36 +02:00
Jon Häggblad 571fd5cb93 wallet: removing an account in a login twice should fail 2022-05-06 09:01:48 +02:00
Jon Häggblad 7f3166d230 changelog: fix link 2022-05-05 17:06:27 +02:00
Jon Häggblad 68050d77df changelog: add note about swagger and validator-api 2022-05-05 16:44:38 +02:00
Jędrzej Stuczyński 943337db0d Running tests with the same opt-level in CI, but preserving debug assertions (#1253) 2022-05-05 15:13:04 +01:00
dependabot[bot] 8fbf84174d Bump ejs from 3.1.6 to 3.1.7 in /nym-wallet/webdriver (#1245)
Bumps [ejs](https://github.com/mde/ejs) from 3.1.6 to 3.1.7.
- [Release notes](https://github.com/mde/ejs/releases)
- [Changelog](https://github.com/mde/ejs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mde/ejs/compare/v3.1.6...v3.1.7)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-05 13:47:24 +01:00
dependabot[bot] d42a175289 Bump async from 2.6.3 to 2.6.4 (#1233)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-05 13:47:18 +01:00
dependabot[bot] ac01df0817 Bump async in /clients/native/examples/js-examples/websocket (#1232)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-05 13:47:12 +01:00
dependabot[bot] 7b27065608 Bump async from 2.6.3 to 2.6.4 in /clients/webassembly/js-example (#1231)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-05 13:47:07 +01:00
Jon Häggblad 0523ccdce8 workflow: add wallet rust ci job (#1251)
* workflow: add wallet rust ci job

* workflow: remove not used matrix file

* workflow: add wallet rust part to nightly build

* workflow: add nym-wallet to step names

* workflow: tweak wallet names

* workflow: run on ubuntu-latest

* workflow: skip sccache

* workflow: tweak name

* workflow: switch back to self-hosted tag
2022-05-05 13:58:07 +02:00
Jędrzej Stuczyński df8ae52d8c Added pull request template with changelog checklist 2022-05-05 11:21:25 +01:00
fmtabbara 7cfaf6fa1e Merge branch 'sprint/wallet-multiple-accounts' into feature/multi-accounts-integrate-rust 2022-05-05 11:16:30 +01:00
fmtabbara 6e9eab4edb allow access to prev steps when creating account 2022-05-05 11:15:56 +01:00
Jon Häggblad 0812378fdd wallet: reset account state when adding and removing accounts 2022-05-05 12:13:31 +02:00
Jon Häggblad 188a7ec91d validator-api: reenable swagger openapi (#1249)
* validator-api: reenable swagger openapi

* rustfmt

* validator-api: rename openapi tag to status
2022-05-05 11:35:00 +02:00
Jon Häggblad c91bf3f8d1 wallet: fix rust unit tests (#1252) 2022-05-05 10:31:00 +02:00
fmtabbara ecb27e2cc2 import accounts wip 2022-05-04 22:36:09 +01:00
fmtabbara a1961dbc2f add accounts wip 2022-05-04 21:34:42 +01:00
fmtabbara ef4af0a1db display account errors 2022-05-04 20:49:19 +01:00
fmtabbara 5930ec1f18 Merge branch 'sprint/wallet-multiple-accounts' into feature/multi-accounts-integrate-rust 2022-05-04 20:46:09 +01:00
fmtabbara 72c7049fca set up loading page for account switch 2022-05-04 20:45:33 +01:00
Jon Häggblad 92cbe651de wallet: add account returns wallet entry 2022-05-04 20:14:01 +02:00
Jędrzej Stuczyński 777fcf8cb3 Release/nym wallet v1.0.4 (#1248)
* Bumped up version numbers to 1.0.1

* Updated changelog attempting to use new format

* Bumped up wallet version number

* Updated changelog attempting to use new format

* Updated tauri version
2022-05-04 17:03:08 +01:00
Jędrzej Stuczyński 945dda0c24 Release/1.0.1 (#1247)
* Bumped up version numbers to 1.0.1

* Updated changelog attempting to use new format
2022-05-04 16:57:36 +01:00
fmtabbara b44b074af7 Merge branch 'sprint/wallet-multiple-accounts' into feature/multi-accounts-integrate-rust 2022-05-04 16:22:31 +01:00
fmtabbara 7a0dff5f00 update main state to respond to account changes 2022-05-04 16:22:00 +01:00
fmtabbara 6685b129bb refactor to use accounts context 2022-05-04 16:21:29 +01:00
fmtabbara e0a80c777e stories broken until mock context added 2022-05-04 16:19:52 +01:00
Jędrzej Stuczyński 479d410d20 Feature/broadcast sync with polling (#1246)
* Broadcast tx in a sync mode and poll for its inclusion

* Adjusted internal type used in TauriTxResult

* Re-exported MsgSend

* Increased polling rate + removed print
2022-05-04 16:19:34 +01:00
fmtabbara ab019266cc derive color from name now address 2022-05-04 16:19:25 +01:00
fmtabbara 92fcae9a37 use context for accounts state 2022-05-04 16:18:56 +01:00
Jon Häggblad 413e2662ff wallet: rustfmt 2022-05-04 16:50:51 +02:00
Jon Häggblad e026a532dd wallet: store unlocked accounts in Vec to preserve order 2022-05-04 16:50:01 +02:00
Mark Sinclair a1ca330ce9 GitHub Actions: upload network requester to releases 2022-05-04 14:31:21 +01:00
Jon Häggblad 139e89643c Endpoints for average mixnode uptime (#1238) 2022-05-04 11:28:41 +02:00
fmtabbara a1e0087760 merge sprint branch updates£ 2022-05-04 10:01:39 +01:00
fmtabbara b15cc094ea small refactors 2022-05-04 10:00:17 +01:00
Jędrzej Stuczyński 8eb3f6f862 Changed opt-level for test code to speed up especially dkg tests 2022-05-04 09:54:18 +01:00
Jędrzej Stuczyński 9032d81d52 Fixed dkg benchmarking code to take into account resharing attributes 2022-05-04 09:44:10 +01:00
Jon Häggblad 7adee63ebe wallet: add show_mnemonic_for_account_in_password 2022-05-04 10:01:34 +02:00
Jon Häggblad 1a5580229b wallet: more unit tests 2022-05-04 01:35:57 +02:00
Jon Häggblad df4c6493d4 wallet: split and add more wallet_store tests 2022-05-03 23:20:16 +02:00
Jon Häggblad 30a41261ea wallet: add more log statements 2022-05-03 20:03:46 +02:00
Jon Häggblad 6d874cc34a wallet: remove list_accounts_for_password 2022-04-29 16:54:25 +02:00
Jon Häggblad 7e356ea3b3 wallet: simplify account handling 2022-04-29 16:49:57 +02:00
Jon Häggblad f7be9e7e6f wallet: remove DecryptedAccount and make Accounts clone 2022-04-29 16:27:24 +02:00
Jon Häggblad 05374393ef wallet: contructors for DecryptedAccount 2022-04-29 15:36:06 +02:00
fmtabbara 580656c002 refactor + wip 2022-04-29 14:12:47 +01:00
Jon Häggblad a7caf97b73 wallet: derive_address 2022-04-29 15:12:07 +02:00
fmtabbara 091507b6d8 Merge branch 'sprint/wallet-multiple-accounts' into feature/multi-accounts-integrate-rust 2022-04-29 11:10:21 +01:00
Jon Häggblad 71b5bc9e71 wallet: backend account switching 2022-04-29 11:48:07 +02:00
fmtabbara 8bbffb6a88 prevent bubbling 2022-04-29 10:45:43 +01:00
fmtabbara 31c4fc6807 update display mnemonic component 2022-04-29 10:45:23 +01:00
fmtabbara 66b9b13edc refactor and add more mocks 2022-04-29 10:05:18 +01:00
fmtabbara 1b945ae918 use account switch function 2022-04-27 13:43:39 +01:00
fmtabbara be9b83a87d set up account switching mechanism 2022-04-27 11:19:19 +01:00
Fouad ba1fb17908 Merge pull request #1209 from nymtech/feature/multi-address-wallet
Feature/multi address wallet
2022-04-26 20:51:50 +01:00
fmtabbara 5446874ebe align top bar with nav 2022-04-26 14:29:17 +01:00
fmtabbara afac630a77 add list accounts function 2022-04-25 11:27:07 +01:00
fmtabbara 3250d6982e refactor to separate logic from view 2022-04-14 14:31:55 +01:00
fmtabbara ecdbe1a6fb refactor 2022-04-13 18:08:57 +01:00
fmtabbara 6ba79ee924 use avatar component 2022-04-13 18:07:54 +01:00
fmtabbara 2bebb4b0c2 remove unused component 2022-04-13 18:07:31 +01:00
fmtabbara 81b7d49624 create avatar component 2022-04-13 18:07:06 +01:00
fmtabbara f118a0c854 move story files 2022-04-13 18:06:53 +01:00
fmtabbara db6ecaaecb Merge branch 'sprint/wallet-multiple-accounts' into feature/multi-address-wallet 2022-04-13 14:41:12 +01:00
fmtabbara bd13aa6f35 add new modals 2022-04-13 11:53:13 +01:00
fmtabbara 0cad7f635d new stories 2022-04-12 12:25:15 +01:00
Jon Häggblad a5e6032393 wallet: support multiple accounts per encrypted login (#1205)
* wallet: support multiple accounts per encrypted login

Rework wallet storage to allow grouping accounts under a single
encrypted entry, in a way that is backwards compatible.

* wallet: remove commented out lines
2022-04-12 11:53:38 +02:00
fmtabbara 4edc0700a1 Merge branch 'sprint/wallet-multiple-accounts' into feature/multi-address-wallet 2022-04-12 10:33:43 +01:00
fmtabbara 019a04c0fc code tidy and restructure 2022-04-12 10:33:16 +01:00
Fouad 2e7b8e911f Merge pull request #1201 from nymtech/feature/multi-address-wallet
Feature/multi address wallet
2022-04-08 13:13:09 +01:00
fmtabbara 50f4699c95 change button size 2022-04-07 22:04:19 +01:00
fmtabbara 3265df019a more multi account ui 2022-04-07 22:00:30 +01:00
fmtabbara 902721bda3 yarn lock updated 2022-04-07 16:28:57 +01:00
fmtabbara 585724fc79 more storybook work for multi accounts 2022-04-07 16:27:30 +01:00
fmtabbara c6578384a8 storybook ui for multi addresses 2022-04-07 14:01:58 +01:00
fmtabbara 3b245e16db more multi account UI work 2022-04-07 11:34:48 +01:00
fmtabbara 6d0e2cf491 start component work for multi accounts 2022-04-06 11:25:08 +01:00
246 changed files with 7951 additions and 1180 deletions
+9
View File
@@ -0,0 +1,9 @@
# Description
Closes: #XXXX
<!-- If appropriate, insert relevant description here -->
# Checklist:
- [ ] added a changelog entry to `CHANGELOG.md`
+72
View File
@@ -0,0 +1,72 @@
name: Continuous integration on dispatch
on: workflow_dispatch
jobs:
build:
runs-on: [ self-hosted, custom-linux-exoscale ]
# Enable sccache via environment variable
env:
RUSTC_WRAPPER: /home/ubuntu/.cargo/bin/sccache
steps:
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
- name: Check out repository code
uses: actions/checkout@v2
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
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 --all-features
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
- 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
with:
command: clippy
args: --workspace -- -D warnings
- 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: Run clippy with coconut enabled
uses: actions-rs/cargo@v1
with:
command: clippy
args: --features=coconut -- -D warnings
+14
View File
@@ -31,6 +31,8 @@ jobs:
# continue-on-error: true
- run: yarn && yarn build
continue-on-error: true
- run: yarn storybook:build
name: Build storybook
- name: Deploy branch to CI www
continue-on-error: true
uses: easingthemes/ssh-deploy@main
@@ -42,6 +44,17 @@ jobs:
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/network-explorer-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/"
- name: Deploy storybook to CI www
continue-on-error: true
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-rltgoDzvO --delete"
SOURCE: "explorer/storybook-static/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/ne-sb-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/"
- name: Keybase - Node Install
run: npm install
working-directory: .github/workflows/support-files
@@ -51,6 +64,7 @@ jobs:
NYM_PROJECT_NAME: "Network Explorer"
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
NYM_CI_WWW_LOCATION: "network-explorer-${{ env.GITHUB_REF_SLUG }}"
NYM_CI_WWW_LOCATION_STORYBOOK: "ne-sb-${{ env.GITHUB_REF_SLUG }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}"
+33
View File
@@ -75,6 +75,12 @@ jobs:
command: clippy
args: --workspace --all-targets -- -D warnings
- name: Reclaim some disk space (because Windows is being annoying)
uses: actions-rs/cargo@v1
if: ${{ matrix.os == 'windows-latest' }}
with:
command: clean
# COCONUT stuff
- name: Build all binaries with coconut enabled
uses: actions-rs/cargo@v1
@@ -100,6 +106,33 @@ jobs:
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
@@ -44,3 +44,4 @@ jobs:
target/release/nym-mixnode
target/release/nym-socks5-client
target/release/nym-validator-api
target/release/nym-network-requester
@@ -1,5 +1,6 @@
🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩
> :rocket: {{ env.NYM_PROJECT_NAME }} ➡️➡️➡️➡️➡️ **View output:** https://{{ env.NYM_CI_WWW_LOCATION }}.{{ env.NYM_CI_WWW_BASE }}/
> `storybook`: https://{{ env.NYM_CI_WWW_LOCATION_STORYBOOK }}.{{ env.NYM_CI_WWW_BASE }}
> ✅ **SUCCESS**
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
+57
View File
@@ -0,0 +1,57 @@
name: Nym Wallet (rust)
on:
push:
paths-ignore:
- 'explorer/**'
pull_request:
paths-ignore:
- 'explorer/**'
jobs:
build:
runs-on: [ self-hosted, custom-linux-exoscale ]
steps:
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
- name: Check out repository code
uses: actions/checkout@v2
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt, clippy
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
command: build
args: --manifest-path nym-wallet/Cargo.toml --workspace
- name: Run all tests
uses: actions-rs/cargo@v1
with:
command: test
args: --manifest-path nym-wallet/Cargo.toml --workspace
- name: Check formatting
uses: actions-rs/cargo@v1
with:
command: fmt
args: --manifest-path nym-wallet/Cargo.toml --all -- --check
- uses: actions-rs/clippy-check@v1
name: Clippy checks
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features
- name: Run clippy
uses: actions-rs/cargo@v1
with:
command: clippy
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features -- -D warnings
+53
View File
@@ -1,5 +1,58 @@
# Changelog
## [Unreleased]
### Added
- wallet: require password to switch accounts
- wallet: add simple CLI tool for decrypting and recovering the wallet file.
- wallet: added support for multiple accounts ([#1265])
- wallet: the wallet backend learned how to keep track of validator name, either hardcoded or by querying the status endpoint.
- mixnet-contract: Replace all naked `-` with `saturating_sub`.
- validator-api: add Swagger to document the REST API ([#1249]).
- all: added network compilation target to `--help` (or `--version`) commands ([#1256]).
- network-requester: send traffic statistics from all network requesters and receive it in a special network-requester that aggregates the data and exposes it via a rest API ([#1267], [#1278]).
### Fixed
- vesting-contract: replaced `checked_sub` with `saturating_sub` to fix the underflow in `get_vesting_tokens` ([#1275])
- 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: Under certain circumstances nodes could not be unbonded ([#1255](https://github.com/nymtech/nym/issues/1255)) ([#1258])
- mixnode, gateway: attempting to determine reconnection backoff to persistently failing mixnode could result in a crash ([#1260])
[#1258]: https://github.com/nymtech/nym/pull/1258
[#1249]: https://github.com/nymtech/nym/pull/1249
[#1256]: https://github.com/nymtech/nym/pull/1256
[#1257]: https://github.com/nymtech/nym/pull/1257
[#1260]: https://github.com/nymtech/nym/pull/1260
[#1265]: https://github.com/nymtech/nym/pull/1265
[#1267]: https://github.com/nymtech/nym/pull/1267
[#1275]: https://github.com/nymtech/nym/pull/1275
[#1278]: https://github.com/nymtech/nym/pull/1278
## [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])
## [v1.0.1](https://github.com/nymtech/nym/tree/v1.0.1) (2022-05-04)
### Added
- validator-api: introduced endpoint for getting average mixnode uptime ([#1238])
### 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])
### Fixed
- nym-network-requester: is included in the Github Actions for building release binaries
[#1238]: https://github.com/nymtech/nym/pull/1238
[#1246]: https://github.com/nymtech/nym/pull/1246
## [v1.0.0](https://github.com/nymtech/nym/tree/v1.0.0) (2022-05-03)
[Full Changelog](https://github.com/nymtech/nym/compare/v0.12.1...v1.0.0)
Generated
+22 -10
View File
@@ -581,7 +581,7 @@ dependencies = [
[[package]]
name = "client-core"
version = "1.0.0"
version = "1.0.1"
dependencies = [
"config",
"crypto",
@@ -648,6 +648,7 @@ dependencies = [
name = "config"
version = "0.1.0"
dependencies = [
"cfg-if 1.0.0",
"handlebars",
"humantime-serde",
"log",
@@ -1062,6 +1063,8 @@ dependencies = [
"pemstore",
"rand 0.7.3",
"rand_chacha 0.2.2",
"serde",
"serde_bytes",
"subtle-encoding",
"x25519-dalek",
]
@@ -1426,6 +1429,7 @@ version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d5c4b5e5959dc2c2b89918d8e2cc40fcdd623cef026ed09d2f0ee05199dc8e4"
dependencies = [
"serde",
"signature",
]
@@ -1439,6 +1443,7 @@ dependencies = [
"ed25519",
"rand 0.7.3",
"serde",
"serde_bytes",
"sha2",
"zeroize",
]
@@ -1594,7 +1599,7 @@ dependencies = [
[[package]]
name = "explorer-api"
version = "1.0.0"
version = "1.0.1"
dependencies = [
"chrono",
"humantime-serde",
@@ -3043,7 +3048,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.0.0"
version = "1.0.1"
dependencies = [
"clap 2.34.0",
"client-core",
@@ -3078,7 +3083,7 @@ dependencies = [
[[package]]
name = "nym-gateway"
version = "1.0.0"
version = "1.0.1"
dependencies = [
"anyhow",
"async-trait",
@@ -3124,7 +3129,7 @@ dependencies = [
[[package]]
name = "nym-mixnode"
version = "1.0.0"
version = "1.0.1"
dependencies = [
"anyhow",
"bs58",
@@ -3162,20 +3167,26 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.0.0"
version = "1.0.1"
dependencies = [
"bincode",
"clap 2.34.0",
"dirs",
"futures",
"ipnetwork",
"log",
"network-defaults",
"nymsphinx",
"ordered-buffer",
"pretty_env_logger",
"proxy-helpers",
"publicsuffix",
"rand 0.7.3",
"rocket",
"serde",
"socks5-requests",
"sqlx",
"thiserror",
"tokio",
"tokio-tungstenite",
"websocket-requests",
@@ -3183,7 +3194,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.0.0"
version = "1.0.1"
dependencies = [
"clap 2.34.0",
"client-core",
@@ -3219,7 +3230,7 @@ dependencies = [
[[package]]
name = "nym-validator-api"
version = "1.0.0"
version = "1.0.1"
dependencies = [
"anyhow",
"async-trait",
@@ -4827,9 +4838,9 @@ dependencies = [
[[package]]
name = "serde_bytes"
version = "0.11.5"
version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9"
checksum = "212e73464ebcde48d723aa02eb270ba62eff38a9b732df31f33f1b4e145f3a54"
dependencies = [
"serde",
]
@@ -5218,6 +5229,7 @@ dependencies = [
"bitflags",
"byteorder",
"bytes",
"chrono",
"crc",
"crossbeam-queue",
"either",
+4
View File
@@ -9,6 +9,10 @@ overflow-checks = true
[profile.dev]
panic = "abort"
[profile.test]
# equivalent of running in `--release` (but since we're in test profile we're keeping overflow checks and all of those by default)
opt-level = 3
[workspace]
resolver = "2"
+1 -1
View File
@@ -26,7 +26,7 @@ clippy-all-wallet:
cargo clippy --workspace --manifest-path nym-wallet/Cargo.toml --all-features -- -D warnings
test-main:
cargo test --all-features --workspace --release
cargo test --all-features --workspace
test-contracts:
cargo test --manifest-path contracts/Cargo.toml --all-features
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "client-core"
version = "1.0.0"
version = "1.0.1"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.0.0"
version = "1.0.1"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
rust-version = "1.56"
@@ -592,9 +592,9 @@
}
},
"node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"dependencies": {
"lodash": "^4.17.14"
@@ -4825,9 +4825,9 @@
"dev": true
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
+4
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, App, ArgMatches};
use network_defaults::DEFAULT_NETWORK;
pub mod client;
pub mod commands;
@@ -67,6 +68,7 @@ fn long_version() -> String {
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
"#,
"Build Timestamp:",
env!("VERGEN_BUILD_TIMESTAMP"),
@@ -84,6 +86,8 @@ fn long_version() -> String {
env!("VERGEN_RUSTC_CHANNEL"),
"cargo Profile:",
env!("VERGEN_CARGO_PROFILE"),
"Network:",
DEFAULT_NETWORK
)
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.0.0"
version = "1.0.1"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
rust-version = "1.56"
+4
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, App, ArgMatches};
use network_defaults::DEFAULT_NETWORK;
pub mod client;
mod commands;
@@ -67,6 +68,7 @@ fn long_version() -> String {
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
"#,
"Build Timestamp:",
env!("VERGEN_BUILD_TIMESTAMP"),
@@ -84,6 +86,8 @@ fn long_version() -> String {
env!("VERGEN_RUSTC_CHANNEL"),
"cargo Profile:",
env!("VERGEN_CARGO_PROFILE"),
"Network:",
DEFAULT_NETWORK
)
}
+5 -3
View File
@@ -16,7 +16,7 @@ use proxy_helpers::connection_controller::{
};
use proxy_helpers::proxy_runner::ProxyRunner;
use rand::RngCore;
use socks5_requests::{ConnectionId, RemoteAddress, Request};
use socks5_requests::{ConnectionId, Message, RemoteAddress, Request};
use std::io;
use std::net::SocketAddr;
use std::pin::Pin;
@@ -224,8 +224,9 @@ impl SocksClient {
async fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) {
let req = Request::new_connect(self.connection_id, remote_address, self.self_address);
let msg = Message::Request(req);
let input_message = InputMessage::new_fresh(self.service_provider, req.into_bytes(), false);
let input_message = InputMessage::new_fresh(self.service_provider, msg.into_bytes(), false);
self.input_sender.unbounded_send(input_message).unwrap();
}
@@ -252,7 +253,8 @@ impl SocksClient {
)
.run(move |conn_id, read_data, socket_closed| {
let provider_request = Request::new_send(conn_id, read_data, socket_closed);
InputMessage::new_fresh(recipient, provider_request.into_bytes(), false)
let provider_message = Message::Request(provider_request);
InputMessage::new_fresh(recipient, provider_message.into_bytes(), false)
})
.await
.into_inner();
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client-wasm"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jedrzej Stuczynski <andrew@nymtech.net>"]
version = "1.0.0"
version = "1.0.1"
edition = "2021"
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
license = "Apache-2.0"
+7 -8
View File
@@ -24,8 +24,7 @@
},
"../pkg": {
"name": "@nymproject/nym-client-wasm",
"version": "0.12.0",
"license": "Apache-2.0"
"version": "0.0.1"
},
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.7",
@@ -586,9 +585,9 @@
}
},
"node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"dependencies": {
"lodash": "^4.17.14"
@@ -4305,9 +4304,9 @@
"dev": true
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
+48 -12
View File
@@ -133,20 +133,14 @@ impl Client {
if current_attempt == 0 {
None
} else {
// according to https://github.com/tokio-rs/tokio/issues/1953 there's an undocumented
// limit of tokio delay of about 2 years.
// let's ensure our delay is always on a sane side of being maximum 1 hour.
let maximum_sane_delay = Duration::from_secs(60 * 60);
let exp = 2_u32.checked_pow(current_attempt);
let backoff = exp
.and_then(|exp| self.config.initial_reconnection_backoff.checked_mul(exp))
.unwrap_or(self.config.maximum_reconnection_backoff);
Some(std::cmp::min(
maximum_sane_delay,
std::cmp::min(
self.config
.initial_reconnection_backoff
.checked_mul(2_u32.pow(current_attempt))
.unwrap_or(self.config.maximum_reconnection_backoff),
self.config.maximum_reconnection_backoff,
),
backoff,
self.config.maximum_reconnection_backoff,
))
}
}
@@ -254,3 +248,45 @@ impl SendWithoutResponse for Client {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dummy_client() -> Client {
Client::new(Config {
initial_reconnection_backoff: Duration::from_millis(10_000),
maximum_reconnection_backoff: Duration::from_millis(300_000),
initial_connection_timeout: Duration::from_millis(1_500),
maximum_connection_buffer_size: 128,
})
}
#[test]
fn determining_backoff_works_regardless_of_attempt() {
let client = dummy_client();
assert!(client.determine_backoff(0).is_none());
assert!(client.determine_backoff(1).is_some());
assert!(client.determine_backoff(2).is_some());
assert_eq!(
client.determine_backoff(16).unwrap(),
client.config.maximum_reconnection_backoff
);
assert_eq!(
client.determine_backoff(32).unwrap(),
client.config.maximum_reconnection_backoff
);
assert_eq!(
client.determine_backoff(1024).unwrap(),
client.config.maximum_reconnection_backoff
);
assert_eq!(
client.determine_backoff(65536).unwrap(),
client.config.maximum_reconnection_backoff
);
assert_eq!(
client.determine_backoff(u32::MAX).unwrap(),
client.config.maximum_reconnection_backoff
);
}
}
@@ -5,6 +5,9 @@ use crate::{validator_api, ValidatorClientError};
use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse};
use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixNodeBond};
use url::Url;
#[cfg(feature = "nymd-client")]
use validator_api_requests::models::UptimeResponse;
use validator_api_requests::models::{
CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse,
StakeSaturationResponse,
@@ -582,6 +585,12 @@ impl<C> Client<C> {
Ok(delegations)
}
pub async fn get_mixnode_avg_uptimes(
&self,
) -> Result<Vec<UptimeResponse>, ValidatorClientError> {
Ok(self.validator_api.get_mixnode_avg_uptimes().await?)
}
pub async fn blind_sign(
&self,
request_body: &BlindSignRequestBody,
@@ -30,12 +30,26 @@ use cosmwasm_std::Coin as CosmWasmCoin;
use prost::Message;
use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
use std::time::Duration;
#[async_trait]
impl CosmWasmClient for HttpClient {}
impl CosmWasmClient for HttpClient {
fn broadcast_polling_rate(&self) -> Duration {
Duration::from_secs(4)
}
fn broadcast_timeout(&self) -> Duration {
Duration::from_secs(60)
}
}
#[async_trait]
pub trait CosmWasmClient: rpc::Client {
// this should probably get redesigned, but I'm leaving those like that temporarily to fix
// the underlying issue more quickly
fn broadcast_polling_rate(&self) -> Duration;
fn broadcast_timeout(&self) -> Duration;
// helper method to remove duplicate code involved in making abci requests with protobuf messages
// TODO: perhaps it should have an additional argument to determine whether the response should
// require proof?
@@ -253,6 +267,42 @@ pub trait CosmWasmClient: rpc::Client {
Ok(rpc::Client::broadcast_tx_commit(self, tx).await?)
}
async fn broadcast_tx(&self, tx: Transaction) -> Result<TxResponse, NymdError> {
let broadcasted = CosmWasmClient::broadcast_tx_sync(self, tx).await?;
if broadcasted.code.is_err() {
let code_val = broadcasted.code.value();
return Err(NymdError::BroadcastTxErrorDeliverTx {
hash: broadcasted.hash,
height: None,
code: code_val,
raw_log: broadcasted.log.to_string(),
});
}
let tx_hash = broadcasted.hash;
let start = tokio::time::Instant::now();
loop {
log::debug!(
"Polling for result of including {} in a block...",
broadcasted.hash
);
if tokio::time::Instant::now().duration_since(start) >= self.broadcast_timeout() {
return Err(NymdError::BroadcastTimeout {
hash: tx_hash,
timeout: self.broadcast_timeout(),
});
}
if let Ok(poll_res) = self.get_tx(tx_hash).await {
return Ok(poll_res);
}
tokio::time::sleep(self.broadcast_polling_rate()).await;
}
}
async fn get_codes(&self) -> Result<Vec<Code>, NymdError> {
let path = Some("/cosmwasm.wasm.v1.Query/Codes".parse().unwrap());
@@ -19,7 +19,7 @@ impl CheckResponse for broadcast::tx_commit::Response {
if self.check_tx.code.is_err() {
return Err(NymdError::BroadcastTxErrorCheckTx {
hash: self.hash,
height: self.height,
height: Some(self.height),
code: self.check_tx.code.value(),
raw_log: self.check_tx.log.value().to_owned(),
});
@@ -28,7 +28,7 @@ impl CheckResponse for broadcast::tx_commit::Response {
if self.deliver_tx.code.is_err() {
return Err(NymdError::BroadcastTxErrorDeliverTx {
hash: self.hash,
height: self.height,
height: Some(self.height),
code: self.deliver_tx.code.value(),
raw_log: self.deliver_tx.log.value().to_owned(),
});
@@ -38,6 +38,21 @@ impl CheckResponse for broadcast::tx_commit::Response {
}
}
impl CheckResponse for crate::nymd::TxResponse {
fn check_response(self) -> Result<Self, NymdError> {
if self.tx_result.code.is_err() {
return Err(NymdError::BroadcastTxErrorDeliverTx {
hash: self.hash,
height: Some(self.height),
code: self.tx_result.code.value(),
raw_log: self.tx_result.log.value().to_owned(),
});
}
Ok(self)
}
}
pub(crate) fn compress_wasm_code(code: &[u8]) -> Result<Vec<u8>, NymdError> {
// using compression level 9, same as cosmjs, that optimises for size
let mut encoder = GzEncoder::new(Vec::new(), Compression::best());
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use std::convert::TryInto;
use std::time::Duration;
use async_trait::async_trait;
use cosmrs::bank::MsgSend;
@@ -24,7 +25,7 @@ use crate::nymd::cosmwasm_client::types::*;
use crate::nymd::error::NymdError;
use crate::nymd::fee::{Fee, DEFAULT_SIMULATED_GAS_MULTIPLIER};
use crate::nymd::wallet::DirectSecp256k1HdWallet;
use crate::nymd::{CosmosCoin, GasPrice};
use crate::nymd::{CosmosCoin, GasPrice, TxResponse};
// we need to have **a** valid secp256k1 signature for simulation purposes.
// it doesn't matter what it is as long as it parses correctly
@@ -35,6 +36,9 @@ const DUMMY_SECP256K1_SIGNATURE: &[u8] = &[
91,
];
const DEFAULT_BROADCAST_POLLING_RATE: Duration = Duration::from_secs(4);
const DEFAULT_BROADCAST_TIMEOUT: Duration = Duration::from_secs(60);
#[async_trait]
pub trait SigningCosmWasmClient: CosmWasmClient {
fn signer(&self) -> &DirectSecp256k1HdWallet;
@@ -111,12 +115,12 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgStoreCode".to_owned()))?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, vec![upload_msg], fee, memo)
.sign_and_broadcast(sender_address, vec![upload_msg], fee, memo)
.await?
.check_response()?;
let logs = parse_raw_logs(tx_res.deliver_tx.log)?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let logs = parse_raw_logs(tx_res.tx_result.log)?;
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
// TODO: should those strings be extracted into some constants?
// the reason I think unwrap here is fine is that if the transaction succeeded and those
@@ -172,12 +176,12 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgInstantiateContract".to_owned()))?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, vec![init_msg], fee, memo)
.sign_and_broadcast(sender_address, vec![init_msg], fee, memo)
.await?
.check_response()?;
let logs = parse_raw_logs(tx_res.deliver_tx.log)?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let logs = parse_raw_logs(tx_res.tx_result.log)?;
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
// TODO: should those strings be extracted into some constants?
// the reason I think unwrap here is fine is that if the transaction succeeded and those
@@ -214,14 +218,14 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgUpdateAdmin".to_owned()))?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, vec![change_admin_msg], fee, memo)
.sign_and_broadcast(sender_address, vec![change_admin_msg], fee, memo)
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
Ok(ChangeAdminResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
logs: parse_raw_logs(tx_res.tx_result.log)?,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -242,14 +246,14 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgClearAdmin".to_owned()))?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, vec![change_admin_msg], fee, memo)
.sign_and_broadcast(sender_address, vec![change_admin_msg], fee, memo)
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
Ok(ChangeAdminResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
logs: parse_raw_logs(tx_res.tx_result.log)?,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -277,14 +281,14 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgMigrateContract".to_owned()))?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, vec![migrate_msg], fee, memo)
.sign_and_broadcast(sender_address, vec![migrate_msg], fee, memo)
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
Ok(MigrateResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
logs: parse_raw_logs(tx_res.tx_result.log)?,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -312,14 +316,15 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.map_err(|_| NymdError::SerializationError("MsgExecuteContract".to_owned()))?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, vec![execute_msg], fee, memo)
.sign_and_broadcast(sender_address, vec![execute_msg], fee, memo)
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
Ok(ExecuteResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
logs: parse_raw_logs(tx_res.tx_result.log)?,
data: tx_res.tx_result.data,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -352,14 +357,15 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.collect::<Result<_, _>>()?;
let tx_res = self
.sign_and_broadcast_commit(sender_address, messages, fee, memo)
.sign_and_broadcast(sender_address, messages, fee, memo)
.await?
.check_response()?;
let gas_info = GasInfo::new(tx_res.deliver_tx.gas_wanted, tx_res.deliver_tx.gas_used);
let gas_info = GasInfo::new(tx_res.tx_result.gas_wanted, tx_res.tx_result.gas_used);
Ok(ExecuteResult {
logs: parse_raw_logs(tx_res.deliver_tx.log)?,
logs: parse_raw_logs(tx_res.tx_result.log)?,
data: tx_res.tx_result.data,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -372,7 +378,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
amount: Vec<Coin>,
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError> {
) -> Result<TxResponse, NymdError> {
let send_msg = MsgSend {
from_address: sender_address.clone(),
to_address: recipient_address.clone(),
@@ -381,7 +387,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.to_any()
.map_err(|_| NymdError::SerializationError("MsgSend".to_owned()))?;
self.sign_and_broadcast_commit(sender_address, vec![send_msg], fee, memo)
self.sign_and_broadcast(sender_address, vec![send_msg], fee, memo)
.await?
.check_response()
}
@@ -392,7 +398,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
msgs: I,
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError>
) -> Result<TxResponse, NymdError>
where
I: IntoIterator<Item = (AccountId, Vec<Coin>)> + Send,
{
@@ -409,7 +415,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
})
.collect::<Result<_, _>>()?;
self.sign_and_broadcast_commit(sender_address, messages, fee, memo)
self.sign_and_broadcast(sender_address, messages, fee, memo)
.await?
.check_response()
}
@@ -421,7 +427,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
amount: Coin,
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError> {
) -> Result<TxResponse, NymdError> {
let delegate_msg = MsgDelegate {
delegator_address: delegator_address.to_owned(),
validator_address: validator_address.to_owned(),
@@ -430,7 +436,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.to_any()
.map_err(|_| NymdError::SerializationError("MsgDelegate".to_owned()))?;
self.sign_and_broadcast_commit(delegator_address, vec![delegate_msg], fee, memo)
self.sign_and_broadcast(delegator_address, vec![delegate_msg], fee, memo)
.await?
.check_response()
}
@@ -442,7 +448,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
amount: Coin,
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError> {
) -> Result<TxResponse, NymdError> {
let undelegate_msg = MsgUndelegate {
delegator_address: delegator_address.to_owned(),
validator_address: validator_address.to_owned(),
@@ -451,7 +457,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.to_any()
.map_err(|_| NymdError::SerializationError("MsgUndelegate".to_owned()))?;
self.sign_and_broadcast_commit(delegator_address, vec![undelegate_msg], fee, memo)
self.sign_and_broadcast(delegator_address, vec![undelegate_msg], fee, memo)
.await?
.check_response()
}
@@ -462,7 +468,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
validator_address: &AccountId,
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError> {
) -> Result<TxResponse, NymdError> {
let withdraw_msg = MsgWithdrawDelegatorReward {
delegator_address: delegator_address.to_owned(),
validator_address: validator_address.to_owned(),
@@ -470,7 +476,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
.to_any()
.map_err(|_| NymdError::SerializationError("MsgWithdrawDelegatorReward".to_owned()))?;
self.sign_and_broadcast_commit(delegator_address, vec![withdraw_msg], fee, memo)
self.sign_and_broadcast(delegator_address, vec![withdraw_msg], fee, memo)
.await?
.check_response()
}
@@ -573,6 +579,27 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
CosmWasmClient::broadcast_tx_commit(self, tx_bytes.into()).await
}
/// Broadcast a transaction to the network and monitors its inclusion in a block.
async fn sign_and_broadcast(
&self,
signer_address: &AccountId,
messages: Vec<Any>,
fee: Fee,
memo: impl Into<String> + Send + 'static,
) -> Result<TxResponse, NymdError> {
let memo = memo.into();
let fee = self
.determine_transaction_fee(signer_address, &messages, fee, &memo)
.await?;
let tx_raw = self.sign(signer_address, messages, fee, memo).await?;
let tx_bytes = tx_raw
.to_bytes()
.map_err(|_| NymdError::SerializationError("Tx".to_owned()))?;
self.broadcast_tx(tx_bytes.into()).await
}
fn sign_direct(
&self,
signer_address: &AccountId,
@@ -638,6 +665,9 @@ pub struct Client {
rpc_client: HttpClient,
signer: DirectSecp256k1HdWallet,
gas_price: GasPrice,
broadcast_polling_rate: Duration,
broadcast_timeout: Duration,
}
impl Client {
@@ -654,8 +684,18 @@ impl Client {
rpc_client,
signer,
gas_price,
broadcast_polling_rate: DEFAULT_BROADCAST_POLLING_RATE,
broadcast_timeout: DEFAULT_BROADCAST_TIMEOUT,
})
}
pub fn set_broadcast_polling_rate(&mut self, broadcast_polling_rate: Duration) {
self.broadcast_polling_rate = broadcast_polling_rate
}
pub fn set_broadcast_timeout(&mut self, broadcast_timeout: Duration) {
self.broadcast_timeout = broadcast_timeout
}
}
#[async_trait]
@@ -669,7 +709,15 @@ impl rpc::Client for Client {
}
#[async_trait]
impl CosmWasmClient for Client {}
impl CosmWasmClient for Client {
fn broadcast_polling_rate(&self) -> Duration {
self.broadcast_polling_rate
}
fn broadcast_timeout(&self) -> Duration {
self.broadcast_timeout
}
}
#[async_trait]
impl SigningCosmWasmClient for Client {
@@ -25,6 +25,7 @@ use cosmrs::proto::cosmwasm::wasm::v1::{
CodeInfoResponse, ContractCodeHistoryEntry as ProtoContractCodeHistoryEntry,
ContractCodeHistoryOperationType, ContractInfo as ProtoContractInfo,
};
use cosmrs::tendermint::abci::Data;
use cosmrs::tendermint::{abci, chain};
use cosmrs::tx::{AccountNumber, Gas, SequenceNumber};
use cosmrs::{tx, AccountId, Any, Coin};
@@ -672,6 +673,8 @@ pub struct MigrateResult {
pub struct ExecuteResult {
pub logs: Vec<Log>,
pub data: Data,
/// Transaction hash (might be used as transaction ID)
pub transaction_hash: tx::Hash,
@@ -5,6 +5,7 @@ use crate::nymd::cosmwasm_client::types::ContractCodeId;
use cosmrs::tendermint::{abci, block};
use cosmrs::{bip32, tx, AccountId};
use std::io;
use std::time::Duration;
use thiserror::Error;
pub use cosmrs::rpc::error::{
@@ -81,21 +82,21 @@ pub enum NymdError {
MalformedLogString,
#[error(
"Error when broadcasting tx {hash} at height {height}. Error occurred during CheckTx phase. Code: {code}; Raw log: {raw_log}"
"Error when broadcasting tx {hash} at height {height:?}. Error occurred during CheckTx phase. Code: {code}; Raw log: {raw_log}"
)]
BroadcastTxErrorCheckTx {
hash: tx::Hash,
height: block::Height,
height: Option<block::Height>,
code: u32,
raw_log: String,
},
#[error(
"Error when broadcasting tx {hash} at height {height}. Error occurred during DeliverTx phase. Code: {code}; Raw log: {raw_log}"
"Error when broadcasting tx {hash} at height {height:?}. Error occurred during DeliverTx phase. Code: {code}; Raw log: {raw_log}"
)]
BroadcastTxErrorDeliverTx {
hash: tx::Hash,
height: block::Height,
height: Option<block::Height>,
code: u32,
raw_log: String,
},
@@ -117,6 +118,9 @@ pub enum NymdError {
#[error("This account does not have BaseAccount information available to it")]
NoBaseAccountInformationAvailable,
#[error("Transaction with ID {hash} has been submitted but not yet found on the chain. You might want to check for it later. There was a total wait of {} seconds", .timeout.as_secs())]
BroadcastTimeout { hash: tx::Hash, timeout: Duration },
}
impl NymdError {
@@ -8,7 +8,6 @@ use crate::nymd::cosmwasm_client::types::{
};
use crate::nymd::error::NymdError;
use crate::nymd::wallet::DirectSecp256k1HdWallet;
use cosmrs::rpc::endpoint::broadcast;
use cosmrs::rpc::Error as TendermintRpcError;
use cosmrs::rpc::HttpClientUrl;
use cosmwasm_std::{Coin, Uint128};
@@ -23,12 +22,14 @@ use mixnet_contract_common::{
PagedRewardedSetResponse, QueryMsg, RewardedSetUpdateDetails,
};
use serde::Serialize;
use std::collections::HashMap;
use std::convert::TryInto;
pub use crate::nymd::cosmwasm_client::client::CosmWasmClient;
pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient;
pub use crate::nymd::fee::Fee;
use crate::nymd::fee::DEFAULT_SIMULATED_GAS_MULTIPLIER;
pub use cosmrs::bank::MsgSend;
pub use cosmrs::rpc::endpoint::tx::Response as TxResponse;
pub use cosmrs::rpc::endpoint::validators::Response as ValidatorResponse;
pub use cosmrs::rpc::HttpClient as QueryNymdClient;
@@ -43,7 +44,6 @@ pub use cosmrs::tx::{self, Gas};
pub use cosmrs::Coin as CosmosCoin;
pub use cosmrs::{AccountId, Decimal, Denom};
pub use signing_client::Client as SigningNymdClient;
use std::collections::HashMap;
pub use traits::{VestingQueryClient, VestingSigningClient};
pub mod cosmwasm_client;
@@ -640,7 +640,7 @@ impl<C> NymdClient<C> {
recipient: &AccountId,
amount: Vec<CosmosCoin>,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError>
) -> Result<TxResponse, NymdError>
where
C: SigningCosmWasmClient + Sync,
{
@@ -655,7 +655,7 @@ impl<C> NymdClient<C> {
&self,
msgs: Vec<(AccountId, Vec<CosmosCoin>)>,
memo: impl Into<String> + Send + 'static,
) -> Result<broadcast::tx_commit::Response, NymdError>
) -> Result<TxResponse, NymdError>
where
C: SigningCosmWasmClient + Sync,
{
@@ -10,7 +10,7 @@ use std::collections::HashMap;
use url::Url;
use validator_api_requests::models::{
CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse,
RewardEstimationResponse, StakeSaturationResponse,
RewardEstimationResponse, StakeSaturationResponse, UptimeResponse,
};
pub mod error;
@@ -253,6 +253,36 @@ impl Client {
.await
}
pub async fn get_mixnode_avg_uptime(
&self,
identity: IdentityKeyRef<'_>,
) -> Result<UptimeResponse, ValidatorAPIError> {
self.query_validator_api(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
identity,
routes::AVG_UPTIME,
],
NO_PARAMS,
)
.await
}
pub async fn get_mixnode_avg_uptimes(&self) -> Result<Vec<UptimeResponse>, ValidatorAPIError> {
self.query_validator_api(
&[
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODES,
routes::AVG_UPTIME,
],
NO_PARAMS,
)
.await
}
pub async fn blind_sign(
&self,
request_body: &BlindSignRequestBody,
@@ -26,5 +26,6 @@ pub const SINCE_ARG: &str = "since";
pub const STATUS: &str = "status";
pub const REWARD_ESTIMATION: &str = "reward-estimation";
pub const AVG_UPTIME: &str = "avg_uptime";
pub const STAKE_SATURATION: &str = "stake-saturation";
pub const INCLUSION_CHANCE: &str = "inclusion-probability";
+1
View File
@@ -7,6 +7,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cfg-if = "1.0.0"
handlebars = "3.0.1"
humantime-serde = "1.0"
log = "0.4"
+10 -8
View File
@@ -69,14 +69,16 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
let location = custom_location
.unwrap_or_else(|| self.config_directory().join(Self::config_file_name()));
fs::write(location.clone(), templated_config)?;
#[cfg(unix)]
let mut perms = fs::metadata(location.clone())?.permissions();
#[cfg(unix)]
perms.set_mode(0o600);
#[cfg(unix)]
fs::set_permissions(location, perms)?;
cfg_if::cfg_if! {
if #[cfg(unix)] {
fs::write(location.clone(), templated_config)?;
let mut perms = fs::metadata(location.clone())?.permissions();
perms.set_mode(0o600);
fs::set_permissions(location, perms)?;
} else {
fs::write(location, templated_config)?;
}
}
Ok(())
}
@@ -47,11 +47,8 @@ impl NodeEpochRewards {
pub fn node_profit(&self) -> U128 {
let reward = U128::from_num(self.reward().u128());
if reward < self.operator_cost() {
U128::from_num(0u128)
} else {
reward - self.operator_cost()
}
// if operating cost is higher then the reward node profit is 0
reward.saturating_sub(self.operator_cost())
}
pub fn operator_reward(&self, profit_margin: U128) -> Result<Uint128, MixnetContractError> {
+3
View File
@@ -19,6 +19,8 @@ cipher = { version = "0.4.3", optional = true }
x25519-dalek = { version = "1.1", optional = true }
ed25519-dalek = { version = "1.0", optional = true }
rand = { version = "0.7.3", features = ["wasm-bindgen"], optional = true }
serde_bytes = { version = "0.11.6", optional = true }
serde_crate = { version = "1.0", optional = true, default_features = false, package = "serde" }
subtle-encoding = { version = "0.5", features = ["bech32-preview"]}
# internal
@@ -30,6 +32,7 @@ config = { path="../../common/config" }
rand_chacha = "0.2"
[features]
serde = ["serde_crate", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
asymmetric = ["x25519-dalek", "ed25519-dalek"]
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array"]
symmetric = ["aes", "ctr", "cipher", "generic-array"]
+9 -3
View File
@@ -68,6 +68,7 @@ pub fn creating_dealing_for_3_parties(c: &mut Criterion) {
threshold,
epoch,
&receivers,
None,
)
})
})
@@ -89,6 +90,7 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri
threshold,
epoch,
&receivers,
None,
);
let first_key = dks.get_mut(0).unwrap();
@@ -99,7 +101,7 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri
|b| {
b.iter(|| {
assert!(dealing
.verify(&params, epoch, threshold, &receivers)
.verify(&params, epoch, threshold, &receivers, None)
.is_ok());
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
})
@@ -128,6 +130,7 @@ pub fn creating_dealing_for_20_parties(c: &mut Criterion) {
threshold,
epoch,
&receivers,
None,
)
})
})
@@ -150,6 +153,7 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter
threshold,
epoch,
&receivers,
None,
);
let first_key = dks.get_mut(0).unwrap();
@@ -160,7 +164,7 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter
|b| {
b.iter(|| {
assert!(dealing
.verify(&params, epoch, threshold, &receivers)
.verify(&params, epoch, threshold, &receivers, None)
.is_ok());
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
})
@@ -189,6 +193,7 @@ pub fn creating_dealing_for_100_parties(c: &mut Criterion) {
threshold,
epoch,
&receivers,
None,
)
})
})
@@ -211,6 +216,7 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite
threshold,
epoch,
&receivers,
None,
);
let first_key = dks.get_mut(0).unwrap();
@@ -221,7 +227,7 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite
|b| {
b.iter(|| {
assert!(dealing
.verify(&params, epoch, threshold, &receivers)
.verify(&params, epoch, threshold, &receivers, None)
.is_ok());
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap());
})
+46 -1
View File
@@ -4,6 +4,8 @@
use pemstore::traits::{PemStorableKey, PemStorableKeyPair};
#[cfg(feature = "rand")]
use rand::{CryptoRng, RngCore};
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::{self, Display, Formatter};
/// Size of a X25519 private key
@@ -127,6 +129,28 @@ impl PublicKey {
}
}
#[cfg(feature = "serde")]
impl Serialize for PublicKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'d> Deserialize<'d> for PublicKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'d>,
{
Ok(PublicKey(x25519_dalek::PublicKey::deserialize(
deserializer,
)?))
}
}
impl PemStorableKey for PublicKey {
type Error = KeyRecoveryError;
@@ -143,7 +167,6 @@ impl PemStorableKey for PublicKey {
}
}
#[derive(Clone)]
pub struct PrivateKey(x25519_dalek::StaticSecret);
impl Display for PrivateKey {
@@ -187,6 +210,28 @@ impl PrivateKey {
}
}
#[cfg(feature = "serde")]
impl Serialize for PrivateKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'d> Deserialize<'d> for PrivateKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'d>,
{
Ok(PrivateKey(x25519_dalek::StaticSecret::deserialize(
deserializer,
)?))
}
}
impl PemStorableKey for PrivateKey {
type Error = KeyRecoveryError;
+74 -1
View File
@@ -10,6 +10,13 @@ use pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use rand::{CryptoRng, RngCore};
use std::fmt::{self, Display, Formatter};
#[cfg(feature = "serde")]
use serde::de::Error as SerdeError;
#[cfg(feature = "serde")]
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[cfg(feature = "serde")]
use serde_bytes::{ByteBuf as SerdeByteBuf, Bytes as SerdeBytes};
#[derive(Debug)]
pub enum Ed25519RecoveryError {
MalformedBytes(SignatureError),
@@ -40,6 +47,7 @@ impl fmt::Display for Ed25519RecoveryError {
impl std::error::Error for Ed25519RecoveryError {}
/// Keypair for usage in ed25519 EdDSA.
#[derive(Debug)]
pub struct KeyPair {
private_key: PrivateKey,
public_key: PublicKey,
@@ -135,6 +143,28 @@ impl PublicKey {
}
}
#[cfg(feature = "serde")]
impl Serialize for PublicKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'d> Deserialize<'d> for PublicKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'d>,
{
Ok(PublicKey(ed25519_dalek::PublicKey::deserialize(
deserializer,
)?))
}
}
impl PemStorableKey for PublicKey {
type Error = Ed25519RecoveryError;
@@ -200,6 +230,28 @@ impl PrivateKey {
}
}
#[cfg(feature = "serde")]
impl Serialize for PrivateKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'d> Deserialize<'d> for PrivateKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'d>,
{
Ok(PrivateKey(ed25519_dalek::SecretKey::deserialize(
deserializer,
)?))
}
}
impl PemStorableKey for PrivateKey {
type Error = Ed25519RecoveryError;
@@ -216,7 +268,7 @@ impl PemStorableKey for PrivateKey {
}
}
#[derive(Debug)]
#[derive(Copy, Clone, Debug)]
pub struct Signature(ed25519_dalek::Signature);
impl Signature {
@@ -237,3 +289,24 @@ impl Signature {
Ok(Signature(ed25519_dalek::Signature::from_bytes(bytes)?))
}
}
#[cfg(feature = "serde")]
impl Serialize for Signature {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
SerdeBytes::new(&self.to_bytes()).serialize(serializer)
}
}
#[cfg(feature = "serde")]
impl<'d> Deserialize<'d> for Signature {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'d>,
{
let bytes = <SerdeByteBuf>::deserialize(deserializer)?;
Signature::from_bytes(bytes.as_ref()).map_err(SerdeError::custom)
}
}
+2 -2
View File
@@ -29,5 +29,5 @@ pub use blake3;
#[cfg(feature = "symmetric")]
pub use ctr;
// TODO: this function uses all three modules: asymmetric crypto, symmetric crypto and derives key...,
// so I don't know where to put it...
#[cfg(feature = "serde")]
extern crate serde_crate as serde;
+4
View File
@@ -56,6 +56,10 @@ impl Network {
self.details().rewarding_validator_address
}
pub fn stats_provider_network_address(&self) -> &str {
self.details().stats_provider_network_address
}
pub fn validators(&self) -> impl Iterator<Item = &ValidatorDetails> {
self.details().validators.iter()
}
+14
View File
@@ -18,6 +18,7 @@ cfg_if::cfg_if! {
if #[cfg(network = "mainnet")] {
pub const DEFAULT_NETWORK: all::Network = all::Network::MAINNET;
pub const DENOM: &str = mainnet::DENOM;
pub const STAKE_DENOM: &str = 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;
@@ -25,6 +26,7 @@ cfg_if::cfg_if! {
} else if #[cfg(network = "qa")] {
pub const DEFAULT_NETWORK: all::Network = all::Network::QA;
pub const DENOM: &str = qa::DENOM;
pub const STAKE_DENOM: &str = 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;
@@ -32,6 +34,7 @@ cfg_if::cfg_if! {
} else if #[cfg(network = "sandbox")] {
pub const DEFAULT_NETWORK: all::Network = all::Network::SANDBOX;
pub const DENOM: &str = sandbox::DENOM;
pub const STAKE_DENOM: &str = 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;
@@ -49,6 +52,7 @@ pub struct DefaultNetworkDetails<'a> {
vesting_contract_address: &'a str,
bandwidth_claim_contract_address: &'a str,
rewarding_validator_address: &'a str,
stats_provider_network_address: &'a str,
validators: Vec<ValidatorDetails>,
}
@@ -60,6 +64,7 @@ static MAINNET_DEFAULTS: Lazy<DefaultNetworkDetails<'static>> =
vesting_contract_address: mainnet::VESTING_CONTRACT_ADDRESS,
bandwidth_claim_contract_address: mainnet::BANDWIDTH_CLAIM_CONTRACT_ADDRESS,
rewarding_validator_address: mainnet::REWARDING_VALIDATOR_ADDRESS,
stats_provider_network_address: mainnet::STATS_PROVIDER_CLIENT_ADDRESS,
validators: mainnet::validators(),
});
@@ -71,6 +76,7 @@ static SANDBOX_DEFAULTS: Lazy<DefaultNetworkDetails<'static>> =
vesting_contract_address: sandbox::VESTING_CONTRACT_ADDRESS,
bandwidth_claim_contract_address: sandbox::BANDWIDTH_CLAIM_CONTRACT_ADDRESS,
rewarding_validator_address: sandbox::REWARDING_VALIDATOR_ADDRESS,
stats_provider_network_address: sandbox::STATS_PROVIDER_CLIENT_ADDRESS,
validators: sandbox::validators(),
});
@@ -81,6 +87,7 @@ static QA_DEFAULTS: Lazy<DefaultNetworkDetails<'static>> = Lazy::new(|| DefaultN
vesting_contract_address: qa::VESTING_CONTRACT_ADDRESS,
bandwidth_claim_contract_address: qa::BANDWIDTH_CLAIM_CONTRACT_ADDRESS,
rewarding_validator_address: qa::REWARDING_VALIDATOR_ADDRESS,
stats_provider_network_address: qa::STATS_PROVIDER_CLIENT_ADDRESS,
validators: qa::validators(),
});
@@ -101,6 +108,13 @@ impl ValidatorDetails {
}
}
pub fn new_with_name(nymd_url: &str, api_url: Option<&str>) -> Self {
ValidatorDetails {
nymd_url: nymd_url.to_string(),
api_url: api_url.map(ToString::to_string),
}
}
pub fn nymd_url(&self) -> Url {
self.nymd_url
.parse()
+4 -1
View File
@@ -5,6 +5,7 @@ use crate::ValidatorDetails;
pub(crate) const BECH32_PREFIX: &str = "n";
pub const DENOM: &str = "unym";
pub const STAKE_DENOM: &str = "unyx";
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str =
"n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g";
@@ -18,9 +19,11 @@ pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
hex_literal::hex!("0000000000000000000000000000000000000000");
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
pub(crate) const STATS_PROVIDER_CLIENT_ADDRESS: &str = "3V3me68qkEYNNShSQ5yLkrzC8rUJmcmtrTFbLKPqytEZ.7dGmnRAheEozNeGAsp9LXM8oPgS5YgJraNmYguj2t7Bn@BNjYZPxzcJwczXHHgBxCAyVJKxN6LPteDRrKapxWmexv";
pub(crate) fn validators() -> Vec<ValidatorDetails> {
vec![ValidatorDetails::new(
"https://rpc.nyx.nodes.guru/",
Some("https://validator.nymtech.net/api"),
Some("https://validator.nymtech.net/api/"),
)]
}
+12 -7
View File
@@ -3,22 +3,27 @@
use crate::ValidatorDetails;
pub(crate) const BECH32_PREFIX: &str = "nymt";
pub const DENOM: &str = "unymt";
pub(crate) const BECH32_PREFIX: &str = "n";
pub const DENOM: &str = "unym";
pub const STAKE_DENOM: &str = "unyx";
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt17x6pt4msccvawgxjeg5nmnygttu56tftg5l6j3";
pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt1t4dmskxea0avvrj8xtmu66hv7dkyg9s8059t3c";
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str =
"n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep";
pub(crate) const VESTING_CONTRACT_ADDRESS: &str =
"n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav";
pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str =
"nymt17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9f8xzkv";
"n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] =
hex_literal::hex!("0000000000000000000000000000000000000000");
pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
hex_literal::hex!("0000000000000000000000000000000000000000");
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "nymt1dn52nx8wv9wkqmrvj6tcmdzh4es6jt8tr7f6j9";
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq";
pub(crate) const STATS_PROVIDER_CLIENT_ADDRESS: &str = "BLFPkyQ68xtR3TmrUWJZUKJF4SVwJR23wzQEmLHi2QcZ.5zms2X4ANsgY1VB4iC9kTqvbsHWmWUNSuvTtYr4Cp5qT@ExyJVqTSrgHTwzXm2r9RawfF5qYpvZjSVN2dLTs6bnWH";
pub(crate) fn validators() -> Vec<ValidatorDetails> {
vec![ValidatorDetails::new(
"https://qa-validator.nymtech.net",
Some("https://qa-validator.nymtech.net/api"),
Some("https://qa-validator-api.nymtech.net/api"),
)]
}
+3
View File
@@ -5,6 +5,7 @@ use crate::ValidatorDetails;
pub(crate) const BECH32_PREFIX: &str = "nymt";
pub const DENOM: &str = "unymt";
pub const STAKE_DENOM: &str = "unyxt";
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt1ghd753shjuwexxywmgs4xz7x2q732vcnstz02j";
pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt14ejqjyq8um4p3xfqj74yld5waqljf88fn549lh";
@@ -16,6 +17,8 @@ pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
hex_literal::hex!("E8883BAeF3869e14E4823F46662e81D4F7d2A81F");
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "nymt1jh0s6qu6tuw9ut438836mmn7f3f2wencrnmdj4";
pub(crate) const STATS_PROVIDER_CLIENT_ADDRESS: &str = "HqYWvCcB4sswYiyMj5Q8H5oc71kLf96vfrLK3npM7stH.CoeC5dcqurgdxr5zcgU77nZBSBCc8ntCiwUivQ9TX3KT@E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM";
pub(crate) fn validators() -> Vec<ValidatorDetails> {
vec![ValidatorDetails::new(
"https://sandbox-validator.nymtech.net",
+2
View File
@@ -1,5 +1,7 @@
pub mod msg;
pub mod request;
pub mod response;
pub use msg::*;
pub use request::*;
pub use response::*;
+63
View File
@@ -0,0 +1,63 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::request::{Request, RequestError};
use crate::response::{Response, ResponseError};
#[derive(Debug)]
pub enum MessageError {
Request(RequestError),
Response(ResponseError),
NoData,
UnknownMessageType,
}
impl std::fmt::Display for MessageError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MessageError::Request(r) => write!(f, "{}", r),
MessageError::Response(r) => write!(f, "{:?}", r),
MessageError::NoData => write!(f, "no data provided"),
MessageError::UnknownMessageType => write!(f, "unknown message type received"),
}
}
}
pub enum Message {
Request(Request),
Response(Response),
}
impl Message {
const REQUEST_FLAG: u8 = 0;
const RESPONSE_FLAG: u8 = 1;
pub fn try_from_bytes(b: &[u8]) -> Result<Message, MessageError> {
if b.is_empty() {
return Err(MessageError::NoData);
}
if b[0] == Self::REQUEST_FLAG {
Request::try_from_bytes(&b[1..])
.map(Message::Request)
.map_err(MessageError::Request)
} else if b[0] == Self::RESPONSE_FLAG {
Response::try_from_bytes(&b[1..])
.map(Message::Response)
.map_err(MessageError::Response)
} else {
Err(MessageError::UnknownMessageType)
}
}
pub fn into_bytes(self) -> Vec<u8> {
match self {
Self::Request(r) => std::iter::once(Self::REQUEST_FLAG)
.chain(r.into_bytes().iter().cloned())
.collect(),
Self::Response(r) => std::iter::once(Self::RESPONSE_FLAG)
.chain(r.into_bytes().iter().cloned())
.collect(),
}
}
}
+1
View File
@@ -227,6 +227,7 @@ dependencies = [
name = "config"
version = "0.1.0"
dependencies = [
"cfg-if",
"handlebars",
"humantime-serde",
"log",
+9 -6
View File
@@ -48,12 +48,15 @@ pub fn query_delegator_reward(
mix_identity: IdentityKey,
proxy: Option<String>,
) -> Result<Uint128, ContractError> {
let proxy = proxy.map(|p| {
deps.api
.addr_validate(&p)
.map_err(|_| ContractError::InvalidAddress(p))
.expect("proxy address is invalid")
});
let proxy = match proxy {
Some(proxy) => Some(
deps.api
.addr_validate(&proxy)
.map_err(|_| ContractError::InvalidAddress(proxy))?,
),
None => None,
};
let key = mixnet_contract_common::delegation::generate_storage_key(
&deps.api.addr_validate(&owner)?,
proxy.as_ref(),
+1 -1
View File
@@ -60,5 +60,5 @@ pub fn decr_reward_pool(
pub fn circulating_supply(storage: &dyn Storage) -> StdResult<Uint128> {
let reward_pool = REWARD_POOL.load(storage)?;
Ok(Uint128::new(TOTAL_SUPPLY) - reward_pool)
Ok(Uint128::new(TOTAL_SUPPLY).saturating_sub(reward_pool))
}
+31 -11
View File
@@ -16,6 +16,7 @@ use crate::rewards::helpers;
use crate::support::helpers::is_authorized;
use config::defaults::DENOM;
use cosmwasm_std::{Addr, Api, Coin, DepsMut, Env, MessageInfo, Order, Response, Storage, Uint128};
use cw_storage_plus::Bound;
use mixnet_contract_common::events::{
new_compound_delegator_reward_event, new_compound_operator_reward_event,
new_mix_operator_rewarding_event, new_not_found_mix_operator_rewarding_event,
@@ -81,7 +82,8 @@ pub fn _try_compound_operator_reward(
let mut updated_bond = bond.clone();
let reward = calculate_operator_reward(storage, api, owner, &bond)?;
updated_bond.accumulated_rewards = Some(updated_bond.accumulated_rewards() - reward);
updated_bond.accumulated_rewards =
Some(updated_bond.accumulated_rewards().saturating_sub(reward));
updated_bond.pledge_amount.amount += reward;
mixnodes().replace(
storage,
@@ -113,9 +115,13 @@ pub fn calculate_operator_reward(
let accumulated_rewards = mixnodes()
.changelog()
.prefix(bond.identity())
.keys(storage, None, None, Order::Ascending)
.keys(
storage,
Some(Bound::exclusive(last_claimed_height)),
None,
Order::Ascending,
)
.filter_map(|height| height.ok())
.filter(|height| last_claimed_height <= *height)
.fold(
Ok(Uint128::zero()),
|acc, height| -> Result<Uint128, ContractError> {
@@ -254,7 +260,7 @@ pub fn _try_compound_delegator_reward(
{
if let Some(mut bond) = mixnodes().may_load(deps.storage, mix_identity)? {
bond.accumulated_rewards = Some(bond.accumulated_rewards() - reward);
bond.accumulated_rewards = Some(bond.accumulated_rewards().saturating_sub(reward));
mixnodes().save(deps.storage, mix_identity, &bond, block_height)?;
}
}
@@ -283,32 +289,46 @@ pub fn calculate_delegator_reward(
// Get delegations newer then last_claimed_height, it would be nice to also fold this into the iteration bellow but it should be ok for now, as
// I doubt folks refresh their delegations often
let delegations = delegations_storage::delegations()
let mut delegations = delegations_storage::delegations()
.prefix((mix_identity.to_string(), key))
.range(storage, None, None, Order::Descending)
.range(
storage,
Some(Bound::exclusive(last_claimed_height)),
None,
Order::Descending,
)
.filter_map(|record| record.ok())
.filter(|(height, _)| last_claimed_height <= *height)
.map(|(_, delegation)| delegation)
.collect::<Vec<Delegation>>();
// Accumulate outside of the loop to gain some speed, on a log of checkpoints
let mut delegation_at_height = Uint128::zero();
// This is a bit gnarly, but we want to avoid loading all heights, the loading mixnodes, so we're doing it all in the iterator
let accumulated_rewards = mixnodes()
.changelog()
.prefix(mix_identity)
.keys(storage, None, None, Order::Ascending)
.keys(
storage,
Some(Bound::exclusive(last_claimed_height)),
None,
Order::Ascending,
)
.filter_map(|height| height.ok())
// Get all checkpoints greater then last claimed delegation height
.filter(|height| last_claimed_height <= *height)
.fold(
Ok(Uint128::zero()),
|acc, height| -> Result<Uint128, ContractError> {
let accumulated_reward = acc?;
let delegation_at_height = delegations
delegation_at_height = delegations
.iter()
.filter(|d| d.block_height <= height)
.fold(Uint128::zero(), |total, delegation| {
.fold(delegation_at_height, |total, delegation| {
total + delegation.amount.amount
});
// Drop what we've processed
// This should be replaced with drain_filter once it stabilizes
delegations.retain(|d| d.block_height > height);
// debug_with_visibility(
// api,
// format!("delegation at height {} - {}", height, delegation_at_height),
@@ -14,24 +14,22 @@ impl VestingAccount for Account {
env: &Env,
storage: &dyn Storage,
) -> Result<Coin, ContractError> {
// Returns 0 in case of underflow.
// 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)?
.amount
.u128()
.checked_sub(
.saturating_sub(
self.get_delegated_vesting(block_time, env, storage)?
.amount
.u128(),
)
.ok_or(ContractError::Underflow)?
.checked_sub(
.saturating_sub(
self.get_pledged_vesting(block_time, env, storage)?
.amount
.u128(),
)
.ok_or(ContractError::Underflow)?,
),
),
denom: DENOM.to_string(),
})
+116 -100
View File
@@ -1,106 +1,122 @@
version: '3.7'
x-bech32-prefix: &BECH32_PREFIX
nymt
x-wasmd-version: &WASMD_VERSION
v0.21.0
x-wasmd-commit-hash: &WASMD_COMMIT_HASH
1d436638af7cacb5aeeb7248b57b085c64f3ae35
x-network: &NETWORK
BECH32_PREFIX: nymt
DENOM: nymt
STAKE_DENOM: nyxt
WASMD_VERSION: v0.26.0
WASMD_COMMIT_HASH: dc5ef6fe84f0a5e3b0894692a18cc48fb5b00adf
services:
genesis_validator:
build:
context: docker/validator
args:
BECH32_PREFIX: *BECH32_PREFIX
WASMD_VERSION: *WASMD_VERSION
WASMD_COMMIT_HASH: *WASMD_COMMIT_HASH
image: validator:latest
ports:
- "26657:26657"
- "1317:1317"
container_name: genesis_validator
volumes:
- "genesis_volume:/genesis_volume"
environment:
BECH32_PREFIX: *BECH32_PREFIX
WASMD_VERSION: *WASMD_VERSION
command: ["genesis"]
secondary_validator:
build:
context: docker/validator
args:
BECH32_PREFIX: *BECH32_PREFIX
WASMD_VERSION: *WASMD_VERSION
image: validator:latest
volumes:
- "genesis_volume:/genesis_volume:ro"
environment:
BECH32_PREFIX: *BECH32_PREFIX
WASMD_VERSION: *WASMD_VERSION
depends_on:
- "genesis_validator"
command: ["secondary"]
mixnet_contract:
build: docker/mixnet_contract
image: contract:latest
volumes:
- ".:/nym"
vesting_contract:
build: docker/vesting_contract
image: vesting_contract:latest
volumes:
- ".:/nym"
contract_uploader:
build: docker/typescript_client
image: contract_uploader:typescript
volumes:
- "genesis_volume:/genesis_volume:ro"
- "contract_volume:/contract_volume"
- ".:/nym"
depends_on:
- "genesis_validator"
- "secondary_validator"
- "mixnet_contract"
environment:
BECH32_PREFIX: *BECH32_PREFIX
mnemonic_echo:
build: docker/mnemonic_echo
image: mnemonic_echo:latest
volumes:
- "genesis_volume:/genesis_volume:ro"
depends_on:
- "genesis_validator"
genesis_validator:
build:
context: docker/validator
args: *NETWORK
image: validator:latest
ports:
- "26657:26657"
- "1317:1317"
container_name: genesis_validator
volumes:
- "genesis_volume:/genesis_volume"
- "genesis_nymd:/root/.nymd"
environment: *NETWORK
networks:
localnet:
ipv4_address: 172.168.10.2
command: [ "genesis" ]
secondary_validator:
build:
context: docker/validator
args: *NETWORK
image: validator:latest
ports:
- "36657:26657"
- "2317:1317"
volumes:
- "genesis_volume:/genesis_volume"
- "secondary_nymd:/root/.nymd"
environment: *NETWORK
networks:
localnet:
ipv4_address: 172.168.10.3
depends_on:
- "genesis_validator"
command: [ "secondary" ]
# mixnet_contract:
# build: docker/mixnet_contract
# image: contract:latest
# volumes:
# - ".:/nym"
# vesting_contract:
# build: docker/vesting_contract
# image: vesting_contract:latest
# volumes:
# - ".:/nym"
# contract_uploader:
# build: docker/typescript_client
# image: contract_uploader:typescript
# volumes:
# - "genesis_volume:/genesis_volume:ro"
# - "contract_volume:/contract_volume"
# - ".:/nym"
# depends_on:
# - "genesis_validator"
# - "secondary_validator"
# - "mixnet_contract"
# environment:
# BECH32_PREFIX: *BECH32_PREFIX
mnemonic_echo:
build: docker/mnemonic_echo
image: mnemonic_echo:latest
volumes:
- "genesis_volume:/genesis_volume:ro"
depends_on:
- "genesis_validator"
- "secondary_validator"
mongo:
image: mongo:latest
command:
- --storageEngine=wiredTiger
volumes:
- mongo_data:/data/db
block_explorer:
build:
context: https://github.com/forbole/big-dipper.git#v0.41.x-7
image: block_explorer:v0.41.x-7
ports:
- "3080:3000"
depends_on:
- "mongo"
environment:
ROOT_URL: ${APP_ROOT_URL:-http://localhost}
MONGO_URL: mongodb://mongo:27017/meteor
PORT: 3000
METEOR_SETTINGS: ${METEOR_SETTINGS}
explorer:
build:
context: docker/explorer
image: explorer:latest
ports:
- "3040:3000"
depends_on:
- "genesis_validator"
- "block_explorer"
# mongo:
# image: mongo:latest
# command:
# - --storageEngine=wiredTiger
# volumes:
# - mongo_data:/data/db
# block_explorer:
# build:
# context: https://github.com/forbole/big-dipper.git#v0.41.x-7
# image: block_explorer:v0.41.x-7
# ports:
# - "3080:3000"
# depends_on:
# - "mongo"
# environment:
# ROOT_URL: ${APP_ROOT_URL:-http://localhost}
# MONGO_URL: mongodb://mongo:27017/meteor
# PORT: 3000
# METEOR_SETTINGS: ${METEOR_SETTINGS}
# explorer:
# build:
# context: docker/explorer
# image: explorer:latest
# ports:
# - "3040:3000"
# depends_on:
# - "genesis_validator"
# - "block_explorer"
volumes:
genesis_volume:
contract_volume:
mongo_data:
genesis_volume:
genesis_nymd:
secondary_nymd:
# contract_volume:
# mongo_data:
networks:
localnet:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.168.10.0/25
+9 -2
View File
@@ -1,9 +1,16 @@
#!/bin/sh
# Wait for the mnemonic to be generated
# Wait for the mnemonic(s) to be generated
while ! [ -s /genesis_volume/genesis_mnemonic ]; do
sleep 1
done
echo "This is the current mnemonic:"
while ! [ -s /genesis_volume/secondary_mnemonic ]; do
sleep 1
done
echo "This is the current genesis mnemonic:"
cat /genesis_volume/genesis_mnemonic
echo "This is the current secondary mnemonic:"
cat /genesis_volume/secondary_mnemonic
+31 -8
View File
@@ -6,17 +6,29 @@ PASSPHRASE=passphrase
cd /root
if [ "$1" = "genesis" ]; then
if [ ! -d "/root/.nymd" ]; then
if [ ! -f "/root/.nymd/config/genesis.json" ]; then
./nymd init nymnet --chain-id nymnet 2> /dev/null
sed -i 's/minimum-gas-prices = ""/minimum-gas-prices = "0.025u'"${BECH32_PREFIX}"'"/' /root/.nymd/config/app.toml
# staking/governance token is hardcoded in config, change this
sed -i "s/\"stake\"/\"u${STAKE_DENOM}\"/" /root/.nymd/config/genesis.json
sed -i 's/minimum-gas-prices = ""/minimum-gas-prices = "0.025u'"${DENOM}"'"/' /root/.nymd/config/app.toml
sed -i '0,/enable = false/s//enable = true/g' /root/.nymd/config/app.toml
sed -i 's/cors_allowed_origins = \[\]/cors_allowed_origins = \["*"\]/' /root/.nymd/config/config.toml
sed -i 's/create_empty_blocks = true/create_empty_blocks = false/' /root/.nymd/config/config.toml
sed -i 's/laddr = "tcp:\/\/127.0.0.1:26657"/laddr = "tcp:\/\/0.0.0.0:26657"/' /root/.nymd/config/config.toml
yes "${PASSPHRASE}" | ./nymd keys add node_admin 2>&1 >/dev/null | tail -n 1 > /genesis_volume/genesis_mnemonic
ADDRESS=$(yes "${PASSPHRASE}" | ./nymd keys show node_admin -a)
yes "${PASSPHRASE}" | ./nymd add-genesis-account "${ADDRESS}" 1000000000000000u${BECH32_PREFIX},1000000000000000stake
yes "${PASSPHRASE}" | ./nymd gentx node_admin 1000000000stake --chain-id nymnet 2> /dev/null
# create accounts
yes "${PASSPHRASE}" | ./nymd keys add node_admin 2>&1 >/dev/null | tail -n 1 > /root/.nymd/mnemonic
yes "${PASSPHRASE}" | ./nymd keys add secondary 2>&1 >/dev/null | tail -n 1 > /root/.nymd/secondary_mnemonic
cp /root/.nymd/mnemonic /genesis_volume/genesis_mnemonic
cp /root/.nymd/secondary_mnemonic /genesis_volume/secondary_mnemonic
# add genesis accounts with some initial tokens
GENESIS_ADDRESS=$(yes "${PASSPHRASE}" | ./nymd keys show node_admin -a)
SECONDARY_ADDRESS=$(yes "${PASSPHRASE}" | ./nymd keys show secondary -a)
yes "${PASSPHRASE}" | ./nymd add-genesis-account "${GENESIS_ADDRESS}" 1000000000000000u"${DENOM}",1000000000000000u"${STAKE_DENOM}"
yes "${PASSPHRASE}" | ./nymd add-genesis-account "${SECONDARY_ADDRESS}" 1000000000000000u"${DENOM}",1000000000000000u"${STAKE_DENOM}"
yes "${PASSPHRASE}" | ./nymd gentx node_admin 1000000000u"${STAKE_DENOM}" --chain-id nymnet 2> /dev/null
./nymd collect-gentxs 2> /dev/null
./nymd validate-genesis > /dev/null
cp /root/.nymd/config/genesis.json /genesis_volume/genesis.json
@@ -26,7 +38,7 @@ if [ "$1" = "genesis" ]; then
fi
./nymd start
elif [ "$1" = "secondary" ]; then
if [ ! -d "/root/.nymd" ]; then
if [ ! -f "/root/.nymd/config/genesis.json" ]; then
./nymd init nymnet --chain-id nym-secondary 2> /dev/null
# Wait until the genesis node writes the genesis.json to the shared volume
@@ -34,16 +46,27 @@ elif [ "$1" = "secondary" ]; then
sleep 1
done
# wait for the actual validator to start up
sleep 5
cp /genesis_volume/genesis.json /root/.nymd/config/genesis.json
GENESIS_PEER=$(cat /root/.nymd/config/genesis.json | grep '"memo"' | cut -d'"' -f 4)
GENESIS_IP=$(cat /root/.nymd/config/genesis.json | grep '"memo"' | cut -d'@' -f2 | cut -d: -f1)
sed -i 's/persistent_peers = ""/persistent_peers = "'"${GENESIS_PEER}"'"/' /root/.nymd/config/config.toml
sed -i 's/minimum-gas-prices = ""/minimum-gas-prices = "0.025u'"${BECH32_PREFIX}"'"/' /root/.nymd/config/app.toml
sed -i '0,/enable = false/s//enable = true/g' /root/.nymd/config/app.toml
sed -i 's/cors_allowed_origins = \[\]/cors_allowed_origins = \["*"\]/' /root/.nymd/config/config.toml
sed -i 's/create_empty_blocks = true/create_empty_blocks = false/' /root/.nymd/config/config.toml
sed -i 's/laddr = "tcp:\/\/127.0.0.1:26657"/laddr = "tcp:\/\/0.0.0.0:26657"/' /root/.nymd/config/config.toml
yes "${PASSPHRASE}" | ./nymd keys add node_admin 2> mnemonic > /dev/null
# import mnemonic generated by the genesis validator (have a local copy for ease of use)
cp /genesis_volume/secondary_mnemonic /root/.nymd/mnemonic
{ cat /root/.nymd/mnemonic; echo "${PASSPHRASE}"; echo "${PASSPHRASE}"; } | ./nymd keys add node_admin --recover #> /dev/null
./nymd validate-genesis > /dev/null
# create validator
# don't even ask about those sleeps...
{ echo "${PASSPHRASE}"; sleep 10; yes; sleep 10; } | ./nymd tx staking create-validator --amount=10000000u"${STAKE_DENOM}" --fees 100000u"${DENOM}" --pubkey="$(./nymd tendermint show-validator)" --moniker="secondary" --commission-rate="0.10" --commission-max-rate="0.20" --commission-max-change-rate="0.01" --min-self-delegation="1" --chain-id=nymnet --from=node_admin -b async --node http://"${GENESIS_IP}":26657
else
echo "Validator already initialized, starting with the existing configuration."
echo "If you want to re-init the validator, destroy the existing container"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "explorer-api"
version = "1.0.0"
version = "1.0.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1
View File
@@ -28,6 +28,7 @@ pub(crate) struct PrettyDetailedMixNodeBond {
pub owner: Addr,
pub layer: Layer,
pub mix_node: MixNode,
pub avg_uptime: Option<u8>,
}
pub(crate) struct MixNodeCache {
+26
View File
@@ -9,7 +9,9 @@ use serde::Serialize;
use tokio::sync::RwLock;
use mixnet_contract_common::MixNodeBond;
use validator_client::models::UptimeResponse;
use crate::cache::Cache;
use crate::mix_node::models::{MixnodeStatus, PrettyDetailedMixNodeBond};
use crate::mix_nodes::location::{Location, LocationCache, LocationCacheItem};
use crate::mix_nodes::CACHE_ENTRY_TTL;
@@ -76,10 +78,16 @@ impl MixNodesResult {
}
}
#[derive(Clone, Debug)]
pub(crate) struct MixNodeHealth {
avg_uptime: u8,
}
#[derive(Clone)]
pub(crate) struct ThreadsafeMixNodesCache {
mixnodes: Arc<RwLock<MixNodesResult>>,
locations: Arc<RwLock<LocationCache>>,
mixnode_health: Arc<RwLock<Cache<MixNodeHealth>>>,
}
impl ThreadsafeMixNodesCache {
@@ -87,6 +95,7 @@ impl ThreadsafeMixNodesCache {
ThreadsafeMixNodesCache {
mixnodes: Arc::new(RwLock::new(MixNodesResult::new())),
locations: Arc::new(RwLock::new(LocationCache::new())),
mixnode_health: Arc::new(RwLock::new(Cache::new())),
}
}
@@ -94,6 +103,7 @@ impl ThreadsafeMixNodesCache {
ThreadsafeMixNodesCache {
mixnodes: Arc::new(RwLock::new(MixNodesResult::new())),
locations: Arc::new(RwLock::new(locations)),
mixnode_health: Arc::new(RwLock::new(Cache::new())),
}
}
@@ -132,9 +142,11 @@ impl ThreadsafeMixNodesCache {
) -> Option<PrettyDetailedMixNodeBond> {
let mixnodes_guard = self.mixnodes.read().await;
let location_guard = self.locations.read().await;
let mixnode_health_guard = self.mixnode_health.read().await;
let bond = mixnodes_guard.get_mixnode(identity_key);
let location = location_guard.get(identity_key);
let health = mixnode_health_guard.get(identity_key);
match bond {
Some(bond) => Some(PrettyDetailedMixNodeBond {
@@ -145,6 +157,7 @@ impl ThreadsafeMixNodesCache {
owner: bond.owner,
layer: bond.layer,
mix_node: bond.mix_node,
avg_uptime: health.map(|m| m.avg_uptime),
}),
None => None,
}
@@ -153,6 +166,7 @@ impl ThreadsafeMixNodesCache {
pub(crate) async fn get_detailed_mixnodes(&self) -> Vec<PrettyDetailedMixNodeBond> {
let mixnodes_guard = self.mixnodes.read().await;
let location_guard = self.locations.read().await;
let mixnode_health_guard = self.mixnode_health.read().await;
mixnodes_guard
.all_mixnodes
@@ -160,6 +174,7 @@ impl ThreadsafeMixNodesCache {
.map(|bond| {
let location = location_guard.get(&bond.mix_node.identity_key);
let copy = bond.clone();
let health = mixnode_health_guard.get(&bond.mix_node.identity_key);
PrettyDetailedMixNodeBond {
location: location.and_then(|l| l.location.clone()),
status: mixnodes_guard.determine_node_status(&bond.mix_node.identity_key),
@@ -168,6 +183,7 @@ impl ThreadsafeMixNodesCache {
owner: copy.owner,
layer: copy.layer,
mix_node: copy.mix_node,
avg_uptime: health.map(|m| m.avg_uptime),
}
})
.collect()
@@ -188,4 +204,14 @@ impl ThreadsafeMixNodesCache {
guard.active_mixnodes = active_nodes;
guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL;
}
pub(crate) async fn update_health_cache(&self, all_uptimes: Vec<UptimeResponse>) {
let mut mixnode_health = self.mixnode_health.write().await;
for uptime in all_uptimes {
let health = MixNodeHealth {
avg_uptime: uptime.avg_uptime,
};
mixnode_health.set(&uptime.identity, health);
}
}
}
+30
View File
@@ -4,6 +4,7 @@
use std::future::Future;
use mixnet_contract_common::{GatewayBond, MixNodeBond};
use validator_client::models::UptimeResponse;
use validator_client::nymd::error::NymdError;
use validator_client::nymd::{Paging, QueryNymdClient, ValidatorResponse};
use validator_client::ValidatorClientError;
@@ -88,6 +89,17 @@ impl ExplorerApiTasks {
.await
}
async fn retrieve_all_mixnode_avg_uptimes(
&self,
) -> Result<Vec<UptimeResponse>, ValidatorClientError> {
self.state
.inner
.validator_client
.0
.get_mixnode_avg_uptimes()
.await
}
async fn update_mixnode_cache(&self) {
let all_bonds = self.retrieve_all_mixnodes().await;
let rewarded_nodes = self
@@ -109,6 +121,21 @@ impl ExplorerApiTasks {
.await;
}
async fn update_mixnode_health_cache(&self) {
match self.retrieve_all_mixnode_avg_uptimes().await {
Ok(response) => {
self.state
.inner
.mixnodes
.update_health_cache(response)
.await
}
Err(e) => {
error!("Failed to get mixnode avg uptimes: {:?}", e)
}
}
}
async fn update_validators_cache(&self) {
match self.retrieve_all_validators().await {
Ok(response) => self.state.inner.validators.update_cache(response).await,
@@ -145,6 +172,9 @@ impl ExplorerApiTasks {
info!("Updating mix node cache...");
self.update_mixnode_cache().await;
info!("Updating mix node health cache...");
self.update_mixnode_health_cache().await;
info!("Done");
}
});
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
extends: [
'@nymproject/eslint-config-react-typescript'
],
overrides: [
{
files: ['*.ts'],
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
}
}
]
}
-5
View File
@@ -1,5 +0,0 @@
{
"extends": [
"@nymproject/eslint-config-react-typescript"
]
}
+63
View File
@@ -0,0 +1,63 @@
/* eslint-disable no-param-reassign */
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
framework: '@storybook/react',
core: {
builder: 'webpack5',
},
// webpackFinal: async (config, { configType }) => {
// // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION'
// // You can change the configuration based on that.
// // 'PRODUCTION' is used when building the static version of storybook.
webpackFinal: async (config) => {
config.module.rules.forEach((rule) => {
// look for SVG import rule and replace
// NOTE: the rule before modification is /\.(svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/
if (rule.test?.toString().includes('svg')) {
rule.test = /\.(ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/;
}
});
// handle asset loading with this
config.module.rules.unshift({
test: /\.svg(\?.*)?$/i,
issuer: /\.[jt]sx?$/,
use: ['@svgr/webpack'],
});
config.resolve.extensions = ['.tsx', '.ts', '.js'];
config.resolve.plugins = [new TsconfigPathsPlugin()];
config.resolve.fallback = {
fs: false,
tls: false,
path: false,
http: false,
https: false,
stream: false,
crypto: false,
net: false,
zlib: false,
};
config.plugins.push(new ForkTsCheckerWebpackPlugin({
typescript: {
mode: 'write-references',
diagnosticOptions: {
semantic: true,
syntactic: true,
},
},
}));
// Return the altered config
return config;
},
features: {
emotionAlias: false,
},
};
+56
View File
@@ -0,0 +1,56 @@
/* eslint-disable react/react-in-jsx-scope */
import { NymNetworkExplorerThemeProvider } from '@nymproject/mui-theme';
import { Box } from '@mui/material';
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}
const withThemeProvider = (Story, context) => (
<div style={{ display: 'grid', height: '100%', gridTemplateColumns: '50% 50%' }}>
<div>
<NymNetworkExplorerThemeProvider mode="light">
<Box
p={4}
sx={{
display: 'grid',
gridTemplateRows: '80vh 2rem',
background: (theme) => theme.palette.background.default,
color: (theme) => theme.palette.text.primary,
}}
>
<Box sx={{ overflowY: 'auto' }}>
<Story {...context} />
</Box>
<h4 style={{ textAlign: 'center' }}>Light mode</h4>
</Box>
</NymNetworkExplorerThemeProvider>
</div>
<div>
<NymNetworkExplorerThemeProvider mode="dark">
<Box
p={4}
sx={{
display: 'grid',
gridTemplateRows: '80vh 2rem',
background: (theme) => theme.palette.background.default,
color: (theme) => theme.palette.text.primary,
}}
>
<Box sx={{ overflowY: 'auto' }}>
<Story {...context} />
</Box>
<h4 style={{ textAlign: 'center' }}>Dark mode</h4>
</Box>
</NymNetworkExplorerThemeProvider>
</div>
</div>
);
export const decorators = [withThemeProvider];
+13 -3
View File
@@ -30,8 +30,15 @@
"use-clipboard-copy": "^0.2.0"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@storybook/addon-actions": "^6.4.19",
"@storybook/addon-essentials": "^6.4.19",
"@storybook/addon-interactions": "^6.4.19",
"@storybook/addon-links": "^6.4.19",
"@storybook/react": "^6.4.19",
"@storybook/testing-library": "^0.0.9",
"@svgr/webpack": "^6.1.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
@@ -95,10 +102,13 @@
"build:serve": "npx serve dist",
"test": "jest",
"test:watch": "jest --watch",
"tsc": "tsc",
"tsc:watch": "tsc --watch",
"tsc": "tsc --noEmit true",
"tsc:watch": "tsc --watch --noEmit true",
"lint": "eslint src",
"lint:fix": "eslint src --fix"
"lint:fix": "eslint src --fix",
"prestorybook": "yarn --cwd .. build",
"storybook": "start-storybook -p 6006",
"storybook:build": "build-storybook"
},
"browserslist": {
"production": [
+4
View File
@@ -18,6 +18,7 @@ import {
MixNodeResponse,
MixNodeResponseItem,
MixnodeStatus,
MixNodeEconomicDynamicsStatsResponse,
StatsResponse,
StatusResponse,
SummaryOverviewResponse,
@@ -122,6 +123,9 @@ export class Api {
static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> =>
(await fetch(`${MIXNODE_API}/${id}/description`)).json();
static fetchMixnodeEconomicDynamicsStatsById = async (id: string): Promise<MixNodeEconomicDynamicsStatsResponse> =>
(await fetch(`${MIXNODE_API}/${id}/economic-dynamics-stats`)).json();
static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json();
static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
+3 -2
View File
@@ -12,12 +12,13 @@ export type ColumnsType = {
headerAlign: string;
flex?: number;
width?: number;
tooltipInfo?: string;
};
export interface UniversalTableProps {
export interface UniversalTableProps<T = any> {
tableName: string;
columnsData: ColumnsType[];
rows: any[];
rows: T[];
}
function formatCellValues(val: string | number, field: string) {
@@ -1,5 +1,5 @@
import * as React from 'react';
import { Alert, Box, CircularProgress, useMediaQuery } from '@mui/material';
import { Alert, Box, CircularProgress, useMediaQuery, Typography } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
@@ -98,7 +98,7 @@ export const BondBreakdownTable: React.FC = () => {
</TableCell>
</TableRow>
<TableRow>
<TableCell align="left">Pledge total</TableCell>
<TableCell align="left">Self</TableCell>
<TableCell align="left" data-testid="pledge-total-amount">
{bonds.pledges}
</TableCell>
@@ -127,21 +127,62 @@ export const BondBreakdownTable: React.FC = () => {
sx={{
maxHeight: 400,
overflowY: 'scroll',
p: 2,
background: theme.palette.background.paper,
}}
>
<Box
sx={{
display: 'flex',
alignItems: 'baseline',
width: '100%',
p: 2,
borderBottom: `1px solid ${theme.palette.divider}`,
}}
data-testid="delegations-total-amount"
>
<Typography
sx={{
fontSize: 16,
fontWeight: 600,
}}
>
Delegations&nbsp;&nbsp;
</Typography>
<Typography
sx={{
fontSize: 12,
fontWeight: 400,
}}
>
{`(${delegations?.data?.length} delegators)`}
</Typography>
</Box>
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell sx={{ fontWeight: 600, background: '#242C3D' }} align="left">
<TableCell
sx={{
fontWeight: 600,
background: theme.palette.background.paper,
}}
align="left"
>
Delegators
</TableCell>
<TableCell sx={{ fontWeight: 600, background: '#242C3D' }} align="left">
<TableCell
sx={{
fontWeight: 600,
background: theme.palette.background.paper,
}}
align="left"
>
Stake
</TableCell>
<TableCell
sx={{
fontWeight: 600,
background: '#242C3D',
background: theme.palette.background.paper,
width: '200px',
}}
align="left"
@@ -0,0 +1,49 @@
import { ColumnsType } from '../../DetailTable';
export const EconomicsInfoColumns: ColumnsType[] = [
{
field: 'estimatedTotalReward',
title: 'Estimated Total Reward',
flex: 1,
headerAlign: 'left',
tooltipInfo: 'Estimated reward per epoch for this profit margin if your node is selected in the active set.',
},
{
field: 'estimatedOperatorReward',
title: 'Estimated Operator Reward',
flex: 1,
headerAlign: 'left',
tooltipInfo: 'Estimated reward per epoch for this profit margin if your node is selected in the active set.',
},
{
field: 'selectionChance',
title: 'Active Set Probability',
flex: 1,
headerAlign: 'left',
tooltipInfo:
'Probability of getting selected in the reward set (active and standby nodes) in the next epoch. The more your stake, the higher the chances to be selected.',
},
{
field: 'stakeSaturation',
title: 'Stake Saturation',
flex: 1,
headerAlign: 'left',
tooltipInfo:
'Level of stake saturation for this node. Nodes receive more rewards the higher their saturation level, up to 100%. Beyond 100% no additional rewards are granted. The current stake saturation level is: 1 million NYM, computed as S/K where S is total amount of tokens available to stakeholders and K is the number of nodes in the reward set.',
},
{
field: 'profitMargin',
title: 'Profit Margin',
flex: 1,
headerAlign: 'left',
tooltipInfo:
'Percentage of the delegates rewards that the operator takes as fee before rewards are distributed to the delegates.',
},
{
field: 'avgUptime',
title: 'Avg. Uptime',
flex: 1,
headerAlign: 'left',
tooltipInfo: 'Nodes average uptime in the last 24h.',
},
];
@@ -0,0 +1,31 @@
import * as React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { EconomicsProgress } from './EconomicsProgress';
export default {
title: 'Mix Node Detail/Economics/ProgressBar',
component: EconomicsProgress,
} as ComponentMeta<typeof EconomicsProgress>;
const Template: ComponentStory<typeof EconomicsProgress> = (args) => <EconomicsProgress {...args} />;
export const Empty = Template.bind({});
Empty.args = {};
export const OverThreshold = Template.bind({});
OverThreshold.args = {
threshold: 100,
value: 120,
};
export const UnderThreshold = Template.bind({});
UnderThreshold.args = {
threshold: 100,
value: 80,
};
export const OnThreshold = Template.bind({});
OnThreshold.args = {
threshold: 100,
value: 100,
};
@@ -0,0 +1,38 @@
import * as React from 'react';
import LinearProgress, { LinearProgressProps } from '@mui/material/LinearProgress';
import { useTheme } from '@mui/material/styles';
import { Box } from '@mui/system';
const parseToNumber = (value: number | undefined | string) =>
typeof value === 'string' ? parseInt(value || '', 10) : value || 0;
export const EconomicsProgress: React.FC<
LinearProgressProps & {
threshold?: number;
}
> = ({ threshold, ...props }) => {
const theme = useTheme();
const { value } = props;
const valueNumber: number = parseToNumber(value);
const thresholdNumber: number = parseToNumber(threshold);
const percentageColor = valueNumber > (threshold || 100) ? 'warning' : 'inherit';
const percentageToDisplay = Math.min(valueNumber, thresholdNumber);
return (
<Box
sx={{
width: 6 / 10,
color: valueNumber > (threshold || 100) ? theme.palette.warning.main : theme.palette.nym.wallet.fee,
}}
>
<LinearProgress
{...props}
variant="determinate"
color={percentageColor}
value={percentageToDisplay}
sx={{ width: '100%', borderRadius: '5px', backgroundColor: theme.palette.common.white }}
/>
</Box>
);
};
@@ -0,0 +1,129 @@
import * as React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { DelegatorsInfoTable } from './Table';
import { EconomicsInfoColumns } from './Columns';
import { EconomicsInfoRowWithIndex } from './types';
export default {
title: 'Mix Node Detail/Economics',
component: DelegatorsInfoTable,
} as ComponentMeta<typeof DelegatorsInfoTable>;
const row: EconomicsInfoRowWithIndex = {
id: 1,
selectionChance: {
value: 'High',
},
avgUptime: {
value: '65 %',
},
estimatedOperatorReward: {
value: '80000.123456 NYM',
},
estimatedTotalReward: {
value: '80000.123456 NYM',
},
profitMargin: {
value: '10 %',
},
stakeSaturation: {
value: '80 %',
progressBarValue: 80,
},
};
const rowVeryHighProbabilitySelection: EconomicsInfoRowWithIndex = {
...row,
selectionChance: {
value: 'Very High',
},
};
const rowModerateProbabilitySelection: EconomicsInfoRowWithIndex = {
...row,
selectionChance: {
value: 'Moderate',
},
};
const rowLowProbabilitySelection: EconomicsInfoRowWithIndex = {
...row,
selectionChance: {
value: 'Low',
},
};
const rowVeryLowProbabilitySelection: EconomicsInfoRowWithIndex = {
...row,
selectionChance: {
value: 'Very Low',
},
};
const emptyRow: EconomicsInfoRowWithIndex = {
id: 1,
selectionChance: {
value: '-',
progressBarValue: 0,
},
avgUptime: {
value: '-',
},
estimatedOperatorReward: {
value: '-',
},
estimatedTotalReward: {
value: '-',
},
profitMargin: {
value: '-',
},
stakeSaturation: {
value: '-',
progressBarValue: 0,
},
};
const Template: ComponentStory<typeof DelegatorsInfoTable> = (args) => <DelegatorsInfoTable {...args} />;
export const Empty = Template.bind({});
Empty.args = {
rows: [emptyRow],
columnsData: EconomicsInfoColumns,
tableName: 'storybook',
};
export const selectionChanceVeryHigh = Template.bind({});
selectionChanceVeryHigh.args = {
rows: [rowVeryHighProbabilitySelection],
columnsData: EconomicsInfoColumns,
tableName: 'storybook',
};
export const selectionChanceHigh = Template.bind({});
selectionChanceHigh.args = {
rows: [row],
columnsData: EconomicsInfoColumns,
tableName: 'storybook',
};
export const selectionChanceModerate = Template.bind({});
selectionChanceModerate.args = {
rows: [rowModerateProbabilitySelection],
columnsData: EconomicsInfoColumns,
tableName: 'storybook',
};
export const selectionChanceLow = Template.bind({});
selectionChanceLow.args = {
rows: [rowLowProbabilitySelection],
columnsData: EconomicsInfoColumns,
tableName: 'storybook',
};
export const selectionChanceVeryLow = Template.bind({});
selectionChanceVeryLow.args = {
rows: [rowVeryLowProbabilitySelection],
columnsData: EconomicsInfoColumns,
tableName: 'storybook',
};
@@ -0,0 +1,55 @@
import { currencyToString } from '../../../utils/currency';
import { useMixnodeContext } from '../../../context/mixnode';
import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '../../../typeDefs/explorer-api';
import { EconomicsInfoRowWithIndex } from './types';
const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) => {
const inclusionProbability = economicDynamicsStats?.data?.active_set_inclusion_probability;
switch (inclusionProbability) {
case 'High':
case 'Moderate':
case 'Low':
return inclusionProbability;
case 'VeryHigh':
return 'Very High';
case 'VeryLow':
return 'Very Low';
default:
return '-';
}
};
export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
const { economicDynamicsStats, mixNode } = useMixnodeContext();
const estimatedNodeRewards =
currencyToString((economicDynamicsStats?.data?.estimated_total_node_reward || '').toString()) || '-';
const estimatedOperatorRewards =
currencyToString((economicDynamicsStats?.data?.estimated_operator_reward || '').toString()) || '-';
const stakeSaturation = economicDynamicsStats?.data?.stake_saturation || '-';
const profitMargin = mixNode?.data?.mix_node.profit_margin_percent || '-';
const avgUptime = economicDynamicsStats?.data?.current_interval_uptime;
return {
id: 1,
estimatedTotalReward: {
value: estimatedNodeRewards,
},
estimatedOperatorReward: {
value: estimatedOperatorRewards,
},
selectionChance: {
value: selectionChance(economicDynamicsStats),
},
stakeSaturation: {
progressBarValue: typeof stakeSaturation === 'number' ? stakeSaturation * 100 : 0,
value: typeof stakeSaturation === 'number' ? `${(stakeSaturation * 100).toFixed(2)} %` : '-',
},
profitMargin: {
value: profitMargin ? `${profitMargin} %` : '-',
},
avgUptime: {
value: avgUptime ? `${avgUptime} %` : '-',
},
};
};
@@ -0,0 +1,175 @@
import * as React from 'react';
import {
IconButton,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
useMediaQuery,
} from '@mui/material';
import { Box } from '@mui/system';
import { styled, useTheme, Theme } from '@mui/material/styles';
import Tooltip, { tooltipClasses, TooltipProps } from '@mui/material/Tooltip';
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
import { EconomicsRowsType, EconomicsInfoRowWithIndex } from './types';
import { EconomicsProgress } from './EconomicsProgress';
import { cellStyles } from '../../Universal-DataGrid';
import { UniversalTableProps } from '../../DetailTable';
const tooltipBackGroundColor = '#A0AED1';
const threshold = 100;
const textColour = (value: EconomicsRowsType, field: string, theme: Theme) => {
const progressBarValue = value?.progressBarValue || 0;
const fieldValue = value.value;
if (progressBarValue > 100) {
return theme.palette.warning.main;
}
if (field === 'selectionChance') {
switch (fieldValue) {
case 'High':
case 'Very High':
return theme.palette.nym.networkExplorer.selectionChance.overModerate;
case 'Moderate':
return theme.palette.nym.networkExplorer.selectionChance.moderate;
case 'Low':
case 'Very Low':
return theme.palette.nym.networkExplorer.selectionChance.underModerate;
default:
return theme.palette.nym.wallet.fee;
}
}
return theme.palette.nym.wallet.fee;
};
const formatCellValues = (value: EconomicsRowsType, field: string, theme: Theme) => {
const isTablet = useMediaQuery(theme.breakpoints.down('lg'));
if (value.progressBarValue) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', flexDirection: isTablet ? 'column' : 'row' }} id="field">
<Typography
sx={{
mr: isTablet ? 0 : 1,
mb: isTablet ? 1 : 0,
fontWeight: '600',
fontSize: '12px',
}}
id={field}
>
{value.value}
</Typography>
<EconomicsProgress threshold={threshold} value={value.progressBarValue} />
</Box>
);
}
return (
<Box sx={{ display: 'flex', alignItems: 'center' }} id="field">
<Typography sx={{ mr: 1, fontWeight: '600', fontSize: '12px' }} id={field}>
{value.value}
</Typography>
</Box>
);
};
export const DelegatorsInfoTable: React.FC<UniversalTableProps<EconomicsInfoRowWithIndex>> = ({
tableName,
columnsData,
rows,
}) => {
const theme = useTheme();
const CustomTooltip = styled(({ className, ...props }: TooltipProps) => (
<Tooltip {...props} classes={{ popper: className }} />
))({
[`& .${tooltipClasses.tooltip}`]: {
maxWidth: 230,
background: tooltipBackGroundColor,
color: theme.palette.nym.networkExplorer.nav.hover,
},
});
return (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
<TableHead>
<TableRow>
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{tooltipInfo && (
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<CustomTooltip
title={tooltipInfo}
id={field}
placement="top-start"
sx={{
'& .MuiTooltip-arrow': {
color: '#A0AED1',
},
}}
arrow
>
<IconButton
sx={{
padding: 0,
py: 1,
pr: 1,
}}
disableFocusRipple
disableRipple
>
<InfoOutlinedIcon
sx={{
height: '18px',
width: '18px',
}}
/>
</IconButton>
</CustomTooltip>
</Box>
)}
{title}
</Box>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{rows?.map((eachRow) => (
<TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
{columnsData?.map((_, index: number) => {
const { field } = columnsData[index];
const value: EconomicsRowsType = (eachRow as any)[field];
return (
<TableCell
key={_.title}
component="th"
scope="row"
variant="body"
sx={{
...cellStyles,
padding: 2,
width: 200,
fontSize: 12,
fontWeight: 600,
color: textColour(value, field, theme),
}}
data-testid={`${_.title.replace(/ /g, '-')}-value`}
>
{formatCellValues(value, columnsData[index].field, theme)}
</TableCell>
);
})}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
@@ -0,0 +1,3 @@
export { DelegatorsInfoTable } from './Table';
export { EconomicsInfoColumns } from './Columns';
export { EconomicsInfoRows } from './Rows';
@@ -0,0 +1,15 @@
export type EconomicsRowsType = {
progressBarValue?: number;
value: string;
};
export interface EconomicsInfoRow {
estimatedTotalReward: EconomicsRowsType;
estimatedOperatorReward: EconomicsRowsType;
selectionChance: EconomicsRowsType;
stakeSaturation: EconomicsRowsType;
profitMargin: EconomicsRowsType;
avgUptime: EconomicsRowsType;
}
export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number };
@@ -11,6 +11,8 @@ export type MixnodeRowType = {
self_percentage: string;
host: string;
layer: string;
profit_percentage: string;
avg_uptime: string;
};
export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] {
@@ -22,6 +24,7 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
const delegations = Number(item.total_delegation.amount) || 0;
const totalBond = pledge + delegations;
const selfPercentage = ((pledge * 100) / totalBond).toFixed(2);
const profitPercentage = item.mix_node.profit_margin_percent || 0;
return {
id: item.owner,
status: item.status,
@@ -32,5 +35,7 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
self_percentage: selfPercentage,
host: item?.mix_node?.host || '',
layer: item?.layer || '',
profit_percentage: `${profitPercentage}%`,
avg_uptime: `${item.avg_uptime}%` || '-',
};
}
+6 -3
View File
@@ -22,9 +22,12 @@ export const Socials: React.FC<{ isFooter?: boolean }> = ({ isFooter }) => {
<IconButton component="a" href={TELEGRAM_LINK} target="_blank" data-testid="telegram">
<TelegramIcon color={color} size={24} />
</IconButton>
<IconButton component="a" href={DISCORD_LINK} target="_blank" data-testid="discord">
<DiscordIcon color={color} size={24} />
</IconButton>
{false && (
<IconButton component="a" href={DISCORD_LINK} target="_blank" data-testid="discord">
<DiscordIcon color={color} size={24} />
</IconButton>
)}
<IconButton component="a" href={TWITTER_LINK} target="_blank" data-testid="twitter">
<TwitterIcon color={color} size={24} />
</IconButton>
@@ -0,0 +1,15 @@
export type RowsType = {
value?: string | number;
visualProgressValue?: number;
};
export interface DelegatorsInfoRow {
estimated_total_reward: RowsType;
estimated_operator_reward: RowsType;
active_set_probability: RowsType;
stake_saturation: RowsType;
profit_margin: RowsType;
avg_uptime: RowsType;
}
export type DelegatorsInfoRowWithIndex = DelegatorsInfoRow & { id: number };
+20 -1
View File
@@ -3,6 +3,7 @@ import {
ApiState,
DelegationsResponse,
MixNodeDescriptionResponse,
MixNodeEconomicDynamicsStatsResponse,
MixNodeResponseItem,
StatsResponse,
StatusResponse,
@@ -18,6 +19,7 @@ import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../componen
interface MixnodeState {
delegations?: ApiState<DelegationsResponse>;
description?: ApiState<MixNodeDescriptionResponse>;
economicDynamicsStats?: ApiState<MixNodeEconomicDynamicsStatsResponse>;
mixNode?: ApiState<MixNodeResponseItem | undefined>;
mixNodeRow?: MixnodeRowType;
stats?: ApiState<StatsResponse>;
@@ -71,6 +73,13 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
'Failed to fetch mixnode description',
);
const [economicDynamicsStats, fetchEconomicDynamicsStats, clearEconomicDynamicsStats] =
useApiState<MixNodeEconomicDynamicsStatsResponse>(
mixNodeIdentityKey,
Api.fetchMixnodeEconomicDynamicsStatsById,
'Failed to fetch mixnode dynamics stats by id',
);
const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState<UptimeStoryResponse>(
mixNodeIdentityKey,
Api.fetchUptimeStoryById,
@@ -84,6 +93,7 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
clearStatus();
clearStats();
clearDescription();
clearEconomicDynamicsStats();
clearUptimeHistory();
// fetch the mixnode, then get all the other stuff
@@ -93,7 +103,14 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
return;
}
setMixnodeRow(mixNodeResponseItemToMixnodeRowType(value.data));
Promise.all([fetchDelegations(), fetchStatus(), fetchStats(), fetchDescription(), fetchUptimeHistory()]);
Promise.all([
fetchDelegations(),
fetchStatus(),
fetchStats(),
fetchDescription(),
fetchEconomicDynamicsStats(),
fetchUptimeHistory(),
]);
});
}, [mixNodeIdentityKey]);
@@ -103,6 +120,7 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
mixNode,
mixNodeRow,
description,
economicDynamicsStats,
stats,
status,
uptimeStory,
@@ -113,6 +131,7 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
mixNode,
mixNodeRow,
description,
economicDynamicsStats,
stats,
status,
uptimeStory,
@@ -3,6 +3,7 @@ import { Alert, AlertTitle, Box, CircularProgress, Grid, Typography } from '@mui
import { useParams } from 'react-router-dom';
import { ColumnsType, DetailTable } from '../../components/DetailTable';
import { BondBreakdownTable } from '../../components/MixNodes/BondBreakdown';
import { DelegatorsInfoTable, EconomicsInfoColumns, EconomicsInfoRows } from '../../components/MixNodes/Economics';
import { ComponentError } from '../../components/ComponentError';
import { ContentCard } from '../../components/ContentCard';
import { TwoColSmallTable } from '../../components/TwoColSmallTable';
@@ -82,6 +83,16 @@ const PageMixnodeDetailWithState: React.FC = () => {
</Grid>
</Grid>
<Grid container spacing={2} mt={0}>
<Grid item xs={12}>
<DelegatorsInfoTable
columnsData={EconomicsInfoColumns}
tableName="Delegators info table"
rows={[EconomicsInfoRows()]}
/>
</Grid>
</Grid>
<Grid container spacing={2} mt={0}>
<Grid item xs={12}>
<ContentCard title="Bond Breakdown">
+40 -5
View File
@@ -16,6 +16,7 @@ import { CustomColumnHeading } from '../../components/CustomColumnHeading';
import { Title } from '../../components/Title';
import { cellStyles, UniversalDataGrid } from '../../components/Universal-DataGrid';
import { currencyToString } from '../../utils/currency';
import { splice } from '../../utils';
import { getMixNodeStatusColor } from '../../components/MixNodes/Status';
import { MixNodeStatusDropdown } from '../../components/MixNodes/StatusDropdown';
@@ -85,7 +86,7 @@ export const PageMixnodes: React.FC = () => {
field: 'owner',
headerName: 'Owner',
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
width: 380,
width: 200,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
@@ -95,7 +96,7 @@ export const PageMixnodes: React.FC = () => {
sx={getCellStyles(theme, params.row)}
data-testid="big-dipper-link"
>
{params.value}
{splice(7, 29, params.value)}
</MuiLink>
),
},
@@ -104,7 +105,7 @@ export const PageMixnodes: React.FC = () => {
headerName: 'Identity Key',
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
headerClassName: 'MuiDataGrid-header-override',
width: 380,
width: 180,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<>
@@ -119,7 +120,7 @@ export const PageMixnodes: React.FC = () => {
to={`/network-components/mixnode/${params.value}`}
data-testid="identity-link"
>
{params.value}
{splice(7, 29, params.value)}
</MuiLink>
</>
),
@@ -130,7 +131,7 @@ export const PageMixnodes: React.FC = () => {
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
type: 'number',
headerClassName: 'MuiDataGrid-header-override',
width: 150,
width: 200,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
@@ -213,6 +214,40 @@ export const PageMixnodes: React.FC = () => {
</MuiLink>
),
},
{
field: 'profit_percentage',
headerName: 'Profit Margin',
renderHeader: () => <CustomColumnHeading headingTitle="Profit Margin" />,
headerClassName: 'MuiDataGrid-header-override',
width: 140,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
component={RRDLink}
to={`/network-components/mixnode/${params.row.identity_key}`}
>
{params.value}
</MuiLink>
),
},
{
field: 'avg_uptime',
headerName: 'Average Uptime',
renderHeader: () => <CustomColumnHeading headingTitle="Average Uptime" />,
headerClassName: 'MuiDataGrid-header-override',
width: 160,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
component={RRDLink}
to={`/network-components/mixnode/${params.row.identity_key}`}
>
{params.value}
</MuiLink>
),
},
];
const handlePageSize = (event: SelectChangeEvent<string>) => {
@@ -0,0 +1,7 @@
import { Meta } from '@storybook/addon-docs';
<Meta title="Introduction" />
# Nym Network Explorer Storybook
This is the Storybook for the Nym Network Explorer.
+11
View File
@@ -83,6 +83,7 @@ export interface MixNodeResponseItem {
two_letter_iso_country_code: string;
};
mix_node: MixNode;
avg_uptime: number;
}
export type MixNodeResponse = MixNodeResponseItem[];
@@ -203,3 +204,13 @@ export type UptimeStoryResponse = {
identity: string;
owner: string;
};
export type MixNodeEconomicDynamicsStatsResponse = {
stake_saturation: number;
active_set_inclusion_probability: 'VeryHigh' | 'High' | 'Moderate' | 'Low' | 'VeryLow';
reserve_set_inclusion_probability: 'VeryHigh' | 'High' | 'Moderate' | 'Low' | 'VeryLow';
estimated_total_node_reward: number;
estimated_operator_reward: number;
estimated_delegators_reward: number;
current_interval_uptime: number;
};
+9
View File
@@ -36,3 +36,12 @@ export function countryDataToGridRow(countriesData: CountryData[]): CountryDataR
const sorted = formatted.sort((a, b) => (a.nodes < b.nodes ? 1 : -1));
return sorted;
}
export const splice = (start: number, deleteCount: number, address?: string): string => {
if (address) {
const array = address.split('');
array.splice(start, deleteCount, '...');
return array.join('');
}
return '';
};
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-gateway"
version = "1.0.0"
version = "1.0.1"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Mixnet Gateway"
edition = "2021"
+4
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use clap::{crate_version, Parser};
use network_defaults::DEFAULT_NETWORK;
use once_cell::sync::OnceCell;
mod commands;
@@ -63,6 +64,7 @@ fn long_version() -> String {
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
"#,
"Build Timestamp:",
env!("VERGEN_BUILD_TIMESTAMP"),
@@ -80,6 +82,8 @@ fn long_version() -> String {
env!("VERGEN_RUSTC_CHANNEL"),
"cargo Profile:",
env!("VERGEN_CARGO_PROFILE"),
"Network:",
DEFAULT_NETWORK
)
}
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-mixnode"
version = "1.0.0"
version = "1.0.1"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+4
View File
@@ -4,6 +4,7 @@ extern crate rocket;
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use ::config::defaults::DEFAULT_NETWORK;
use clap::{crate_version, Parser};
use lazy_static::lazy_static;
@@ -65,6 +66,7 @@ fn long_version() -> String {
{:<20}{}
{:<20}{}
{:<20}{}
{:<20}{}
"#,
"Build Timestamp:",
env!("VERGEN_BUILD_TIMESTAMP"),
@@ -82,6 +84,8 @@ fn long_version() -> String {
env!("VERGEN_RUSTC_CHANNEL"),
"cargo Profile:",
env!("VERGEN_CARGO_PROFILE"),
"Network:",
DEFAULT_NETWORK
)
}
+97 -7
View File
@@ -75,9 +75,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.55"
version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd"
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
[[package]]
name = "argon2"
@@ -87,7 +87,18 @@ checksum = "25df3c03f1040d0069fcd3907e24e36d59f9b6fa07ba49be0eb25a794f036ba7"
dependencies = [
"base64ct",
"blake2",
"password-hash",
"password-hash 0.3.2",
]
[[package]]
name = "argon2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27e27b63e4a34caee411ade944981136fdfa535522dc9944d6700196cbd899f"
dependencies = [
"base64ct",
"blake2",
"password-hash 0.4.1",
]
[[package]]
@@ -619,6 +630,45 @@ dependencies = [
"generic-array 0.14.5",
]
[[package]]
name = "clap"
version = "3.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"clap_lex",
"indexmap",
"lazy_static",
"strsim 0.10.0",
"termcolor",
"textwrap",
]
[[package]]
name = "clap_derive"
version = "3.1.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c"
dependencies = [
"heck 0.4.0",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
dependencies = [
"os_str_bytes",
]
[[package]]
name = "cloudabi"
version = "0.0.3"
@@ -694,6 +744,7 @@ dependencies = [
name = "config"
version = "0.1.0"
dependencies = [
"cfg-if",
"handlebars",
"humantime-serde",
"log",
@@ -2946,10 +2997,10 @@ dependencies = [
[[package]]
name = "nym_wallet"
version = "1.0.3"
version = "1.0.4"
dependencies = [
"aes-gcm",
"argon2",
"argon2 0.3.4",
"base64",
"bip39",
"cfg-if",
@@ -2965,6 +3016,7 @@ dependencies = [
"itertools",
"log",
"mixnet-contract-common",
"once_cell",
"pretty_env_logger",
"rand 0.6.5",
"reqwest",
@@ -3106,6 +3158,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "os_str_bytes"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
[[package]]
name = "pairing"
version = "0.20.0"
@@ -3182,6 +3240,17 @@ dependencies = [
"subtle",
]
[[package]]
name = "password-hash"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e029e94abc8fb0065241c308f1ac6bc8d20f450e8f7c5f0b25cd9b8d526ba294"
dependencies = [
"base64ct",
"rand_core 0.6.3",
"subtle",
]
[[package]]
name = "paste"
version = "1.0.6"
@@ -4260,9 +4329,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.79"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
dependencies = [
"itoa 1.0.1",
"ryu",
@@ -5049,6 +5118,12 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "textwrap"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
[[package]]
name = "thin-slice"
version = "0.1.1"
@@ -5498,6 +5573,21 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wallet-recovery-cli"
version = "0.1.0"
dependencies = [
"aes-gcm",
"anyhow",
"argon2 0.4.0",
"base64",
"bip39",
"clap",
"log",
"pretty_env_logger",
"serde_json",
]
[[package]]
name = "want"
version = "0.3.0"
+6 -1
View File
@@ -1,2 +1,7 @@
[workspace]
members = ["src-tauri"]
resolver = "2"
members = [
"src-tauri",
"wallet-recovery-cli",
]
+3
View File
@@ -42,7 +42,9 @@
"react-hook-form": "^7.14.2",
"react-router-dom": "^5.2.0",
"semver": "^6.3.0",
"string-to-color": "^2.2.2",
"use-clipboard-copy": "^0.2.0",
"uuid": "^8.3.2",
"yup": "^0.32.9"
},
"devDependencies": {
@@ -71,6 +73,7 @@
"@types/react-router": "^5.1.18",
"@types/react-router-dom": "^5.1.8",
"@types/semver": "^7.3.8",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"babel-loader": "^8.2.2",
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym_wallet"
version = "1.0.3"
version = "1.0.4"
description = "Nym Native Wallet"
authors = ["Nym Technologies SA"]
license = ""
@@ -28,6 +28,7 @@ eyre = "0.6.5"
futures = "0.3.15"
itertools = "0.10"
log = "0.4"
once_cell = "1.7.2"
pretty_env_logger = "0.4"
rand = "0.6.5"
reqwest = "0.11.9"
+69 -22
View File
@@ -57,7 +57,7 @@ pub struct NetworkConfig {
// Additional user provided validators.
// It is an option for the purpuse of file serialization.
validator_urls: Option<Vec<ValidatorUrl>>,
validator_urls: Option<Vec<ValidatorConfigEntry>>,
}
impl Default for Base {
@@ -89,7 +89,7 @@ impl Default for NetworkConfig {
}
impl NetworkConfig {
fn validators(&self) -> impl Iterator<Item = &ValidatorUrl> {
fn validators(&self) -> impl Iterator<Item = &ValidatorConfigEntry> {
self.validator_urls.iter().flat_map(|v| v.iter())
}
}
@@ -192,7 +192,7 @@ impl Config {
pub fn get_base_validators(
&self,
network: WalletNetwork,
) -> impl Iterator<Item = ValidatorUrl> + '_ {
) -> impl Iterator<Item = ValidatorConfigEntry> + '_ {
self.base.networks.validators(network.into()).map(|v| {
v.clone()
.try_into()
@@ -203,7 +203,7 @@ impl Config {
pub fn get_configured_validators(
&self,
network: WalletNetwork,
) -> impl Iterator<Item = ValidatorUrl> + '_ {
) -> impl Iterator<Item = ValidatorConfigEntry> + '_ {
self
.networks
.get(&network.as_key())
@@ -272,7 +272,7 @@ impl Config {
}
}
pub fn get_selected_validator_nymd_url(&self, network: &WalletNetwork) -> Option<Url> {
pub fn get_selected_validator_nymd_url(&self, network: WalletNetwork) -> Option<Url> {
self
.networks
.get(&network.as_key())
@@ -286,7 +286,7 @@ impl Config {
.and_then(|config| config.selected_api_url.clone())
}
pub fn add_validator_url(&mut self, url: ValidatorUrl, network: WalletNetwork) {
pub fn add_validator_url(&mut self, url: ValidatorConfigEntry, network: WalletNetwork) {
if let Some(network_config) = self.networks.get_mut(&network.as_key()) {
if let Some(ref mut urls) = network_config.validator_urls {
urls.push(url);
@@ -304,7 +304,7 @@ impl Config {
}
}
pub fn remove_validator_url(&mut self, url: ValidatorUrl, network: WalletNetwork) {
pub fn remove_validator_url(&mut self, url: ValidatorConfigEntry, network: WalletNetwork) {
if let Some(network_config) = self.networks.get_mut(&network.as_key()) {
if let Some(ref mut urls) = network_config.validator_urls {
// Removes duplicates too if there are any
@@ -325,17 +325,19 @@ where
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct ValidatorUrl {
pub struct ValidatorConfigEntry {
pub nymd_url: Url,
pub nymd_name: Option<String>,
pub api_url: Option<Url>,
}
impl TryFrom<ValidatorDetails> for ValidatorUrl {
impl TryFrom<ValidatorDetails> for ValidatorConfigEntry {
type Error = BackendError;
fn try_from(validator: ValidatorDetails) -> Result<Self, Self::Error> {
Ok(ValidatorUrl {
Ok(ValidatorConfigEntry {
nymd_url: validator.nymd_url.parse()?,
nymd_name: None,
api_url: match &validator.api_url {
Some(url) => Some(url.parse()?),
None => None,
@@ -344,12 +346,13 @@ impl TryFrom<ValidatorDetails> for ValidatorUrl {
}
}
impl TryFrom<network_config::Validator> for ValidatorUrl {
impl TryFrom<network_config::Validator> for ValidatorConfigEntry {
type Error = BackendError;
fn try_from(validator: network_config::Validator) -> Result<Self, Self::Error> {
Ok(ValidatorUrl {
Ok(ValidatorConfigEntry {
nymd_url: validator.nymd_url.parse()?,
nymd_name: validator.nymd_name,
api_url: match &validator.api_url {
Some(url) => Some(url.parse()?),
None => None,
@@ -358,14 +361,21 @@ impl TryFrom<network_config::Validator> for ValidatorUrl {
}
}
impl fmt::Display for ValidatorUrl {
impl fmt::Display for ValidatorConfigEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s1 = format!("nymd_url: {}", self.nymd_url);
let name = self.nymd_name.as_ref().map(|name| format!(" ({})", name));
let s2 = self
.api_url
.as_ref()
.map(|url| format!(", api_url: {}", url));
write!(f, " {}{},", s1, s2.unwrap_or_default())
write!(
f,
" {}{}{},",
s1,
name.unwrap_or_default(),
s2.unwrap_or_default()
)
}
}
@@ -374,13 +384,13 @@ impl fmt::Display for ValidatorUrl {
pub struct OptionalValidators {
// User supplied additional validator urls in addition to the hardcoded ones.
// These are separate fields, rather than a map, to force the serialization order.
mainnet: Option<Vec<ValidatorUrl>>,
sandbox: Option<Vec<ValidatorUrl>>,
qa: Option<Vec<ValidatorUrl>>,
mainnet: Option<Vec<ValidatorConfigEntry>>,
sandbox: Option<Vec<ValidatorConfigEntry>>,
qa: Option<Vec<ValidatorConfigEntry>>,
}
impl OptionalValidators {
pub fn validators(&self, network: WalletNetwork) -> impl Iterator<Item = &ValidatorUrl> {
pub fn validators(&self, network: WalletNetwork) -> impl Iterator<Item = &ValidatorConfigEntry> {
match network {
WalletNetwork::MAINNET => self.mainnet.as_ref(),
WalletNetwork::SANDBOX => self.sandbox.as_ref(),
@@ -422,16 +432,19 @@ mod tests {
selected_api_url: Some("https://my_api_url.com".parse().unwrap()),
validator_urls: Some(vec![
ValidatorUrl {
ValidatorConfigEntry {
nymd_url: "https://foo".parse().unwrap(),
nymd_name: Some("FooName".to_string()),
api_url: None,
},
ValidatorUrl {
ValidatorConfigEntry {
nymd_url: "https://bar".parse().unwrap(),
nymd_name: None,
api_url: Some("https://bar/api".parse().unwrap()),
},
ValidatorUrl {
ValidatorConfigEntry {
nymd_url: "https://baz".parse().unwrap(),
nymd_name: None,
api_url: Some("https://baz/api".parse().unwrap()),
},
]),
@@ -458,6 +471,7 @@ selected_api_url = 'https://my_api_url.com/'
[[validator_urls]]
nymd_url = 'https://foo/'
nymd_name = 'FooName'
[[validator_urls]]
nymd_url = 'https://bar/'
@@ -469,6 +483,39 @@ api_url = 'https://baz/api'
"#
);
}
#[test]
fn serialize_to_json() {
let config = test_config();
let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()];
println!("{}", serde_json::to_string_pretty(netconfig).unwrap());
assert_eq!(
serde_json::to_string_pretty(netconfig).unwrap(),
r#"{
"version": 1,
"selected_nymd_url": null,
"selected_api_url": "https://my_api_url.com/",
"validator_urls": [
{
"nymd_url": "https://foo/",
"nymd_name": "FooName",
"api_url": null
},
{
"nymd_url": "https://bar/",
"nymd_name": null,
"api_url": "https://bar/api"
},
{
"nymd_url": "https://baz/",
"nymd_name": null,
"api_url": "https://baz/api"
}
]
}"#
);
}
#[test]
fn serialize_and_deserialize_to_toml() {
let config = test_config();
@@ -513,6 +560,6 @@ api_url = 'https://baz/api'
.next()
.and_then(|v| v.api_url)
.unwrap();
assert_eq!(api_url.as_ref(), "https://api.nyx.nodes.guru/",);
assert_eq!(api_url.as_ref(), "https://validator.nymtech.net/api/",);
}
}
+14 -4
View File
@@ -83,12 +83,22 @@ pub enum BackendError {
WalletFileAlreadyExists,
#[error("The wallet file is not found")]
WalletFileNotFound,
#[error("Account ID not found in wallet")]
NoSuchIdInWallet,
#[error("Account ID already found in wallet")]
IdAlreadyExistsInWallet,
#[error("Login ID not found in wallet")]
WalletNoSuchLoginId,
#[error("Account ID not found in wallet login")]
WalletNoSuchAccountIdInWalletLogin,
#[error("Login ID already found in wallet")]
WalletLoginIdAlreadyExists,
#[error("Account ID already found in wallet login")]
WalletAccountIdAlreadyExistsInWalletLogin,
#[error("Adding a different password to the wallet not currently supported")]
WalletDifferentPasswordDetected,
#[error("Unexpted mnemonic account for login")]
WalletUnexpectedMnemonicAccount,
#[error("Unexpted multiple account entry for login")]
WalletUnexpectedMultipleAccounts,
#[error("Failed to derive address from mnemonic")]
FailedToDeriveAddress,
}
impl Serialize for BackendError {
+5
View File
@@ -35,15 +35,20 @@ fn main() {
tauri::Builder::default()
.manage(Arc::new(RwLock::new(State::default())))
.invoke_handler(tauri::generate_handler![
mixnet::account::add_account_for_password,
mixnet::account::connect_with_mnemonic,
mixnet::account::create_new_account,
mixnet::account::create_new_mnemonic,
mixnet::account::create_password,
mixnet::account::does_password_file_exist,
mixnet::account::get_balance,
mixnet::account::list_accounts,
mixnet::account::logout,
mixnet::account::remove_account_for_password,
mixnet::account::remove_password,
mixnet::account::show_mnemonic_for_account_in_password,
mixnet::account::sign_in_with_password,
mixnet::account::sign_in_with_password_and_account_id,
mixnet::account::switch_network,
mixnet::account::validate_mnemonic,
mixnet::admin::get_contract_settings,
+16 -10
View File
@@ -9,18 +9,30 @@ use serde::{Deserialize, Serialize};
use std::{fmt, sync::Arc};
use tokio::sync::RwLock;
// When the UI queries validator urls we use this type
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurls.ts"))]
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct ValidatorUrls {
pub urls: Vec<String>,
pub urls: Vec<ValidatorUrl>,
}
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurl.ts"))]
#[derive(Debug, Serialize, Deserialize)]
pub struct ValidatorUrl {
pub url: String,
pub name: Option<String>,
}
// The type used when adding or removing validators, effectively the input.
// NOTE: we should consider if we want to split this up
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurls.ts"))]
#[derive(Debug, Serialize, Deserialize)]
pub struct Validator {
pub nymd_url: String,
pub nymd_name: Option<String>,
pub api_url: Option<String>,
}
@@ -42,10 +54,7 @@ pub async fn get_validator_nymd_urls(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<ValidatorUrls, BackendError> {
let state = state.read().await;
let urls: Vec<String> = state
.get_nymd_urls(network)
.map(|url| url.to_string())
.collect();
let urls: Vec<ValidatorUrl> = state.get_nymd_urls(network).collect();
Ok(ValidatorUrls { urls })
}
@@ -55,10 +64,7 @@ pub async fn get_validator_api_urls(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<ValidatorUrls, BackendError> {
let state = state.read().await;
let urls: Vec<String> = state
.get_api_urls(network)
.map(|url| url.to_string())
.collect();
let urls: Vec<ValidatorUrl> = state.get_api_urls(network).collect();
Ok(ValidatorUrls { urls })
}
@@ -4,8 +4,8 @@ use crate::error::BackendError;
use crate::network::Network as WalletNetwork;
use crate::network_config;
use crate::nymd_client;
use crate::state::State;
use crate::wallet_storage::{self, DEFAULT_WALLET_ACCOUNT_ID};
use crate::state::{State, WalletAccountIds};
use crate::wallet_storage::{self, DEFAULT_LOGIN_ID};
use bip39::{Language, Mnemonic};
use config::defaults::all::Network;
@@ -21,6 +21,7 @@ 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};
@@ -51,6 +52,14 @@ pub struct CreatedAccount {
mnemonic: String,
}
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/createdaccount.ts"))]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AccountEntry {
id: String,
address: String,
}
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/balance.ts"))]
#[derive(Serialize, Deserialize)]
@@ -107,9 +116,8 @@ pub async fn create_new_account(
}
#[tauri::command]
pub fn create_new_mnemonic() -> Result<String, BackendError> {
let rand_mnemonic = random_mnemonic();
Ok(rand_mnemonic.to_string())
pub fn create_new_mnemonic() -> String {
random_mnemonic().to_string()
}
#[tauri::command]
@@ -169,7 +177,7 @@ async fn _connect_with_mnemonic(
for network in WalletNetwork::iter() {
log::debug!(
"List of validators for {network}: [\n{}\n]",
state.get_validators(network).format(",\n")
state.get_config_validator_entries(network).format(",\n")
);
}
@@ -221,6 +229,10 @@ async fn _connect_with_mnemonic(
};
// Register all the clients
{
let mut w_state = state.write().await;
w_state.logout();
}
for client in clients {
let network: WalletNetwork = client.network.into();
let mut w_state = state.write().await;
@@ -268,7 +280,7 @@ fn create_clients(
) -> Result<Vec<Client<SigningNymdClient>>, 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) {
let nymd_url = if let Some(url) = config.get_selected_validator_nymd_url(network) {
log::debug!("Using selected nymd_url for {network}: {url}");
url.clone()
} else {
@@ -353,18 +365,18 @@ pub fn does_password_file_exist() -> Result<bool, BackendError> {
}
#[tauri::command]
pub fn create_password(mnemonic: String, password: String) -> Result<(), BackendError> {
pub fn create_password(mnemonic: &str, password: String) -> Result<(), BackendError> {
if does_password_file_exist()? {
return Err(BackendError::WalletFileAlreadyExists);
}
log::info!("Creating password");
let mnemonic = Mnemonic::from_str(&mnemonic)?;
let mnemonic = Mnemonic::from_str(mnemonic)?;
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
// Currently we only support a single, default, id in the wallet
let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
// Currently we only support a single, default, login id in the wallet
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
let password = wallet_storage::UserPassword::new(password);
wallet_storage::store_wallet_login_information(mnemonic, hd_path, id, &password)
wallet_storage::store_login_with_multiple_accounts(mnemonic, hd_path, login_id, &password)
}
#[tauri::command]
@@ -375,15 +387,301 @@ pub async fn sign_in_with_password(
log::info!("Signing in with password");
// Currently we only support a single, default, id in the wallet
let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
let password = wallet_storage::UserPassword::new(password);
let stored_account = wallet_storage::load_existing_wallet_login_information(&id, &password)?;
_connect_with_mnemonic(stored_account.mnemonic().clone(), state).await
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
let mnemonic = extract_first_mnemonic(&stored_login)?;
let first_login_id_when_converting = login_id.into();
set_state_with_all_accounts(stored_login, first_login_id_when_converting, state.clone()).await?;
_connect_with_mnemonic(mnemonic, state).await
}
fn extract_first_mnemonic(
stored_login: &wallet_storage::StoredLogin,
) -> Result<Mnemonic, BackendError> {
let mnemonic = match stored_login {
wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(),
wallet_storage::StoredLogin::Multiple(ref accounts) => {
// Login using the first account in the list
accounts
.get_accounts()
.next()
.ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)?
.mnemonic()
.clone()
}
};
Ok(mnemonic)
}
#[tauri::command]
pub async fn sign_in_with_password_and_account_id(
account_id: &str,
password: &str,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<Account, BackendError> {
log::info!("Signing in with password");
// Currently we only support a single, default, id in the wallet
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
let account_id = wallet_storage::AccountId::new(account_id.to_string());
let password = wallet_storage::UserPassword::new(password.to_string());
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
let mnemonic = extract_mnemonic(&stored_login, &account_id)?;
let first_login_id_when_converting = login_id.into();
set_state_with_all_accounts(stored_login, first_login_id_when_converting, state.clone()).await?;
_connect_with_mnemonic(mnemonic, state).await
}
fn extract_mnemonic(
stored_login: &wallet_storage::StoredLogin,
account_id: &wallet_storage::AccountId,
) -> Result<Mnemonic, BackendError> {
let mnemonic = match stored_login {
wallet_storage::StoredLogin::Mnemonic(_) => {
return Err(BackendError::WalletNoSuchAccountIdInWalletLogin);
}
wallet_storage::StoredLogin::Multiple(ref accounts) => accounts
.get_account(account_id)
.ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)?
.mnemonic()
.clone(),
};
Ok(mnemonic)
}
#[tauri::command]
pub fn remove_password() -> Result<(), BackendError> {
log::info!("Removing password");
let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
wallet_storage::remove_wallet_login_information(&id)
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
wallet_storage::remove_login(&login_id)
}
#[tauri::command]
pub async fn add_account_for_password(
mnemonic: &str,
password: &str,
account_id: &str,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<AccountEntry, BackendError> {
log::info!("Adding account for the current password: {account_id}");
let mnemonic = Mnemonic::from_str(mnemonic)?;
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
// Currently we only support a single, default, login id in the wallet
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
let account_id = wallet_storage::AccountId::new(account_id.to_string());
let password = wallet_storage::UserPassword::new(password.to_string());
wallet_storage::append_account_to_login(
mnemonic.clone(),
hd_path,
login_id.clone(),
account_id.clone(),
&password,
)?;
let address = {
let state = state.read().await;
let network: Network = state.current_network().into();
derive_address(mnemonic, network.bech32_prefix())?.to_string()
};
// Re-read all the acccounts from the wallet to reset the state, rather than updating it
// incrementally
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
// NOTE: since we are appending, this id shouldn't be needed, but setting the state is supposed
// to be a general function
let first_id_when_converting = login_id.into();
set_state_with_all_accounts(stored_login, first_id_when_converting, state).await?;
Ok(AccountEntry {
id: account_id.to_string(),
address,
})
}
// The first `AccoundId` when converting is the `LoginId` for the entry that was loaded.
async fn set_state_with_all_accounts(
stored_login: wallet_storage::StoredLogin,
first_id_when_converting: wallet_storage::AccountId,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<(), BackendError> {
log::trace!("Set state with accounts:");
let all_accounts: Vec<_> = stored_login
.unwrap_into_multiple_accounts(first_id_when_converting)
.into_accounts()
.collect();
for account in &all_accounts {
log::trace!("account: {:?}", account.id());
}
let all_account_ids: Vec<WalletAccountIds> = all_accounts
.iter()
.map(|account| {
let mnemonic = account.mnemonic();
let addresses: HashMap<WalletNetwork, cosmrs::AccountId> = WalletNetwork::iter()
.map(|network| {
let config_network: Network = network.into();
(
network,
derive_address(mnemonic.clone(), config_network.bech32_prefix()).unwrap(),
)
})
.collect();
WalletAccountIds {
id: account.id().clone(),
addresses,
}
})
.collect();
let mut w_state = state.write().await;
w_state.set_all_accounts(all_account_ids);
Ok(())
}
#[tauri::command]
pub async fn remove_account_for_password(
password: &str,
account_id: &str,
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<(), BackendError> {
log::info!("Removing account: {account_id}");
// Currently we only support a single, default, id in the wallet
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
let account_id = wallet_storage::AccountId::new(account_id.to_string());
let password = wallet_storage::UserPassword::new(password.to_string());
wallet_storage::remove_account_from_login(&login_id, &account_id, &password)?;
// Load to reset the internal state
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
// NOTE: Since we removed from a multi-account login, this id shouldn't be needed, but setting
// the state is supposed to be a general function
let first_account_id_when_converting = login_id.into();
set_state_with_all_accounts(stored_login, first_account_id_when_converting, state).await
}
fn derive_address(
mnemonic: bip39::Mnemonic,
prefix: &str,
) -> Result<cosmrs::AccountId, BackendError> {
DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic)?
.try_derive_accounts()?
.first()
.map(AccountData::address)
.cloned()
.ok_or(BackendError::FailedToDeriveAddress)
}
#[tauri::command]
pub async fn list_accounts(
state: tauri::State<'_, Arc<RwLock<State>>>,
) -> Result<Vec<AccountEntry>, BackendError> {
log::trace!("Listing accounts");
let state = state.read().await;
let network = state.current_network();
let all_accounts = state
.get_all_accounts()
.map(|account| AccountEntry {
id: account.id.to_string(),
address: account.addresses[&network].to_string(),
})
.map(|account| {
log::trace!("{:?}", account);
account
})
.collect();
Ok(all_accounts)
}
#[tauri::command]
pub fn show_mnemonic_for_account_in_password(
account_id: String,
password: String,
) -> Result<String, BackendError> {
log::info!("Getting mnemonic for: {account_id}");
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
let account_id = wallet_storage::AccountId::new(account_id);
let password = wallet_storage::UserPassword::new(password);
let mnemonic = _show_mnemonic_for_account_in_password(&login_id, &account_id, &password)?;
Ok(mnemonic.to_string())
}
fn _show_mnemonic_for_account_in_password(
login_id: &wallet_storage::LoginId,
account_id: &wallet_storage::AccountId,
password: &wallet_storage::UserPassword,
) -> Result<bip39::Mnemonic, BackendError> {
let stored_account = wallet_storage::load_existing_login(login_id, password)?;
let mnemonic = match stored_account {
wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(),
wallet_storage::StoredLogin::Multiple(ref accounts) => {
for account in accounts.get_accounts() {
log::debug!("{:?}", account);
}
accounts
.get_account(account_id)
.ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)?
.mnemonic()
.clone()
}
};
Ok(mnemonic)
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
use crate::wallet_storage::{
self,
account_data::{MnemonicAccount, WalletAccount},
};
// This decryptes a stored wallet file using the same procedure as when signing in. Most tests
// related to the encryped wallet storage is in `wallet_storage`.
#[test]
fn decrypt_stored_wallet_for_sign_in() {
const SAVED_WALLET: &str = "src/wallet_storage/test-data/saved-wallet.json";
let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET);
let login_id = wallet_storage::LoginId::new("first".to_string());
let account_id = wallet_storage::AccountId::new("first".to_string());
let password = wallet_storage::UserPassword::new("password".to_string());
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
let stored_login =
wallet_storage::load_existing_login_at_file(&wallet_file, &login_id, &password).unwrap();
let mnemonic = extract_first_mnemonic(&stored_login).unwrap();
let expected_mnemonic = bip39::Mnemonic::from_str("country mean universe text phone begin deputy reject result good cram illness common cluster proud swamp digital patrol spread bar face december base kick").unwrap();
assert_eq!(mnemonic, expected_mnemonic);
let all_accounts: Vec<_> = stored_login
.unwrap_into_multiple_accounts(account_id.clone())
.into_accounts()
.collect();
assert_eq!(
all_accounts,
vec![WalletAccount::new(
account_id,
MnemonicAccount::new(expected_mnemonic, hd_path),
)]
);
}
#[test]
fn decrypt_stored_wallet_multiple_for_sign_in() {
// WIP(JON): same as above but with file containing multiple accounts
}
}
@@ -5,9 +5,8 @@ use crate::state::State;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::sync::Arc;
use tendermint_rpc::endpoint::broadcast::tx_commit::Response;
use tokio::sync::RwLock;
use validator_client::nymd::{AccountId, CosmosCoin};
use validator_client::nymd::{AccountId, CosmosCoin, TxResponse};
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/tauritxresult.ts"))]
@@ -34,13 +33,13 @@ pub struct TransactionDetails {
}
impl TauriTxResult {
fn new(t: Response, details: TransactionDetails) -> TauriTxResult {
fn new(t: TxResponse, details: TransactionDetails) -> TauriTxResult {
TauriTxResult {
block_height: t.height.value(),
code: t.check_tx.code.value(),
code: t.tx_result.code.value(),
details,
gas_used: t.check_tx.gas_used.value(),
gas_wanted: t.check_tx.gas_wanted.value(),
gas_used: t.tx_result.gas_used.value(),
gas_wanted: t.tx_result.gas_wanted.value(),
tx_hash: t.hash.to_string(),
}
}
+211 -24
View File
@@ -1,12 +1,13 @@
use crate::config::{Config, OptionalValidators, ValidatorUrl};
use crate::error::BackendError;
use crate::network::Network;
use crate::{config, network_config};
use strum::IntoEnumIterator;
use validator_client::nymd::SigningNymdClient;
use validator_client::Client;
use itertools::Itertools;
use once_cell::sync::Lazy;
use tokio::sync::RwLock;
use url::Url;
@@ -14,6 +15,16 @@ use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
// Some hardcoded metadata overrides
static METADATA_OVERRIDES: Lazy<Vec<(Url, ValidatorMetadata)>> = Lazy::new(|| {
vec![(
"https://rpc.nyx.nodes.guru/".parse().unwrap(),
ValidatorMetadata {
name: Some("Nodes.Guru".to_string()),
},
)]
});
#[tauri::command]
pub async fn load_config_from_files(
state: tauri::State<'_, Arc<RwLock<State>>>,
@@ -31,12 +42,26 @@ pub async fn save_config_to_files(
#[derive(Default)]
pub struct State {
config: Config,
config: config::Config,
signing_clients: HashMap<Network, Client<SigningNymdClient>>,
current_network: Network,
// All the accounts the we get from decrypting the wallet. We hold on to these for being able to
// switch accounts on-the-fly
all_accounts: Vec<WalletAccountIds>,
/// Validators that have been fetched dynamically, probably during startup.
fetched_validators: OptionalValidators,
fetched_validators: config::OptionalValidators,
/// We fetch (and cache) some metadata, such as names, when available
validator_metadata: HashMap<Url, ValidatorMetadata>,
}
pub(crate) struct WalletAccountIds {
// The wallet account id
pub id: crate::wallet_storage::AccountId,
// The set of corresponding network identities derived from the mnemonic
pub addresses: HashMap<Network, cosmrs::AccountId>,
}
impl State {
@@ -72,13 +97,13 @@ impl State {
.ok_or(BackendError::ClientNotInitialized)
}
pub fn config(&self) -> &Config {
pub fn config(&self) -> &config::Config {
&self.config
}
/// Load configuration from files. If unsuccessful we just log it and move on.
pub fn load_config_files(&mut self) {
self.config = Config::load_from_files();
self.config = config::Config::load_from_files();
}
#[allow(unused)]
@@ -98,6 +123,14 @@ impl State {
self.current_network
}
pub(crate) fn set_all_accounts(&mut self, all_accounts: Vec<WalletAccountIds>) {
self.all_accounts = all_accounts
}
pub(crate) fn get_all_accounts(&self) -> impl Iterator<Item = &WalletAccountIds> {
self.all_accounts.iter()
}
pub fn logout(&mut self) {
self.signing_clients = HashMap::new();
}
@@ -106,37 +139,98 @@ impl State {
/// 1. from the configuration file
/// 2. provided remotely
/// 3. hardcoded fallback
pub fn get_validators(&self, network: Network) -> impl Iterator<Item = ValidatorUrl> + '_ {
/// The format is the config backend format, which is flat due to serialization preference.
pub fn get_config_validator_entries(
&self,
network: Network,
) -> impl Iterator<Item = config::ValidatorConfigEntry> + '_ {
let validators_in_config = self.config.get_configured_validators(network);
let fetched_validators = self.fetched_validators.validators(network).cloned();
let default_validators = self.config.get_base_validators(network);
validators_in_config
// All the validators, in decending list of priority
let validators = validators_in_config
.chain(fetched_validators)
.chain(default_validators)
.unique()
.unique_by(|v| (v.nymd_url.clone(), v.api_url.clone()));
// Annotate with dynamic metadata
validators.map(|v| {
let metadata = self.validator_metadata.get(&v.nymd_url);
let name = v
.nymd_name
.or_else(|| metadata.and_then(|m| m.name.clone()));
config::ValidatorConfigEntry {
nymd_url: v.nymd_url,
nymd_name: name,
api_url: v.api_url,
}
})
}
pub fn get_nymd_urls(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
self.get_validators(network).into_iter().map(|v| v.nymd_url)
}
pub fn get_api_urls(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
pub fn get_nymd_urls_only(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
self
.get_validators(network)
.get_config_validator_entries(network)
.into_iter()
.map(|v| v.nymd_url)
}
pub fn get_api_urls_only(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
self
.get_config_validator_entries(network)
.into_iter()
.filter_map(|v| v.api_url)
}
/// Get the list of validator nymd urls in the network config format, suitable for passing on to
/// the UI
pub fn get_nymd_urls(
&self,
network: Network,
) -> impl Iterator<Item = network_config::ValidatorUrl> + '_ {
self
.get_config_validator_entries(network)
.into_iter()
.map(|v| network_config::ValidatorUrl {
url: v.nymd_url.to_string(),
name: v.nymd_name,
})
}
/// Get the list of validator-api urls in the network config format, suitable for passing on to
/// the UI
pub fn get_api_urls(
&self,
network: Network,
) -> impl Iterator<Item = network_config::ValidatorUrl> + '_ {
self
.get_config_validator_entries(network)
.into_iter()
.filter_map(|v| {
v.api_url.map(|u| network_config::ValidatorUrl {
url: u.to_string(),
name: None,
})
})
}
pub fn get_all_nymd_urls(&self) -> HashMap<Network, Vec<Url>> {
Network::iter()
.flat_map(|network| self.get_nymd_urls(network).map(move |url| (network, url)))
.flat_map(|network| {
self
.get_nymd_urls_only(network)
.map(move |url| (network, url))
})
.into_group_map()
}
pub fn get_all_api_urls(&self) -> HashMap<Network, Vec<Url>> {
Network::iter()
.flat_map(|network| self.get_api_urls(network).map(move |url| (network, url)))
.flat_map(|network| {
self
.get_api_urls_only(network)
.map(move |url| (network, url))
})
.into_group_map()
}
@@ -154,11 +248,71 @@ impl State {
.get(crate::config::REMOTE_SOURCE_OF_VALIDATOR_URLS.to_string())
.send()
.await?;
self.fetched_validators = serde_json::from_str(&response.text().await?)?;
log::debug!("Received validator urls: \n{}", self.fetched_validators);
self.refresh_validator_status().await?;
Ok(())
}
pub async fn refresh_validator_status(&mut self) -> Result<(), BackendError> {
log::debug!("Refreshing validator status");
// All urls for all networks
let nymd_urls = self
.get_all_nymd_urls()
.into_iter()
.flat_map(|(_, urls)| urls.into_iter());
// Fetch status for all urls
let responses = fetch_status_for_urls(nymd_urls).await?;
// Update the stored metadata
self.apply_responses(responses)?;
// Override some overrides for usability
self.apply_metadata_override(METADATA_OVERRIDES.to_vec());
Ok(())
}
fn apply_responses(
&mut self,
responses: Vec<Result<(Url, String), reqwest::Error>>,
) -> Result<(), BackendError> {
for response in responses.into_iter().flatten() {
let json: serde_json::Value = serde_json::from_str(&response.1)?;
let moniker = &json["result"]["node_info"]["moniker"];
log::debug!("Fetched moniker for: {}: {}", response.0, moniker);
// Insert into metadata map
if let Some(ref mut m) = self.validator_metadata.get_mut(&response.0) {
m.name = Some(moniker.to_string());
} else {
self.validator_metadata.insert(
response.0,
ValidatorMetadata {
name: Some(moniker.to_string()),
},
);
}
}
Ok(())
}
fn apply_metadata_override(&mut self, metadata_overrides: Vec<(Url, ValidatorMetadata)>) {
for (url, metadata) in metadata_overrides {
log::debug!("Overriding (some) metadata for: {url}");
if let Some(m) = self.validator_metadata.get_mut(&url) {
m.name = metadata.name;
} else {
self.validator_metadata.insert(url, metadata);
}
}
}
pub fn select_validator_nymd_url(
&mut self,
url: &str,
@@ -183,15 +337,41 @@ impl State {
Ok(())
}
pub fn add_validator_url(&mut self, url: ValidatorUrl, network: Network) {
pub fn add_validator_url(&mut self, url: config::ValidatorConfigEntry, network: Network) {
self.config.add_validator_url(url, network);
}
pub fn remove_validator_url(&mut self, url: ValidatorUrl, network: Network) {
pub fn remove_validator_url(&mut self, url: config::ValidatorConfigEntry, network: Network) {
self.config.remove_validator_url(url, network)
}
}
async fn fetch_status_for_urls(
nymd_urls: impl Iterator<Item = Url>,
) -> Result<Vec<Result<(Url, String), reqwest::Error>>, BackendError> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(3))
.build()?;
let responses = futures::future::join_all(nymd_urls.into_iter().map(|url| {
let client = &client;
let status_url = url.join("status").unwrap_or_else(|_| url.clone());
async move {
let resp = client.get(status_url).send().await?;
resp.text().await.map(|text| (url, text))
}
}))
.await;
Ok(responses)
}
// Validator metadata that can by dynamically populated
#[derive(Clone, Debug)]
pub struct ValidatorMetadata {
pub name: Option<String>,
}
#[macro_export]
macro_rules! client {
($state:ident) => {
@@ -223,31 +403,36 @@ mod tests {
let _api_urls = state.get_api_urls(Network::MAINNET).collect::<Vec<_>>();
state.add_validator_url(
ValidatorUrl {
config::ValidatorConfigEntry {
nymd_url: "http://nymd_url.com".parse().unwrap(),
nymd_name: Some("NymdUrl".to_string()),
api_url: Some("http://nymd_url.com/api".parse().unwrap()),
},
Network::MAINNET,
);
state.add_validator_url(
ValidatorUrl {
config::ValidatorConfigEntry {
nymd_url: "http://foo.com".parse().unwrap(),
nymd_name: None,
api_url: None,
},
Network::MAINNET,
);
state.add_validator_url(
ValidatorUrl {
config::ValidatorConfigEntry {
nymd_url: "http://bar.com".parse().unwrap(),
nymd_name: None,
api_url: None,
},
Network::MAINNET,
);
assert_eq!(
state.get_nymd_urls(Network::MAINNET).collect::<Vec<_>>(),
state
.get_nymd_urls_only(Network::MAINNET)
.collect::<Vec<_>>(),
vec![
"http://nymd_url.com/".parse().unwrap(),
"http://foo.com".parse().unwrap(),
@@ -256,10 +441,12 @@ mod tests {
],
);
assert_eq!(
state.get_api_urls(Network::MAINNET).collect::<Vec<_>>(),
state
.get_api_urls_only(Network::MAINNET)
.collect::<Vec<_>>(),
vec![
"http://nymd_url.com/api".parse().unwrap(),
"https://api.nyx.nodes.guru".parse().unwrap(),
"https://validator.nymtech.net/api/".parse().unwrap(),
],
);
assert_eq!(
@@ -1,6 +1,19 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// The wallet storage is a single json file, containing multiple entries. These are referred to as
// Logins, and has a plaintext id tag attached.
//
// Each encrypted login contains either a single account, or a list of multiple accounts.
//
// NOTE: A not insignificant amount of complexity comes from being able to handle both these cases,
// instead of, for example, converting a single account to a list of multiple accounts with a single
// entry. This also avoids resaving the wallet file when opening a file created with an earlier
// version of the wallet.
//
// In the future we might want to simplify by dropping the support for a single account entry,
// instead treating as muliple accounts with one entry.
use cosmrs::bip32::DerivationPath;
use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
@@ -8,15 +21,16 @@ use zeroize::Zeroize;
use crate::error::BackendError;
use super::encryption::EncryptedData;
use super::password::WalletAccountId;
use super::password::{AccountId, LoginId};
use super::UserPassword;
const CURRENT_WALLET_FILE_VERSION: u32 = 1;
/// The wallet, stored as a serialized json file.
#[derive(Serialize, Deserialize, Debug)]
pub(crate) struct StoredWallet {
version: u32,
accounts: Vec<EncryptedAccount>,
accounts: Vec<EncryptedLogin>,
}
impl StoredWallet {
@@ -25,16 +39,52 @@ impl StoredWallet {
self.version
}
pub fn is_empty(&self) -> bool {
self.accounts.is_empty()
}
#[allow(unused)]
pub fn len(&self) -> usize {
self.accounts.len()
}
pub fn remove_account(&mut self, id: &WalletAccountId) -> Option<EncryptedAccount> {
pub fn is_empty(&self) -> bool {
self.accounts.is_empty()
}
pub fn add_encrypted_login(&mut self, new_login: EncryptedLogin) -> Result<(), BackendError> {
if self.get_encrypted_login(&new_login.id).is_ok() {
return Err(BackendError::WalletLoginIdAlreadyExists);
}
self.accounts.push(new_login);
Ok(())
}
fn get_encrypted_login(&self, id: &LoginId) -> Result<&EncryptedData<StoredLogin>, BackendError> {
self
.accounts
.iter()
.find(|account| &account.id == id)
.map(|account| &account.account)
.ok_or(BackendError::WalletNoSuchLoginId)
}
fn get_encrypted_login_mut(&mut self, id: &LoginId) -> Result<&mut EncryptedLogin, BackendError> {
self
.accounts
.iter_mut()
.find(|account| &account.id == id)
.ok_or(BackendError::WalletNoSuchLoginId)
}
#[cfg(test)]
pub fn get_encrypted_login_by_index(&self, index: usize) -> Option<&EncryptedLogin> {
self.accounts.get(index)
}
pub fn replace_encrypted_login(&mut self, new_login: EncryptedLogin) -> Result<(), BackendError> {
let login = self.get_encrypted_login_mut(&new_login.id)?;
*login = new_login;
Ok(())
}
pub fn remove_encrypted_login(&mut self, id: &LoginId) -> Option<EncryptedLogin> {
if let Some(index) = self.accounts.iter().position(|account| &account.id == id) {
log::info!("Removing from wallet file: {id}");
Some(self.accounts.remove(index))
@@ -44,43 +94,15 @@ impl StoredWallet {
}
}
#[allow(unused)]
pub fn encrypted_account_by_index(&self, index: usize) -> Option<&EncryptedAccount> {
self.accounts.get(index)
}
fn encrypted_account(
pub fn decrypt_login(
&self,
id: &WalletAccountId,
) -> Result<&EncryptedData<StoredAccount>, BackendError> {
self
.accounts
.iter()
.find(|account| &account.id == id)
.map(|account| &account.account)
.ok_or(BackendError::NoSuchIdInWallet)
}
pub fn add_encrypted_account(
&mut self,
new_account: EncryptedAccount,
) -> Result<(), BackendError> {
if self.encrypted_account(&new_account.id).is_ok() {
return Err(BackendError::IdAlreadyExistsInWallet);
}
self.accounts.push(new_account);
Ok(())
}
pub fn decrypt_account(
&self,
id: &WalletAccountId,
id: &LoginId,
password: &UserPassword,
) -> Result<StoredAccount, BackendError> {
self.encrypted_account(id)?.decrypt_struct(password)
) -> Result<StoredLogin, BackendError> {
self.get_encrypted_login(id)?.decrypt_struct(password)
}
pub fn decrypt_all(&self, password: &UserPassword) -> Result<Vec<StoredAccount>, BackendError> {
pub fn decrypt_all(&self, password: &UserPassword) -> Result<Vec<StoredLogin>, BackendError> {
self
.accounts
.iter()
@@ -102,39 +124,176 @@ impl Default for StoredWallet {
}
}
/// Each entry in the stored wallet file. An id field in plaintext and an encrypted stored login.
#[derive(Serialize, Deserialize, Debug)]
pub(crate) struct EncryptedAccount {
pub id: WalletAccountId,
pub account: EncryptedData<StoredAccount>,
pub(crate) struct EncryptedLogin {
pub id: LoginId,
pub account: EncryptedData<StoredLogin>,
}
// future-proofing
impl EncryptedLogin {
pub(crate) fn encrypt(
id: LoginId,
login: &StoredLogin,
password: &UserPassword,
) -> Result<Self, BackendError> {
Ok(EncryptedLogin {
id,
account: super::encryption::encrypt_struct(login, password)?,
})
}
}
/// A stored login is either a account, such as a mnemonic, or a list of multiple accounts where
/// each has an inner id. Future proofed for having private key backed accounts.
#[derive(Serialize, Deserialize, Debug, Zeroize)]
#[serde(untagged)]
#[zeroize(drop)]
pub(crate) enum StoredAccount {
pub(crate) enum StoredLogin {
Mnemonic(MnemonicAccount),
// PrivateKey(PrivateKeyAccount)
Multiple(MultipleAccounts),
}
impl StoredAccount {
pub(crate) fn new_mnemonic_backed_account(
mnemonic: bip39::Mnemonic,
hd_path: DerivationPath,
) -> StoredAccount {
StoredAccount::Mnemonic(MnemonicAccount { mnemonic, hd_path })
impl StoredLogin {
#[cfg(test)]
pub(crate) fn as_mnemonic_account(&self) -> Option<&MnemonicAccount> {
match self {
StoredLogin::Mnemonic(mn) => Some(mn),
StoredLogin::Multiple(_) => None,
}
}
// If we add accounts backed by something that is not a mnemonic, this should probably be changed
// to return `Option<..>`.
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
#[cfg(test)]
pub(crate) fn as_multiple_accounts(&self) -> Option<&MultipleAccounts> {
match self {
StoredAccount::Mnemonic(account) => account.mnemonic(),
StoredLogin::Mnemonic(_) => None,
StoredLogin::Multiple(accounts) => Some(accounts),
}
}
// Return the login as multiple accounts, and if there is only a single mnemonic backed account,
// return a set containing only the single account paired with the account id passed as function
// argument.
pub(crate) fn unwrap_into_multiple_accounts(self, id: AccountId) -> MultipleAccounts {
match self {
StoredLogin::Mnemonic(ref account) => vec![WalletAccount::new(id, account.clone())].into(),
StoredLogin::Multiple(ref accounts) => accounts.clone(),
}
}
}
#[derive(Serialize, Deserialize, Debug)]
/// Multiple stored accounts, each entry having an id and a data field.
#[derive(Serialize, Deserialize, Clone, Debug, Zeroize, PartialEq, Eq)]
pub(crate) struct MultipleAccounts {
accounts: Vec<WalletAccount>,
}
impl MultipleAccounts {
pub(crate) fn new() -> Self {
MultipleAccounts {
accounts: Vec::new(),
}
}
pub(crate) fn get_accounts(&self) -> impl Iterator<Item = &WalletAccount> {
self.accounts.iter()
}
pub(crate) fn get_account(&self, id: &AccountId) -> Option<&WalletAccount> {
self.accounts.iter().find(|account| &account.id == id)
}
pub(crate) fn into_accounts(self) -> impl Iterator<Item = WalletAccount> {
self.accounts.into_iter()
}
#[allow(unused)]
pub(crate) fn len(&self) -> usize {
self.accounts.len()
}
pub(crate) fn is_empty(&self) -> bool {
self.accounts.is_empty()
}
pub(crate) fn add(
&mut self,
id: AccountId,
mnemonic: bip39::Mnemonic,
hd_path: DerivationPath,
) -> Result<(), BackendError> {
if self.get_account(&id).is_some() {
Err(BackendError::WalletAccountIdAlreadyExistsInWalletLogin)
} else {
self.accounts.push(WalletAccount::new(
id,
MnemonicAccount::new(mnemonic, hd_path),
));
Ok(())
}
}
pub(crate) fn remove(&mut self, id: &AccountId) -> Result<(), BackendError> {
if self.get_account(id).is_none() {
return Err(BackendError::WalletNoSuchAccountIdInWalletLogin);
}
self.accounts.retain(|accounts| &accounts.id != id);
Ok(())
}
}
impl From<Vec<WalletAccount>> for MultipleAccounts {
fn from(accounts: Vec<WalletAccount>) -> MultipleAccounts {
Self { accounts }
}
}
/// An entry in the list of stored accounts
#[derive(Serialize, Deserialize, Clone, Debug, Zeroize, PartialEq, Eq)]
pub(crate) struct WalletAccount {
id: AccountId,
account: AccountData,
}
impl WalletAccount {
pub(crate) fn new(id: AccountId, mnemonic_account: MnemonicAccount) -> Self {
Self {
id,
account: AccountData::Mnemonic(mnemonic_account),
}
}
pub(crate) fn id(&self) -> &AccountId {
&self.id
}
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
match self.account {
AccountData::Mnemonic(ref account) => account.mnemonic(),
}
}
#[cfg(test)]
pub(crate) fn hd_path(&self) -> &DerivationPath {
match self.account {
AccountData::Mnemonic(ref account) => account.hd_path(),
}
}
}
/// An account usually is a mnemonic account, but in the future it might be backed by a private
/// key.
#[derive(Serialize, Deserialize, Clone, Debug, Zeroize, PartialEq, Eq)]
#[serde(untagged)]
#[zeroize(drop)]
enum AccountData {
Mnemonic(MnemonicAccount),
// PrivateKey(PrivateKeyAccount)
}
/// An account backed by a unique mnemonic.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub(crate) struct MnemonicAccount {
mnemonic: bip39::Mnemonic,
#[serde(with = "display_hd_path")]
@@ -142,11 +301,15 @@ pub(crate) struct MnemonicAccount {
}
impl MnemonicAccount {
pub(crate) fn new(mnemonic: bip39::Mnemonic, hd_path: DerivationPath) -> Self {
Self { mnemonic, hd_path }
}
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
&self.mnemonic
}
#[allow(unused)]
#[cfg(test)]
pub(crate) fn hd_path(&self) -> &DerivationPath {
&self.hd_path
}
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More