Compare commits

...

147 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
fmtabbara f2fa221489 big tidy and some refactoring of high level component hierachy 2022-05-07 17:53:46 +01:00
fmtabbara 0e4787f078 minor refactors 2022-05-06 15:54:38 +01:00
fmtabbara 9b0b961d43 content updates 2022-05-06 09:56:13 +01: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
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
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
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
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
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
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
180 changed files with 6406 additions and 944 deletions
+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
+6
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
+12
View File
@@ -4,19 +4,31 @@
### 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)
Generated
+14 -2
View File
@@ -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",
]
@@ -3164,18 +3169,24 @@ dependencies = [
name = "nym-network-requester"
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",
@@ -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",
File diff suppressed because one or more lines are too long
+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();
+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
);
}
}
@@ -324,6 +324,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
Ok(ExecuteResult {
logs: parse_raw_logs(tx_res.tx_result.log)?,
data: tx_res.tx_result.data,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -364,6 +365,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
Ok(ExecuteResult {
logs: parse_raw_logs(tx_res.tx_result.log)?,
data: tx_res.tx_result.data,
transaction_hash: tx_res.hash,
gas_info,
})
@@ -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,
+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(())
}
+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"]
+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",
+28 -9
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,
@@ -114,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> {
@@ -284,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"
@@ -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>
@@ -12,6 +12,7 @@ export type MixnodeRowType = {
host: string;
layer: string;
profit_percentage: string;
avg_uptime: string;
};
export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] {
@@ -35,5 +36,6 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
host: item?.mix_node?.host || '',
layer: item?.layer || '',
profit_percentage: `${profitPercentage}%`,
avg_uptime: `${item.avg_uptime}%` || '-',
};
}
+17
View File
@@ -231,6 +231,23 @@ export const PageMixnodes: React.FC = () => {
</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>) => {
+1
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[];
+96 -6
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",
@@ -2949,7 +3000,7 @@ name = "nym_wallet"
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",
+1
View File
@@ -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://validator.nymtech.net/api",);
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
}
}
+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://validator.nymtech.net/api".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
@@ -6,22 +6,75 @@ use std::fmt;
use serde::{Deserialize, Serialize};
use zeroize::Zeroize;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct WalletAccountId(String);
// The `LoginId` is the top level id in the wallet file, and is not stored encrypted
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Zeroize)]
pub(crate) struct LoginId(String);
impl WalletAccountId {
pub(crate) fn new(id: String) -> WalletAccountId {
WalletAccountId(id)
impl LoginId {
pub(crate) fn new(id: String) -> LoginId {
LoginId(id)
}
}
impl AsRef<str> for WalletAccountId {
impl AsRef<str> for LoginId {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl fmt::Display for WalletAccountId {
impl From<String> for LoginId {
fn from(id: String) -> Self {
Self::new(id)
}
}
impl From<&str> for LoginId {
fn from(id: &str) -> Self {
Self::new(id.to_string())
}
}
impl fmt::Display for LoginId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
// For each encrypted login, we can have multiple encrypted accounts.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Zeroize)]
pub(crate) struct AccountId(String);
impl AccountId {
pub(crate) fn new(id: String) -> AccountId {
AccountId(id)
}
}
impl AsRef<str> for AccountId {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl From<String> for AccountId {
fn from(id: String) -> Self {
Self::new(id)
}
}
impl From<&str> for AccountId {
fn from(id: &str) -> Self {
Self::new(id.to_string())
}
}
impl From<LoginId> for AccountId {
fn from(login_id: LoginId) -> Self {
Self::new(login_id.0)
}
}
impl fmt::Display for AccountId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
@@ -0,0 +1,13 @@
{
"version": 1,
"accounts": [
{
"id": "default",
"account": {
"ciphertext": "cq5w4W5ex5eFFRcqLG+824XyUAUoYmrRY3NGw/rue6/mLoQKQE/07+BxzRuKjyYFasC1HBPg41KJwp2IY+/7+80rB9aXPpaKVLUcG9U40qgCw66WhgxTrXOnrt5toefpSTBL7f9N/PVwpuumfAgD9CS0ioB7/9Qoea7nYKkextGX15ex26B/ndQddvUkQ4gx+Vq7OLymv4l+nkdZ2nKMja349zd/BjnzPBB68/iIjyYlivVjtQ7FRbvpNRj6Mjg4905wGlO7bTpkw+RGiaGK4pK8fTWz8gAKr8GYoXPD",
"salt": "SPVGdbVyoEayD4ZzM4I+Jg==",
"iv": "tjpn/tRjD1gty+fQ"
}
}
]
}
@@ -0,0 +1,13 @@
{
"version": 1,
"accounts": [
{
"id": "default",
"account": {
"ciphertext": "3MDgoU2i5QMc9r80yPeq2AMk5wpkke0tXum5NsOE5NcFciF+aHLQW0dvXbGszap1y3nN4+YZD3cgGrmtKh/cChqRGJDkniaxdf3XPHh9RkiWXw2KSHHeyGrFY0INJeiky1ZtUFhWhopcHJWSnfCmVC15YFpnM5xOKpITjHAFhGt98MaYR+mS+3zoUFrjYbaZRh2TR2lFWsbR8YU1uaTYqJZ1HX1PBCub6aS3vjQm0Fwa+hAtR/gMymXJc5qtruTO4NbqYtMj3Z9eIgoVB+56SLAXlIF1Uo1pjvV0mx1hWNNiIc10ujF/wl/nnKF6icOcmrfm9XhOtsvUYBsE/wAIJZw3LKXgSX+hJbOl+zLAwJZK1xiL8n/nM1IJZDn+Wu6z0OzRaj9S7T16+brMw1oaqjk56saM8n5z725fizJj+ur6gnPBWnoyHPaCHgHdB2PKQNY0ZlwRM6dVncRaEWQDLAboyMq3FXxK9UbusNcDFpYw6bdnuJlNVf6y9yxwyvkUrt5YtgfkLyoW42z1PVtVWsV9P8eE/A/tnYjXf34xvba3K8Y1/3DTi7uuydNrSR/XhA+pevz68VWCbY+j746Yi8Lz7altePphkjfJAezodobKvMplXzqInopIWNovyemw/+1E7WZbkQIOAXg1WC1+Y/df+dffRGuGRdDerfRLmA5XLej1M/wE3WQ7b9KwlAo6XJ4hnQKwyDCqYP/ButBXW1AOnnZpCq59gGbiccZJsTMZB4OP95yFPgz8//IeDgma2PDixVmDEp0SGHhN7dlSoNa5eoglblqzJu/TcTA6jmQFA3ef0GiA3QzBjmyB4bz0bFybh8XA1brVIVlsjRwXb3/UYaVqsP6Hy1QDUpZofXIJs5lK0hUd0ECdaNFXXgHd25ifPocp09WLFyK92H6i3ABDZ7pu3b4lTUt6kHt6LTVsKkyylmYf2iMHnCcmfy4uxGTXxRjPjMgKL8pd++OZ3q62jLBuoTjgdj6pccwDvD+NYQ2FFeHmBzxyTLqUyKltYiyFlJHWLKOcXyeDHzRhHic+e/wn3VhM3NdrvtqYWA9m72Ye1L1I7VX7KatGurG6CeiFiY5xHxxpLT7dF0fJ7uxRye4JnRyYQuU7iK72qCKjgYjwjCIha4qPi5Q/x6S+uVe7yX5Eb73L3eB+IlkyW9wPHmSOcE4GpbMU96tK8xoxT0T9eQlj050GDnJ/oI2XHfZTs1bIxsjfZqW03g==",
"salt": "wXR3RnPmsoA3ncrixIvaUw==",
"iv": "/Zjn1OXsLJhA43n/"
}
}
]
}
@@ -18,4 +18,4 @@
}
}
]
}
}
@@ -0,0 +1,8 @@
import React from 'react';
import { Avatar } from '@mui/material';
import stc from 'string-to-color';
import { TAccount } from 'src/types';
export const AccountAvatar = ({ name }: Pick<TAccount, 'name'>) => (
<Avatar sx={{ bgcolor: stc(name), width: 35, height: 35 }}>{name?.split('')[0]}</Avatar>
);
@@ -0,0 +1,76 @@
import React, { useContext } from 'react';
import { Box, ListItem, ListItemAvatar, ListItemButton, ListItemText, Tooltip, Typography } from '@mui/material';
import { useClipboard } from 'use-clipboard-copy';
import { AccountsContext } from 'src/context';
import { AccountAvatar } from './AccountAvatar';
export const AccountItem = ({
name,
address,
onSelectAccount,
}: {
name: string;
address: string;
onSelectAccount: () => void;
}) => {
const { selectedAccount, setDialogToDisplay, setAccountMnemonic } = useContext(AccountsContext);
const { copy, copied } = useClipboard({ copiedTimeout: 1000 });
return (
<ListItem
disablePadding
disableGutters
sx={selectedAccount?.id === name ? { bgcolor: 'rgba(33, 208, 115, 0.1)' } : {}}
>
<ListItemButton disableRipple onClick={onSelectAccount}>
<ListItemAvatar sx={{ minWidth: 0, mr: 2 }}>
<AccountAvatar name={name} />
</ListItemAvatar>
<ListItemText
primary={name}
secondary={
<Box>
<Tooltip title={copied ? 'Copied!' : `Click to copy address ${address}`}>
<Typography
component="span"
variant="body2"
onClick={(e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
copy(address);
}}
sx={{ '&:hover': { color: 'grey.900' } }}
>
{address}
</Typography>
</Tooltip>
<Box sx={{ mt: 0.5 }}>
<Typography
variant="body2"
component="span"
sx={{ textDecoration: 'underline', mb: 0.5, '&:hover': { color: 'primary.main' } }}
onClick={(e: React.MouseEvent<HTMLElement>) => {
e.stopPropagation();
setDialogToDisplay('Mnemonic');
setAccountMnemonic((accountMnemonic) => ({ ...accountMnemonic, accountName: name }));
}}
>
Show mnemonic
</Typography>
</Box>
</Box>
}
/>
{/* edit and remove accounts todo */}
{/* <ListItemIcon>
<IconButton
onClick={(e) => {
e.stopPropagation();
handleAccountToEdit(name);
}}
>
<Edit />
</IconButton>
</ListItemIcon> */}
</ListItemButton>
</ListItem>
);
};
@@ -0,0 +1,15 @@
import React from 'react';
import { Button } from '@mui/material';
import { AccountEntry } from 'src/types';
import { AccountAvatar } from './AccountAvatar';
export const AccountOverview = ({ account, onClick }: { account: AccountEntry; onClick: () => void }) => (
<Button
startIcon={<AccountAvatar name={account.id} />}
sx={{ color: 'nym.text.dark' }}
onClick={onClick}
disableRipple
>
{account.id}
</Button>
);
@@ -0,0 +1,36 @@
import React, { useContext, useState } from 'react';
import { AccountsContext, AppContext } from 'src/context';
import { EditAccountModal } from './modals/EditAccountModal';
import { AddAccountModal } from './modals/AddAccountModal';
import { AccountsModal } from './modals/AccountsModal';
import { MnemonicModal } from './modals/MnemonicModal';
import { AccountOverview } from './AccountOverview';
import { MultiAccountHowTo } from './MultiAccountHowTo';
export const Accounts = () => {
const { accounts, selectedAccount, setDialogToDisplay } = useContext(AccountsContext);
return accounts && selectedAccount ? (
<>
<AccountOverview account={selectedAccount} onClick={() => setDialogToDisplay('Accounts')} />
<AccountsModal />
<AddAccountModal />
<EditAccountModal />
<MnemonicModal />
</>
) : null;
};
export const SingleAccount = () => {
const [showHowToDialog, setShowHowToDialog] = useState(false);
const { clientDetails } = useContext(AppContext);
return (
<>
<AccountOverview
account={{ id: 'Account 1', address: clientDetails?.client_address || '' }}
onClick={() => setShowHowToDialog(true)}
/>
<MultiAccountHowTo show={showHowToDialog} handleClose={() => setShowHowToDialog(false)} />
</>
);
};
@@ -0,0 +1,39 @@
import React from 'react';
import { Alert, Box, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
import { Close } from '@mui/icons-material';
const passwordCreationSteps = [
'Log out',
'When signing in, select “Sign in with mnemonic”',
'On the next screen click “Create a password for your account”',
'Sign in to wallet with your new password',
'Now you can create multiple accounts',
];
export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handleClose: () => void }) => (
<Dialog open={show} onClose={handleClose} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Multi accounts</Typography>
<IconButton onClick={handleClose}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
How to set up multiple accounts
</Typography>
</DialogTitle>
<DialogContent>
<Stack spacing={2}>
<Alert severity="warning" icon={false}>
<Typography>In order to create multiple accounts your wallet needs a password.</Typography>
<Typography>Follow steps below to create password.</Typography>
</Alert>
<Typography>How to create a password for your account</Typography>
{passwordCreationSteps.map((step, index) => (
<Typography key={step}>{`${index + 1}. ${step}`}</Typography>
))}
</Stack>
</DialogContent>
</Dialog>
);
@@ -0,0 +1,16 @@
import React, { useContext } from 'react';
import { AccountsProvider, AppContext } from 'src/context';
import { Accounts, SingleAccount } from './Accounts';
export const MultiAccounts = () => {
const { loginType } = useContext(AppContext);
if (loginType === 'password') {
return (
<AccountsProvider>
<Accounts />
</AccountsProvider>
);
}
return <SingleAccount />;
};
@@ -0,0 +1,10 @@
export const accounts = [
{
id: 'Account 1',
address: 'n107wsxkj08hycflnkp5ayfg6rt3pt0psm7w2t9r',
},
{
id: 'Account 2',
address: 'n1dgp04lqaasnzaww66zwdp6u24smqe7ltuny8vk',
},
];
@@ -0,0 +1,76 @@
import React, { useContext, useState } from 'react';
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Typography } from '@mui/material';
import { Add, ArrowDownwardSharp, Close } from '@mui/icons-material';
import { AccountsContext } from 'src/context';
import { AccountItem } from '../AccountItem';
import { ConfirmPasswordModal } from './ConfirmPasswordModal';
export const AccountsModal = () => {
const { accounts, dialogToDisplay, setDialogToDisplay, setError, handleSelectAccount, selectedAccount } =
useContext(AccountsContext);
const [accountToSwitchTo, setAccountToSwitchTo] = useState<string>();
const handleClose = () => {
setDialogToDisplay(undefined);
setError(undefined);
setAccountToSwitchTo(undefined);
};
if (accountToSwitchTo)
return (
<ConfirmPasswordModal
accountName={accountToSwitchTo}
onClose={() => {
handleClose();
setDialogToDisplay('Accounts');
}}
onConfirm={async (password) => {
const isSuccessful = await handleSelectAccount({ password, accountName: accountToSwitchTo });
if (isSuccessful) handleClose();
}}
/>
);
return (
<Dialog open={dialogToDisplay === 'Accounts'} onClose={handleClose} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Accounts</Typography>
<IconButton onClick={handleClose}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
Switch between accounts
</Typography>
</DialogTitle>
<DialogContent sx={{ padding: 0 }}>
{accounts?.map(({ id, address }) => (
<AccountItem
name={id}
address={address}
key={address}
onSelectAccount={() => {
if (selectedAccount?.id !== id) {
setAccountToSwitchTo(id);
}
}}
/>
))}
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button startIcon={<ArrowDownwardSharp />} onClick={() => setDialogToDisplay('Import')}>
Import account
</Button>
<Button
disableElevation
variant="contained"
startIcon={<Add fontSize="small" />}
onClick={() => setDialogToDisplay('Add')}
>
Add new account
</Button>
</DialogActions>
</Dialog>
);
};
@@ -0,0 +1,238 @@
import React, { useContext, useEffect, useState } from 'react';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
TextField,
Typography,
} from '@mui/material';
import { ArrowBackSharp } from '@mui/icons-material';
import { useClipboard } from 'use-clipboard-copy';
import { createMnemonic, validateMnemonic } from 'src/requests';
import { Console } from 'src/utils/console';
import { AccountsContext } from 'src/context';
import { ConfirmPassword, Mnemonic } from 'src/components';
import { MnemonicInput } from 'src/components/textfields';
const createAccountSteps = [
'Copy and save mnemonic for your new account',
'Name your new account',
'Confirm the password used to login to your wallet',
];
const importAccountSteps = [
'Provide mnemonic of account you want to import',
'Name your new account',
'Confirm the password used to login to your wallet',
];
const MnemonicStep = ({ mnemonic, onNext }: { mnemonic: string; onNext: () => void }) => {
const { copy, copied } = useClipboard({ copiedTimeout: 5000 });
return (
<Box sx={{ mt: 1 }}>
<DialogContent>
<Mnemonic mnemonic={mnemonic} handleCopy={copy} copied={copied} />
</DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}>
<Button disabled={!copied} fullWidth disableElevation variant="contained" size="large" onClick={onNext}>
I saved my mnemonic
</Button>
</DialogActions>
</Box>
);
};
const ImportMnemonic = ({
value,
onChange,
onNext,
}: {
value: string;
onChange: (value: string) => void;
onNext: () => void;
}) => {
const [error, setError] = useState<string>();
const handleOnNext = async () => {
const isValid = await validateMnemonic(value);
if (!isValid) setError('Please enter a valid mnemonic. Mnemonic must have a word count that is a multiple of 6.');
else onNext();
};
return (
<>
<DialogContent>
<Typography variant="body1" sx={{ color: 'error.main', my: 2 }}>
{error}
</Typography>
<MnemonicInput
mnemonic={value}
onUpdateMnemonic={(mnemon) => {
onChange(mnemon);
setError(undefined);
}}
/>
</DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}>
<Button
disabled={value.length === 0}
fullWidth
disableElevation
variant="contained"
size="large"
onClick={handleOnNext}
>
Next
</Button>
</DialogActions>
</>
);
};
const NameAccount = ({ onNext }: { onNext: (value: string) => void }) => {
const [value, setValue] = useState('');
const [error, setError] = useState<string>();
const nameValidation = /^([a-zA-Z0-9\s]){1,20}$/;
const handleNext = (accountName: string) => {
if (!nameValidation.test(accountName)) {
setError('Account name must contain only letters and numbers and be between 1 and 20 characters');
} else onNext(value);
};
return (
<>
<DialogContent>
<Typography variant="body1" sx={{ color: 'error.main', my: 2 }}>
{error}
</Typography>
<TextField
placeholder="Account name"
value={value}
onChange={(e) => {
setValue(e.target.value);
setError(undefined);
}}
fullWidth
/>
</DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}>
<Button
disabled={!value.length}
fullWidth
disableElevation
variant="contained"
size="large"
onClick={() => handleNext(value)}
>
Next
</Button>
</DialogActions>
</>
);
};
export const AddAccountModal = () => {
const [step, setStep] = useState(0);
const [data, setData] = useState({
mnemonic: '',
accountName: '',
});
const { dialogToDisplay, setDialogToDisplay, handleAddAccount, setError, isLoading, error } =
useContext(AccountsContext);
const generateMnemonic = async () => {
const mnemon = await createMnemonic();
setData((d) => ({ ...d, mnemonic: mnemon }));
};
const resetState = () => {
setData({ mnemonic: '', accountName: '' });
setStep(0);
setError(undefined);
};
const handleClose = () => {
setDialogToDisplay('Accounts');
resetState();
};
useEffect(() => {
if (dialogToDisplay === 'Add') generateMnemonic();
if (dialogToDisplay === 'Accounts') resetState();
}, [dialogToDisplay]);
useEffect(() => {
setError(undefined);
}, [step]);
return (
<Dialog
open={dialogToDisplay === 'Add' || dialogToDisplay === 'Import'}
onClose={handleClose}
fullWidth
hideBackdrop
>
<DialogTitle sx={{ pb: 0 }}>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">{`${dialogToDisplay} new account`}</Typography>
<IconButton onClick={() => (step === 0 ? handleClose() : setStep((s) => s - 1))}>
<ArrowBackSharp />
</IconButton>
</Box>
<Typography sx={{ mt: 2 }}>
{dialogToDisplay === 'Add' ? createAccountSteps[step] : importAccountSteps[step]}
</Typography>
</DialogTitle>
{(() => {
switch (step) {
case 0:
return dialogToDisplay === 'Add' ? (
<MnemonicStep mnemonic={data.mnemonic} onNext={() => setStep((s) => s + 1)} />
) : (
<ImportMnemonic
value={data.mnemonic}
onChange={(value) => setData((d) => ({ ...d, mnemonic: value }))}
onNext={() => setStep((s) => s + 1)}
/>
);
case 1:
return (
<NameAccount
onNext={(accountName) => {
setData((d) => ({ ...d, accountName }));
setStep((s) => s + 1);
}}
/>
);
case 2:
return (
<ConfirmPassword
buttonTitle="Add account"
onConfirm={async (password) => {
if (data.accountName && data.mnemonic) {
try {
await handleAddAccount({ accountName: data.accountName, mnemonic: data.mnemonic, password });
setStep(0);
setDialogToDisplay('Accounts');
} catch (e) {
Console.error(e as string);
}
}
}}
isLoading={isLoading}
error={error}
/>
);
default:
return null;
}
})()}
</Dialog>
);
};
@@ -0,0 +1,34 @@
import React, { useContext } from 'react';
import { Box, Dialog, DialogTitle, IconButton, Typography } from '@mui/material';
import { ArrowBack } from '@mui/icons-material';
import { ConfirmPassword } from 'src/components/ConfirmPassword';
import { AccountsContext } from 'src/context';
export const ConfirmPasswordModal = ({
accountName,
onClose,
onConfirm,
}: {
accountName?: string;
onClose: () => void;
onConfirm: (password: string) => Promise<void>;
}) => {
const { isLoading, error } = useContext(AccountsContext);
return (
<Dialog open={Boolean(accountName)} onClose={onClose} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Switch account</Typography>
<IconButton onClick={onClose}>
<ArrowBack />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
Confirm password
</Typography>
</DialogTitle>
<ConfirmPassword onConfirm={onConfirm} error={error} isLoading={isLoading} buttonTitle="Switch account" />
</Dialog>
);
};
@@ -0,0 +1,68 @@
import React, { useContext, useEffect, useState } from 'react';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
TextField,
Typography,
} from '@mui/material';
import { Close } from '@mui/icons-material';
import { AccountsContext } from 'src/context';
export const EditAccountModal = () => {
const [accountName, setAccountName] = useState('');
const { accountToEdit, dialogToDisplay, setDialogToDisplay, handleEditAccount } = useContext(AccountsContext);
useEffect(() => {
setAccountName(accountToEdit ? accountToEdit?.id : '');
}, [accountToEdit]);
return (
<Dialog open={dialogToDisplay === 'Edit'} onClose={() => setDialogToDisplay('Accounts')} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Edit account name</Typography>
<IconButton onClick={() => setDialogToDisplay('Accounts')}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
New wallet address
</Typography>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<Box sx={{ px: 3, mt: 1 }}>
<TextField
label="Account name"
fullWidth
value={accountName}
onChange={(e) => setAccountName(e.target.value)}
autoFocus
/>
</Box>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button
fullWidth
disableElevation
variant="contained"
size="large"
onClick={() => {
if (accountToEdit) {
handleEditAccount({ ...accountToEdit, id: accountName });
setDialogToDisplay('Accounts');
}
}}
disabled={!accountName?.length}
>
Edit
</Button>
</DialogActions>
</Dialog>
);
};
@@ -0,0 +1,66 @@
import React, { useContext, useState } from 'react';
import {
Box,
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
TextField,
Typography,
} from '@mui/material';
import { Close } from '@mui/icons-material';
import { AccountsContext } from 'src/context';
export const ImportAccountModal = () => {
const [mnemonic, setMnemonic] = useState('');
const { dialogToDisplay, setDialogToDisplay, handleImportAccount } = useContext(AccountsContext);
const handleClose = () => {
setMnemonic('');
setDialogToDisplay('Accounts');
};
return (
<Dialog open={dialogToDisplay === 'Import'} onClose={handleClose} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Import account</Typography>
<IconButton onClick={handleClose}>
<Close />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
Provide mnemonic of account you want to import
</Typography>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<Box sx={{ px: 3, mt: 1 }}>
<TextField
placeholder="Paste or type your mnemonic here"
fullWidth
value={mnemonic}
onChange={(e) => setMnemonic(e.target.value)}
autoFocus
multiline
rows={3}
/>
</Box>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
<Button
fullWidth
disableElevation
variant="contained"
size="large"
onClick={() => handleImportAccount({ id: '', address: '' })}
disabled={!mnemonic.length}
>
Import account
</Button>
</DialogActions>
</Dialog>
);
};
@@ -0,0 +1,99 @@
import React, { useContext, useState } from 'react';
import {
Box,
Button,
CircularProgress,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
IconButton,
Typography,
} from '@mui/material';
import { ArrowBackSharp } from '@mui/icons-material';
import { AccountsContext } from 'src/context';
import { useClipboard } from 'use-clipboard-copy';
import { PasswordInput, Mnemonic } from 'src/components';
export const MnemonicModal = () => {
const [password, setPassword] = useState('');
const { copy, copied } = useClipboard({ copiedTimeout: 5000 });
const {
dialogToDisplay,
setDialogToDisplay,
accountMnemonic,
setAccountMnemonic,
handleGetAccountMnemonic,
error,
setError,
isLoading,
} = useContext(AccountsContext);
const handleClose = () => {
setAccountMnemonic({ value: undefined, accountName: undefined });
setError(undefined);
setDialogToDisplay('Accounts');
setPassword('');
};
return (
<Dialog open={dialogToDisplay === 'Mnemonic'} onClose={handleClose} fullWidth hideBackdrop>
<DialogTitle>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6">Display mnemonic</Typography>
<IconButton onClick={handleClose}>
<ArrowBackSharp />
</IconButton>
</Box>
<Typography variant="body1" sx={{ color: 'grey.600' }}>
{`Display mnemonic for: ${accountMnemonic?.accountName}`}
</Typography>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
<Box sx={{ px: 3, mt: 1 }}>
{error && (
<Typography variant="body1" sx={{ color: 'error.main', mb: 2 }}>
{error}
</Typography>
)}
{!accountMnemonic.value ? (
<>
<Typography sx={{ mb: 2 }}>Enter the password used to login to your wallet</Typography>
<PasswordInput
label="Password"
password={password}
onUpdatePassword={(pswrd) => setPassword(pswrd)}
autoFocus
/>
</>
) : (
<Mnemonic mnemonic={accountMnemonic.value} handleCopy={copy} copied={copied} />
)}
</Box>
</DialogContent>
<DialogActions sx={{ p: 3 }}>
{!accountMnemonic.value && (
<Button
disableRipple
disabled={!password.length || isLoading}
fullWidth
disableElevation
variant="contained"
size="large"
onClick={async () => {
if (accountMnemonic?.accountName) {
setError(undefined);
await handleGetAccountMnemonic({ password, accountName: accountMnemonic?.accountName });
}
}}
endIcon={isLoading && <CircularProgress size={20} />}
>
Display mnemonic
</Button>
)}
</DialogActions>
</Dialog>
);
};
@@ -0,0 +1,18 @@
import React from 'react';
import { Box } from '@mui/material';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { MockAccountsProvider } from 'src/context/mocks/accounts';
import { Accounts } from '../Accounts';
export default {
title: 'Wallet / Multi Account',
component: Accounts,
} as ComponentMeta<typeof Accounts>;
export const Default: ComponentStory<typeof Accounts> = () => (
<Box display="flex" alignContent="center">
<MockAccountsProvider>
<Accounts />
</MockAccountsProvider>
</Box>
);
@@ -0,0 +1 @@
export type TDialog = 'Accounts' | 'Add' | 'Edit' | 'Import';
+21 -7
View File
@@ -1,21 +1,28 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext } from 'react';
import { AppBar as MuiAppBar, Grid, IconButton, Toolbar } from '@mui/material';
import { useHistory } from 'react-router-dom';
import { Logout } from '@mui/icons-material';
import TerminalIcon from '@mui/icons-material/Terminal';
import { ClientContext } from '../context/main';
import { AppContext } from '../context/main';
import { NetworkSelector } from './NetworkSelector';
import { Node as NodeIcon } from '../svg-icons/node';
import { MultiAccounts } from './Accounts';
import { config } from '../../config';
export const AppBar = () => {
const { showSettings, logOut, handleShowSettings, handleShowTerminal, appEnv } = useContext(ClientContext);
const { showSettings, logOut, handleShowSettings, handleShowTerminal, appEnv } = useContext(AppContext);
const history = useHistory();
return (
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent' }}>
<Toolbar disableGutters>
<Grid container justifyContent="space-between" alignItems="center" flexWrap="nowrap">
<Grid item>
<NetworkSelector />
<Grid item container alignItems="center" spacing={1}>
<Grid item>
<MultiAccounts />
</Grid>
<Grid item>
<NetworkSelector />
</Grid>
</Grid>
<Grid item container justifyContent="flex-end" md={12} lg={5} spacing={2}>
{(appEnv?.SHOW_TERMINAL || config.IS_DEV_MODE) && (
@@ -35,7 +42,14 @@ export const AppBar = () => {
</IconButton>
</Grid>
<Grid item>
<IconButton size="small" onClick={logOut} sx={{ color: 'nym.background.dark' }}>
<IconButton
size="small"
onClick={async () => {
await logOut();
history.push('/');
}}
sx={{ color: 'nym.background.dark' }}
>
<Logout fontSize="small" />
</IconButton>
</Grid>
@@ -1,6 +1,6 @@
import * as React from 'react';
import { ComponentMeta, ComponentStory } from '@storybook/react';
import { Box, Typography } from '@mui/material';
import { Box } from '@mui/material';
import { ClientAddressDisplay } from './ClientAddress';
export default {
+2 -2
View File
@@ -1,6 +1,6 @@
import React, { FC, useContext } from 'react';
import { Box, Typography, Tooltip } from '@mui/material';
import { ClientContext } from '../context/main';
import { AppContext } from '../context/main';
import { CopyToClipboard } from './CopyToClipboard';
import { splice } from '../utils';
@@ -53,6 +53,6 @@ export const ClientAddressDisplay: FC<ClientAddressProps & { address?: string }>
);
export const ClientAddress: FC<ClientAddressProps> = ({ ...props }) => {
const { clientDetails } = useContext(ClientContext);
const { clientDetails } = useContext(AppContext);
return <ClientAddressDisplay {...props} address={clientDetails?.client_address} />;
};
@@ -0,0 +1,47 @@
import React, { useState } from 'react';
import { Button, CircularProgress, DialogActions, DialogContent, Typography } from '@mui/material';
import { PasswordInput } from './textfields';
export const ConfirmPassword = ({
error,
isLoading,
onConfirm,
buttonTitle,
}: {
error?: string;
isLoading?: boolean;
buttonTitle: string;
onConfirm: (password: string) => void;
}) => {
const [value, setValue] = useState('');
return (
<>
<DialogContent>
<Typography variant="body1" sx={{ color: 'error.main', my: 2 }}>
{error}
</Typography>
<PasswordInput
password={value}
onUpdatePassword={(pswrd) => setValue(pswrd)}
placeholder="Confirm password"
autoFocus
/>
</DialogContent>
<DialogActions sx={{ p: 3, pt: 0 }}>
<Button
disabled={!value.length || isLoading}
fullWidth
disableElevation
variant="contained"
size="large"
onClick={() => onConfirm(value)}
endIcon={isLoading && <CircularProgress size={20} />}
>
{buttonTitle}
</Button>
</DialogActions>
</>
);
};
+5 -14
View File
@@ -1,17 +1,8 @@
import React from 'react';
import { FallbackProps } from 'react-error-boundary';
import { Alert, AlertTitle, Button } from '@mui/material';
import { Alert } from '@mui/material';
export const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => (
<div>
<Alert severity="error" data-testid="error-message">
<AlertTitle>{error.name}</AlertTitle>
{error.message}
</Alert>
<Alert severity="error" data-testid="stack-trace">
<AlertTitle>Stack trace</AlertTitle>
{error.stack}
</Alert>
<Button onClick={resetErrorBoundary}>Back to safety</Button>
</div>
export const Error = ({ message }: { message: string }) => (
<Alert severity="error" variant="outlined" data-testid="error" sx={{ color: 'error.light', width: '100%' }}>
{message}
</Alert>
);
@@ -0,0 +1,17 @@
import React from 'react';
import { FallbackProps } from 'react-error-boundary';
import { Alert, AlertTitle, Button } from '@mui/material';
export const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => (
<div>
<Alert severity="error" data-testid="error-message">
<AlertTitle>{error.name}</AlertTitle>
{error.message}
</Alert>
<Alert severity="error" data-testid="stack-trace">
<AlertTitle>Stack trace</AlertTitle>
{error.stack}
</Alert>
<Button onClick={resetErrorBoundary}>Back to safety</Button>
</div>
);
+2 -2
View File
@@ -2,11 +2,11 @@ import React, { useState, useEffect, useContext } from 'react';
import { Typography } from '@mui/material';
import { Operation } from '../types';
import { getGasFee } from '../requests';
import { ClientContext } from '../context/main';
import { AppContext } from '../context/main';
export const Fee = ({ feeType }: { feeType: Operation }) => {
const [fee, setFee] = useState<string>();
const { currency } = useContext(ClientContext);
const { currency } = useContext(AppContext);
const getFee = async () => {
const res = await getGasFee(feeType);
+38
View File
@@ -0,0 +1,38 @@
import React from 'react';
import { Box, LinearProgress, Stack } from '@mui/material';
import { NymWordmark } from '@nymproject/react';
import { AuthTheme } from 'src/theme';
export const LoadingPage = () => (
<AuthTheme>
<Box
sx={{
position: 'fixed',
height: '100vh',
width: '100vw',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'auto',
bgcolor: 'nym.background.dark',
zIndex: 2000,
}}
>
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
margin: 'auto',
}}
>
<Stack spacing={3} alignItems="center" sx={{ width: 1080 }}>
<NymWordmark width={75} fill="white" />
<Box width="25%">
<LinearProgress variant="indeterminate" color="primary" />
</Box>
</Stack>
</Box>
</Box>
</AuthTheme>
);
+37
View File
@@ -0,0 +1,37 @@
import React from 'react';
import { Alert, Button, Stack, TextField, Typography } from '@mui/material';
import { Check, ContentCopySharp } from '@mui/icons-material';
export const Mnemonic = ({
mnemonic,
copied,
handleCopy,
}: {
mnemonic: string;
copied: boolean;
handleCopy: (text?: string) => void;
}) => (
<Stack spacing={2} alignItems="center">
<Alert severity="warning" icon={false} sx={{ display: 'block' }}>
<Typography sx={{ textAlign: 'center' }}>
Below is your 24 word mnemonic, make sure to store it in a safe place for accessing your wallet in the future
</Typography>
</Alert>
<TextField multiline rows={3} value={mnemonic} fullWidth />
<Button
color="inherit"
disableElevation
size="large"
onClick={() => {
handleCopy(mnemonic);
}}
sx={{
width: 250,
}}
endIcon={!copied ? <ContentCopySharp /> : <Check color="success" />}
>
Copy mnemonic
</Button>
</Stack>
);
+2 -2
View File
@@ -2,7 +2,7 @@ import React, { useContext, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material';
import { ClientContext } from '../context/main';
import { AppContext } from '../context/main';
import { Bond, Delegate, Unbond, Undelegate } from '../svg-icons';
const routesSchema = [
@@ -44,7 +44,7 @@ const routesSchema = [
];
export const Nav = () => {
const { isAdminAddress, handleShowAdmin } = useContext(ClientContext);
const { isAdminAddress, handleShowAdmin } = useContext(AppContext);
const location = useLocation();
useEffect(() => {
@@ -1,7 +1,7 @@
import React, { useState, useContext } from 'react';
import { Button, List, ListItem, ListItemIcon, ListItemText, ListSubheader, Popover } from '@mui/material';
import { ArrowDropDown, CheckSharp } from '@mui/icons-material';
import { ClientContext } from '../context/main';
import { AppContext } from '../context/main';
import { config } from '../../config';
import { Network } from '../types';
@@ -23,7 +23,7 @@ const NetworkItem: React.FC<{ title: string; isSelected: boolean; onSelect: () =
);
export const NetworkSelector = () => {
const { network, switchNetwork } = useContext(ClientContext);
const { network, switchNetwork } = useContext(AppContext);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
+4 -3
View File
@@ -14,10 +14,11 @@ export const NymCard: React.FC<{
title: string | React.ReactElement;
subheader?: string;
Action?: React.ReactNode;
Icon?: any;
Icon?: React.ReactNode;
noPadding?: boolean;
}> = ({ title, subheader, Action, Icon, noPadding, children }) => (
<Card variant="outlined" sx={{ overflow: 'auto' }}>
borderless?: boolean;
}> = ({ title, subheader, Action, Icon, noPadding, borderless, children }) => (
<Card variant="outlined" sx={{ overflow: 'auto', ...(borderless && { border: 'none', dropShadow: 'none' }) }}>
<CardHeader
sx={{ p: 3, color: 'nym.background.dark' }}
title={<Title title={title} Icon={Icon} />}
+2 -2
View File
@@ -1,9 +1,9 @@
import React from 'react';
import { Box, Typography } from '@mui/material';
export const Title: React.FC<{ title: string | React.ReactNode; Icon: any }> = ({ title, Icon }) => (
export const Title: React.FC<{ title: string | React.ReactNode; Icon?: React.ReactNode }> = ({ title, Icon }) => (
<Box display="flex" alignItems="center">
{Icon && <Icon sx={{ mr: 1 }} />}{' '}
{Icon}
<Typography variant="h6" sx={{ fontWeight: 600 }}>
{title}
</Typography>
@@ -1,6 +1,6 @@
import React, { useContext, useEffect, useState } from 'react';
import { FormControl, InputLabel, ListItemText, MenuItem, Select, SelectChangeEvent, Typography } from '@mui/material';
import { ClientContext } from '../context/main';
import { AppContext } from '../context/main';
type TPoolOption = 'balance' | 'locked';
@@ -12,7 +12,7 @@ export const TokenPoolSelector: React.FC<{ disabled: boolean; onSelect: (pool: T
const {
userBalance: { tokenAllocation, balance, fetchBalance, fetchTokenAllocation },
currency,
} = useContext(ClientContext);
} = useContext(AppContext);
useEffect(() => {
(async () => {
+16 -12
View File
@@ -1,17 +1,21 @@
export * from './Error';
export * from './CopyToClipboard';
export * from './NymCard';
export * from './Nav';
export * from './NodeTypeSelector';
export * from './RequestStatus';
export * from './NoClientError';
export * from './SuccessResponse';
export * from './TransactionDetails';
export * from './NymLogo';
export * from './Fee';
export * from './AppBar';
export * from './NetworkSelector';
export * from './ClientAddress';
export * from './ConfirmPassword';
export * from './CopyToClipboard';
export * from './ErrorFallback';
export * from './Fee';
export * from './InfoToolTip';
export * from './LoadingPage';
export * from './Mnemonic';
export * from './Nav';
export * from './NetworkSelector';
export * from './NoClientError';
export * from './NodeTypeSelector';
export * from './NymCard';
export * from './NymLogo';
export * from './RequestStatus';
export * from './SuccessResponse';
export * from './textfields';
export * from './Title';
export * from './TokenPoolSelector';
export * from './TransactionDetails';
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Box, IconButton, Stack, TextField } from '@mui/material';
import { Visibility, VisibilityOff } from '@mui/icons-material';
import { Error } from './error';
import { Error } from './Error';
export const MnemonicInput: React.FC<{
mnemonic: string;
@@ -36,10 +36,11 @@ export const MnemonicInput: React.FC<{
export const PasswordInput: React.FC<{
password: string;
error?: string;
label: string;
label?: string;
placeholder?: string;
autoFocus?: boolean;
onUpdatePassword: (password: string) => void;
}> = ({ password, label, error, autoFocus, onUpdatePassword }) => {
}> = ({ password, label, placeholder, error, autoFocus, onUpdatePassword }) => {
const [showPassword, setShowPassword] = useState(false);
return (
@@ -47,6 +48,7 @@ export const PasswordInput: React.FC<{
<Box>
<TextField
label={label}
placeholder={placeholder}
fullWidth
value={password}
onChange={(e) => onUpdatePassword(e.target.value)}
+139
View File
@@ -0,0 +1,139 @@
import React, { createContext, Dispatch, SetStateAction, useContext, useEffect, useMemo, useState } from 'react';
import { AccountEntry } from 'src/types';
import { addAccount as addAccountRequest, showMnemonicForAccount } from 'src/requests';
import { useSnackbar } from 'notistack';
import { AppContext } from './main';
type TAccounts = {
accounts?: AccountEntry[];
selectedAccount?: AccountEntry;
accountToEdit?: AccountEntry;
dialogToDisplay?: TAccountsDialog;
isLoading: boolean;
error?: string;
accountMnemonic: TAccountMnemonic;
setError: Dispatch<SetStateAction<string | undefined>>;
setAccountMnemonic: Dispatch<SetStateAction<TAccountMnemonic>>;
handleAddAccount: (data: { accountName: string; mnemonic: string; password: string }) => void;
setDialogToDisplay: (dialog?: TAccountsDialog) => void;
handleSelectAccount: (data: { accountName: string; password: string }) => Promise<boolean>;
handleAccountToEdit: (accountId: string) => void;
handleEditAccount: (account: AccountEntry) => void;
handleImportAccount: (account: AccountEntry) => void;
handleGetAccountMnemonic: (data: { password: string; accountName: string }) => void;
};
export type TAccountsDialog = 'Accounts' | 'Add' | 'Edit' | 'Import' | 'Mnemonic';
export type TAccountMnemonic = { value?: string; accountName?: string };
export const AccountsContext = createContext({} as TAccounts);
export const AccountsProvider: React.FC = ({ children }) => {
const [accounts, setAccounts] = useState<AccountEntry[]>([]);
const [selectedAccount, setSelectedAccount] = useState<AccountEntry>();
const [accountToEdit, setAccountToEdit] = useState<AccountEntry>();
const [dialogToDisplay, setDialogToDisplay] = useState<TAccountsDialog>();
const [accountMnemonic, setAccountMnemonic] = useState<TAccountMnemonic>({
value: undefined,
accountName: undefined,
});
const [error, setError] = useState<string>();
const [isLoading, setIsLoading] = useState(false);
const { onAccountChange, storedAccounts } = useContext(AppContext);
const { enqueueSnackbar } = useSnackbar();
const handleAddAccount = async ({
accountName,
mnemonic,
password,
}: {
accountName: string;
mnemonic: string;
password: string;
}) => {
setIsLoading(true);
try {
const newAccount = await addAccountRequest({
accountName,
mnemonic,
password,
});
setAccounts((accs) => [...accs, newAccount]);
enqueueSnackbar('New account created', { variant: 'success' });
} catch (e) {
setError(`Error adding account: ${e}`);
throw new Error();
} finally {
setIsLoading(false);
}
};
const handleEditAccount = (account: AccountEntry) =>
setAccounts((accs) => accs?.map((acc) => (acc.address === account.address ? account : acc)));
const handleImportAccount = (account: AccountEntry) => setAccounts((accs) => [...(accs ? [...accs] : []), account]);
const handleAccountToEdit = (accountName: string) =>
setAccountToEdit(accounts?.find((acc) => acc.id === accountName));
const handleSelectAccount = async ({ accountName, password }: { accountName: string; password: string }) => {
try {
await onAccountChange({ accountId: accountName, password });
const match = accounts?.find((acc) => acc.id === accountName);
setSelectedAccount(match);
return true;
} catch (e) {
setError('Error switching account. Please check your password');
return false;
}
};
const handleGetAccountMnemonic = async ({ password, accountName }: { password: string; accountName: string }) => {
try {
setIsLoading(true);
const mnemonic = await showMnemonicForAccount({ password, accountName });
setAccountMnemonic({ value: mnemonic, accountName });
} catch (e) {
setError(e as string);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (storedAccounts) {
setAccounts(storedAccounts);
}
if (storedAccounts && !selectedAccount) {
setSelectedAccount(storedAccounts[0]);
}
}, [storedAccounts]);
return (
<AccountsContext.Provider
value={useMemo(
() => ({
error,
setError,
accounts,
selectedAccount,
accountToEdit,
dialogToDisplay,
accountMnemonic,
setDialogToDisplay,
setAccountMnemonic,
isLoading,
handleAddAccount,
handleEditAccount,
handleAccountToEdit,
handleSelectAccount,
handleImportAccount,
handleGetAccountMnemonic,
}),
[accounts, selectedAccount, accountToEdit, dialogToDisplay, isLoading, error, accountMnemonic],
)}
>
{children}
</AccountsContext.Provider>
);
};
@@ -1,11 +1,10 @@
import React, { createContext, useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { createMnemonic } from 'src/requests';
import { TMnemonicWords } from '../types';
import { TMnemonicWords } from 'src/pages/auth/types';
export const SignInContext = createContext({} as TSignInContent);
export const AuthContext = createContext({} as TAuthContext);
export type TSignInContent = {
export type TAuthContext = {
error?: string;
password: string;
mnemonic: string;
@@ -22,23 +21,17 @@ const mnemonicToArray = (mnemonic: string): TMnemonicWords =>
.split(' ')
.reduce((a, c: string, index) => [...a, { name: c, index: index + 1, disabled: false }], [] as TMnemonicWords);
export const SignInProvider: React.FC = ({ children }) => {
export const AuthProvider: React.FC = ({ children }) => {
const [password, setPassword] = useState('');
const [mnemonic, setMnemonic] = useState('');
const [mnemonicWords, setMnemonicWords] = useState<TMnemonicWords>([]);
const [error, setError] = useState<string>();
const history = useHistory();
const generateMnemonic = async () => {
const mnemonicPhrase = await createMnemonic();
setMnemonic(mnemonicPhrase);
};
useEffect(() => {
history.push('/welcome');
}, []);
useEffect(() => {
if (mnemonic.length > 0) {
const mnemonicArray = mnemonicToArray(mnemonic);
@@ -54,7 +47,7 @@ export const SignInProvider: React.FC = ({ children }) => {
};
return (
<SignInContext.Provider
<AuthContext.Provider
value={useMemo(
() => ({
error,
@@ -71,6 +64,6 @@ export const SignInProvider: React.FC = ({ children }) => {
)}
>
{children}
</SignInContext.Provider>
</AuthContext.Provider>
);
};
+3
View File
@@ -0,0 +1,3 @@
export * from './main';
export * from './auth';
export * from './accounts';
+71 -26
View File
@@ -1,8 +1,7 @@
import React, { useMemo, createContext, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { useSnackbar } from 'notistack';
import { TLoginType } from 'src/pages/sign-in/types';
import { Account, AppEnv, Network, TCurrency, TMixnodeBondDetails } from '../types';
import { Account, Network, TCurrency, TMixnodeBondDetails, AccountEntry, AppEnv } from '../types';
import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance';
import {
getMixnodeBondDetails,
@@ -10,7 +9,9 @@ import {
signInWithMnemonic,
signInWithPassword,
signOut,
switchAccount,
getEnv,
listAccounts,
} from '../requests';
import { currencyMap } from '../utils';
import { Console } from '../utils/console';
@@ -26,10 +27,13 @@ export const urls = (networkName?: Network) =>
networkExplorer: `https://${networkName}-explorer.nymtech.net`,
};
type TClientContext = {
type TLoginType = 'mnemonic' | 'password';
type TAppContext = {
mode: 'light' | 'dark';
appEnv?: AppEnv;
clientDetails?: Account;
storedAccounts?: AccountEntry[];
mixnodeDetails?: TMixnodeBondDetails | null;
userBalance: TUseuserBalance;
showAdmin: boolean;
@@ -40,30 +44,34 @@ type TClientContext = {
isLoading: boolean;
isAdminAddress: boolean;
error?: string;
loginType?: TLoginType;
setIsLoading: (isLoading: boolean) => void;
setError: (value?: string) => void;
switchNetwork: (network: Network) => void;
getBondDetails: () => Promise<void>;
handleShowSettings: () => void;
handleShowAdmin: () => void;
logIn: (opts: { type: TLoginType; value: string }) => void;
handleShowTerminal: () => void;
logIn: (opts: { type: 'mnemonic' | 'password'; value: string }) => void;
signInWithPassword: (password: string) => void;
logOut: () => void;
onAccountChange: ({ accountId, password }: { accountId: string; password: string }) => void;
};
export const ClientContext = createContext({} as TClientContext);
export const AppContext = createContext({} as TAppContext);
export const ClientContextProvider = ({ children }: { children: React.ReactNode }) => {
const [appEnv, setAppEnv] = useState<AppEnv>();
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [clientDetails, setClientDetails] = useState<Account>();
const [storedAccounts, setStoredAccounts] = useState<AccountEntry[]>();
const [mixnodeDetails, setMixnodeDetails] = useState<TMixnodeBondDetails | null>();
const [network, setNetwork] = useState<Network | undefined>();
const [appEnv, setAppEnv] = useState<AppEnv>();
const [currency, setCurrency] = useState<TCurrency>();
const [showAdmin, setShowAdmin] = useState(false);
const [showSettings, setShowSettings] = useState(false);
const [showTerminal, setShowTerminal] = useState(false);
const [mode] = useState<'light' | 'dark'>('light');
const [loginType, setLoginType] = useState<'mnemonic' | 'password'>();
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string>();
@@ -71,6 +79,15 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
const history = useHistory();
const { enqueueSnackbar } = useSnackbar();
const clearState = () => {
userBalance.clearAll();
setStoredAccounts(undefined);
setNetwork(undefined);
setError(undefined);
setIsLoading(false);
setMixnodeDetails(undefined);
};
const loadAccount = async (n: Network) => {
try {
const client = await selectNetwork(n);
@@ -83,6 +100,11 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
}
};
const loadStoredAccounts = async () => {
const accounts = await listAccounts();
setStoredAccounts(accounts);
};
const getBondDetails = async () => {
setMixnodeDetails(undefined);
try {
@@ -93,19 +115,25 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
}
};
useEffect(() => {
getEnv().then(setAppEnv);
}, []);
const refreshAccount = async (_network: Network) => {
await loadAccount(_network);
if (loginType === 'password') {
await loadStoredAccounts();
}
};
useEffect(() => {
const refreshAccount = async () => {
if (network) {
await loadAccount(network);
await getBondDetails();
await userBalance.fetchBalance();
}
};
refreshAccount();
if (!clientDetails) {
clearState();
history.push('/');
}
}, [clientDetails]);
useEffect(() => {
if (network) {
refreshAccount(network);
getEnv().then(setAppEnv);
}
}, [network]);
const logIn = async ({ type, value }: { type: TLoginType; value: string }) => {
@@ -117,8 +145,10 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
setIsLoading(true);
if (type === 'mnemonic') {
await signInWithMnemonic(value);
setLoginType('mnemonic');
} else {
await signInWithPassword(value);
setLoginType('password');
}
setNetwork('MAINNET');
history.push('/balance');
@@ -130,16 +160,26 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
};
const logOut = async () => {
userBalance.clearAll();
setClientDetails(undefined);
setNetwork(undefined);
setError(undefined);
setIsLoading(false);
setMixnodeDetails(undefined);
await signOut();
setClientDetails(undefined);
enqueueSnackbar('Successfully logged out', { variant: 'success' });
};
const onAccountChange = async ({ accountId, password }: { accountId: string; password: string }) => {
if (network) {
setIsLoading(true);
try {
await switchAccount({ accountId, password });
await loadAccount(network);
enqueueSnackbar('Account switch success', { variant: 'success', preventDuplicate: true });
} catch (e) {
throw new Error(`Error swtiching account: ${e}`);
} finally {
setIsLoading(false);
}
}
};
const handleShowAdmin = () => setShowAdmin((show) => !show);
const handleShowSettings = () => setShowSettings((show) => !show);
const handleShowTerminal = () => setShowTerminal((show) => !show);
@@ -153,6 +193,7 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
isLoading,
error,
clientDetails,
storedAccounts,
mixnodeDetails,
userBalance,
showAdmin,
@@ -160,6 +201,7 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
showTerminal,
network,
currency,
loginType,
setIsLoading,
setError,
signInWithPassword,
@@ -170,8 +212,10 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
handleShowTerminal,
logIn,
logOut,
onAccountChange,
}),
[
loginType,
mode,
appEnv,
isLoading,
@@ -181,11 +225,12 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
userBalance,
showAdmin,
showSettings,
showTerminal,
network,
currency,
storedAccounts,
showTerminal,
],
);
return <ClientContext.Provider value={memoizedValue}>{children}</ClientContext.Provider>;
return <AppContext.Provider value={memoizedValue}>{children}</AppContext.Provider>;
};
+83
View File
@@ -0,0 +1,83 @@
import React, { useMemo, useState } from 'react';
import { AccountEntry } from 'src/types';
import { AccountsContext, TAccountMnemonic, TAccountsDialog } from '../accounts';
export const MockAccountsProvider: React.FC = ({ children }) => {
const [accounts, setAccounts] = useState<AccountEntry[]>([{ id: 'Account_1', address: 'abc123' }]);
const [selectedAccount, setSelectedAccount] = useState<AccountEntry | undefined>({
id: 'Account_1',
address: 'abc123',
});
const [accountToEdit, setAccountToEdit] = useState<AccountEntry>();
const [dialogToDisplay, setDialogToDisplay] = useState<TAccountsDialog>();
const [accountMnemonic, setAccountMnemonic] = useState<TAccountMnemonic>({
value: undefined,
accountName: undefined,
});
const [error, setError] = useState<string>();
const [isLoading, setIsLoading] = useState(false);
const handleAddAccount = async ({ accountName }: { accountName: string; mnemonic: string; password: string }) => {
setIsLoading(true);
try {
setAccounts((accs) => [...accs, { address: 'abc123', id: accountName }]);
setDialogToDisplay('Accounts');
} catch (e) {
setError(`Error adding account: ${e}`);
} finally {
setIsLoading(false);
}
};
const handleEditAccount = (account: AccountEntry) =>
setAccounts((accs) => accs?.map((acc) => (acc.address === account.address ? account : acc)));
const handleImportAccount = (account: AccountEntry) => setAccounts((accs) => [...(accs ? [...accs] : []), account]);
const handleAccountToEdit = (accountName: string) =>
setAccountToEdit(accounts?.find((acc) => acc.id === accountName));
const handleSelectAccount = async ({ accountName }: { accountName: string; password: string }) => {
const match = accounts?.find((acc) => acc.id === accountName);
setSelectedAccount(match);
return true;
};
const handleGetAccountMnemonic = async ({ accountName }: { password: string; accountName: string }) => {
try {
setIsLoading(true);
const mnemonic = 'test mnemonic';
setAccountMnemonic({ value: mnemonic, accountName });
} catch (e) {
setError(e as string);
} finally {
setIsLoading(false);
}
};
return (
<AccountsContext.Provider
value={useMemo(
() => ({
error,
setError,
accounts,
selectedAccount,
accountToEdit,
dialogToDisplay,
accountMnemonic,
setDialogToDisplay,
setAccountMnemonic,
isLoading,
handleAddAccount,
handleEditAccount,
handleAccountToEdit,
handleSelectAccount,
handleImportAccount,
handleGetAccountMnemonic,
}),
[accounts, selectedAccount, accountToEdit, dialogToDisplay, isLoading, error, accountMnemonic],
)}
>
{children}
</AccountsContext.Provider>
);
};
+2 -2
View File
@@ -1,6 +1,6 @@
import { useCallback, useContext, useEffect, useState } from 'react';
import { Console } from '../utils/console';
import { ClientContext } from '../context/main';
import { AppContext } from '../context/main';
import { checkGatewayOwnership, checkMixnodeOwnership, getVestingPledgeInfo } from '../requests';
import { EnumNodeType, TNodeOwnership } from '../types';
@@ -11,7 +11,7 @@ const initial = {
};
export const useCheckOwnership = () => {
const { clientDetails } = useContext(ClientContext);
const { clientDetails } = useContext(AppContext);
const [ownership, setOwnership] = useState<TNodeOwnership>(initial);
const [isLoading, setIsLoading] = useState(true);
+1 -3
View File
@@ -92,9 +92,7 @@ export const useGetBalance = (address?: string): TUseuserBalance => {
} catch (err) {
setError(err as string);
} finally {
setTimeout(() => {
setIsLoading(false);
}, 1000);
setIsLoading(false);
}
}, []);
+22 -43
View File
@@ -1,60 +1,39 @@
import React, { useContext, useEffect } from 'react';
import React, { useEffect } from 'react';
import ReactDOM from 'react-dom';
import { ErrorBoundary } from 'react-error-boundary';
import { BrowserRouter as Router } from 'react-router-dom';
import { SnackbarProvider } from 'notistack';
import { AppRoutes, SignInRoutes } from './routes';
import { ClientContext, ClientContextProvider } from './context/main';
import { ApplicationLayout } from './layouts';
import { Admin, Settings } from './pages';
import { Routes } from './routes';
import { AppProvider } from './context/main';
import { ErrorFallback } from './components';
import { NymWalletTheme, WelcomeTheme } from './theme';
import { NymWalletTheme } from './theme';
import { maximizeWindow } from './utils';
import { SignInProvider } from './pages/sign-in/context';
import { Terminal } from './pages/terminal';
const App = () => {
const { clientDetails } = useContext(ClientContext);
useEffect(() => {
maximizeWindow();
}, []);
return !clientDetails ? (
<WelcomeTheme>
<SignInProvider>
<SignInRoutes />
</SignInProvider>
</WelcomeTheme>
) : (
<NymWalletTheme>
<ApplicationLayout>
<Settings />
<Admin />
<Terminal />
<AppRoutes />
</ApplicationLayout>
</NymWalletTheme>
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Router>
<SnackbarProvider
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
>
<AppProvider>
<NymWalletTheme>
<Routes />
</NymWalletTheme>
</AppProvider>
</SnackbarProvider>
</Router>
</ErrorBoundary>
);
};
const AppWrapper = () => (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Router>
<SnackbarProvider
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
>
<ClientContextProvider>
<App />
</ClientContextProvider>
</SnackbarProvider>
</Router>
</ErrorBoundary>
);
const root = document.getElementById('root');
ReactDOM.render(<AppWrapper />, root);
ReactDOM.render(<App />, root);
+36 -28
View File
@@ -1,44 +1,52 @@
import React from 'react';
import React, { useContext } from 'react';
import { NymWordmark } from '@nymproject/react';
import { Box, Container } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { AppBar, Nav } from '../components';
import { AppContext } from 'src/context';
import { Settings } from 'src/pages';
import { AppBar, LoadingPage, Nav } from '../components';
export const ApplicationLayout: React.FC = ({ children }) => {
const theme = useTheme();
const { isLoading, showSettings } = useContext(AppContext);
return (
<Box
sx={{
height: '100vh',
width: '100vw',
display: 'grid',
gridTemplateColumns: '240px auto',
gridTemplateRows: '100%',
overflow: 'hidden',
}}
>
<>
{isLoading && <LoadingPage />}
{showSettings && <Settings />}
<Box
sx={{
background: '#121726',
overflow: 'auto',
py: 4,
px: 5,
height: '100vh',
width: '100vw',
display: 'grid',
gridTemplateColumns: '240px auto',
gridTemplateRows: '100%',
overflow: 'hidden',
}}
display="flex"
flexDirection="column"
justifyContent="space-between"
>
<Box>
<Box sx={{ mb: 4 }}>
<NymWordmark height={14} fill={theme.palette.background.paper} />
<Box
sx={{
background: '#121726',
overflow: 'auto',
py: 3,
px: 5,
}}
display="flex"
flexDirection="column"
justifyContent="space-between"
>
<Box>
<Box sx={{ mb: 4 }}>
<NymWordmark height={14} fill={theme.palette.background.paper} />
</Box>
<Nav />
</Box>
<Nav />
</Box>
<Container>
<AppBar />
{children}
</Container>
</Box>
<Container>
<AppBar />
{children}
</Container>
</Box>
</>
);
};
+41
View File
@@ -0,0 +1,41 @@
import React, { useContext } from 'react';
import { NymWordmark } from '@nymproject/react';
import { Stack, Box } from '@mui/material';
import { AppContext } from 'src/context';
import { LoadingPage } from 'src/components';
import { Step } from '../pages/auth/components/step';
export const AuthLayout: React.FC = ({ children }) => {
const { isLoading } = useContext(AppContext);
return isLoading ? (
<LoadingPage />
) : (
<Box
sx={{
height: '100vh',
width: '100vw',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
overflow: 'auto',
bgcolor: 'nym.background.dark',
}}
>
<Box
sx={{
width: '100%',
display: 'flex',
justifyContent: 'center',
margin: 'auto',
}}
>
<Stack spacing={3} alignItems="center" sx={{ width: 1080 }}>
<NymWordmark width={75} />
<Step />
{children}
</Stack>
</Box>
</Box>
);
};
+2 -2
View File
@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { Backdrop, Box, Button, CircularProgress, FormControl, Grid, Paper, Slide, TextField } from '@mui/material';
import { ClientContext } from '../../context/main';
import { AppContext } from '../../context/main';
import { NymCard } from '../../components';
import { getContractParams, setContractParams } from '../../requests';
import { TauriContractStateParams } from '../../types';
@@ -99,7 +99,7 @@ const AdminForm: React.FC<{
};
export const Admin: React.FC = () => {
const { showAdmin, handleShowAdmin } = useContext(ClientContext);
const { showAdmin, handleShowAdmin } = useContext(AppContext);
const [isLoading, setIsLoading] = useState(false);
const [params, setParams] = useState<TauriContractStateParams>();
@@ -1,8 +1,4 @@
export * from './heading';
export * from './word-tiles';
export * from './render-page';
export * from './password-strength';
export * from './error';
export * from './textfields';
export * from './step';
export * from './page-layout';
+9
View File
@@ -0,0 +1,9 @@
import React from 'react';
import { AuthProvider } from 'src/context';
import { AuthRoutes } from 'src/routes/auth';
export const Auth = () => (
<AuthProvider>
<AuthRoutes />
</AuthProvider>
);
@@ -2,11 +2,12 @@ import React, { useContext, useEffect, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Button, Stack } from '@mui/material';
import { validateMnemonic } from 'src/requests';
import { MnemonicInput, Subtitle } from '../components';
import { SignInContext } from '../context';
import { MnemonicInput } from 'src/components';
import { AuthContext } from 'src/context/auth';
import { Subtitle } from '../components';
export const ConfirmMnemonic = () => {
const { error, setError, setMnemonic, mnemonic } = useContext(SignInContext);
const { error, setError, setMnemonic, mnemonic } = useContext(AuthContext);
const [localMnemonic, setLocalMnemonic] = useState(mnemonic);
const history = useHistory();
@@ -2,17 +2,17 @@ import React, { useContext, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Button, CircularProgress, FormControl, Stack } from '@mui/material';
import { useSnackbar } from 'notistack';
import { AuthContext } from 'src/context/auth';
import { createPassword } from 'src/requests';
import { PasswordInput } from 'src/components';
import { Subtitle, Title, PasswordStrength } from '../components';
import { PasswordInput } from '../components/textfields';
import { SignInContext } from '../context';
import { createPassword } from '../../../requests';
export const ConnectPassword = () => {
const [confirmedPassword, setConfirmedPassword] = useState<string>('');
const [isStrongPassword, setIsStrongPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { mnemonic, password, setPassword, resetState } = useContext(SignInContext);
const { mnemonic, password, setPassword, resetState } = useContext(AuthContext);
const history = useHistory();
const { enqueueSnackbar } = useSnackbar();
@@ -1,13 +1,13 @@
import React, { useContext, useEffect } from 'react';
import { useHistory } from 'react-router-dom';
import { Alert, Button, Stack, Typography } from '@mui/material';
import { AuthContext } from 'src/context/auth';
import { Check, ContentCopySharp } from '@mui/icons-material';
import { useClipboard } from 'use-clipboard-copy';
import { WordTiles } from '../components';
import { SignInContext } from '../context';
export const CreateMnemonic = () => {
const { mnemonic, mnemonicWords, generateMnemonic, resetState } = useContext(SignInContext);
const { mnemonic, mnemonicWords, generateMnemonic, resetState } = useContext(AuthContext);
const history = useHistory();
useEffect(() => {
@@ -2,18 +2,17 @@ import React, { useContext, useState } from 'react';
import { useHistory } from 'react-router-dom';
import { Button, FormControl, Stack } from '@mui/material';
import { useSnackbar } from 'notistack';
import { AuthContext } from 'src/context/auth';
import { createPassword } from 'src/requests';
import { PasswordInput } from 'src/components';
import { Subtitle, Title, PasswordStrength } from '../components';
import { PasswordInput } from '../components/textfields';
import { SignInContext } from '../context';
import { createPassword } from '../../../requests';
export const CreatePassword = () => {
const { password, setPassword, resetState } = useContext(SignInContext);
const { password, setPassword, resetState, mnemonic } = useContext(AuthContext);
const [confirmedPassword, setConfirmedPassword] = useState<string>('');
const [isStrongPassword, setIsStrongPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { mnemonic } = useContext(SignInContext);
const history = useHistory();
const handleSkip = () => {

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