Compare commits

..

9 Commits

Author SHA1 Message Date
durch 6dbb3fbd3f Trigger rewarded_set update on bootstrap error 2022-03-30 14:35:03 +02:00
Drazen Urch 95b6ac50be Type safe bounds (#1179)
* Type safe bounds

* Debug get_all_delegations

* List raw delegations

* query all delegation values

* Get all keys and values

* Differentiate on len

* Clean up duplicates

* Remove stop gap

* Cleanup, fix paged query key

* Reduce queries required to get all delegations

* Sandbox migration

* Delete migration
2022-03-30 12:20:03 +02:00
Tommy Verrall 5f2247ab83 Merge pull request #1180 from nymtech/update-wallet-unbond
fix unbond page
2022-03-30 09:35:22 +01:00
Tommy Verrall 842bcfa782 fix unbond page 2022-03-30 09:34:17 +01:00
Drazen Urch c2938e0672 Fix delegation paging (#1174)
* Fix delegation paging and tests

* Circuit breaker, upgrade storage and cosmwasm

* beta6

* Cleanup delegation events
2022-03-29 17:02:56 +02:00
Tommy Verrall 278516ad11 Merge pull request #1172 from nymtech/feature/update-binary-version
Update binaries to rc version
2022-03-29 10:02:48 +01:00
Mark Sinclair ce241339bf Create nym-release-publish.yml 2022-03-28 18:16:08 +01:00
Tommy Verrall 8d7428923a Update binaries to rc version 2022-03-28 15:32:31 +01:00
Drazen Urch 4a1a4b6415 Different values for mixes and gateways (#1169) 2022-03-28 13:33:55 +02:00
73 changed files with 832 additions and 1319 deletions
+43
View File
@@ -0,0 +1,43 @@
name: Publish Nym binaries
on:
release:
types: [created]
jobs:
publish-nym:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
- name: Check the release tag starts with `nym-binaries-`
if: startsWith(github.ref, 'refs/tags/nym-binaries-') == false
uses: actions/github-script@v3
with:
script: |
core.setFailed('Release tag did not start with nym-binaries-...')
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --release
- name: Upload to release based on tag name
uses: softprops/action-gh-release@v1
with:
files: |
target/release/nym-client
target/release/nym-gateway
target/release/nym-mixnode
target/release/nym-socks5-client
target/release/nym-validator-api
Generated
+24 -34
View File
@@ -549,7 +549,7 @@ dependencies = [
[[package]]
name = "client-core"
version = "0.12.0"
version = "1.0.0-rc.1"
dependencies = [
"config",
"crypto",
@@ -750,31 +750,31 @@ dependencies = [
[[package]]
name = "cosmwasm-crypto"
version = "1.0.0-beta6"
version = "1.0.0-beta7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dddc1443004c6340e55ca66d98e9d2f1a44aadf4ce2bed2c4f29baa8a15e7b7"
checksum = "88c2565b1e73a816fb659ef4838fc356143fbd35f43c48a51d2d7d4e5d6679d3"
dependencies = [
"digest 0.9.0",
"ed25519-zebra",
"k256 0.9.6",
"rand_core 0.5.1",
"rand_core 0.6.3",
"thiserror",
]
[[package]]
name = "cosmwasm-derive"
version = "1.0.0-beta6"
version = "1.0.0-beta7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4f0f10f165b8bcc558a13cddb498140960544519aa0581532c766dd80b5598"
checksum = "fa89fcdf8dbbe0088e663d0a814aa7368e7ebe8fb045a3a150fb5fdc2ffe3b45"
dependencies = [
"syn",
]
[[package]]
name = "cosmwasm-std"
version = "1.0.0-beta6"
version = "1.0.0-beta7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0f3145097b692b2d95fa5d2c7c6fdd60f193ccc709857e7e1987a608725300"
checksum = "bcb8f99a61d0b9069e1afc80a4ffea87dcc3523edd992080923870b13a677da0"
dependencies = [
"base64",
"cosmwasm-crypto",
@@ -1071,20 +1071,9 @@ dependencies = [
[[package]]
name = "cw-storage-plus"
version = "0.11.1"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d7ee1963302b0ac2a9d42fe0faec826209c17452bfd36fbfd9d002a88929261"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
[[package]]
name = "cw-storage-plus"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c087ff98fb0475db4c2b5298a5fd12b2848d2854b39d1115d930ee6da24d1eed"
checksum = "5e8b7f9a758c030d375520df947323c052704f784561fc28dcaab4f988c50a30"
dependencies = [
"cosmwasm-std",
"schemars",
@@ -1322,16 +1311,17 @@ dependencies = [
[[package]]
name = "ed25519-zebra"
version = "2.2.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409"
checksum = "403ef3e961ab98f0ba902771d29f842058578bb1ce7e3c59dad5a6a93e784c69"
dependencies = [
"curve25519-dalek",
"hex",
"rand_core 0.5.1",
"rand_core 0.6.3",
"serde",
"sha2",
"thiserror",
"zeroize",
]
[[package]]
@@ -1470,7 +1460,7 @@ dependencies = [
[[package]]
name = "explorer-api"
version = "0.1.0"
version = "1.0.0-rc.1"
dependencies = [
"chrono",
"humantime-serde",
@@ -2902,7 +2892,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "0.12.1"
version = "1.0.0-rc.1"
dependencies = [
"clap 2.34.0",
"client-core",
@@ -2936,7 +2926,7 @@ dependencies = [
[[package]]
name = "nym-gateway"
version = "0.12.1"
version = "1.0.0-rc.1"
dependencies = [
"anyhow",
"async-trait",
@@ -2982,7 +2972,7 @@ dependencies = [
[[package]]
name = "nym-mixnode"
version = "0.12.1"
version = "1.0.0-rc.1"
dependencies = [
"anyhow",
"bs58",
@@ -3020,7 +3010,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "0.12.0"
version = "1.0.0-rc.1"
dependencies = [
"clap 2.34.0",
"dirs",
@@ -3041,7 +3031,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "0.12.1"
version = "1.0.0-rc.1"
dependencies = [
"clap 2.34.0",
"client-core",
@@ -3076,7 +3066,7 @@ dependencies = [
[[package]]
name = "nym-validator-api"
version = "0.12.0"
version = "1.0.0-rc.1"
dependencies = [
"anyhow",
"attohttpc",
@@ -6062,11 +6052,11 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vesting-contract"
version = "0.1.0"
version = "1.0.0-rc.1"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus 0.12.1",
"cw-storage-plus",
"mixnet-contract-common",
"schemars",
"serde",
@@ -6080,7 +6070,7 @@ version = "0.1.0"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus 0.11.1",
"cw-storage-plus",
"mixnet-contract-common",
"schemars",
"serde",
+1 -1
View File
@@ -1,4 +1,4 @@
<svg viewBox="0 0 210 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="210" height="56" viewBox="0 0 210 56" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M45.8829 0.142822H45.7169V0.28114V48.637L25.3289 0.225818L25.3012 0.142822H25.1905H13.6272H0.652966H0.514648V0.28114V55.7189V55.8572H0.652966H13.6272H13.7655V55.7189V7.28002L34.2365 55.7742L34.2642 55.8572H34.3748H45.8829H58.8294H58.9677V55.7189V0.28114V0.142822H58.8294H45.8829Z"/>
<path d="M209.347 0.142822H184.616H184.477L184.45 0.253483L171.78 48.8583L159.082 0.253483L159.054 0.142822H158.944H134.157H133.991V0.28114V55.7189V55.8572H134.157H147.104H147.242V55.7189V7.66731L159.774 55.7466L159.801 55.8572H159.94H183.564H183.675L183.703 55.7466L196.234 7.66731V55.7189V55.8572H196.373H209.347H209.485V55.7189V0.28114V0.142822H209.347Z"/>
<path d="M112.663 0.142822H112.58L112.552 0.198153L96.8116 27.5574L80.988 0.198153L80.9604 0.142822H80.8774H65.9114H65.6348L65.7731 0.364136L90.1447 42.5787V55.7189V55.8572H90.283H103.257H103.396V55.7189V42.5787L127.767 0.364136L127.905 0.142822H127.629H112.663Z"/>

Before

Width:  |  Height:  |  Size: 1011 B

After

Width:  |  Height:  |  Size: 1.0 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "client-core"
version = "0.12.0"
version = "1.0.0-rc.1"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "0.12.1"
version = "1.0.0-rc.1"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
rust-version = "1.56"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "0.12.1"
version = "1.0.0-rc.1"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
rust-version = "1.56"
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client-wasm"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jedrzej Stuczynski <andrew@nymtech.net>"]
version = "0.12.0"
version = "1.0.0-rc.1"
edition = "2021"
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
license = "Apache-2.0"
@@ -38,7 +38,7 @@ prost = { version = "0.9", default-features = false, optional = true }
flate2 = { version = "1.0.20", optional = true }
sha2 = { version = "0.9.5", optional = true }
itertools = { version = "0.10", optional = true }
cosmwasm-std = { version = "1.0.0-beta3", optional = true }
cosmwasm-std = { version = "1.0.0-beta6", optional = true }
[dev-dependencies]
ts-rs = "6.1.2"
@@ -589,6 +589,7 @@ impl<C> NymdClient<C> {
&self,
mix_identity: IdentityKey,
delegator: &AccountId,
proxy: Option<String>,
) -> Result<Delegation, NymdError>
where
C: CosmWasmClient + Sync,
@@ -596,6 +597,7 @@ impl<C> NymdClient<C> {
let request = QueryMsg::GetDelegationDetails {
mix_identity,
delegator: delegator.to_string(),
proxy,
};
self.client
.query_contract_smart(self.mixnet_contract_address()?, &request)
@@ -7,4 +7,4 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmwasm-std = "1.0.0-beta3"
cosmwasm-std = "1.0.0-beta6"
@@ -7,7 +7,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmwasm-std = "1.0.0-beta3"
cosmwasm-std = "1.0.0-beta6"
serde = { version = "1.0", features = ["derive"] }
serde_repr = "0.1"
@@ -9,6 +9,23 @@ use cosmwasm_std::{Coin, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::hash::{Hash, Hasher};
type OwnerAddressBytes = Vec<u8>;
type BlockHeight = u64;
pub fn generate_storage_key(address: &Addr, proxy: Option<&Addr>) -> Vec<u8> {
if let Some(proxy) = &proxy {
address
.as_bytes()
.iter()
.zip(proxy.as_bytes())
.map(|(x, y)| x ^ y)
.collect()
} else {
address.as_bytes().to_vec()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)]
pub struct Delegation {
@@ -19,12 +36,24 @@ pub struct Delegation {
pub proxy: Option<Addr>, // proxy address used to delegate the funds on behalf of anouther address
}
impl Eq for Delegation {}
#[allow(clippy::derive_hash_xor_eq)]
impl Hash for Delegation {
fn hash<H: Hasher>(&self, state: &mut H) {
self.owner.hash(state);
self.node_identity.hash(state);
self.block_height.hash(state);
self.proxy.hash(state);
}
}
impl Delegation {
pub fn new(
owner: Addr,
node_identity: IdentityKey,
amount: Coin,
block_height: u64,
block_height: BlockHeight,
proxy: Option<Addr>,
) -> Self {
Delegation {
@@ -36,15 +65,30 @@ impl Delegation {
}
}
// TODO: change that to use .joined_key() and return Vec<u8>
pub fn storage_key(&self) -> (IdentityKey, Vec<u8>, u64) {
pub fn storage_key(&self) -> (IdentityKey, OwnerAddressBytes, BlockHeight) {
(
self.node_identity(),
self.owner().as_bytes().to_vec(),
self.proxy_storage_key(),
self.block_height(),
)
}
pub fn event_storage_key(&self) -> (OwnerAddressBytes, BlockHeight, IdentityKey) {
(
self.proxy_storage_key(),
self.block_height(),
self.node_identity(),
)
}
pub fn proxy_storage_key(&self) -> OwnerAddressBytes {
generate_storage_key(&self.owner, self.proxy.as_ref())
}
pub fn proxy(&self) -> Option<&Addr> {
self.proxy.as_ref()
}
pub fn increment_amount(&mut self, amount: Uint128, at_height: Option<u64>) {
self.amount.amount += amount;
if let Some(at_height) = at_height {
@@ -86,10 +130,10 @@ pub struct PagedMixDelegationsResponse {
}
impl PagedMixDelegationsResponse {
pub fn new(delegations: Vec<Delegation>, start_next_after: Option<(Addr, u64)>) -> Self {
pub fn new(delegations: Vec<Delegation>, start_next_after: Option<(String, u64)>) -> Self {
PagedMixDelegationsResponse {
delegations,
start_next_after: start_next_after.map(|(s, h)| (s.to_string(), h)),
start_next_after,
}
}
}
@@ -1,7 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
mod delegation;
pub mod delegation;
pub mod error;
pub mod events;
mod gateway;
@@ -78,6 +78,35 @@ impl PendingUndelegate {
pub fn block_height(&self) -> u64 {
self.block_height
}
pub fn proxy_storage_key(&self) -> Vec<u8> {
if let Some(proxy) = &self.proxy {
self.delegate()
.as_bytes()
.iter()
.zip(proxy.as_bytes())
.map(|(x, y)| x ^ y)
.collect()
} else {
self.delegate().as_bytes().to_vec()
}
}
pub fn storage_key(&self) -> (IdentityKey, Vec<u8>) {
(self.mix_identity(), self.proxy_storage_key())
}
pub fn delegation_key(&self, block_height: u64) -> (IdentityKey, Vec<u8>, u64) {
(self.mix_identity(), self.proxy_storage_key(), block_height)
}
pub fn event_storage_key(&self) -> (Vec<u8>, u64, IdentityKey) {
(
self.proxy_storage_key(),
self.block_height(),
self.mix_identity(),
)
}
}
#[cfg_attr(test, derive(ts_rs::TS))]
@@ -101,6 +101,8 @@ pub enum ExecuteMsg {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
GetAllDelegationKeys {},
DebugGetAllDelegationValues {},
GetContractVersion {},
GetMixNodes {
limit: Option<u32>,
@@ -137,6 +139,7 @@ pub enum QueryMsg {
GetDelegationDetails {
mix_identity: IdentityKey,
delegator: String,
proxy: Option<String>,
},
LayerDistribution {},
GetRewardPool {},
@@ -6,11 +6,11 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmwasm-std = "1.0.0-beta3"
cosmwasm-std = "1.0.0-beta6"
mixnet-contract-common = { path = "../mixnet-contract" }
serde = { version = "1.0", features = ["derive"] }
schemars = "0.8"
cw-storage-plus = "0.11.1"
cw-storage-plus = "0.13.1"
config = { path = "../../config" }
[dev-dependencies]
+22 -31
View File
@@ -41,7 +41,7 @@ checksum = "f771a5d1f5503f7f4279a30f3643d3421ba149848b89ecaaec0ea2acf04a5ac4"
[[package]]
name = "bandwidth-claim"
version = "0.1.0"
version = "1.0.0-rc.1"
dependencies = [
"bandwidth-claim-contract",
"config",
@@ -218,22 +218,22 @@ dependencies = [
[[package]]
name = "cosmwasm-crypto"
version = "1.0.0-beta6"
version = "1.0.0-beta7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dddc1443004c6340e55ca66d98e9d2f1a44aadf4ce2bed2c4f29baa8a15e7b7"
checksum = "88c2565b1e73a816fb659ef4838fc356143fbd35f43c48a51d2d7d4e5d6679d3"
dependencies = [
"digest 0.9.0",
"ed25519-zebra",
"k256",
"rand_core 0.5.1",
"rand_core 0.6.3",
"thiserror",
]
[[package]]
name = "cosmwasm-derive"
version = "1.0.0-beta6"
version = "1.0.0-beta7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe4f0f10f165b8bcc558a13cddb498140960544519aa0581532c766dd80b5598"
checksum = "fa89fcdf8dbbe0088e663d0a814aa7368e7ebe8fb045a3a150fb5fdc2ffe3b45"
dependencies = [
"syn",
]
@@ -250,9 +250,9 @@ dependencies = [
[[package]]
name = "cosmwasm-std"
version = "1.0.0-beta6"
version = "1.0.0-beta7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f0f3145097b692b2d95fa5d2c7c6fdd60f193ccc709857e7e1987a608725300"
checksum = "bcb8f99a61d0b9069e1afc80a4ffea87dcc3523edd992080923870b13a677da0"
dependencies = [
"base64",
"cosmwasm-crypto",
@@ -267,9 +267,9 @@ dependencies = [
[[package]]
name = "cosmwasm-storage"
version = "1.0.0-beta3"
version = "1.0.0-beta7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4a4e55f0d64fed54cd2202301b8d466af8de044589247dabd77a4222f52f749"
checksum = "07f856099c824aa8f2488e62d1da3fc06383d3fdbc764573595f451be43441a2"
dependencies = [
"cosmwasm-std",
"serde",
@@ -360,20 +360,9 @@ dependencies = [
[[package]]
name = "cw-storage-plus"
version = "0.11.1"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d7ee1963302b0ac2a9d42fe0faec826209c17452bfd36fbfd9d002a88929261"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
[[package]]
name = "cw-storage-plus"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c087ff98fb0475db4c2b5298a5fd12b2848d2854b39d1115d930ee6da24d1eed"
checksum = "5e8b7f9a758c030d375520df947323c052704f784561fc28dcaab4f988c50a30"
dependencies = [
"cosmwasm-std",
"schemars",
@@ -450,16 +439,17 @@ dependencies = [
[[package]]
name = "ed25519-zebra"
version = "2.2.0"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a128b76af6dd4b427e34a6fd43dc78dbfe73672ec41ff615a2414c1a0ad0409"
checksum = "403ef3e961ab98f0ba902771d29f842058578bb1ce7e3c59dad5a6a93e784c69"
dependencies = [
"curve25519-dalek",
"hex",
"rand_core 0.5.1",
"rand_core 0.6.3",
"serde",
"sha2",
"thiserror",
"zeroize",
]
[[package]]
@@ -820,7 +810,7 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "mixnet-contract"
version = "0.1.0"
version = "1.0.0-rc.1"
dependencies = [
"az",
"bs58",
@@ -829,8 +819,9 @@ dependencies = [
"cosmwasm-std",
"cosmwasm-storage",
"crypto",
"cw-storage-plus 0.12.1",
"cw-storage-plus",
"fixed",
"hex",
"mixnet-contract-common",
"rand",
"rand_chacha",
@@ -1527,11 +1518,11 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vesting-contract"
version = "0.1.0"
version = "1.0.0-rc.1"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus 0.12.1",
"cw-storage-plus",
"mixnet-contract-common",
"schemars",
"serde",
@@ -1545,7 +1536,7 @@ version = "0.1.0"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus 0.11.1",
"cw-storage-plus",
"mixnet-contract-common",
"schemars",
"serde",
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "bandwidth-claim"
version = "0.1.0"
version = "1.0.0-rc.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -14,8 +14,8 @@ config = { path = "../../common/config"}
[dependencies]
bandwidth-claim-contract = { path = "../../common/bandwidth-claim-contract" }
cosmwasm-std = "1.0.0-beta3"
cosmwasm-storage = "1.0.0-beta3"
cosmwasm-std = "1.0.0-beta6"
cosmwasm-storage = "1.0.0-beta6"
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
+5 -4
View File
@@ -1,6 +1,6 @@
[package]
name = "mixnet-contract"
version = "0.1.0"
version = "1.0.0-rc.1"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
@@ -20,9 +20,9 @@ mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" }
config = { path = "../../common/config"}
cosmwasm-std = "1.0.0-beta3"
cosmwasm-storage = "1.0.0-beta3"
cw-storage-plus = "0.12.1"
cosmwasm-std = "1.0.0-beta6"
cosmwasm-storage = "1.0.0-beta6"
cw-storage-plus = "0.13.1"
az = "1.2.0"
bs58 = "0.4.0"
@@ -30,6 +30,7 @@ schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
thiserror = { version = "1.0.23" }
time = { version = "0.3", features = ["macros"] }
hex = "0.4.3"
[dev-dependencies]
cosmwasm-schema = "1.0.0-beta3"
+9 -54
View File
@@ -32,7 +32,7 @@ use cosmwasm_std::{
entry_point, to_binary, Addr, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Uint128,
};
use mixnet_contract_common::{
ContractStateParams, Delegation, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg,
ContractStateParams, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg,
};
use time::OffsetDateTime;
@@ -312,11 +312,13 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
QueryMsg::GetDelegationDetails {
mix_identity,
delegator,
proxy,
} => to_binary(&query_mixnode_delegation(
deps.storage,
deps.api,
mix_identity,
delegator,
proxy,
)?),
QueryMsg::GetRewardPool {} => to_binary(&query_reward_pool(deps)?),
QueryMsg::GetCirculatingSupply {} => to_binary(&query_circulating_supply(deps)?),
@@ -363,66 +365,19 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
QueryMsg::GetPendingDelegationEvents { owner_address } => to_binary(
&query_pending_delegation_events(deps.storage, owner_address)?,
),
QueryMsg::GetAllDelegationKeys {} => to_binary(
&crate::delegations::queries::query_all_delegation_keys(deps.storage)?,
),
QueryMsg::DebugGetAllDelegationValues {} => to_binary(
&crate::delegations::queries::debug_query_all_delegation_values(deps.storage)?,
),
};
Ok(query_res?)
}
// MIGRATE OLD DELEGATION STORAGE
// applied on QAnet
#[allow(dead_code)]
fn migrate_delegations(deps: DepsMut<'_>) -> Result<(), ContractError> {
use crate::delegations::storage::{
DelegationIndex, DELEGATION_MIXNODE_IDX_NAMESPACE, DELEGATION_OWNER_IDX_NAMESPACE,
DELEGATION_PK_NAMESPACE,
};
use cosmwasm_std::Order;
use cw_storage_plus::{IndexedMap, MultiIndex};
type PrimaryKey = Vec<u8>;
fn old_delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, DelegationIndex<'a>> {
let indexes = DelegationIndex {
owner: MultiIndex::new(
|d| d.owner.clone(),
DELEGATION_PK_NAMESPACE,
DELEGATION_OWNER_IDX_NAMESPACE,
),
mixnode: MultiIndex::new(
|d| d.node_identity.clone(),
DELEGATION_PK_NAMESPACE,
DELEGATION_MIXNODE_IDX_NAMESPACE,
),
};
IndexedMap::new(DELEGATION_PK_NAMESPACE, indexes)
}
let old_delegations = old_delegations()
.range(deps.storage, None, None, Order::Ascending)
.filter_map(|r| r.ok())
.map(|(_key, delegation)| delegation)
.collect::<Vec<Delegation>>();
for delegation in old_delegations {
crate::delegations::storage::delegations().save(
deps.storage,
(
delegation.node_identity(),
delegation.owner().as_bytes().to_vec(),
delegation.block_height(),
),
&delegation,
)?;
}
Ok(())
}
#[entry_point]
pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
// TODO: Uncomment mainnet
// migrate_delegations(deps)?;
Ok(Default::default())
}
+170 -113
View File
@@ -43,14 +43,18 @@ pub(crate) fn query_delegator_delegations_paged(
.idx
.owner
.prefix(validated_owner)
.range(deps.storage, start, None, Order::Ascending)
.range_raw(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|record| record.map(|r| r.1))
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = delegations
.last()
.map(|delegation| delegation.node_identity());
let start_next_after = if delegations.len() < limit {
None
} else {
delegations
.last()
.map(|delegation| delegation.node_identity())
};
Ok(PagedDelegatorDelegationsResponse::new(
delegations,
@@ -58,17 +62,70 @@ pub(crate) fn query_delegator_delegations_paged(
))
}
pub fn query_all_delegation_keys(storage: &dyn Storage) -> Result<Vec<String>, ContractError> {
Ok(storage::delegations()
.keys_raw(storage, None, None, Order::Ascending)
.map(hex::encode)
.collect())
}
use std::collections::HashSet;
// This should only be exposed directly on the contract via nymd binary, not through the nymd clients
pub fn debug_query_all_delegation_values(
storage: &dyn Storage,
) -> Result<HashSet<Delegation>, ContractError> {
use crate::delegations::storage::{
DelegationIndex, DELEGATION_MIXNODE_IDX_NAMESPACE, DELEGATION_OWNER_IDX_NAMESPACE,
DELEGATION_PK_NAMESPACE,
};
use cw_storage_plus::{IndexedMap, MultiIndex};
type PrimaryKey = Vec<u8>;
fn all_delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, DelegationIndex<'a>> {
let indexes = DelegationIndex {
owner: MultiIndex::new(
|d| d.owner.clone(),
DELEGATION_PK_NAMESPACE,
DELEGATION_OWNER_IDX_NAMESPACE,
),
mixnode: MultiIndex::new(
|d| d.node_identity.clone(),
DELEGATION_PK_NAMESPACE,
DELEGATION_MIXNODE_IDX_NAMESPACE,
),
};
IndexedMap::new(DELEGATION_PK_NAMESPACE, indexes)
}
let all_delegations = all_delegations()
.range(storage, None, None, Order::Ascending)
.filter_map(|r| r.ok())
.map(|(_key, delegation)| delegation)
.collect::<HashSet<Delegation>>();
Ok(all_delegations)
}
// queries for delegation value of given address for particular node
pub(crate) fn query_mixnode_delegation(
storage: &dyn Storage,
api: &dyn Api,
mix_identity: IdentityKey,
delegator: String,
proxy: Option<String>,
) -> Result<Vec<Delegation>, ContractError> {
let validated_delegator = api.addr_validate(&delegator)?;
let proxy = proxy.map(|p| api.addr_validate(&p).expect("Invalid proxy address"));
let storage_key = (
mix_identity.clone(),
validated_delegator.as_bytes().to_vec(),
mixnet_contract_common::delegation::generate_storage_key(
&validated_delegator,
proxy.as_ref(),
),
);
let delegations = storage::delegations()
@@ -99,21 +156,30 @@ pub(crate) fn query_mixnode_delegations_paged(
.min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize;
let start = start_after.map(|(addr, height)| {
Bound::ExclusiveRaw((mix_identity.clone(), addr.as_bytes(), height).joined_key())
Bound::exclusive((
hex::decode(addr).expect("Could not hex decode proxy_storage_key"),
height,
))
});
let delegations = storage::delegations()
.idx
.mixnode
.prefix(mix_identity)
.sub_prefix(mix_identity)
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|record| record.map(|r| r.1))
.collect::<StdResult<Vec<_>>>()?;
.filter_map(|r| r.ok())
.map(|record| record.1)
.collect::<Vec<Delegation>>();
let start_next_after = delegations
.last()
.map(|delegation| (delegation.owner(), delegation.block_height()));
let start_next_after = if delegations.len() < limit {
None
} else {
delegations.last().map(|delegation| {
(
hex::encode(delegation.proxy_storage_key()),
delegation.block_height(),
)
})
};
Ok(PagedMixDelegationsResponse::new(
delegations,
@@ -127,18 +193,22 @@ pub(crate) mod tests {
use crate::support::tests::test_helpers;
use config::defaults::DENOM;
use cosmwasm_std::{coin, Addr, Storage};
use rand::Rng;
pub fn store_n_mix_delegations(n: u32, storage: &mut dyn Storage, node_identity: &str) {
for i in 0..n {
let address = format!("address{}", i);
test_helpers::save_dummy_delegation(storage, node_identity, address);
test_helpers::save_dummy_delegation(storage, node_identity, address, 1);
}
}
#[cfg(test)]
mod querying_for_mixnode_delegations_paged {
use std::collections::HashSet;
use super::*;
use mixnet_contract_common::IdentityKey;
use rand::{distributions::Alphanumeric, SeedableRng};
#[test]
fn retrieval_obeys_limits() {
@@ -203,39 +273,37 @@ pub(crate) mod tests {
#[test]
fn pagination_works() {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let mut deps = test_helpers::init_contract();
let node_identity: IdentityKey = "foo".into();
let node_identity2: IdentityKey = "bar".into();
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "100");
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity2, "100");
let mut delegation_test_data = vec![];
let mut returned_delegation_data = HashSet::new();
let per_page = 2;
let page1 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity.clone(),
None,
Option::from(per_page),
)
.unwrap();
// Crete a bunch of randomly ordered (in storage) delegations
for _ in 0..200 {
delegation_test_data.push((
rng.clone()
.sample_iter(&Alphanumeric)
.take(30)
.map(char::from)
.collect::<String>(),
rng.gen::<u32>() as u64,
))
}
// page should have 1 result on it
assert_eq!(1, page1.delegations.len());
for (address, block_height) in delegation_test_data.iter() {
test_helpers::save_dummy_delegation(
&mut deps.storage,
&node_identity,
address,
*block_height,
);
}
// save another
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "200");
// page1 should have 2 results on it
let page1 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity.clone(),
None,
Option::from(per_page),
)
.unwrap();
assert_eq!(2, page1.delegations.len());
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "300");
let per_page = 100;
// page1 still has 2 results
let page1 = query_mixnode_delegations_paged(
@@ -245,10 +313,20 @@ pub(crate) mod tests {
Option::from(per_page),
)
.unwrap();
println!("{:?}", page1);
let start_after = page1.start_next_after.unwrap();
assert_eq!(2, page1.delegations.len());
assert_eq!(("200".to_string(), 12345), start_after);
assert_eq!(100, page1.delegations.len());
assert_eq!(
((
"5874735a724c52587679656777795a446a754a746c59694735423165694a".to_string(),
1594717548
)),
start_after
);
for delegation in page1.delegations {
returned_delegation_data.insert(delegation.owner().to_string());
}
// retrieving the next page should start after the last key on this page
@@ -259,37 +337,33 @@ pub(crate) mod tests {
Option::from(per_page),
)
.unwrap();
println!("{:?}", page2);
assert_eq!(1, page2.delegations.len());
assert_eq!(page2.delegations.last().unwrap().owner(), "300");
// save another one
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "400");
let start_after = page2.start_next_after.unwrap();
assert_eq!(
(
"7a6b48546c63674f57417948384e6f494a326c6b5a63767668597346696b".to_string(),
3448133410
),
start_after
);
let page2 = query_mixnode_delegations_paged(
for delegation in page2.delegations {
returned_delegation_data.insert(delegation.owner().to_string());
}
let page3 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity.clone(),
Option::from(start_after),
Option::from(per_page),
)
.unwrap();
// println!("{:?}", page2);
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.delegations.len());
assert_eq!(page2.delegations.last().unwrap().owner(), "400");
// Should be empty
let page3 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity,
page2.start_next_after,
Option::from(per_page),
)
.unwrap();
assert!(page3.delegations.is_empty());
assert!(page3.start_next_after.is_none());
for delegation in delegation_test_data {
assert!(returned_delegation_data.contains(&*delegation.0));
}
}
}
@@ -317,7 +391,8 @@ pub(crate) mod tests {
&deps.storage,
&deps.api,
node_identity,
delegation_owner.to_string()
delegation_owner.to_string(),
None
)
)
}
@@ -340,7 +415,8 @@ pub(crate) mod tests {
&deps.storage,
&deps.api,
node_identity1.clone(),
delegation_owner1.to_string()
delegation_owner1.to_string(),
None
)
);
@@ -366,7 +442,8 @@ pub(crate) mod tests {
&deps.storage,
&deps.api,
node_identity1.clone(),
delegation_owner1.to_string()
delegation_owner1.to_string(),
None
)
);
@@ -392,7 +469,8 @@ pub(crate) mod tests {
&deps.storage,
&deps.api,
node_identity1,
delegation_owner1.to_string()
delegation_owner1.to_string(),
None
)
)
}
@@ -404,7 +482,7 @@ pub(crate) mod tests {
fn store_n_reverse_delegations(n: u32, storage: &mut dyn Storage, delegation_owner: &str) {
for i in 0..n {
let node_identity = format!("node{}", i);
test_helpers::save_dummy_delegation(storage, node_identity, delegation_owner);
test_helpers::save_dummy_delegation(storage, node_identity, delegation_owner, 1);
}
}
@@ -475,9 +553,18 @@ pub(crate) mod tests {
let mut deps = test_helpers::init_contract();
let delegation_owner = "bar".to_string();
test_helpers::save_dummy_delegation(&mut deps.storage, "100", &delegation_owner);
for j in 0..20 {
for i in 0..10 {
test_helpers::save_dummy_delegation(
&mut deps.storage,
format!("{}-{}", j, i),
delegation_owner.clone(),
i,
);
}
}
let per_page = 2;
let per_page = 100;
let page1 = query_delegator_delegations_paged(
deps.as_ref(),
delegation_owner.clone(),
@@ -486,60 +573,30 @@ pub(crate) mod tests {
)
.unwrap();
// page should have 1 result on it
assert_eq!(1, page1.delegations.len());
let start_after = page1.start_next_after.unwrap();
assert_eq!(per_page as usize, page1.delegations.len());
assert_eq!(start_after, "9-9".to_string());
// save another
test_helpers::save_dummy_delegation(&mut deps.storage, "200", &delegation_owner);
// page1 should have 2 results on it
let page1 = query_delegator_delegations_paged(
deps.as_ref(),
delegation_owner.clone(),
None,
Option::from(per_page),
)
.unwrap();
assert_eq!(2, page1.delegations.len());
test_helpers::save_dummy_delegation(&mut deps.storage, "300", &delegation_owner);
// page1 still has 2 results
let page1 = query_delegator_delegations_paged(
deps.as_ref(),
delegation_owner.clone(),
None,
Option::from(per_page),
)
.unwrap();
assert_eq!(2, page1.delegations.len());
// retrieving the next page should start after the last key on this page
let start_after: IdentityKey = page1.start_next_after.unwrap();
let page2 = query_delegator_delegations_paged(
deps.as_ref(),
delegation_owner.clone(),
Option::from(start_after),
Some(start_after),
Option::from(per_page),
)
.unwrap();
assert_eq!(1, page2.delegations.len());
let start_after = page2.start_next_after.unwrap();
assert_eq!(start_after, "19-9".to_string());
// save another one
test_helpers::save_dummy_delegation(&mut deps.storage, "400", &delegation_owner);
let start_after = String::from("2");
let page2 = query_delegator_delegations_paged(
let page3 = query_delegator_delegations_paged(
deps.as_ref(),
delegation_owner,
Option::from(start_after),
delegation_owner.clone(),
Some(start_after),
Option::from(per_page),
)
.unwrap();
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.delegations.len());
assert!(page3.start_next_after.is_none());
}
}
}
+2 -2
View File
@@ -24,9 +24,9 @@ type OwnerAddress = Vec<u8>;
type PrimaryKey = (IdentityKey, OwnerAddress, BlockHeight);
pub(crate) struct DelegationIndex<'a> {
pub(crate) owner: MultiIndex<'a, Addr, Delegation>,
pub(crate) owner: MultiIndex<'a, Addr, Delegation, PrimaryKey>,
pub(crate) mixnode: MultiIndex<'a, IdentityKey, Delegation>,
pub(crate) mixnode: MultiIndex<'a, IdentityKey, Delegation, PrimaryKey>,
}
impl<'a> IndexList<Delegation> for DelegationIndex<'a> {
@@ -4,7 +4,6 @@ use super::storage::{self, PENDING_DELEGATION_EVENTS};
use crate::error::ContractError;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::mixnodes::storage as mixnodes_storage;
use crate::support::helpers::generate_storage_key;
use config::defaults::DENOM;
use cosmwasm_std::{
coins, wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, Event, MessageInfo, Order,
@@ -192,19 +191,18 @@ pub(crate) fn _try_delegate_to_mixnode(
});
}
let maybe_proxy_storage = generate_storage_key(&delegate, proxy.as_ref());
let storage_key = (maybe_proxy_storage, block_height, mix_identity.to_string());
let delegation = Delegation::new(
delegate.to_owned(),
mix_identity.to_string(),
amount.clone(),
block_height,
proxy.clone(),
);
storage::PENDING_DELEGATION_EVENTS.save(
storage,
storage_key,
&DelegationEvent::Delegate(Delegation::new(
delegate.to_owned(),
mix_identity.to_string(),
amount.clone(),
block_height,
proxy.clone(),
)),
delegation.event_storage_key(),
&DelegationEvent::Delegate(delegation),
)?;
Ok(Response::new().add_event(new_pending_delegation_event(
@@ -246,17 +244,9 @@ pub(crate) fn try_reconcile_undelegation(
pending_undelegate: &PendingUndelegate,
) -> Result<ReconcileUndelegateResponse, ContractError> {
let delegation_map = storage::delegations();
let maybe_proxy_storage = generate_storage_key(
&pending_undelegate.delegate(),
pending_undelegate.proxy().as_ref(),
);
let storage_key = (
pending_undelegate.mix_identity(),
maybe_proxy_storage.clone(),
);
let any_delegations = delegation_map
.prefix(storage_key.clone())
.prefix(pending_undelegate.storage_key())
.keys(storage, None, None, cosmwasm_std::Order::Ascending)
.filter_map(|v| v.ok())
.next()
@@ -287,7 +277,7 @@ pub(crate) fn try_reconcile_undelegation(
// Might want to introduce paging here
let delegation_heights = delegation_map
.prefix(storage_key)
.prefix(pending_undelegate.storage_key())
.keys(storage, None, None, cosmwasm_std::Order::Ascending)
.filter_map(|v| v.ok())
.collect::<Vec<u64>>();
@@ -317,14 +307,15 @@ pub(crate) fn try_reconcile_undelegation(
}
for h in delegation_heights {
let storage_key = (
pending_undelegate.mix_identity(),
maybe_proxy_storage.clone(),
h,
);
let delegation = delegation_map.load(storage, storage_key.clone())?;
let delegation =
delegation_map.load(storage, pending_undelegate.delegation_key(h).clone())?;
total_delegation += delegation.amount.amount;
delegation_map.replace(storage, storage_key, None, Some(&delegation))?;
delegation_map.replace(
storage,
pending_undelegate.delegation_key(h),
None,
Some(&delegation),
)?;
}
let bank_msg = BankMsg::Send {
@@ -386,19 +377,17 @@ pub(crate) fn _try_remove_delegation_from_mixnode(
) -> Result<Response, ContractError> {
let delegate = deps.api.addr_validate(delegate)?;
let event = PendingUndelegate::new(
mix_identity.to_string(),
delegate.clone(),
proxy.clone(),
env.block.height,
);
PENDING_DELEGATION_EVENTS.save(
deps.storage,
(
delegate.as_bytes().to_vec(),
env.block.height,
mix_identity.to_string(),
),
&DelegationEvent::Undelegate(PendingUndelegate::new(
mix_identity.to_string(),
delegate.clone(),
proxy.clone(),
env.block.height,
)),
event.event_storage_key(),
&DelegationEvent::Undelegate(event),
)?;
Ok(Response::new().add_event(new_pending_undelegation_event(
@@ -738,6 +727,7 @@ mod tests {
&deps.api,
identity,
delegation_owner.to_string(),
None,
)
.unwrap();
@@ -1074,6 +1064,7 @@ mod tests {
&deps.api,
identity.clone(),
delegation_owner.clone().into_string(),
None,
)
.unwrap();
@@ -1143,6 +1134,7 @@ mod tests {
&deps.api,
identity.clone(),
delegation_owner.clone().into_string(),
None,
)
.unwrap();
-13
View File
@@ -40,19 +40,6 @@ pub(crate) fn epoch_reward_params(
Ok(epoch_reward_params)
}
pub fn generate_storage_key(address: &Addr, proxy: Option<&Addr>) -> Vec<u8> {
if let Some(proxy) = &proxy {
address
.as_bytes()
.iter()
.zip(proxy.as_bytes())
.map(|(x, y)| x ^ y)
.collect()
} else {
address.as_bytes().to_vec()
}
}
// check if the target address has already bonded a mixnode or gateway,
// in either case, return an appropriate error
pub(crate) fn ensure_no_existing_bond(
+2 -1
View File
@@ -103,12 +103,13 @@ pub mod test_helpers {
storage: &mut dyn Storage,
mix: impl Into<String>,
owner: impl Into<String>,
block_height: u64,
) {
let delegation = Delegation {
owner: Addr::unchecked(owner.into()),
node_identity: mix.into(),
amount: coin(12345, DENOM),
block_height: 12345,
block_height: block_height,
proxy: None,
};
+3 -3
View File
@@ -1,6 +1,6 @@
[package]
name = "vesting-contract"
version = "0.1.0"
version = "1.0.0-rc.1"
authors = ["Drazen Urch <durch@users.noreply.github.com>"]
edition = "2021"
@@ -18,8 +18,8 @@ mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" }
config = { path = "../../common/config" }
cosmwasm-std = { version = "1.0.0-beta4"}
cw-storage-plus = { version = "0.12.1", features = ["iterator"] }
cosmwasm-std = { version = "1.0.0-beta6"}
cw-storage-plus = { version = "0.13.1", features = ["iterator"] }
schemars = "0.8"
serde = { version = "1.0", default-features = false, features = ["derive"] }
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "explorer-api"
version = "0.1.0"
version = "1.0.0-rc.1"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-gateway"
version = "0.12.1"
version = "1.0.0-rc.1"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Mixnet Gateway"
edition = "2021"
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-mixnode"
version = "0.12.1"
version = "1.0.0-rc.1"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
-1
View File
@@ -2843,7 +2843,6 @@ dependencies = [
"argon2",
"base64",
"bip39",
"cfg-if",
"coconut-interface",
"config",
"cosmrs",
-1
View File
@@ -38,7 +38,6 @@
"react-hook-form": "^7.14.2",
"react-router-dom": "^5.2.0",
"semver": "^6.3.0",
"use-clipboard-copy": "^0.2.0",
"yup": "^0.32.9"
},
"devDependencies": {
-1
View File
@@ -20,7 +20,6 @@ tauri-macros = "=1.0.0-rc.1"
[dependencies]
bip39 = "1.0"
cfg-if = "1.0.0"
dirs = "4.0"
eyre = "0.6.5"
futures = "0.3.15"
+2 -10
View File
@@ -77,18 +77,10 @@ pub enum BackendError {
NetworkNotSupported(config::defaults::all::Network),
#[error("Could not access the local data storage directory")]
UnknownStorageDirectory,
#[error("No nymd validator configured")]
NoNymdValidatorConfigured,
#[error("No validator API URL configured")]
NoValidatorApiUrlConfigured,
#[error("The wallet file already exists")]
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("Adding a different password to the wallet not currently supported")]
WalletDifferentPasswordDetected,
}
impl Serialize for BackendError {
-4
View File
@@ -15,7 +15,6 @@ mod error;
mod menu;
mod network;
mod operations;
mod platform_constants;
mod state;
mod utils;
// temporarily until it is actually used
@@ -38,11 +37,8 @@ fn main() {
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::logout,
mixnet::account::sign_in_with_password,
mixnet::account::switch_network,
mixnet::account::update_validator_urls,
mixnet::admin::get_contract_settings,
@@ -4,11 +4,8 @@ use crate::error::BackendError;
use crate::network::Network;
use crate::nymd_client;
use crate::state::State;
use crate::wallet_storage::{self, DEFAULT_WALLET_ACCOUNT_ID};
use bip39::{Language, Mnemonic};
use config::defaults::COSMOS_DERIVATION_PATH;
use cosmrs::bip32::DerivationPath;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::convert::TryInto;
@@ -230,10 +227,9 @@ async fn choose_validators(
.next()
// We always have at least one hardcoded default validator
.unwrap();
log::info!(
println!(
"Using default for {network}: {}, {}",
default_validator.nymd_url,
default_validator.api_url,
default_validator.nymd_url, default_validator.api_url,
);
default_validator
});
@@ -309,10 +305,9 @@ async fn try_connect_to_validator(
)?;
if is_validator_connection_ok(&client).await {
log::info!(
println!(
"Connection ok for {network}: {}, {}",
validator.nymd_url,
validator.api_url
validator.nymd_url, validator.api_url
);
Ok(Some((network, validator.clone())))
} else {
@@ -327,45 +322,3 @@ async fn is_validator_connection_ok(client: &Client<SigningNymdClient>) -> bool
Err(_) | Ok(_) => true,
}
}
#[tauri::command]
pub fn does_password_file_exist() -> Result<bool, BackendError> {
log::info!("Checking wallet file");
let file = wallet_storage::wallet_login_filepath()?;
if file.exists() {
log::info!("Exists: {}", file.to_string_lossy());
Ok(true)
} else {
log::info!("Does not exist: {}", file.to_string_lossy());
Ok(false)
}
}
#[tauri::command]
pub fn create_password(mnemonic: String, password: String) -> Result<(), BackendError> {
if does_password_file_exist()? {
return Err(BackendError::WalletFileAlreadyExists);
}
log::info!("Creating password");
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());
let password = wallet_storage::UserPassword::new(password);
wallet_storage::store_wallet_login_information(mnemonic, hd_path, id, &password)
}
#[tauri::command]
pub async fn sign_in_with_password(
password: String,
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 id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_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
}
@@ -1,22 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// Specify filenames and other platform specific constants to respect platform conventions, or at
// least, something popular on each respective platform.
cfg_if::cfg_if! {
if #[cfg(target_os = "linux")] {
pub const STORAGE_DIR_NAME: &str = "nym-wallet";
pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json";
} else if #[cfg(taret_os = "macos")] {
pub const STORAGE_DIR_NAME: &str = "nym-wallet";
pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json";
} else if #[cfg(taret_os = "windows")] {
pub const STORAGE_DIR_NAME: &str = "NymWallet";
pub const WALLET_INFO_FILENAME: &str = "saved_wallet.json";
} else {
// This case is likely to be a unix-y system
pub const STORAGE_DIR_NAME: &str = "nym-wallet";
pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json";
}
}
@@ -8,96 +8,6 @@ use std::fmt::Formatter;
use zeroize::Zeroize;
use zeroize::Zeroizing;
use crate::error::BackendError;
use super::encryption::EncryptedData;
use super::password::WalletAccountId;
use super::UserPassword;
const CURRENT_WALLET_FILE_VERSION: u32 = 1;
#[derive(Serialize, Deserialize, Debug)]
pub(crate) struct StoredWallet {
version: u32,
accounts: Vec<EncryptedAccount>,
}
impl StoredWallet {
pub fn version(&self) -> u32 {
self.version
}
pub fn is_empty(&self) -> bool {
self.accounts.is_empty()
}
pub fn len(&self) -> usize {
self.accounts.len()
}
pub fn encrypted_account_by_index(&self, index: usize) -> Option<&EncryptedAccount> {
self.accounts.get(index)
}
fn encrypted_account(
&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,
password: &UserPassword,
) -> Result<StoredAccount, BackendError> {
self.encrypted_account(id)?.decrypt_struct(password)
}
pub fn decrypt_all(&self, password: &UserPassword) -> Result<Vec<StoredAccount>, BackendError> {
self
.accounts
.iter()
.map(|account| account.account.decrypt_struct(password))
.collect::<Result<Vec<_>, _>>()
}
pub fn password_can_decrypt_all(&self, password: &UserPassword) -> bool {
self.decrypt_all(password).is_ok()
}
}
impl Default for StoredWallet {
fn default() -> Self {
StoredWallet {
version: CURRENT_WALLET_FILE_VERSION,
accounts: Vec::new(),
}
}
}
#[derive(Serialize, Deserialize, Debug)]
pub(crate) struct EncryptedAccount {
pub id: WalletAccountId,
pub account: EncryptedData<StoredAccount>,
}
// future-proofing
#[derive(Serialize, Deserialize, Debug, Zeroize)]
#[serde(untagged)]
@@ -114,14 +24,6 @@ impl StoredAccount {
) -> StoredAccount {
StoredAccount::Mnemonic(MnemonicAccount { mnemonic, hd_path })
}
// 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 {
match self {
StoredAccount::Mnemonic(account) => account.mnemonic(),
}
}
}
#[derive(Serialize, Deserialize, Debug)]
@@ -131,6 +33,8 @@ pub(crate) struct MnemonicAccount {
hd_path: DerivationPath,
}
// we only ever want to expose those getters in the test code
#[cfg(test)]
impl MnemonicAccount {
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
&self.mnemonic
@@ -42,7 +42,7 @@ pub(crate) struct EncryptedData<T> {
impl<T> Drop for EncryptedData<T> {
fn drop(&mut self) {
self.zeroize();
self.zeroize()
}
}
+64 -121
View File
@@ -1,26 +1,19 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub(crate) use crate::wallet_storage::password::{UserPassword, WalletAccountId};
use crate::error::BackendError;
use crate::operations::mixnet::account::create_new_account;
use crate::platform_constants::{STORAGE_DIR_NAME, WALLET_INFO_FILENAME};
use crate::wallet_storage::account_data::StoredAccount;
use crate::wallet_storage::encryption::{encrypt_struct, EncryptedData};
use crate::wallet_storage::password::UserPassword;
use cosmrs::bip32::DerivationPath;
use serde::{Deserialize, Serialize};
use std::fs::{create_dir_all, OpenOptions};
use std::path::PathBuf;
use self::account_data::{EncryptedAccount, StoredWallet};
pub(crate) mod account_data;
pub(crate) mod encryption;
mod password;
pub(crate) const DEFAULT_WALLET_ACCOUNT_ID: &str = "default";
const STORAGE_DIR_NAME: &str = "NymWallet";
const WALLET_INFO_FILENAME: &str = "saved_wallet.json";
fn get_storage_directory() -> Result<PathBuf, BackendError> {
tauri::api::path::local_data_dir()
@@ -32,76 +25,52 @@ pub(crate) fn wallet_login_filepath() -> Result<PathBuf, BackendError> {
get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME))
}
pub(crate) fn load_existing_wallet(password: &UserPassword) -> Result<StoredWallet, BackendError> {
let store_dir = get_storage_directory()?;
let filepath = store_dir.join(WALLET_INFO_FILENAME);
load_existing_wallet_at_file(filepath)
}
fn load_existing_wallet_at_file(filepath: PathBuf) -> Result<StoredWallet, BackendError> {
if !filepath.exists() {
return Err(BackendError::WalletFileNotFound);
}
let file = OpenOptions::new().read(true).open(filepath)?;
let wallet: StoredWallet = serde_json::from_reader(file)?;
Ok(wallet)
}
pub(crate) fn load_existing_wallet_login_information(
id: &WalletAccountId,
password: &UserPassword,
) -> Result<StoredAccount, BackendError> {
) -> Result<Vec<StoredAccount>, BackendError> {
let store_dir = get_storage_directory()?;
let filepath = store_dir.join(WALLET_INFO_FILENAME);
load_existing_wallet_login_information_at_file(filepath, id, password)
load_existing_wallet_login_information_at_file(filepath, password)
}
fn load_existing_wallet_login_information_at_file(
filepath: PathBuf,
id: &WalletAccountId,
password: &UserPassword,
) -> Result<StoredAccount, BackendError> {
load_existing_wallet_at_file(filepath)?.decrypt_account(id, password)
) -> Result<Vec<StoredAccount>, BackendError> {
if !filepath.exists() {
return Ok(Vec::new());
}
let file = OpenOptions::new().read(true).open(filepath)?;
let encrypted_data: EncryptedData<Vec<StoredAccount>> = serde_json::from_reader(file)?;
encrypted_data.decrypt_struct(password)
}
pub(crate) fn store_wallet_login_information(
mnemonic: bip39::Mnemonic,
hd_path: DerivationPath,
id: WalletAccountId,
password: &UserPassword,
password: UserPassword,
) -> Result<(), BackendError> {
// make sure the entire directory structure exists
let store_dir = get_storage_directory()?;
create_dir_all(&store_dir)?;
let filepath = store_dir.join(WALLET_INFO_FILENAME);
store_wallet_login_information_at_file(filepath, mnemonic, hd_path, id, password)
store_wallet_login_information_at_file(filepath, mnemonic, hd_path, &password)
}
fn store_wallet_login_information_at_file(
filepath: PathBuf,
mnemonic: bip39::Mnemonic,
hd_path: DerivationPath,
id: WalletAccountId,
password: &UserPassword,
) -> Result<(), BackendError> {
let mut stored_wallet = match load_existing_wallet_at_file(filepath.clone()) {
Err(BackendError::WalletFileNotFound) => StoredWallet::default(),
result => result?,
};
// Confirm that the given password also can unlock the other entries
if !stored_wallet.password_can_decrypt_all(password) {
return Err(BackendError::WalletDifferentPasswordDetected);
}
let mut all_accounts =
load_existing_wallet_login_information_at_file(filepath.clone(), password)?;
let new_account = StoredAccount::new_mnemonic_backed_account(mnemonic, hd_path);
let new_encrypted_account = EncryptedAccount {
id,
account: encrypt_struct(&new_account, password)?,
};
all_accounts.push(new_account);
stored_wallet.add_encrypted_account(new_encrypted_account)?;
let encrypted = encrypt_struct(&all_accounts, password)?;
let file = OpenOptions::new()
.create(true)
@@ -109,7 +78,9 @@ fn store_wallet_login_information_at_file(
.truncate(true)
.open(filepath)?;
Ok(serde_json::to_writer_pretty(file, &stored_wallet)?)
serde_json::to_writer_pretty(file, &encrypted)?;
Ok(())
}
// this function should probably exist, but I guess we need to discuss how it should behave in the context of the UX
@@ -120,11 +91,15 @@ fn store_wallet_login_information_at_file(
#[cfg(test)]
mod tests {
use super::*;
use crate::wallet_storage::encryption::encrypt_data;
use config::defaults::COSMOS_DERIVATION_PATH;
use std::path::Path;
use tempfile::tempdir;
fn read_encrypted_blob(file: PathBuf) -> EncryptedData<Vec<StoredAccount>> {
let file = OpenOptions::new().read(true).open(&file).unwrap();
serde_json::from_reader(file).unwrap()
}
// I'm not 100% sure how to feel about having to touch the file system at all
#[test]
fn storing_wallet_information() {
@@ -139,36 +114,29 @@ mod tests {
let password = UserPassword::new("password".to_string());
let bad_password = UserPassword::new("bad-password".to_string());
let id1 = WalletAccountId::new("first".to_string());
let id2 = WalletAccountId::new("second".to_string());
// nothing was stored on the disk, so regardless of password used, there will be no error, but
// returned list will be empty
assert!(
load_existing_wallet_login_information_at_file(wallet_file.clone(), &password)
.unwrap()
.is_empty()
);
assert!(
load_existing_wallet_login_information_at_file(wallet_file.clone(), &bad_password)
.unwrap()
.is_empty()
);
// Nothing was stored on the disk
assert!(matches!(
load_existing_wallet_at_file(wallet_file.clone()),
Err(BackendError::WalletFileNotFound),
));
assert!(matches!(
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password),
Err(BackendError::WalletFileNotFound),
));
// Store the first account
// store the first account
store_wallet_login_information_at_file(
wallet_file.clone(),
dummy_account1.clone(),
cosmos_hd_path.clone(),
id1.clone(),
&password,
)
.unwrap();
let stored_wallet = load_existing_wallet_at_file(wallet_file.clone()).unwrap();
assert_eq!(stored_wallet.len(), 1);
assert_eq!(
stored_wallet.encrypted_account_by_index(0).unwrap().id,
WalletAccountId::new("first".to_string())
);
let encrypted_blob = &stored_wallet.encrypted_account_by_index(0).unwrap().account;
let encrypted_blob = read_encrypted_blob(wallet_file.clone());
// some actual ciphertext was saved
assert!(!encrypted_blob.ciphertext().is_empty());
@@ -178,78 +146,53 @@ mod tests {
let original_salt = encrypted_blob.salt().to_vec();
// trying to load it with wrong password now fails
assert!(matches!(
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &bad_password),
Err(BackendError::DecryptionError),
));
// and with the wrong id also fails
assert!(matches!(
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id2, &password),
Err(BackendError::NoSuchIdInWallet),
));
assert!(
load_existing_wallet_login_information_at_file(wallet_file.clone(), &bad_password).is_err()
);
// and storing the same id again fails
assert!(matches!(
store_wallet_login_information_at_file(
wallet_file.clone(),
dummy_account1.clone(),
cosmos_hd_path.clone(),
id1.clone(),
&password,
),
Err(BackendError::IdAlreadyExistsInWallet),
));
let loaded_accounts =
load_existing_wallet_login_information_at_file(wallet_file.clone(), &password).unwrap();
println!("{:?}", loaded_accounts);
assert_eq!(1, loaded_accounts.len());
let loaded_account =
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password).unwrap();
let StoredAccount::Mnemonic(ref acc) = loaded_account;
let StoredAccount::Mnemonic(acc) = &loaded_accounts[0];
assert_eq!(&dummy_account1, acc.mnemonic());
assert_eq!(&cosmos_hd_path, acc.hd_path());
// Can't store extra account if you use different password
assert!(matches!(
store_wallet_login_information_at_file(
wallet_file.clone(),
dummy_account2.clone(),
cosmos_hd_path.clone(),
id2.clone(),
&bad_password
),
Err(BackendError::WalletDifferentPasswordDetected),
));
// can't store extra account if you use different password
assert!(store_wallet_login_information_at_file(
wallet_file.clone(),
dummy_account2.clone(),
cosmos_hd_path.clone(),
&bad_password,
)
.is_err());
// add extra account properly now
store_wallet_login_information_at_file(
wallet_file.clone(),
dummy_account2.clone(),
different_hd_path.clone(),
id2.clone(),
&password,
)
.unwrap();
let loaded_accounts = load_existing_wallet_at_file(wallet_file.clone()).unwrap();
assert_eq!(2, loaded_accounts.len());
let encrypted_blob = &loaded_accounts
.encrypted_account_by_index(1)
.unwrap()
.account;
let encrypted_blob = read_encrypted_blob(wallet_file.clone());
// fresh IV and salt are used
assert_ne!(original_iv, encrypted_blob.iv());
assert_ne!(original_salt, encrypted_blob.salt());
let loaded_accounts =
load_existing_wallet_login_information_at_file(wallet_file, &password).unwrap();
assert_eq!(2, loaded_accounts.len());
// first account should be unchanged
let loaded_account =
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id1, &password).unwrap();
let StoredAccount::Mnemonic(ref acc1) = loaded_account;
let StoredAccount::Mnemonic(acc1) = &loaded_accounts[0];
assert_eq!(&dummy_account1, acc1.mnemonic());
assert_eq!(&cosmos_hd_path, acc1.hd_path());
let loaded_account =
load_existing_wallet_login_information_at_file(wallet_file, &id2, &password).unwrap();
let StoredAccount::Mnemonic(ref acc2) = loaded_account;
let StoredAccount::Mnemonic(acc2) = &loaded_accounts[1];
assert_eq!(&dummy_account2, acc2.mnemonic());
assert_eq!(&different_hd_path, acc2.hd_path());
}
@@ -1,24 +1,8 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use zeroize::Zeroize;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct WalletAccountId(String);
impl WalletAccountId {
pub(crate) fn new(id: String) -> WalletAccountId {
WalletAccountId(id)
}
}
impl AsRef<str> for WalletAccountId {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
// simple wrapper for String that will get zeroized on drop
#[derive(Zeroize)]
#[zeroize(drop)]
+6 -20
View File
@@ -4,10 +4,9 @@ import { useSnackbar } from 'notistack';
import { Account, Network, TCurrency, TMixnodeBondDetails } from '../types';
import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance';
import { config } from '../../config';
import { getMixnodeBondDetails, selectNetwork, signInWithMnemonic, signInWithPassword, signOut } from '../requests';
import { getMixnodeBondDetails, selectNetwork, signInWithMnemonic, signOut } from '../requests';
import { currencyMap } from '../utils';
import { Console } from '../utils/console';
import { TLoginType } from 'src/pages/welcome/types';
export const { ADMIN_ADDRESS, IS_DEV_MODE } = config;
@@ -33,14 +32,11 @@ type TClientContext = {
currency?: TCurrency;
isLoading: boolean;
error?: string;
setIsLoading: (isLoading: boolean) => void;
setError: (value?: string) => void;
switchNetwork: (network: Network) => void;
getBondDetails: () => Promise<void>;
handleShowSettings: () => void;
handleShowAdmin: () => void;
logIn: (opts: { type: 'mnemonic' | 'password'; value: string }) => void;
signInWithPassword: (password: string) => void;
logIn: (mnemonic: string) => void;
logOut: () => void;
};
@@ -94,23 +90,16 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
refreshAccount();
}, [network]);
const logIn = async ({ type, value }: { type: TLoginType; value: string }) => {
if (value.length === 0) {
setError(`A ${type} must be provided`);
return;
}
const logIn = async (mnemonic: string) => {
try {
setIsLoading(true);
if (type === 'mnemonic') {
await signInWithMnemonic(value);
} else {
await signInWithPassword(value);
}
await signInWithMnemonic(mnemonic || '');
await getBondDetails();
setNetwork('MAINNET');
} catch (e) {
setIsLoading(false);
setError(e as string);
} finally {
setIsLoading(false);
history.push('/balance');
}
};
@@ -142,9 +131,6 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
showSettings,
network,
currency,
setIsLoading,
setError,
signInWithPassword,
switchNetwork,
getBondDetails,
handleShowSettings,
+1 -4
View File
@@ -10,7 +10,6 @@ import { Admin, Welcome, Settings } from './pages';
import { ErrorFallback } from './components';
import { NymWalletTheme, WelcomeTheme } from './theme';
import { maximizeWindow } from './utils';
import { SignInProvider } from './pages/welcome/context';
const App = () => {
const { clientDetails } = useContext(ClientContext);
@@ -21,9 +20,7 @@ const App = () => {
return !clientDetails ? (
<WelcomeTheme>
<SignInProvider>
<Welcome />
</SignInProvider>
<Welcome />
</WelcomeTheme>
) : (
<NymWalletTheme>
@@ -16,9 +16,6 @@ export const SendConfirmation = ({
isLoading: boolean;
}) => {
const { userBalance, currency, network } = useContext(ClientContext);
if (!data && !error && !isLoading) return null;
return (
<Box
sx={{
+8 -13
View File
@@ -128,9 +128,13 @@ export const SendWizard = () => {
px: 3,
}}
>
{activeStep === 0 && <SendForm />}
{activeStep === 1 && <SendReview transferFee={transferFee} />}
<SendConfirmation data={confirmedData} isLoading={isLoading} error={requestError} />
{activeStep === 0 ? (
<SendForm />
) : activeStep === 1 ? (
<SendReview transferFee={transferFee} />
) : (
<SendConfirmation data={confirmedData} isLoading={isLoading} error={requestError} />
)}
</Box>
<Box
sx={{
@@ -150,16 +154,7 @@ export const SendWizard = () => {
color="primary"
disableElevation
data-testid="button"
onClick={() => {
switch (activeStep) {
case 0:
return handleNextStep();
case 1:
return handleSend();
default:
return handleFinish();
}
}}
onClick={activeStep === 0 ? handleNextStep : activeStep === 1 ? handleSend : handleFinish}
disabled={!!(methods.formState.errors.amount || methods.formState.errors.to || isLoading)}
size="large"
>
@@ -139,13 +139,13 @@ export const SystemVariables = ({
pt: 0,
}}
>
{nodeUpdateResponse === 'success' && (
{nodeUpdateResponse === 'success' ? (
<Typography sx={{ color: 'success.main', fontWeight: 600 }}>Node successfully updated</Typography>
)}
{nodeUpdateResponse === 'failed' && (
) : nodeUpdateResponse === 'failed' ? (
<Typography sx={{ color: 'error.main', fontWeight: 600 }}>Node update failed</Typography>
) : (
<Fee feeType="UpdateMixnodeConfig" />
)}
{!nodeUpdateResponse && <Fee feeType="UpdateMixnodeConfig" />}
<Button
variant="contained"
color="primary"
+18 -6
View File
@@ -1,9 +1,10 @@
import React, { useContext, useEffect, useState } from 'react';
import { Alert, Box, Button, CircularProgress } from '@mui/material';
import { useSnackbar } from 'notistack';
import { Fee, NymCard } from '../../components';
import { useCheckOwnership } from '../../hooks/useCheckOwnership';
import { ClientContext } from '../../context/main';
import { unbond } from '../../requests';
import { unbond, vestingUnbond } from '../../requests';
import { PageLayout } from '../../layouts';
export const Unbond = () => {
@@ -11,6 +12,8 @@ export const Unbond = () => {
const { checkOwnership, ownership } = useCheckOwnership();
const { userBalance, getBondDetails } = useContext(ClientContext);
const { enqueueSnackbar } = useSnackbar();
useEffect(() => {
const initialiseForm = async () => {
await checkOwnership();
@@ -32,11 +35,20 @@ export const Unbond = () => {
disabled={isLoading}
onClick={async () => {
setIsLoading(true);
await unbond(ownership.nodeType!);
await userBalance.fetchBalance();
await getBondDetails();
await checkOwnership();
setIsLoading(false);
try {
if (ownership.vestingPledge) {
await vestingUnbond(ownership.nodeType!);
} else {
await unbond(ownership.nodeType!);
}
} catch (e) {
enqueueSnackbar(`Failed to unbond ${ownership.nodeType}}`, { variant: 'error' });
} finally {
await getBondDetails();
await checkOwnership();
await userBalance.fetchBalance();
setIsLoading(false);
}
}}
color="inherit"
>
@@ -17,7 +17,7 @@ export const SignInContent: React.FC = () => {
setInputError(undefined);
try {
await logIn({ type: 'mnemonic', value: mnemonic });
await logIn(mnemonic || '');
setIsLoading(false);
} catch (error: any) {
setIsLoading(false);
@@ -1,8 +0,0 @@
import React from 'react';
import { Alert } from '@mui/material';
export const Error = ({ message }: { message: string }) => (
<Alert severity="error" variant="outlined" data-testid="error" sx={{ color: 'error.light', width: '100%' }}>
{message}
</Alert>
);
@@ -2,6 +2,3 @@ export * from './heading';
export * from './word-tiles';
export * from './render-page';
export * from './password-strength';
export * from './error';
export * from './textfields';
export * from './step';
@@ -5,8 +5,8 @@ import { LinearProgress, Stack, Typography, Box } from '@mui/material';
type TStrength = 'weak' | 'medium' | 'strong' | 'init';
const strong = /^(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/;
const medium = /^(((?=.*[a-z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[0-9])))(?=.{6,})/;
const strong = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/;
const medium = /^(((?=.*[a-z])(?=.*[A-Z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{6,})/;
const colorMap = {
init: 'inherit' as 'inherit',
@@ -41,24 +41,7 @@ const getTextColor = (strength: TStrength) => {
}
};
const getPasswordStrength = (strength: TStrength) => {
switch (strength) {
case 'strong':
return 100;
case 'medium':
return 50;
default:
return 0;
}
};
export const PasswordStrength = ({
password,
onChange,
}: {
password: string;
onChange: (isStrong: boolean) => void;
}) => {
export const PasswordStrength = ({ password }: { password: string }) => {
const [strength, setStrength] = useState<TStrength>('init');
useEffect(() => {
@@ -79,17 +62,13 @@ export const PasswordStrength = ({
setStrength('weak');
}, [password]);
useEffect(() => {
if (strength === 'strong') {
onChange(true);
} else {
onChange(false);
}
}, [strength]);
return (
<Stack spacing={0.5}>
<LinearProgress variant="determinate" color={colorMap[strength]} value={getPasswordStrength(strength)} />
<LinearProgress
variant="determinate"
color={colorMap[strength]}
value={strength === 'strong' ? 100 : strength === 'medium' ? 50 : 0}
/>
<Box display="flex" alignItems="center">
<LockOutlined sx={{ fontSize: 15, color: getTextColor(strength) }} />
<Typography variant="caption" sx={{ ml: 0.5, color: getTextColor(strength) }}>
@@ -1,27 +0,0 @@
import React, { useCallback } from 'react';
import { Typography } from '@mui/material';
import { TPages } from '../types';
export const Step = ({ currentPage, totalSteps }: { currentPage: TPages; totalSteps: number }) => {
const mapPage = useCallback(() => {
switch (currentPage) {
case 'create mnemonic':
return 1;
case 'verify mnemonic':
return 2;
case 'create password':
return 3;
default:
return 0;
}
}, [currentPage]);
if (mapPage() === 0) {
return null;
}
return (
<Typography sx={{ color: 'grey.400' }}>
Create account. Step {mapPage()}/{totalSteps}
</Typography>
);
};
@@ -1,78 +0,0 @@
import React, { useState } from 'react';
import { Box, IconButton, Link, Stack, TextField, Typography } from '@mui/material';
import { Visibility, VisibilityOff } from '@mui/icons-material';
import { Error } from './error';
export const MnemonicInput: React.FC<{
mnemonic: string;
error?: string;
onUpdateMnemonic: (mnemonic: string) => void;
}> = ({ mnemonic, error, onUpdateMnemonic }) => {
const [showPassword, setShowPassword] = useState(false);
return (
<Stack spacing={2}>
<TextField
label="Mnemonic"
type={showPassword ? 'input' : 'password'}
value={mnemonic}
onChange={(e) => onUpdateMnemonic(e.target.value)}
multiline={!!showPassword}
rows={4}
autoFocus
fullWidth
InputProps={{
endAdornment: (
<IconButton onClick={() => setShowPassword((show) => !show)}>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
{error && <Error message={error} />}
</Stack>
);
};
export const PasswordInput: React.FC<{
password: string;
error?: string;
label: string;
showForgottenPassword?: boolean;
autoFocus?: boolean;
onUpdatePassword: (password: string) => void;
}> = ({ password, label, error, showForgottenPassword, autoFocus, onUpdatePassword }) => {
const [showPassword, setShowPassword] = useState(false);
return (
<Stack spacing={2}>
<Box>
<TextField
label={label}
fullWidth
value={password}
onChange={(e) => onUpdatePassword(e.target.value)}
type={showPassword ? 'input' : 'password'}
autoFocus={autoFocus}
InputProps={{
endAdornment: (
<IconButton onClick={() => setShowPassword((show) => !show)}>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
{showForgottenPassword && (
<Link
underline="none"
variant="body2"
component="div"
sx={{ mt: 1, textAlign: 'right', color: 'info.main', cursor: 'pointer' }}
>
Forgotten password?
</Link>
)}
</Box>
{error && <Error message={error} />}
</Stack>
);
};
@@ -7,19 +7,17 @@ export const WordTile = ({
index,
disabled,
onClick,
button,
}: {
mnemonicWord: string;
index?: number;
disabled?: boolean;
onClick?: boolean;
button?: boolean;
}) => (
<Card
variant="outlined"
sx={{
background: button ? '#151A2C' : 'transparent',
border: button ? '1px solid #3A4053' : 'none',
background: '#151A2C',
border: '1px solid #3A4053',
cursor: onClick ? 'pointer' : 'default',
opacity: disabled ? 0.2 : 1,
}}
@@ -42,12 +40,10 @@ export const WordTiles = ({
mnemonicWords,
showIndex,
onClick,
buttons,
}: {
mnemonicWords?: TMnemonicWords;
showIndex?: boolean;
onClick?: ({ name, index }: { name: string; index: number }) => void;
buttons?: boolean;
}) => {
if (mnemonicWords) {
return (
@@ -59,7 +55,6 @@ export const WordTiles = ({
index={showIndex ? index : undefined}
onClick={!!onClick}
disabled={disabled}
button={buttons}
/>
</Grid>
))}
@@ -1,72 +0,0 @@
import React, { createContext, useEffect, useMemo, useState } from 'react';
import { createMnemonic, signInWithMnemonic } from 'src/requests';
import { TMnemonicWords } from '../types';
export const SignInContext = createContext({} as TSignInContent);
export type TSignInContent = {
error?: string;
password: string;
mnemonic: string;
mnemonicWords: TMnemonicWords;
setError: (err?: string) => void;
setMnemonic: (mnc: string) => void;
generateMnemonic: () => Promise<void>;
validateMnemonic: () => Promise<void>;
setPassword: (paswd: string) => void;
};
const mnemonicToArray = (mnemonic: string): TMnemonicWords =>
mnemonic
.split(' ')
.reduce((a, c: string, index) => [...a, { name: c, index: index + 1, disabled: false }], [] as TMnemonicWords);
export const SignInProvider: React.FC = ({ children }) => {
const [password, setPassword] = useState('');
const [mnemonic, setMnemonic] = useState('');
const [mnemonicWords, setMnemonicWords] = useState<TMnemonicWords>([]);
const [error, setError] = useState<string>();
const generateMnemonic = async () => {
const mnemonicPhrase = await createMnemonic();
setMnemonic(mnemonicPhrase);
};
const validateMnemonic = async () => {
try {
await signInWithMnemonic(mnemonic);
} catch (e) {
setError(e as string);
}
};
useEffect(() => {
if (mnemonic.length > 0) {
const mnemonicArray = mnemonicToArray(mnemonic);
setMnemonicWords(mnemonicArray);
} else {
setMnemonicWords([]);
}
}, [mnemonic]);
return (
<SignInContext.Provider
value={useMemo(
() => ({
error,
password,
mnemonic,
mnemonicWords,
setError,
setMnemonic,
generateMnemonic,
validateMnemonic,
setPassword,
}),
[error, password, mnemonic, mnemonicWords],
)}
>
{children}
</SignInContext.Provider>
);
};
+38 -42
View File
@@ -1,24 +1,31 @@
import React, { useContext, useState } from 'react';
import { Stack, Box, CircularProgress } from '@mui/material';
import { NymWordmark } from '@nymproject/react';
import {
CreatePassword,
ExistingAccount,
CreateMnemonic,
VerifyMnemonic,
WelcomeContent,
SignInMnemonic,
} from './pages';
import { TLoginType, TPages } from './types';
import { RenderPage, Step } from './components';
import { NymLogo } from '@nymproject/react';
import { CircularProgress, Stack, Box } from '@mui/material';
import { ExistingAccount, WelcomeContent } from './pages';
import { TPages } from './types';
import { RenderPage } from './components';
import { CreateAccountContent } from './_legacy_create-account';
import { ClientContext } from '../../context/main';
import { SignInPassword } from './pages/signin-password';
// const testMnemonic =
// 'futuristic big receptive caption saw hug odd spoon internal dime bike rake helpless left distribution gusty eyes beg enormous word influence trashy pets curl';
//
// const mnemonicToArray = (mnemonic: string): TMnemonicWords =>
// mnemonic
// .split(' ')
// .reduce((a, c: string, index) => [...a, { name: c, index: index + 1, disabled: false }], [] as TMnemonicWords);
export const Welcome = () => {
const [page, setPage] = useState<TPages>('welcome');
const [loginType, setLoginType] = useState<TLoginType>('mnemonic');
// const [mnemonicWords, setMnemonicWords] = useState<TMnemonicWords>();
const { isLoading } = useContext(ClientContext);
// useEffect(() => {
// const mnemonicArray = mnemonicToArray(testMnemonic)
// setMnemonicWords(mnemonicArray)
// }, [])
return (
<Box
sx={{
@@ -43,39 +50,28 @@ export const Welcome = () => {
<CircularProgress size={72} />
) : (
<Stack spacing={3} alignItems="center" sx={{ width: 1080 }}>
<NymWordmark width={75} />
<Step currentPage={page} totalSteps={3} />
<NymLogo width={75} />
<RenderPage page={page}>
<WelcomeContent
onUseExisting={() => setPage('existing account')}
onCreateAccount={() => setPage('create mnemonic')}
onCreateAccountComplete={() => setPage('legacy create account')}
page="welcome"
/>
<CreateMnemonic
onNext={() => setPage('verify mnemonic')}
onPrev={() => setPage('create password')}
page="create mnemonic"
/>
<VerifyMnemonic onNext={() => setPage('create password')} onPrev={() => {}} page="verify mnemonic" />
<CreatePassword
onSkip={() => {
setLoginType('mnemonic');
setPage('existing account');
}}
onNext={() => {
setLoginType('password');
setPage('existing account');
}}
page="create password"
/>
<ExistingAccount
onPrev={() => setPage('welcome')}
page="existing account"
loginType={loginType}
setLoginType={(loginType) => setLoginType(loginType)}
/>
<SignInMnemonic onPrev={() => setPage('welcome')} page="sign in with mnemonic" />
<SignInPassword onPrev={() => setPage('welcome')} page="sign in with password" />
<CreateAccountContent page="legacy create account" showSignIn={() => setPage('existing account')} />
{/* <MnemonicWords
mnemonicWords={mnemonicWords}
onNext={() => setPage('verify mnemonic')}
onPrev={() => setPage('welcome')}
page="create account"
/>
<VerifyMnemonic
mnemonicWords={mnemonicWords}
onComplete={() => setPage('create password')}
page="verify mnemonic"
/>
<CreatePassword page="create password" /> */}
<ExistingAccount onPrev={() => setPage('welcome')} page="existing account" />
</RenderPage>
</Stack>
)}
@@ -1,60 +0,0 @@
import React, { useContext, useEffect } from 'react';
import { Alert, Button, Grid, Stack, Typography } from '@mui/material';
import { Check, ContentCopySharp } from '@mui/icons-material';
import { useClipboard } from 'use-clipboard-copy';
import { WordTiles } from '../components';
import { TPages } from '../types';
import { SignInContext } from '../context';
export const CreateMnemonic = ({ onNext }: { page: TPages; onNext: () => void; onPrev: () => void }) => {
const { mnemonic, mnemonicWords, generateMnemonic, validateMnemonic, setMnemonic, setError } =
useContext(SignInContext);
useEffect(() => {
generateMnemonic();
}, []);
const { copy, copied } = useClipboard({ copiedTimeout: 5000 });
return (
<Stack alignItems="center" spacing={3}>
<Typography sx={{ color: 'common.white', fontWeight: 600 }} textAlign="center">
Write down your mnemonic
</Typography>
<Alert variant="outlined" severity="warning" sx={{ textAlign: 'center' }}>
<Typography>Below is your 24 word mnemonic, please store the mnemonic in a safe place.</Typography>
<Typography>This is the only way to access your wallet!</Typography>
</Alert>
<WordTiles mnemonicWords={mnemonicWords} showIndex />
<Button
color="inherit"
disableElevation
size="large"
onClick={() => {
copy(mnemonic);
}}
sx={{
width: 250,
}}
endIcon={!copied ? <ContentCopySharp /> : <Check color="success" />}
>
Copy mnemonic
</Button>
<Button
variant="contained"
color="primary"
disableElevation
size="large"
onClick={onNext}
sx={{ width: 250 }}
disabled={!copied}
>
I saved my mnemonic
</Button>
</Stack>
);
};
@@ -1,74 +1,60 @@
import React, { useContext, useState } from 'react';
import { Alert, Button, FormControl, Stack } from '@mui/material';
import { useSnackbar } from 'notistack';
import { TPages } from '../types';
import React, { useState } from 'react';
import { Button, FormControl, Grid, IconButton, Stack, TextField } from '@mui/material';
import { VisibilityOff, Visibility } from '@mui/icons-material';
import { Subtitle, Title, PasswordStrength } from '../components';
import { PasswordInput } from '../components/textfields';
import { SignInContext } from '../context';
import { createPassword } from '../../../requests';
export const CreatePassword = ({ onSkip, onNext }: { page: TPages; onNext: () => void; onSkip: () => void }) => {
const { password, setPassword } = useContext(SignInContext);
const [confirmedPassword, setConfirmedPassword] = useState<string>('');
const [isStrongPassword, setIsStrongPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const { mnemonic } = useContext(SignInContext);
const handleSkip = () => {
setPassword('');
onSkip();
};
const { enqueueSnackbar } = useSnackbar();
const storePassword = async () => {
try {
setIsLoading(true);
await createPassword({ mnemonic, password });
enqueueSnackbar('Password successfully created', { variant: 'success' });
setPassword('');
onNext();
} catch (e) {
enqueueSnackbar(e as string, { variant: 'error' });
} finally {
setIsLoading(false);
}
};
export const CreatePassword = () => {
const [password, setPassword] = useState<string>('');
const [confirmedPassword, setConfirmedPassword] = useState<string>();
const [showPassword, setShowPassword] = useState(false);
const [showConfirmedPassword, setShowConfirmedPassword] = useState(false);
return (
<Stack spacing={3} alignItems="center" minWidth="50%">
<Title title="Create optional password" />
<Subtitle subtitle="Password should be min 8 characters, at least one number and one symbol" />
<FormControl fullWidth>
<Stack spacing={2}>
<>
<PasswordInput
password={password}
onUpdatePassword={(pswd) => setPassword(pswd)}
label="Password"
autoFocus
/>
<PasswordStrength password={password} onChange={(isStrong) => setIsStrongPassword(isStrong)} />
</>
<PasswordInput
password={confirmedPassword}
onUpdatePassword={(pswd) => setConfirmedPassword(pswd)}
label="Confirm password"
/>
<Button
size="large"
variant="contained"
disabled={password !== confirmedPassword || password.length === 0 || !isStrongPassword || isLoading}
onClick={storePassword}
>
Next
</Button>
<Button size="large" color="info" onClick={handleSkip}>
Skip and sign in with mnemonic
</Button>
</Stack>
</FormControl>
</Stack>
<>
<Title title="Create password" />
<Subtitle subtitle="Create a strong password. Min 8 characters, at least one capital letter, number and special symbol" />
<Grid container justifyContent="center">
<Grid item xs={6}>
<FormControl fullWidth>
<Stack spacing={2}>
<TextField
label="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
type={showPassword ? 'input' : 'password'}
InputProps={{
endAdornment: (
<IconButton onClick={() => setShowPassword((show) => !show)}>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
<PasswordStrength password={password} />
<TextField
label="Confirm password"
value={confirmedPassword}
onChange={(e) => setConfirmedPassword(e.target.value)}
type={showConfirmedPassword ? 'input' : 'password'}
InputProps={{
endAdornment: (
<IconButton onClick={() => setShowConfirmedPassword((show) => !show)}>
{showConfirmedPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
<Button
size="large"
variant="contained"
disabled={password !== confirmedPassword || password.length === 0}
>
Next
</Button>
</Stack>
</FormControl>
</Grid>
</Grid>
</>
);
};
@@ -1,78 +1,55 @@
/* eslint-disable react/no-unused-prop-types */
import React, { useContext, useState } from 'react';
import { Alert, Button, FormControl, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material';
import { ClientContext } from 'src/context/main';
import { Subtitle, MnemonicInput, PasswordInput } from '../components';
import { TLoginType, TPages } from '../types';
import { Alert, Button, Stack, TextField } from '@mui/material';
import { Subtitle } from '../components';
import { ClientContext } from '../../../context/main';
import { TPages } from '../types';
export const ExistingAccount: React.FC<{
page: TPages;
loginType: TLoginType;
setLoginType: (type: 'mnemonic' | 'password') => void;
onPrev: () => void;
}> = ({ loginType, setLoginType, onPrev }) => {
const [password, setPassword] = useState('');
const [mnemonic, setMnemonic] = useState('');
const { setError, logIn, error } = useContext(ClientContext);
export const ExistingAccount: React.FC<{ page: TPages; onPrev: () => void }> = ({ onPrev }) => {
const [mnemonic, setMnemonic] = useState<string>('');
const { logIn, error } = useContext(ClientContext);
const handleSignIn = async () => {
await logIn(mnemonic);
};
const handleSignInOnEnter = ({ key }: React.KeyboardEvent<HTMLDivElement>) => {
if (key.toLowerCase() === 'enter') {
logIn(mnemonic);
}
};
return (
<>
<Subtitle subtitle={`Enter your ${loginType} for existing wallet`} />
<Alert variant="outlined" severity="info">
You can use either a mnemonic or a password to access your wallet
</Alert>
<Stack spacing={2} minWidth="50%">
<ToggleButtonGroup
fullWidth
value={loginType}
exclusive
onChange={(_: React.MouseEvent<HTMLElement>, value: TLoginType) => {
setError(undefined);
setLoginType(value);
}}
>
<ToggleButton value="mnemonic">Mnemonic</ToggleButton>
<ToggleButton value="password">Password</ToggleButton>
</ToggleButtonGroup>
<FormControl fullWidth>
<Stack spacing={2}>
{loginType === 'mnemonic' && (
<MnemonicInput mnemonic={mnemonic} onUpdateMnemonic={(mnc) => setMnemonic(mnc)} error={error} />
)}
{loginType === 'password' && (
<PasswordInput
password={password}
onUpdatePassword={(pswd) => setPassword(pswd)}
label="Password"
autoFocus
error={error}
/>
)}
<Stack spacing={2} sx={{ width: 400 }} alignItems="center">
<Subtitle subtitle="Enter your mnemonic from existing wallet" />
<TextField
value={mnemonic}
onChange={(e) => setMnemonic(e.target.value)}
multiline
rows={5}
fullWidth
onKeyDown={handleSignInOnEnter}
/>
{error && (
<Alert severity="error" variant="outlined" data-testid="error" sx={{ color: 'error.light', width: '100%' }}>
{error}
</Alert>
)}
<Button
variant="contained"
size="large"
fullWidth
onClick={() => logIn({ type: loginType, value: loginType === 'mnemonic' ? mnemonic : password })}
>
{`Sign in with ${loginType}`}
</Button>
<Button
variant="outlined"
disableElevation
size="large"
onClick={() => {
setError(undefined);
onPrev();
}}
fullWidth
sx={{ color: 'common.white', border: '1px solid white', '&:hover': { border: '1px solid white' } }}
>
Back
</Button>
</Stack>
</FormControl>
</Stack>
</>
<Button variant="contained" size="large" fullWidth onClick={handleSignIn}>
Sign in
</Button>
<Button
variant="outlined"
disableElevation
size="large"
onClick={onPrev}
fullWidth
sx={{ color: 'common.white', border: '1px solid white', '&:hover': { border: '1px solid white' } }}
>
Back
</Button>
</Stack>
);
};
+1 -2
View File
@@ -1,6 +1,5 @@
export * from './welcome-content';
export * from './create-mnemonic';
export * from './mnemonic-words';
export * from './verify-mnemonic';
export * from './create-password';
export * from './existing-account';
export * from './signin-mnemonic';
@@ -0,0 +1,34 @@
import React from 'react';
import { Alert, Button, Typography } from '@mui/material';
import { WordTiles } from '../components';
import { TMnemonicWords } from '../types';
export const MnemonicWords = ({
mnemonicWords,
onNext,
onPrev,
}: {
mnemonicWords?: TMnemonicWords;
onNext: () => void;
onPrev: () => void;
}) => (
<>
<Typography sx={{ color: 'common.white', fontWeight: 600 }}>Write down your mnemonic</Typography>
<Alert icon={false} severity="info" sx={{ bgcolor: '#18263B', color: '#50ABFF', width: 625 }}>
Please store your mnemonic in a safe place. This is the only way to access your wallet!
</Alert>
<WordTiles mnemonicWords={mnemonicWords} showIndex />
<Button variant="contained" color="primary" disableElevation size="large" onClick={onNext} sx={{ width: 250 }}>
Verify mnemonic
</Button>
<Button
variant="outlined"
disableElevation
size="large"
onClick={onPrev}
sx={{ color: 'common.white', border: '1px solid white', '&:hover': { border: '1px solid white' }, width: 250 }}
>
Back
</Button>
</>
);
@@ -1,43 +0,0 @@
import React, { useContext, useState } from 'react';
import { Button, FormControl, Stack } from '@mui/material';
import { MnemonicInput, Subtitle } from '../components';
import { ClientContext } from '../../../context/main';
import { TPages } from '../types';
export const SignInMnemonic = ({ page, onPrev }: { page: TPages; onPrev: () => void }) => {
const [mnemonic, setMnemonic] = useState('');
const { setError, logIn, error } = useContext(ClientContext);
return (
<Stack spacing={2} alignItems="center" minWidth="50%">
<Subtitle subtitle="Enter a mnemonic to sign in" />
<FormControl fullWidth>
<Stack spacing={2}>
<MnemonicInput mnemonic={mnemonic} onUpdateMnemonic={(mnc) => setMnemonic(mnc)} error={error} />
<Button
variant="contained"
size="large"
fullWidth
onClick={() => logIn({ type: 'mnemonic', value: mnemonic })}
>
{`Sign in with mnemonic`}
</Button>
<Button
variant="outlined"
disableElevation
size="large"
onClick={() => {
setError(undefined);
onPrev();
}}
fullWidth
sx={{ color: 'common.white', border: '1px solid white', '&:hover': { border: '1px solid white' } }}
>
Back
</Button>
</Stack>
</FormControl>
</Stack>
);
};
@@ -1,50 +0,0 @@
import React, { useContext, useState } from 'react';
import { Button, FormControl, Stack } from '@mui/material';
import { PasswordInput, Subtitle } from '../components';
import { ClientContext } from '../../../context/main';
import { TPages } from '../types';
export const SignInPassword = ({ onPrev }: { page: TPages; onPrev: () => void }) => {
const [password, setPassword] = useState('');
const { setError, logIn, error } = useContext(ClientContext);
return (
<>
<Stack spacing={2} alignItems="center" minWidth="50%">
<Subtitle subtitle="Enter a password to sign in" />
<FormControl fullWidth>
<Stack spacing={2}>
<PasswordInput
label="Enter password"
password={password}
onUpdatePassword={(pswd) => setPassword(pswd)}
error={error}
autoFocus
/>
<Button
variant="contained"
size="large"
fullWidth
onClick={() => logIn({ type: 'password', value: password })}
>
{`Sign in with password`}
</Button>
<Button
variant="outlined"
disableElevation
size="large"
onClick={() => {
setError(undefined);
onPrev();
}}
fullWidth
sx={{ color: 'common.white', border: '1px solid white', '&:hover': { border: '1px solid white' } }}
>
Back
</Button>
</Stack>
</FormControl>
</Stack>
</>
);
};
@@ -1,19 +1,22 @@
import React, { useContext, useEffect, useState } from 'react';
import { Button, Stack } from '@mui/material';
import React, { useEffect, useState } from 'react';
import { Button } from '@mui/material';
import { HiddenWords, Subtitle, Title, WordTiles } from '../components';
import { THiddenMnemonicWord, THiddenMnemonicWords, TMnemonicWord, TMnemonicWords, TPages } from '../types';
import { THiddenMnemonicWord, THiddenMnemonicWords, TMnemonicWord, TMnemonicWords } from '../types';
import { randomNumberBetween } from '../../../utils';
import { SignInContext } from '../context';
const numberOfRandomWords = 6;
const numberOfRandomWords = 4;
export const VerifyMnemonic = ({ onNext }: { page: TPages; onNext: () => void; onPrev: () => void }) => {
export const VerifyMnemonic = ({
mnemonicWords,
onComplete,
}: {
mnemonicWords?: TMnemonicWords;
onComplete: () => void;
}) => {
const [randomWords, setRandomWords] = useState<TMnemonicWords>();
const [hiddenRandomWords, setHiddenRandomWords] = useState<THiddenMnemonicWords>();
const [currentSelection, setCurrentSelection] = useState(0);
const { mnemonicWords } = useContext(SignInContext);
useEffect(() => {
if (mnemonicWords) {
const newRandomWords = getRandomEntriesFromArray<TMnemonicWord>(mnemonicWords, numberOfRandomWords);
@@ -45,21 +48,16 @@ export const VerifyMnemonic = ({ onNext }: { page: TPages; onNext: () => void; o
<WordTiles
mnemonicWords={randomWords}
onClick={currentSelection !== numberOfRandomWords ? revealWord : undefined}
buttons
/>
<Stack spacing={3} sx={{ width: 300 }}>
<Button
variant="contained"
fullWidth
size="large"
disabled={currentSelection !== numberOfRandomWords}
onClick={() => {
onNext();
}}
>
Next
</Button>
</Stack>
<Button
variant="contained"
sx={{ width: 300 }}
size="large"
disabled={currentSelection !== numberOfRandomWords}
onClick={onComplete}
>
Next
</Button>
</>
);
}
@@ -7,17 +7,35 @@ import { TPages } from '../types';
export const WelcomeContent: React.FC<{
page: TPages;
onUseExisting: () => void;
onCreateAccount: () => void;
}> = ({ onUseExisting, onCreateAccount }) => (
onCreateAccountComplete: () => void;
}> = ({ onUseExisting, onCreateAccountComplete }) => (
<>
<Title title="Welcome to NYM" />
<SubtitleSlick subtitle="Next generation of privacy" />
<Stack spacing={3} minWidth={300}>
<Button fullWidth color="primary" variant="contained" size="large" onClick={onUseExisting}>
Sign in
<Stack spacing={3} sx={{ width: 300 }}>
<Button
fullWidth
variant="contained"
color="primary"
disableElevation
size="large"
onClick={onCreateAccountComplete}
>
Create Account
</Button>
<Button fullWidth color="inherit" disableElevation size="large" onClick={onCreateAccount}>
Create account
<Button
fullWidth
variant="outlined"
size="large"
sx={{
color: 'common.white',
border: '1px solid white',
'&:hover': { border: '1px solid white', '&:hover': { background: 'none' } },
}}
onClick={onUseExisting}
disableRipple
>
Sign in
</Button>
</Stack>
</>
+2 -6
View File
@@ -1,13 +1,11 @@
export type TPages =
| 'welcome'
| 'create mnemonic'
| 'create account'
| 'verify mnemonic'
| 'create password'
| 'existing account'
| 'select network'
| 'legacy create account'
| 'sign in with mnemonic'
| 'sign in with password';
| 'legacy create account';
export type TMnemonicWord = {
name: string;
@@ -19,5 +17,3 @@ export type TMnemonicWords = TMnemonicWord[];
export type THiddenMnemonicWord = { hidden: boolean } & TMnemonicWord;
export type THiddenMnemonicWords = THiddenMnemonicWord[];
export type TLoginType = 'mnemonic' | 'password';
+8 -9
View File
@@ -1,10 +1,14 @@
import { invoke } from '@tauri-apps/api';
import { Account } from '../types';
import { Account, TCreateAccount } from '../types';
export const createMnemonic = async (): Promise<string> => invoke('create_new_mnemonic');
export const createAccount = async (): Promise<TCreateAccount> => {
const res: TCreateAccount = await invoke('create_new_account');
return res;
};
export const createPassword = async ({ mnemonic, password }: { mnemonic: string; password: string }): Promise<void> => {
await invoke('create_password', { mnemonic, password });
export const createMnemonic = async (): Promise<string> => {
const res: string = await invoke('create_new_mnemonic');
return res;
};
export const signInWithMnemonic = async (mnemonic: string): Promise<Account> => {
@@ -12,11 +16,6 @@ export const signInWithMnemonic = async (mnemonic: string): Promise<Account> =>
return res;
};
export const signInWithPassword = async (password: string): Promise<Account> => {
const res: Account = await invoke('sign_in_with_password', { password });
return res;
};
export const signOut = async (): Promise<void> => {
await invoke('logout');
};
@@ -3,7 +3,7 @@
[package]
name = "nym-network-requester"
version = "0.12.0"
version = "1.0.0-rc.1"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
+1 -1
View File
@@ -3,7 +3,7 @@
[package]
name = "nym-validator-api"
version = "0.12.0"
version = "1.0.0-rc.1"
authors = [
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
"Jędrzej Stuczyński <andrew@nymtech.net>",
+22 -6
View File
@@ -34,7 +34,8 @@ const DEFAULT_PER_NODE_TEST_PACKETS: usize = 3;
const DEFAULT_CACHE_INTERVAL: Duration = Duration::from_secs(30);
const DEFAULT_MONITOR_THRESHOLD: u8 = 60;
const DEFAULT_MIN_RELIABILITY: u8 = 50;
const DEFAULT_MIN_MIXNODE_RELIABILITY: u8 = 50;
const DEFAULT_MIN_GATEWAY_RELIABILITY: u8 = 20;
#[derive(Debug, Default, Deserialize, PartialEq, Serialize)]
pub struct Config {
@@ -109,8 +110,8 @@ impl Default for Base {
#[serde(default)]
pub struct NetworkMonitor {
// Mixnodes and gateways with relialability lower the this get blacklisted by network monitor, get no traffic and cannot be selected into a rewarded set.
min_reliability: u8,
min_mixnode_reliability: u8, // defaults to 50
min_gateway_reliability: u8, // defaults to 20
/// Specifies whether network monitoring service is enabled in this process.
enabled: bool,
@@ -192,7 +193,8 @@ impl NetworkMonitor {
impl Default for NetworkMonitor {
fn default() -> Self {
NetworkMonitor {
min_reliability: DEFAULT_MIN_RELIABILITY,
min_mixnode_reliability: DEFAULT_MIN_MIXNODE_RELIABILITY,
min_gateway_reliability: DEFAULT_MIN_GATEWAY_RELIABILITY,
enabled: false,
testnet_mode: false,
all_validator_apis: default_api_endpoints(),
@@ -334,6 +336,16 @@ impl Config {
self
}
pub fn with_min_mixnode_reliability(mut self, min_mixnode_reliability: u8) -> Self {
self.network_monitor.min_mixnode_reliability = min_mixnode_reliability;
self
}
pub fn with_min_gateway_reliability(mut self, min_gateway_reliability: u8) -> Self {
self.network_monitor.min_gateway_reliability = min_gateway_reliability;
self
}
#[cfg(not(feature = "coconut"))]
pub fn with_eth_private_key(mut self, eth_private_key: String) -> Self {
self.network_monitor.eth_private_key = eth_private_key;
@@ -423,8 +435,12 @@ impl Config {
self.network_monitor.minimum_test_routes
}
pub fn get_min_reliability(&self) -> u8 {
self.network_monitor.min_reliability
pub fn get_min_mixnode_reliability(&self) -> u8 {
self.network_monitor.min_mixnode_reliability
}
pub fn get_min_gateway_reliability(&self) -> u8 {
self.network_monitor.min_gateway_reliability
}
pub fn get_route_test_packets(&self) -> usize {
+2 -1
View File
@@ -21,7 +21,8 @@ mixnet_contract_address = '{{ base.mixnet_contract_address }}'
[network_monitor]
# Mixnodes and gateways with relialability lower the this get blacklisted by network monitor, get no traffic and cannot be selected into a rewarded set.
min_reliability = {{ network_monitor.min_reliability }}
min_mixnode_reliability = {{ network_monitor.min_mixnode_reliability }} # deafults to 50
min_gateway_reliability = {{ network_monitor.min_gateway_reliability }} # defaults to 20
# Specifies whether network monitoring service is enabled in this process.
enabled = {{ network_monitor.enabled }}
+17 -9
View File
@@ -131,9 +131,13 @@ impl<C> ValidatorCacheRefresher<C> {
self.nymd_client.get_gateways(),
)?;
let rewarded_set_identities = self.nymd_client.get_rewarded_set_identities().await?;
let (rewarded_set, active_set) =
self.collect_rewarded_and_active_set_details(&mixnodes, rewarded_set_identities);
let (rewarded_set, active_set) = if let Ok(rewarded_set_identities) =
self.nymd_client.get_rewarded_set_identities().await
{
self.collect_rewarded_and_active_set_details(&mixnodes, rewarded_set_identities)
} else {
(Vec::new(), Vec::new())
};
let epoch_rewarding_params = self.nymd_client.get_current_epoch_reward_params().await?;
@@ -154,15 +158,19 @@ impl<C> ValidatorCacheRefresher<C> {
.await;
if let Some(notify) = &self.update_rewarded_set_notify {
let update_details = self
if let Ok(update_details) = self
.nymd_client
.get_current_rewarded_set_update_details()
.await?;
if update_details.last_refreshed_block + (update_details.refresh_rate_blocks as u64)
< update_details.current_height
.await
{
// there's only ever a single waiter -> the set updater
if update_details.last_refreshed_block + (update_details.refresh_rate_blocks as u64)
< update_details.current_height
{
// there's only ever a single waiter -> the set updater
notify.notify_one()
}
} else {
// This has the potential to be spammy, we'll find out
notify.notify_one()
}
}
+21
View File
@@ -64,6 +64,9 @@ const ETH_PRIVATE_KEY: &str = "eth_private_key";
const REWARDING_MONITOR_THRESHOLD_ARG: &str = "monitor-threshold";
const MIN_MIXNODE_RELIABILITY_ARG: &str = "min_mixnode_reliability";
const MIN_GATEWAY_RELIABILITY_ARG: &str = "min_gateway_reliability";
fn parse_validators(raw: &str) -> Vec<Url> {
raw.split(',')
.map(|raw_validator| {
@@ -276,6 +279,24 @@ fn override_config(mut config: Config, matches: &ArgMatches<'_>) -> Config {
config = config.with_minimum_interval_monitor_threshold(monitor_threshold)
}
if let Some(reliability) = matches
.value_of(MIN_MIXNODE_RELIABILITY_ARG)
.map(|t| t.parse::<u8>())
{
config = config.with_min_mixnode_reliability(
reliability.expect("Provided reliability is not a u8 number!"),
)
}
if let Some(reliability) = matches
.value_of(MIN_GATEWAY_RELIABILITY_ARG)
.map(|t| t.parse::<u8>())
{
config = config.with_min_gateway_reliability(
reliability.expect("Provided reliability is not a u8 number!"),
)
}
#[cfg(feature = "coconut")]
if let Some(keypair_path) = matches.value_of(KEYPAIR_ARG) {
let keypair_bs58 = std::fs::read_to_string(keypair_path)
@@ -42,7 +42,8 @@ pub(super) struct Monitor {
/// The minimum number of test routes that need to be constructed (and working) in order for
/// a monitor test run to be valid.
minimum_test_routes: usize,
min_reliability: u8,
min_mixnode_reliability: u8,
min_gateway_reliability: u8,
}
impl Monitor {
@@ -67,7 +68,8 @@ impl Monitor {
route_test_packets: config.get_route_test_packets(),
test_routes: config.get_test_routes(),
minimum_test_routes: config.get_minimum_test_routes(),
min_reliability: config.get_min_reliability(),
min_mixnode_reliability: config.get_min_mixnode_reliability(),
min_gateway_reliability: config.get_min_gateway_reliability(),
}
}
@@ -78,7 +80,7 @@ impl Monitor {
// uptime calculations
for result in test_summary.mixnode_results.iter() {
if result.reliability < self.min_reliability {
if result.reliability < self.min_mixnode_reliability {
self.packet_preparer
.validator_cache()
.insert_mixnodes_blacklist(result.identity.clone())
@@ -92,7 +94,7 @@ impl Monitor {
}
for result in test_summary.gateway_results.iter() {
if result.reliability < self.min_reliability {
if result.reliability < self.min_gateway_reliability {
self.packet_preparer
.validator_cache()
.insert_gateways_blacklist(result.identity.clone())