Merge branch 'develop' into feature/validator_selector
This commit is contained in:
@@ -64,12 +64,16 @@ jobs:
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_IDENTITY_ID }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
run: yarn && yarn build
|
||||
|
||||
- name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: nym-wallet/target/release/bundle/dmg/*.dmg
|
||||
files: |
|
||||
nym-wallet/target/release/bundle/dmg/*.dmg
|
||||
nym-wallet/target/release/bundle/macos/*.app.tar.gz*
|
||||
|
||||
- name: Clean up keychain
|
||||
if: ${{ always() }}
|
||||
|
||||
@@ -37,10 +37,16 @@ jobs:
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
- name: Install app dependencies and build it
|
||||
run: yarn && yarn build
|
||||
|
||||
- name: Install app dependencies
|
||||
run: yarn
|
||||
- name: Build app
|
||||
run: yarn build
|
||||
env:
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
- name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: nym-wallet/target/release/bundle/appimage/*.AppImage
|
||||
files: |
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage.tar.gz*
|
||||
|
||||
@@ -63,9 +63,13 @@ jobs:
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
run: yarn build
|
||||
|
||||
- name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: nym-wallet/target/release/bundle/msi/*.msi
|
||||
files: |
|
||||
nym-wallet/target/release/bundle/msi/*.msi
|
||||
nym-wallet/target/release/bundle/msi/*.msi.zip*
|
||||
|
||||
Generated
+1
@@ -623,6 +623,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"handlebars",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
"network-defaults",
|
||||
"serde",
|
||||
"toml",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<svg width="210" height="56" viewBox="0 0 210 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg 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: 1.0 KiB After Width: | Height: | Size: 1011 B |
@@ -93,7 +93,11 @@ async fn test_nymd_connection(
|
||||
{
|
||||
Ok(Err(NymdError::TendermintError(e))) => {
|
||||
// If we get a tendermint-rpc error, we classify the node as not contactable
|
||||
log::debug!("Checking: nymd_url: {network}: {url}: failed: {}", e);
|
||||
log::debug!(
|
||||
"Checking: nymd_url: {network}: {url}: {}: {}",
|
||||
"failed".red(),
|
||||
e
|
||||
);
|
||||
false
|
||||
}
|
||||
Ok(Err(NymdError::AbciError(code, log))) => {
|
||||
|
||||
@@ -9,8 +9,9 @@ edition = "2021"
|
||||
[dependencies]
|
||||
handlebars = "3.0.1"
|
||||
humantime-serde = "1.0"
|
||||
log = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
toml = "0.5.6"
|
||||
url = "2.2"
|
||||
|
||||
network-defaults = { path = "../network-defaults" }
|
||||
network-defaults = { path = "../network-defaults" }
|
||||
|
||||
@@ -13,6 +13,7 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
fn template() -> &'static str;
|
||||
|
||||
fn config_file_name() -> String {
|
||||
log::trace!("NymdConfig::config_file_name");
|
||||
"config.toml".to_string()
|
||||
}
|
||||
|
||||
@@ -20,6 +21,7 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
|
||||
// default, most probable, implementations; can be easily overridden where required
|
||||
fn default_config_directory(id: Option<&str>) -> PathBuf {
|
||||
log::trace!("NymdConfig::default_config_directory");
|
||||
if let Some(id) = id {
|
||||
Self::default_root_directory().join(id).join("config")
|
||||
} else {
|
||||
@@ -28,6 +30,7 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
}
|
||||
|
||||
fn default_data_directory(id: Option<&str>) -> PathBuf {
|
||||
log::trace!("NymdConfig::default_data_path");
|
||||
if let Some(id) = id {
|
||||
Self::default_root_directory().join(id).join("data")
|
||||
} else {
|
||||
@@ -36,6 +39,7 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
}
|
||||
|
||||
fn default_config_file_path(id: Option<&str>) -> PathBuf {
|
||||
log::trace!("NymdConfig::default_config_file_path");
|
||||
Self::default_config_directory(id).join(Self::config_file_name())
|
||||
}
|
||||
|
||||
@@ -68,7 +72,9 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
}
|
||||
|
||||
fn load_from_file(id: Option<&str>) -> io::Result<Self> {
|
||||
let config_contents = fs::read_to_string(Self::default_config_file_path(id))?;
|
||||
let file = Self::default_config_file_path(id);
|
||||
log::trace!("Loading from file: {:#?}", file);
|
||||
let config_contents = fs::read_to_string(file)?;
|
||||
|
||||
toml::from_str(&config_contents)
|
||||
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
|
||||
|
||||
@@ -215,7 +215,7 @@ mod test {
|
||||
));
|
||||
assert!(!BandwidthVoucher::verify_against_plain(
|
||||
&voucher.get_public_attributes(),
|
||||
&vec![],
|
||||
&[],
|
||||
));
|
||||
assert!(!BandwidthVoucher::verify_against_plain(
|
||||
&voucher.get_public_attributes(),
|
||||
|
||||
Generated
+1
@@ -219,6 +219,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"handlebars",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
"network-defaults",
|
||||
"serde",
|
||||
"toml",
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
{
|
||||
"rinkeby":{
|
||||
"NYM_ERC20":"0xe58c12f8bA4a28e87841b10BdEdf80A47F86BA9B",
|
||||
"BANDWIDTH_GENERATOR":"0x5FbDB2315678afecb367f032d93F642f64180aa3",
|
||||
"GRAVITY":"0xe58c12f8bA4a28e87841b10BdEdf80A47F86BA9B"
|
||||
},
|
||||
"mainnet":{
|
||||
"NYM_ERC20":"0x525A8F6F3Ba4752868cde25164382BfbaE3990e1",
|
||||
"NYMT": "0xE8883BAeF3869e14E4823F46662e81D4F7d2A81F",
|
||||
"BANDWIDTH_GENERATOR":"",
|
||||
"rinkeby":
|
||||
{"NYM_ERC20":"0xe58c12f8bA4a28e87841b10BdEdf80A47F86BA9B",
|
||||
"BANDWIDTH_GENERATOR":"0xfa2714Bf14EB5Bb887e4A54984C6F7A7e3E6c84b",
|
||||
"GRAVITY":"0xe58c12f8bA4a28e87841b10BdEdf80A47F86BA9B"},
|
||||
"mainnet":
|
||||
{"NYM_ERC20":"0x525A8F6F3Ba4752868cde25164382BfbaE3990e1",
|
||||
"NYMT":"0xE8883BAeF3869e14E4823F46662e81D4F7d2A81F",
|
||||
"BANDWIDTH_GENERATOR":"0x3FfEb99acca159A182f35F9944dAf3BF41Ae8165",
|
||||
"BANDWIDTH_GENERATOR_NYMT":"0xB3BF30DD53044c9050B7309031Bbf26b2cecF3be",
|
||||
"GRAVITY":"0xa4108aA1Ec4967F8b52220a4f7e94A8201F2D906"
|
||||
}
|
||||
"GRAVITY":"0xa4108aA1Ec4967F8b52220a4f7e94A8201F2D906"}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ We use the following:
|
||||
|
||||
## Development mode
|
||||
|
||||
Copy the `.env.prod` file to `.env` to configure your environment. Using the live sandbox Explorer API is the best way to do development, so the prod settings are good.
|
||||
|
||||
Run the following:
|
||||
|
||||
```
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DiscordIcon } from '../icons/socials/DiscordIcon';
|
||||
export const TELEGRAM_LINK = 'https://t.me/nymchan';
|
||||
export const TWITTER_LINK = 'https://twitter.com/nymproject';
|
||||
export const GITHUB_LINK = 'https://github.com/nymtech';
|
||||
export const DISCORD_LINK = 'https://discord.gg/jUqJYGB5';
|
||||
export const DISCORD_LINK = 'https://discord.gg/ggxrUpbNnn';
|
||||
|
||||
export const Socials: React.FC<{ isFooter?: boolean }> = ({ isFooter }) => {
|
||||
const theme = useTheme();
|
||||
@@ -22,11 +22,9 @@ export const Socials: React.FC<{ isFooter?: boolean }> = ({ isFooter }) => {
|
||||
<IconButton component="a" href={TELEGRAM_LINK} target="_blank" data-testid="telegram">
|
||||
<TelegramIcon color={color} size={24} />
|
||||
</IconButton>
|
||||
{false && (
|
||||
<IconButton component="a" href={DISCORD_LINK} target="_blank" data-testid="discord">
|
||||
<DiscordIcon color={color} size={24} />
|
||||
</IconButton>
|
||||
)}
|
||||
<IconButton component="a" href={DISCORD_LINK} target="_blank" data-testid="discord">
|
||||
<DiscordIcon color={color} size={24} />
|
||||
</IconButton>
|
||||
<IconButton component="a" href={TWITTER_LINK} target="_blank" data-testid="twitter">
|
||||
<TwitterIcon color={color} size={24} />
|
||||
</IconButton>
|
||||
|
||||
Generated
+380
-1
@@ -102,6 +102,101 @@ version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6"
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5eea0a7a98b3bd2832eb087e1078f6f58db5a54447574d3007cdac6309c1e9f1"
|
||||
dependencies = [
|
||||
"enumflags2",
|
||||
"futures",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-broadcast"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90622698a1218e0b2fb846c97b5f19a0831f6baddee73d9454156365ccfa473b"
|
||||
dependencies = [
|
||||
"easy-parallel",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-channel"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-executor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
"futures-lite",
|
||||
"once_cell",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"futures-lite",
|
||||
"libc",
|
||||
"log",
|
||||
"once_cell",
|
||||
"parking",
|
||||
"polling",
|
||||
"slab",
|
||||
"socket2",
|
||||
"waker-fn",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-lock"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6"
|
||||
dependencies = [
|
||||
"event-listener",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-recursion"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.52"
|
||||
@@ -137,6 +232,24 @@ dependencies = [
|
||||
"system-deps 6.0.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "attohttpc"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e69e13a99a7e6e070bb114f7ff381e58c7ccc188630121fc4c2fe4bcf24cd072"
|
||||
dependencies = [
|
||||
"flate2",
|
||||
"http",
|
||||
"log",
|
||||
"native-tls",
|
||||
"openssl",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"url",
|
||||
"wildmatch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
@@ -398,6 +511,12 @@ dependencies = [
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cache-padded"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c"
|
||||
|
||||
[[package]]
|
||||
name = "cairo-rs"
|
||||
version = "0.15.6"
|
||||
@@ -562,12 +681,22 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3"
|
||||
dependencies = [
|
||||
"cache-padded",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "config"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"handlebars",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
"network-defaults",
|
||||
"serde",
|
||||
"toml",
|
||||
@@ -1034,6 +1163,17 @@ dependencies = [
|
||||
"const-oid 0.7.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derivative"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.17"
|
||||
@@ -1150,6 +1290,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf"
|
||||
|
||||
[[package]]
|
||||
name = "easy-parallel"
|
||||
version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6907e25393cdcc1f4f3f513d9aac1e840eb1cc341a0fccb01171f7d14d10b946"
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.12.4"
|
||||
@@ -1264,6 +1410,27 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b3ab37dc79652c9d85f1f7b6070d77d321d2467f5fe7b00d6b7a86c57b092ae"
|
||||
dependencies = [
|
||||
"enumflags2_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enumflags2_derive"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.7.1"
|
||||
@@ -1277,6 +1444,12 @@ dependencies = [
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71"
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.7"
|
||||
@@ -2513,6 +2686,12 @@ version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||
|
||||
[[package]]
|
||||
name = "minisign-verify"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "933dca44d65cdd53b355d0b73d380a2ff5da71f87f036053188bf1eab6a19881"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.3.7"
|
||||
@@ -2660,6 +2839,19 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodrop"
|
||||
version = "0.1.14"
|
||||
@@ -2758,7 +2950,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym_wallet"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
@@ -2827,6 +3019,17 @@ dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-foundation"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
|
||||
dependencies = [
|
||||
"block",
|
||||
"objc",
|
||||
"objc_id",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc_id"
|
||||
version = "0.1.1"
|
||||
@@ -2897,6 +3100,16 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ordered-stream"
|
||||
version = "0.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44630c059eacfd6e08bdaa51b1db2ce33119caa4ddc1235e923109aa5f25ccb1"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pairing"
|
||||
version = "0.20.0"
|
||||
@@ -3251,6 +3464,25 @@ dependencies = [
|
||||
"miniz_oxide 0.3.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"log",
|
||||
"wepoll-ffi",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7"
|
||||
|
||||
[[package]]
|
||||
name = "polyval"
|
||||
version = "0.5.3"
|
||||
@@ -3728,6 +3960,32 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfd"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2aaf1d71ccd44689f7c2c72da1117fd8db71f72a76fe9b5c5dbb17ab903007e0"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"block",
|
||||
"dispatch",
|
||||
"glib-sys 0.15.6",
|
||||
"gobject-sys 0.15.5",
|
||||
"gtk-sys",
|
||||
"js-sys",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"objc",
|
||||
"objc-foundation",
|
||||
"objc_id",
|
||||
"pollster",
|
||||
"raw-window-handle",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.16.20"
|
||||
@@ -4115,6 +4373,21 @@ dependencies = [
|
||||
"digest 0.10.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
|
||||
dependencies = [
|
||||
"sha1_smol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1_smol"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.9.9"
|
||||
@@ -4497,6 +4770,8 @@ version = "1.0.0-rc.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3571de0bcfd1f4f7599cbb3fe42f28ea9f52c3e89914ff04eaba68b5ee36bb51"
|
||||
dependencies = [
|
||||
"attohttpc",
|
||||
"base64",
|
||||
"bincode",
|
||||
"cfg_aliases",
|
||||
"dirs-next",
|
||||
@@ -4510,12 +4785,14 @@ dependencies = [
|
||||
"gtk",
|
||||
"http",
|
||||
"ignore",
|
||||
"minisign-verify",
|
||||
"once_cell",
|
||||
"open",
|
||||
"percent-encoding",
|
||||
"rand 0.8.5",
|
||||
"raw-window-handle",
|
||||
"regex",
|
||||
"rfd",
|
||||
"semver 1.0.6",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -5422,6 +5699,21 @@ dependencies = [
|
||||
"windows-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wepoll-ffi"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wildmatch"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6c48bd20df7e4ced539c12f570f937c6b4884928a87fee70a479d72f031d4e0"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@@ -5618,6 +5910,67 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7bb86f3d4592e26a48b2719742aec94f8ae6238ebde20d98183ee185d1275e9a"
|
||||
dependencies = [
|
||||
"async-broadcast",
|
||||
"async-channel",
|
||||
"async-executor",
|
||||
"async-io",
|
||||
"async-lock",
|
||||
"async-recursion",
|
||||
"async-task",
|
||||
"async-trait",
|
||||
"byteorder",
|
||||
"derivative",
|
||||
"enumflags2",
|
||||
"event-listener",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"hex",
|
||||
"lazy_static",
|
||||
"nix",
|
||||
"once_cell",
|
||||
"ordered-stream",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"sha1",
|
||||
"static_assertions",
|
||||
"winapi",
|
||||
"zbus_macros",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_macros"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36823cc10fddc3c6b19f048903262dacaf8274170e9a255784bdd8b4570a8040"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.1.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zbus_names"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45dfcdcf87b71dad505d30cc27b1b7b88a64b6d1c435648f48f9dbc1fdc4b7e1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"zvariant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.4.3"
|
||||
@@ -5681,3 +6034,29 @@ dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "3.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49ea5dc38b2058fae6a5b79009388143dadce1e91c26a67f984a0fc0381c8033"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"enumflags2",
|
||||
"libc",
|
||||
"serde",
|
||||
"static_assertions",
|
||||
"zvariant_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant_derive"
|
||||
version = "3.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c2cecc5a61c2a053f7f653a24cd15b3b0195d7f7ddb5042c837fb32e161fb7a"
|
||||
dependencies = [
|
||||
"proc-macro-crate 1.1.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
"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,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym_wallet"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
description = "Nym Native Wallet"
|
||||
authors = ["Nym Technologies SA"]
|
||||
license = ""
|
||||
@@ -34,7 +34,7 @@ reqwest = "0.11.9"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
strum = { version = "0.23", features = ["derive"] }
|
||||
tauri = { version = "=1.0.0-rc.2", features = ["clipboard-all", "shell-open", "window-maximize"] }
|
||||
tauri = { version = "=1.0.0-rc.2", features = ["clipboard-all", "shell-open", "updater", "window-maximize"] }
|
||||
tendermint-rpc = "0.23.0"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.10", features = ["sync", "time"] }
|
||||
|
||||
@@ -1,39 +1,60 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::platform_constants::{CONFIG_DIR_NAME, CONFIG_FILENAME};
|
||||
use crate::{error::BackendError, network::Network as WalletNetwork};
|
||||
use config::defaults::all::Network;
|
||||
use config::defaults::{all::SupportedNetworks, ValidatorDetails};
|
||||
use config::NymConfig;
|
||||
use core::fmt;
|
||||
use itertools::Itertools;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::{fs, io, path::PathBuf};
|
||||
use strum::IntoEnumIterator;
|
||||
use url::Url;
|
||||
|
||||
const REMOTE_SOURCE_OF_VALIDATOR_URLS: &str =
|
||||
pub const REMOTE_SOURCE_OF_VALIDATOR_URLS: &str =
|
||||
"https://nymtech.net/.wellknown/wallet/validators.json";
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct Config {
|
||||
// Base configuration is not part of the configuration file as it's not intended to be changed.
|
||||
#[serde(skip)]
|
||||
base: Base,
|
||||
|
||||
// Network level configuration
|
||||
network: OptionalValidators,
|
||||
// Global configuration file
|
||||
global: Option<GlobalConfig>,
|
||||
|
||||
// One configuration file per network
|
||||
networks: HashMap<String, NetworkConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Base {
|
||||
/// Information on all the networks that the wallet connects to.
|
||||
networks: SupportedNetworks,
|
||||
}
|
||||
|
||||
/// Validators that have been fetched dynamically, probably during startup.
|
||||
fetched_validators: OptionalValidators,
|
||||
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct GlobalConfig {
|
||||
// TODO: there are no global settings (yet)
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
pub struct NetworkConfig {
|
||||
// User selected urls
|
||||
selected_nymd_url: Option<Url>,
|
||||
selected_api_url: Option<Url>,
|
||||
|
||||
// Additional user provided validators
|
||||
validator_urls: Option<Vec<ValidatorUrl>>,
|
||||
}
|
||||
|
||||
impl NetworkConfig {
|
||||
fn validators(&self) -> impl Iterator<Item = &ValidatorUrl> {
|
||||
self.validator_urls.iter().flat_map(|v| v.iter())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Base {
|
||||
@@ -41,91 +62,125 @@ impl Default for Base {
|
||||
let networks = WalletNetwork::iter().map(Into::into).collect();
|
||||
Base {
|
||||
networks: SupportedNetworks::new(networks),
|
||||
fetched_validators: OptionalValidators::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NymConfig for Config {
|
||||
fn template() -> &'static str {
|
||||
// For now we're not using a template
|
||||
unimplemented!();
|
||||
impl Config {
|
||||
fn root_directory() -> PathBuf {
|
||||
tauri::api::path::config_dir().expect("Failed to get config directory")
|
||||
}
|
||||
|
||||
fn default_root_directory() -> PathBuf {
|
||||
dirs::home_dir()
|
||||
.expect("Failed to evaluate $HOME value")
|
||||
.join(".nym")
|
||||
.join("wallet")
|
||||
fn config_directory() -> PathBuf {
|
||||
Self::root_directory().join(CONFIG_DIR_NAME)
|
||||
}
|
||||
|
||||
fn root_directory(&self) -> PathBuf {
|
||||
Self::default_root_directory()
|
||||
fn config_file_path(network: Option<WalletNetwork>) -> PathBuf {
|
||||
if let Some(network) = network {
|
||||
let network_filename = format!("{}.toml", network.as_key());
|
||||
Self::config_directory().join(network_filename)
|
||||
} else {
|
||||
Self::config_directory().join(CONFIG_FILENAME)
|
||||
}
|
||||
}
|
||||
|
||||
fn config_directory(&self) -> PathBuf {
|
||||
self.root_directory().join("config")
|
||||
}
|
||||
|
||||
fn data_directory(&self) -> PathBuf {
|
||||
self.root_directory().join("data")
|
||||
}
|
||||
|
||||
fn save_to_file(&self, custom_location: Option<PathBuf>) -> io::Result<()> {
|
||||
let config_toml = toml::to_string_pretty(&self)
|
||||
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))?;
|
||||
pub fn save_to_files(&self) -> io::Result<()> {
|
||||
log::trace!("Config::save_to_file");
|
||||
|
||||
// Make sure the whole directory structure actually exists
|
||||
match custom_location.clone() {
|
||||
Some(loc) => {
|
||||
if let Some(parent_dir) = loc.parent() {
|
||||
fs::create_dir_all(parent_dir)
|
||||
} else {
|
||||
Ok(())
|
||||
fs::create_dir_all(Self::config_directory())?;
|
||||
|
||||
// Global config
|
||||
if let Some(global) = &self.global {
|
||||
let location = Self::config_file_path(None);
|
||||
|
||||
match toml::to_string_pretty(&global)
|
||||
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
|
||||
.map(|toml| fs::write(location.clone(), toml))
|
||||
{
|
||||
Ok(_) => log::debug!("Writing to: {:#?}", location),
|
||||
Err(err) => log::warn!("Failed to write to {:#?}: {err}", location),
|
||||
}
|
||||
}
|
||||
|
||||
// One file per network
|
||||
for (network, config) in &self.networks {
|
||||
let network = match Network::from_str(network).map(Into::into) {
|
||||
Ok(network) => network,
|
||||
Err(err) => {
|
||||
log::warn!("Unexpected name for network configuration, not saving: {err}");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let location = Self::config_file_path(Some(network));
|
||||
match toml::to_string_pretty(config)
|
||||
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
|
||||
.map(|toml| fs::write(location.clone(), toml))
|
||||
{
|
||||
Ok(_) => log::debug!("Writing to: {:#?}", location),
|
||||
Err(err) => log::warn!("Failed to write to {:#?}: {err}", location),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn load_from_files() -> Self {
|
||||
// Global
|
||||
let global = {
|
||||
let file = Self::config_file_path(None);
|
||||
match load_from_file::<GlobalConfig>(file.clone()) {
|
||||
Ok(global) => {
|
||||
log::debug!("Loaded from file {:#?}", file);
|
||||
Some(global)
|
||||
}
|
||||
Err(err) => {
|
||||
log::trace!("Not loading {:#?}: {}", file, err);
|
||||
None
|
||||
}
|
||||
}
|
||||
None => fs::create_dir_all(self.config_directory()),
|
||||
}?;
|
||||
};
|
||||
|
||||
fs::write(
|
||||
custom_location.unwrap_or_else(|| self.config_directory().join(Self::config_file_name())),
|
||||
config_toml,
|
||||
)
|
||||
// One file per network
|
||||
let mut networks = HashMap::new();
|
||||
for network in WalletNetwork::iter() {
|
||||
let file = Self::config_file_path(Some(network));
|
||||
match load_from_file::<NetworkConfig>(file.clone()) {
|
||||
Ok(config) => {
|
||||
log::trace!("Loaded from file {:#?}", file);
|
||||
networks.insert(network.as_key(), config);
|
||||
}
|
||||
Err(err) => log::trace!("Not loading {:#?}: {}", file, err),
|
||||
};
|
||||
}
|
||||
|
||||
Self {
|
||||
base: Base::default(),
|
||||
global,
|
||||
networks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Get the available validators in the order
|
||||
/// 1. from the configuration file
|
||||
/// 2. provided remotely
|
||||
/// 3. hardcoded fallback
|
||||
pub fn get_validators(&self, network: WalletNetwork) -> impl Iterator<Item = ValidatorUrl> + '_ {
|
||||
// The base validators are (currently) stored as strings
|
||||
let base_validators = self.base.networks.validators(network.into()).map(|v| {
|
||||
pub fn get_base_validators(
|
||||
&self,
|
||||
network: WalletNetwork,
|
||||
) -> impl Iterator<Item = ValidatorUrl> + '_ {
|
||||
self.base.networks.validators(network.into()).map(|v| {
|
||||
v.clone()
|
||||
.try_into()
|
||||
.expect("The hardcoded validators are assumed to be valid urls")
|
||||
});
|
||||
|
||||
self
|
||||
.base
|
||||
.fetched_validators
|
||||
.validators(network)
|
||||
.chain(self.network.validators(network))
|
||||
.cloned()
|
||||
.chain(base_validators)
|
||||
.unique()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_nymd_urls(&self, network: WalletNetwork) -> impl Iterator<Item = Url> + '_ {
|
||||
self.get_validators(network).into_iter().map(|v| v.nymd_url)
|
||||
}
|
||||
|
||||
pub fn get_api_urls(&self, network: WalletNetwork) -> impl Iterator<Item = Url> + '_ {
|
||||
pub fn get_configured_validators(
|
||||
&self,
|
||||
network: WalletNetwork,
|
||||
) -> impl Iterator<Item = ValidatorUrl> + '_ {
|
||||
self
|
||||
.get_validators(network)
|
||||
.networks
|
||||
.get(&network.as_key())
|
||||
.into_iter()
|
||||
.filter_map(|v| v.api_url)
|
||||
.flat_map(|c| c.validators().cloned())
|
||||
}
|
||||
|
||||
pub fn get_mixnet_contract_address(&self, network: WalletNetwork) -> Option<cosmrs::AccountId> {
|
||||
@@ -161,25 +216,80 @@ impl Config {
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn fetch_updated_validator_urls(&mut self) -> Result<(), BackendError> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(3))
|
||||
.build()?;
|
||||
log::debug!(
|
||||
"Fetching validator urls from: {}",
|
||||
REMOTE_SOURCE_OF_VALIDATOR_URLS
|
||||
);
|
||||
let response = client
|
||||
.get(REMOTE_SOURCE_OF_VALIDATOR_URLS.to_string())
|
||||
.send()
|
||||
.await?;
|
||||
self.base.fetched_validators = serde_json::from_str(&response.text().await?)?;
|
||||
log::debug!(
|
||||
"Received validator urls: \n{}",
|
||||
self.base.fetched_validators
|
||||
);
|
||||
Ok(())
|
||||
pub fn select_validator_nymd_url(&mut self, nymd_url: Url, network: WalletNetwork) {
|
||||
if let Some(net) = self.networks.get_mut(&network.as_key()) {
|
||||
net.selected_nymd_url = Some(nymd_url);
|
||||
} else {
|
||||
self.networks.insert(
|
||||
network.as_key(),
|
||||
NetworkConfig {
|
||||
selected_nymd_url: Some(nymd_url),
|
||||
..NetworkConfig::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_validator_api_url(&mut self, api_url: Url, network: WalletNetwork) {
|
||||
if let Some(net) = self.networks.get_mut(&network.as_key()) {
|
||||
net.selected_api_url = Some(api_url);
|
||||
} else {
|
||||
self.networks.insert(
|
||||
network.as_key(),
|
||||
NetworkConfig {
|
||||
selected_nymd_url: Some(api_url),
|
||||
..NetworkConfig::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_selected_validator_nymd_url(&self, network: &WalletNetwork) -> Option<Url> {
|
||||
self
|
||||
.networks
|
||||
.get(&network.as_key())
|
||||
.and_then(|config| config.selected_nymd_url.clone())
|
||||
}
|
||||
|
||||
pub fn get_selected_validator_api_url(&self, network: &WalletNetwork) -> Option<Url> {
|
||||
self
|
||||
.networks
|
||||
.get(&network.as_key())
|
||||
.and_then(|config| config.selected_api_url.clone())
|
||||
}
|
||||
|
||||
pub fn add_validator_url(&mut self, url: ValidatorUrl, network: WalletNetwork) {
|
||||
if let Some(net) = self.networks.get_mut(&network.as_key()) {
|
||||
if let Some(ref mut urls) = net.validator_urls {
|
||||
urls.push(url);
|
||||
} else {
|
||||
net.validator_urls = Some(vec![url]);
|
||||
}
|
||||
} else {
|
||||
self.networks.insert(
|
||||
network.as_key(),
|
||||
NetworkConfig {
|
||||
validator_urls: Some(vec![url]),
|
||||
..NetworkConfig::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn remove_validator_url(&mut self, _url: ValidatorUrl, _network: WalletNetwork) {
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
|
||||
fn load_from_file<T>(file: PathBuf) -> Result<T, io::Error>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
fs::read_to_string(file).and_then(|contents| {
|
||||
toml::from_str::<T>(&contents)
|
||||
.map_err(|toml_err| io::Error::new(io::ErrorKind::Other, toml_err))
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
@@ -215,7 +325,7 @@ impl fmt::Display for ValidatorUrl {
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct OptionalValidators {
|
||||
pub struct OptionalValidators {
|
||||
// User supplied additional validator urls in addition to the hardcoded ones.
|
||||
// These are separate fields, rather than a map, to force the serialization order.
|
||||
mainnet: Option<Vec<ValidatorUrl>>,
|
||||
@@ -224,7 +334,7 @@ struct OptionalValidators {
|
||||
}
|
||||
|
||||
impl OptionalValidators {
|
||||
fn validators(&self, network: WalletNetwork) -> impl Iterator<Item = &ValidatorUrl> {
|
||||
pub fn validators(&self, network: WalletNetwork) -> impl Iterator<Item = &ValidatorUrl> {
|
||||
match network {
|
||||
WalletNetwork::MAINNET => self.mainnet.as_ref(),
|
||||
WalletNetwork::SANDBOX => self.sandbox.as_ref(),
|
||||
@@ -261,53 +371,63 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_config() -> Config {
|
||||
Config {
|
||||
base: Base::default(),
|
||||
network: OptionalValidators {
|
||||
mainnet: Some(vec![
|
||||
ValidatorDetails {
|
||||
nymd_url: "https://foo".to_string(),
|
||||
api_url: None,
|
||||
}
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
ValidatorUrl {
|
||||
nymd_url: "https://baz".parse().unwrap(),
|
||||
api_url: Some("https://baz/api".parse().unwrap()),
|
||||
},
|
||||
]),
|
||||
sandbox: Some(vec![ValidatorUrl {
|
||||
let netconfig = NetworkConfig {
|
||||
selected_nymd_url: None,
|
||||
selected_api_url: Some("https://my_api_url.com".parse().unwrap()),
|
||||
|
||||
validator_urls: Some(vec![
|
||||
ValidatorUrl {
|
||||
nymd_url: "https://foo".parse().unwrap(),
|
||||
api_url: None,
|
||||
},
|
||||
ValidatorUrl {
|
||||
nymd_url: "https://bar".parse().unwrap(),
|
||||
api_url: Some("https://bar/api".parse().unwrap()),
|
||||
}]),
|
||||
qa: None,
|
||||
},
|
||||
},
|
||||
ValidatorUrl {
|
||||
nymd_url: "https://baz".parse().unwrap(),
|
||||
api_url: Some("https://baz/api".parse().unwrap()),
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
Config {
|
||||
base: Base::default(),
|
||||
global: Some(GlobalConfig::default()),
|
||||
networks: [(WalletNetwork::MAINNET.as_key(), netconfig)]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_to_toml() {
|
||||
let config = test_config();
|
||||
let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()];
|
||||
assert_eq!(
|
||||
toml::to_string_pretty(&test_config()).unwrap(),
|
||||
r#"[[network.mainnet]]
|
||||
toml::to_string_pretty(netconfig).unwrap(),
|
||||
r#"selected_api_url = 'https://my_api_url.com/'
|
||||
|
||||
[[validator_urls]]
|
||||
nymd_url = 'https://foo/'
|
||||
|
||||
[[network.mainnet]]
|
||||
nymd_url = 'https://baz/'
|
||||
api_url = 'https://baz/api'
|
||||
|
||||
[[network.sandbox]]
|
||||
[[validator_urls]]
|
||||
nymd_url = 'https://bar/'
|
||||
api_url = 'https://bar/api'
|
||||
|
||||
[[validator_urls]]
|
||||
nymd_url = 'https://baz/'
|
||||
api_url = 'https://baz/api'
|
||||
"#
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn serialize_and_deserialize_to_toml() {
|
||||
let config = test_config();
|
||||
let config_str = toml::to_string_pretty(&config).unwrap();
|
||||
let config_from_toml = toml::from_str(&config_str).unwrap();
|
||||
assert_eq!(config, config_from_toml);
|
||||
let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()];
|
||||
let config_str = toml::to_string_pretty(netconfig).unwrap();
|
||||
let config_from_toml: NetworkConfig = toml::from_str(&config_str).unwrap();
|
||||
assert_eq!(netconfig, &config_from_toml);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -315,7 +435,7 @@ api_url = 'https://bar/api'
|
||||
let config = test_config();
|
||||
|
||||
let nymd_url = config
|
||||
.get_validators(WalletNetwork::MAINNET)
|
||||
.get_configured_validators(WalletNetwork::MAINNET)
|
||||
.next()
|
||||
.map(|v| v.nymd_url)
|
||||
.unwrap();
|
||||
@@ -323,7 +443,7 @@ api_url = 'https://bar/api'
|
||||
|
||||
// The first entry is missing an API URL
|
||||
let api_url = config
|
||||
.get_validators(WalletNetwork::MAINNET)
|
||||
.get_configured_validators(WalletNetwork::MAINNET)
|
||||
.next()
|
||||
.and_then(|v| v.api_url);
|
||||
assert_eq!(api_url, None);
|
||||
@@ -334,14 +454,14 @@ api_url = 'https://bar/api'
|
||||
let config = Config::default();
|
||||
|
||||
let nymd_url = config
|
||||
.get_validators(WalletNetwork::MAINNET)
|
||||
.get_base_validators(WalletNetwork::MAINNET)
|
||||
.next()
|
||||
.map(|v| v.nymd_url)
|
||||
.unwrap();
|
||||
assert_eq!(nymd_url.as_ref(), "https://rpc.nyx.nodes.guru/");
|
||||
|
||||
let api_url = config
|
||||
.get_validators(WalletNetwork::MAINNET)
|
||||
.get_base_validators(WalletNetwork::MAINNET)
|
||||
.next()
|
||||
.and_then(|v| v.api_url)
|
||||
.unwrap();
|
||||
|
||||
@@ -18,8 +18,6 @@ mod operations;
|
||||
mod platform_constants;
|
||||
mod state;
|
||||
mod utils;
|
||||
// temporarily until it is actually used
|
||||
#[allow(unused)]
|
||||
mod wallet_storage;
|
||||
|
||||
use crate::menu::AddDefaultSubmenus;
|
||||
@@ -37,6 +35,7 @@ fn main() {
|
||||
.manage(Arc::new(RwLock::new(State::default())))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
mixnet::account::connect_with_mnemonic,
|
||||
mixnet::account::validate_mnemonic,
|
||||
mixnet::account::create_new_account,
|
||||
mixnet::account::create_new_mnemonic,
|
||||
mixnet::account::create_password,
|
||||
@@ -45,9 +44,11 @@ fn main() {
|
||||
mixnet::account::get_validator_nymd_urls,
|
||||
mixnet::account::get_validator_api_urls,
|
||||
mixnet::account::logout,
|
||||
mixnet::account::remove_password,
|
||||
mixnet::account::sign_in_with_password,
|
||||
mixnet::account::switch_network,
|
||||
mixnet::account::update_validator_urls,
|
||||
mixnet::account::validate_mnemonic,
|
||||
mixnet::admin::get_contract_settings,
|
||||
mixnet::admin::update_contract_settings,
|
||||
mixnet::bond::bond_gateway,
|
||||
|
||||
@@ -21,6 +21,10 @@ pub enum Network {
|
||||
}
|
||||
|
||||
impl Network {
|
||||
pub fn as_key(&self) -> String {
|
||||
self.to_string().to_lowercase()
|
||||
}
|
||||
|
||||
pub fn denom(&self) -> Denom {
|
||||
match self {
|
||||
// network defaults should be correctly formatted
|
||||
|
||||
@@ -21,9 +21,7 @@ use strum::IntoEnumIterator;
|
||||
use tokio::sync::RwLock;
|
||||
use url::Url;
|
||||
|
||||
use validator_client::{
|
||||
connection_tester::run_validator_connection_test, nymd::SigningNymdClient, Client,
|
||||
};
|
||||
use validator_client::{nymd::SigningNymdClient, Client};
|
||||
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/account.ts"))]
|
||||
@@ -120,6 +118,11 @@ pub async fn create_new_mnemonic() -> Result<String, BackendError> {
|
||||
Ok(rand_mnemonic.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn validate_mnemonic(mnemonic: &str) -> bool {
|
||||
Mnemonic::from_str(mnemonic).is_ok()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn switch_network(
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
@@ -169,8 +172,8 @@ pub async fn get_validator_nymd_urls(
|
||||
network: WalletNetwork,
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<ValidatorUrls, BackendError> {
|
||||
let config = state.read().await.config();
|
||||
let urls: Vec<String> = config
|
||||
let state = state.read().await;
|
||||
let urls: Vec<String> = state
|
||||
.get_nymd_urls(network)
|
||||
.map(|url| url.to_string())
|
||||
.collect();
|
||||
@@ -182,8 +185,8 @@ pub async fn get_validator_api_urls(
|
||||
network: WalletNetwork,
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<ValidatorUrls, BackendError> {
|
||||
let config = state.read().await.config();
|
||||
let urls: Vec<String> = config
|
||||
let state = state.read().await;
|
||||
let urls: Vec<String> = state
|
||||
.get_api_urls(network)
|
||||
.map(|url| url.to_string())
|
||||
.collect();
|
||||
@@ -194,36 +197,54 @@ async fn _connect_with_mnemonic(
|
||||
mnemonic: Mnemonic,
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<Account, BackendError> {
|
||||
update_validator_urls(state.clone()).await?;
|
||||
let config = state.read().await.config();
|
||||
|
||||
for network in WalletNetwork::iter() {
|
||||
log::debug!(
|
||||
"List of validators for {network}: [\n{}\n]",
|
||||
config.get_validators(network).format(",\n")
|
||||
);
|
||||
{
|
||||
let mut w_state = state.write().await;
|
||||
w_state.load_config_files();
|
||||
}
|
||||
|
||||
// Run connection tests on all nymd and validator-api endpoints
|
||||
let (nymd_urls, api_urls) = {
|
||||
let mixnet_contract_address = WalletNetwork::iter()
|
||||
.map(|network| (network.into(), config.get_mixnet_contract_address(network)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let nymd_urls = WalletNetwork::iter().flat_map(|network| {
|
||||
config
|
||||
.get_nymd_urls(network)
|
||||
.map(move |url| (network.into(), url))
|
||||
});
|
||||
let api_urls = WalletNetwork::iter().flat_map(|network| {
|
||||
config
|
||||
.get_api_urls(network)
|
||||
.map(move |url| (network.into(), url))
|
||||
});
|
||||
update_validator_urls(state.clone()).await?;
|
||||
|
||||
run_validator_connection_test(nymd_urls, api_urls, mixnet_contract_address).await
|
||||
let config = {
|
||||
let state = state.read().await;
|
||||
|
||||
// Take the oppertunity to list all the known validators while we have the state.
|
||||
for network in WalletNetwork::iter() {
|
||||
log::debug!(
|
||||
"List of validators for {network}: [\n{}\n]",
|
||||
state.get_validators(network).format(",\n")
|
||||
);
|
||||
}
|
||||
|
||||
state.config().clone()
|
||||
};
|
||||
|
||||
let clients = create_clients(&nymd_urls, &api_urls, &mnemonic, &config)?;
|
||||
// Get all the urls needed for the connection test
|
||||
let (untested_nymd_urls, untested_api_urls) = {
|
||||
let state = state.read().await;
|
||||
(state.get_all_nymd_urls(), state.get_all_api_urls())
|
||||
};
|
||||
let default_nymd_urls: HashMap<WalletNetwork, Url> = untested_nymd_urls
|
||||
.iter()
|
||||
.map(|(network, urls)| (*network, urls.iter().next().unwrap().clone()))
|
||||
.collect();
|
||||
let default_api_urls: HashMap<WalletNetwork, Url> = untested_api_urls
|
||||
.iter()
|
||||
.map(|(network, urls)| (*network, urls.iter().next().unwrap().clone()))
|
||||
.collect();
|
||||
|
||||
// Run connection tests on all nymd and validator-api endpoints
|
||||
let (nymd_urls, api_urls) =
|
||||
run_connection_test(untested_nymd_urls, untested_api_urls, &config).await;
|
||||
|
||||
// Create clients for all networks
|
||||
let clients = create_clients(
|
||||
&nymd_urls,
|
||||
&api_urls,
|
||||
&default_nymd_urls,
|
||||
&default_api_urls,
|
||||
&config,
|
||||
&mnemonic,
|
||||
)?;
|
||||
|
||||
// Set the default account
|
||||
let default_network: WalletNetwork = config::defaults::DEFAULT_NETWORK.into();
|
||||
@@ -251,63 +272,69 @@ async fn _connect_with_mnemonic(
|
||||
account_for_default_network
|
||||
}
|
||||
|
||||
fn select_random_responding_nymd_url(
|
||||
nymd_urls: &HashMap<Network, Vec<(Url, bool)>>,
|
||||
network: WalletNetwork,
|
||||
async fn run_connection_test(
|
||||
untested_nymd_urls: HashMap<WalletNetwork, Vec<Url>>,
|
||||
untested_api_urls: HashMap<WalletNetwork, Vec<Url>>,
|
||||
config: &Config,
|
||||
) -> Url {
|
||||
// We pick a randon responding nymd url, and if not, fall back on the first one in the list.
|
||||
nymd_urls
|
||||
.get(&network.into())
|
||||
.and_then(|urls| {
|
||||
let nymd_urls: Vec<_> = urls
|
||||
.iter()
|
||||
.filter_map(|(url, result)| if *result { Some(url.clone()) } else { None })
|
||||
.collect();
|
||||
nymd_urls.choose(&mut rand::thread_rng()).cloned()
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::debug!("No passing nymd_urls for {network}: using default");
|
||||
config
|
||||
.get_nymd_urls(network)
|
||||
.next()
|
||||
.expect("Expected at least one hardcoded nymd url")
|
||||
})
|
||||
}
|
||||
) -> (
|
||||
HashMap<Network, Vec<(Url, bool)>>,
|
||||
HashMap<Network, Vec<(Url, bool)>>,
|
||||
) {
|
||||
let mixnet_contract_address = WalletNetwork::iter()
|
||||
.map(|network| (network.into(), config.get_mixnet_contract_address(network)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
fn select_first_responding_api_url(
|
||||
api_urls: &HashMap<Network, Vec<(Url, bool)>>,
|
||||
network: WalletNetwork,
|
||||
config: &Config,
|
||||
) -> Url {
|
||||
// We pick the first API url among the responding ones. If none exists, fall back on the first
|
||||
// one in the list.
|
||||
api_urls
|
||||
.get(&network.into())
|
||||
.and_then(|urls| {
|
||||
urls
|
||||
.iter()
|
||||
.find_map(|(url, result)| if *result { Some(url.clone()) } else { None })
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
log::debug!("No passing api_urls for {network}: using default");
|
||||
config
|
||||
.get_api_urls(network)
|
||||
.next()
|
||||
.expect("Expected at least one hardcoded api url")
|
||||
})
|
||||
let untested_nymd_urls = untested_nymd_urls
|
||||
.into_iter()
|
||||
.flat_map(|(net, urls)| urls.into_iter().map(move |url| (net.into(), url)));
|
||||
|
||||
let untested_api_urls = untested_api_urls
|
||||
.into_iter()
|
||||
.flat_map(|(net, urls)| urls.into_iter().map(move |url| (net.into(), url)));
|
||||
|
||||
validator_client::connection_tester::run_validator_connection_test(
|
||||
untested_nymd_urls,
|
||||
untested_api_urls,
|
||||
mixnet_contract_address,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn create_clients(
|
||||
nymd_urls: &HashMap<Network, Vec<(Url, bool)>>,
|
||||
api_urls: &HashMap<Network, Vec<(Url, bool)>>,
|
||||
mnemonic: &Mnemonic,
|
||||
default_nymd_urls: &HashMap<WalletNetwork, Url>,
|
||||
default_api_urls: &HashMap<WalletNetwork, Url>,
|
||||
config: &Config,
|
||||
mnemonic: &Mnemonic,
|
||||
) -> Result<Vec<Client<SigningNymdClient>>, BackendError> {
|
||||
let mut clients = Vec::new();
|
||||
for network in WalletNetwork::iter() {
|
||||
let nymd_url = select_random_responding_nymd_url(nymd_urls, network, config);
|
||||
let api_url = select_first_responding_api_url(api_urls, network, config);
|
||||
let nymd_url = if let Some(url) = config.get_selected_validator_nymd_url(&network) {
|
||||
log::debug!("Using selected nymd_url for {network}: {url}");
|
||||
url.clone()
|
||||
} else {
|
||||
let default_nymd_url = default_nymd_urls
|
||||
.get(&network)
|
||||
.expect("Expected at least one nymd_url");
|
||||
select_random_responding_url(nymd_urls, network).unwrap_or_else(|| {
|
||||
log::debug!("No successful nymd_urls for {network}: using default: {default_nymd_url}");
|
||||
default_nymd_url.clone()
|
||||
})
|
||||
};
|
||||
|
||||
let api_url = if let Some(url) = config.get_selected_validator_api_url(&network) {
|
||||
log::debug!("Using selected api_url for {network}: {url}");
|
||||
url.clone()
|
||||
} else {
|
||||
let default_api_url = default_api_urls
|
||||
.get(&network)
|
||||
.expect("Expected at least one api url");
|
||||
select_first_responding_url(api_urls, network).unwrap_or_else(|| {
|
||||
log::debug!("No passing api_urls for {network}: using default: {default_api_url}");
|
||||
default_api_url.clone()
|
||||
})
|
||||
};
|
||||
|
||||
log::info!("Connecting to: nymd_url: {nymd_url} for {network}");
|
||||
log::info!("Connecting to: api_url: {api_url} for {network}");
|
||||
@@ -328,6 +355,31 @@ fn create_clients(
|
||||
Ok(clients)
|
||||
}
|
||||
|
||||
fn select_random_responding_url(
|
||||
urls: &HashMap<Network, Vec<(Url, bool)>>,
|
||||
network: WalletNetwork,
|
||||
) -> Option<Url> {
|
||||
urls.get(&network.into()).and_then(|urls| {
|
||||
let urls: Vec<_> = urls
|
||||
.iter()
|
||||
.filter_map(|(url, result)| if *result { Some(url.clone()) } else { None })
|
||||
.collect();
|
||||
urls.choose(&mut rand::thread_rng()).cloned()
|
||||
})
|
||||
}
|
||||
|
||||
fn select_first_responding_url(
|
||||
urls: &HashMap<Network, Vec<(Url, bool)>>,
|
||||
network: WalletNetwork,
|
||||
//config: &Config,
|
||||
) -> Option<Url> {
|
||||
urls.get(&network.into()).and_then(|urls| {
|
||||
urls
|
||||
.iter()
|
||||
.find_map(|(url, result)| if *result { Some(url.clone()) } else { None })
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn does_password_file_exist() -> Result<bool, BackendError> {
|
||||
log::info!("Checking wallet file");
|
||||
@@ -369,3 +421,10 @@ pub async fn sign_in_with_password(
|
||||
let stored_account = wallet_storage::load_existing_wallet_login_information(&id, &password)?;
|
||||
_connect_with_mnemonic(stored_account.mnemonic().clone(), state).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_password() -> Result<(), BackendError> {
|
||||
log::info!("Removing password");
|
||||
let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
wallet_storage::remove_wallet_login_information(&id)
|
||||
}
|
||||
|
||||
@@ -6,16 +6,24 @@
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(target_os = "linux")] {
|
||||
pub const CONFIG_DIR_NAME: &str = "nym-wallet";
|
||||
pub const CONFIG_FILENAME: &str = "config.toml";
|
||||
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 CONFIG_DIR_NAME: &str = "nym-wallet";
|
||||
pub const CONFIG_FILENAME: &str = "config.toml";
|
||||
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 CONFIG_DIR_NAME: &str = "NymWallet";
|
||||
pub const CONFIG_FILENAME: &str = "Config.toml";
|
||||
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 CONFIG_DIR_NAME: &str = "nym-wallet";
|
||||
pub const CONFIG_FILENAME: &str = "config.toml";
|
||||
pub const STORAGE_DIR_NAME: &str = "nym-wallet";
|
||||
pub const WALLET_INFO_FILENAME: &str = "saved-wallet.json";
|
||||
}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
use crate::config::Config;
|
||||
use crate::config::{Config, OptionalValidators, ValidatorUrl};
|
||||
use crate::error::BackendError;
|
||||
use crate::network::Network;
|
||||
|
||||
use strum::IntoEnumIterator;
|
||||
use validator_client::nymd::SigningNymdClient;
|
||||
use validator_client::Client;
|
||||
|
||||
use itertools::Itertools;
|
||||
use url::Url;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
config: Config,
|
||||
signing_clients: HashMap<Network, Client<SigningNymdClient>>,
|
||||
current_network: Network,
|
||||
|
||||
/// Validators that have been fetched dynamically, probably during startup.
|
||||
fetched_validators: OptionalValidators,
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -28,8 +37,18 @@ impl State {
|
||||
.ok_or(BackendError::ClientNotInitialized)
|
||||
}
|
||||
|
||||
pub fn config(&self) -> Config {
|
||||
self.config.clone()
|
||||
pub fn config(&self) -> &Config {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Load configuration from files. If unsuccessful we just log it and move on.
|
||||
pub fn load_config_files(&mut self) {
|
||||
self.config = Config::load_from_files();
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn save_config_files(&self) -> Result<(), BackendError> {
|
||||
Ok(self.config.save_to_files()?)
|
||||
}
|
||||
|
||||
pub fn add_client(&mut self, network: Network, client: Client<SigningNymdClient>) {
|
||||
@@ -48,8 +67,91 @@ impl State {
|
||||
self.signing_clients = HashMap::new();
|
||||
}
|
||||
|
||||
/// Get the available validators in the order
|
||||
/// 1. from the configuration file
|
||||
/// 2. provided remotely
|
||||
/// 3. hardcoded fallback
|
||||
pub fn get_validators(&self, network: Network) -> impl Iterator<Item = ValidatorUrl> + '_ {
|
||||
let validators_in_config = self.config.get_configured_validators(network);
|
||||
let fetched_validators = self.fetched_validators.validators(network).cloned();
|
||||
let default_validators = self.config.get_base_validators(network);
|
||||
|
||||
validators_in_config
|
||||
.chain(fetched_validators)
|
||||
.chain(default_validators)
|
||||
.unique()
|
||||
}
|
||||
|
||||
pub fn get_nymd_urls(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
|
||||
self.get_validators(network).into_iter().map(|v| v.nymd_url)
|
||||
}
|
||||
|
||||
pub fn get_api_urls(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
|
||||
self
|
||||
.get_validators(network)
|
||||
.into_iter()
|
||||
.filter_map(|v| v.api_url)
|
||||
}
|
||||
|
||||
pub fn get_all_nymd_urls(&self) -> HashMap<Network, Vec<Url>> {
|
||||
Network::iter()
|
||||
.flat_map(|network| self.get_nymd_urls(network).map(move |url| (network, url)))
|
||||
.into_group_map()
|
||||
}
|
||||
|
||||
pub fn get_all_api_urls(&self) -> HashMap<Network, Vec<Url>> {
|
||||
Network::iter()
|
||||
.flat_map(|network| self.get_api_urls(network).map(move |url| (network, url)))
|
||||
.into_group_map()
|
||||
}
|
||||
|
||||
/// Fetch validator urls remotely. These are used to in addition to the base ones, and the user
|
||||
/// configured ones.
|
||||
pub async fn fetch_updated_validator_urls(&mut self) -> Result<(), BackendError> {
|
||||
self.config.fetch_updated_validator_urls().await
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(3))
|
||||
.build()?;
|
||||
log::debug!(
|
||||
"Fetching validator urls from: {}",
|
||||
crate::config::REMOTE_SOURCE_OF_VALIDATOR_URLS
|
||||
);
|
||||
let response = client
|
||||
.get(crate::config::REMOTE_SOURCE_OF_VALIDATOR_URLS.to_string())
|
||||
.send()
|
||||
.await?;
|
||||
self.fetched_validators = serde_json::from_str(&response.text().await?)?;
|
||||
log::debug!("Received validator urls: \n{}", self.fetched_validators);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn select_validator_nymd_url(
|
||||
&mut self,
|
||||
url: &str,
|
||||
network: Network,
|
||||
) -> Result<(), BackendError> {
|
||||
self.config.select_validator_nymd_url(url.parse()?, network);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn select_validator_api_url(
|
||||
&mut self,
|
||||
url: &str,
|
||||
network: Network,
|
||||
) -> Result<(), BackendError> {
|
||||
self.config.select_validator_api_url(url.parse()?, network);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn add_validator_url(&mut self, url: ValidatorUrl, network: Network) {
|
||||
self.config.add_validator_url(url, network);
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn remove_validator_url(&mut self, url: ValidatorUrl, network: Network) {
|
||||
self.config.remove_validator_url(url, network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,3 +175,68 @@ macro_rules! api_client {
|
||||
$state.read().await.current_client()?.validator_api
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn adding_validators_urls_prepends() {
|
||||
let mut state = State::default();
|
||||
let _api_urls = state.get_api_urls(Network::MAINNET).collect::<Vec<_>>();
|
||||
|
||||
state.add_validator_url(
|
||||
ValidatorUrl {
|
||||
nymd_url: "http://nymd_url.com".parse().unwrap(),
|
||||
api_url: Some("http://nymd_url.com/api".parse().unwrap()),
|
||||
},
|
||||
Network::MAINNET,
|
||||
);
|
||||
|
||||
state.add_validator_url(
|
||||
ValidatorUrl {
|
||||
nymd_url: "http://foo.com".parse().unwrap(),
|
||||
api_url: None,
|
||||
},
|
||||
Network::MAINNET,
|
||||
);
|
||||
|
||||
state.add_validator_url(
|
||||
ValidatorUrl {
|
||||
nymd_url: "http://bar.com".parse().unwrap(),
|
||||
api_url: None,
|
||||
},
|
||||
Network::MAINNET,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
state.get_nymd_urls(Network::MAINNET).collect::<Vec<_>>(),
|
||||
vec![
|
||||
"http://nymd_url.com/".parse().unwrap(),
|
||||
"http://foo.com".parse().unwrap(),
|
||||
"http://bar.com".parse().unwrap(),
|
||||
"https://rpc.nyx.nodes.guru".parse().unwrap(),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
state.get_api_urls(Network::MAINNET).collect::<Vec<_>>(),
|
||||
vec![
|
||||
"http://nymd_url.com/api".parse().unwrap(),
|
||||
"https://api.nyx.nodes.guru".parse().unwrap(),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
state
|
||||
.get_all_nymd_urls()
|
||||
.get(&Network::MAINNET)
|
||||
.unwrap()
|
||||
.clone(),
|
||||
vec![
|
||||
"http://nymd_url.com/".parse().unwrap(),
|
||||
"http://foo.com".parse().unwrap(),
|
||||
"http://bar.com".parse().unwrap(),
|
||||
"https://rpc.nyx.nodes.guru".parse().unwrap(),
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmrs::bip32::DerivationPath;
|
||||
use serde::de::Visitor;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt::Formatter;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zeroize::Zeroize;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::error::BackendError;
|
||||
|
||||
@@ -23,6 +20,7 @@ pub(crate) struct StoredWallet {
|
||||
}
|
||||
|
||||
impl StoredWallet {
|
||||
#[allow(unused)]
|
||||
pub fn version(&self) -> u32 {
|
||||
self.version
|
||||
}
|
||||
@@ -31,10 +29,22 @@ impl StoredWallet {
|
||||
self.accounts.is_empty()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.accounts.len()
|
||||
}
|
||||
|
||||
pub fn remove_account(&mut self, id: &WalletAccountId) -> Option<EncryptedAccount> {
|
||||
if let Some(index) = self.accounts.iter().position(|account| &account.id == id) {
|
||||
log::info!("Removing from wallet file: {id}");
|
||||
Some(self.accounts.remove(index))
|
||||
} else {
|
||||
log::debug!("Tried to remove non-existent id from wallet: {id}");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn encrypted_account_by_index(&self, index: usize) -> Option<&EncryptedAccount> {
|
||||
self.accounts.get(index)
|
||||
}
|
||||
@@ -136,6 +146,7 @@ impl MnemonicAccount {
|
||||
&self.mnemonic
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn hd_path(&self) -> &DerivationPath {
|
||||
&self.hd_path
|
||||
}
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
use super::password::UserPassword;
|
||||
use crate::error::BackendError;
|
||||
use aes_gcm::aead::generic_array::ArrayLength;
|
||||
use aes_gcm::aead::{Aead, NewAead, Payload};
|
||||
use aes_gcm::aead::{Aead, NewAead};
|
||||
use aes_gcm::{Aes256Gcm, Key, Nonce};
|
||||
use argon2::{
|
||||
password_hash::rand_core::{OsRng, RngCore},
|
||||
Algorithm, Argon2, Params, Version,
|
||||
};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::marker::PhantomData;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
@@ -78,6 +77,7 @@ mod base64 {
|
||||
}
|
||||
|
||||
impl<T> EncryptedData<T> {
|
||||
#[allow(unused)]
|
||||
pub(crate) fn encrypt_struct(data: &T, password: &UserPassword) -> Result<Self, BackendError>
|
||||
where
|
||||
T: Serialize,
|
||||
@@ -94,10 +94,12 @@ impl<T> EncryptedData<T> {
|
||||
}
|
||||
|
||||
impl EncryptedData<Vec<u8>> {
|
||||
#[allow(unused)]
|
||||
pub(crate) fn encrypt_data(data: &[u8], password: &UserPassword) -> Result<Self, BackendError> {
|
||||
encrypt_data(data, password)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn decrypt_data(&self, password: &UserPassword) -> Result<Vec<u8>, BackendError> {
|
||||
decrypt_data(self, password)
|
||||
}
|
||||
@@ -159,6 +161,7 @@ fn decrypt(
|
||||
.map_err(|_| BackendError::DecryptionError)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn encrypt_data(
|
||||
data: &[u8],
|
||||
password: &UserPassword,
|
||||
@@ -194,6 +197,7 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn decrypt_data(
|
||||
encrypted_data: &EncryptedData<Vec<u8>>,
|
||||
password: &UserPassword,
|
||||
|
||||
@@ -4,13 +4,11 @@
|
||||
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::encryption::encrypt_struct;
|
||||
use cosmrs::bip32::DerivationPath;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::{create_dir_all, OpenOptions};
|
||||
use std::fs::{self, create_dir_all, OpenOptions};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use self::account_data::{EncryptedAccount, StoredWallet};
|
||||
@@ -32,6 +30,7 @@ pub(crate) fn wallet_login_filepath() -> Result<PathBuf, BackendError> {
|
||||
get_storage_directory().map(|dir| dir.join(WALLET_INFO_FILENAME))
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
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);
|
||||
@@ -112,21 +111,54 @@ fn store_wallet_login_information_at_file(
|
||||
Ok(serde_json::to_writer_pretty(file, &stored_wallet)?)
|
||||
}
|
||||
|
||||
// this function should probably exist, but I guess we need to discuss how it should behave in the context of the UX
|
||||
// pub(crate) fn remove_wallet_login_information(
|
||||
//
|
||||
// )
|
||||
pub(crate) fn remove_wallet_login_information(id: &WalletAccountId) -> Result<(), BackendError> {
|
||||
let store_dir = get_storage_directory()?;
|
||||
let filepath = store_dir.join(WALLET_INFO_FILENAME);
|
||||
remove_wallet_login_information_at_file(filepath, id)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_wallet_login_information_at_file(
|
||||
filepath: PathBuf,
|
||||
id: &WalletAccountId,
|
||||
) -> Result<(), BackendError> {
|
||||
let mut stored_wallet = match load_existing_wallet_at_file(filepath.clone()) {
|
||||
Err(BackendError::WalletFileNotFound) => StoredWallet::default(),
|
||||
result => result?,
|
||||
};
|
||||
|
||||
if stored_wallet.is_empty() {
|
||||
log::info!("Removing file: {:#?}", filepath);
|
||||
return Ok(fs::remove_file(filepath)?);
|
||||
}
|
||||
|
||||
stored_wallet
|
||||
.remove_account(id)
|
||||
.ok_or(BackendError::NoSuchIdInWallet)?;
|
||||
|
||||
if stored_wallet.is_empty() {
|
||||
log::info!("Removing file: {:#?}", filepath);
|
||||
Ok(fs::remove_file(filepath)?)
|
||||
} else {
|
||||
let file = OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(filepath)?;
|
||||
|
||||
Ok(serde_json::to_writer_pretty(file, &stored_wallet)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::wallet_storage::encryption::encrypt_data;
|
||||
use config::defaults::COSMOS_DERIVATION_PATH;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use tempfile::tempdir;
|
||||
|
||||
// I'm not 100% sure how to feel about having to touch the file system at all
|
||||
#[test]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn storing_wallet_information() {
|
||||
let store_dir = tempdir().unwrap();
|
||||
let wallet_file = store_dir.path().join(WALLET_INFO_FILENAME);
|
||||
@@ -248,9 +280,67 @@ mod tests {
|
||||
assert_eq!(&cosmos_hd_path, acc1.hd_path());
|
||||
|
||||
let loaded_account =
|
||||
load_existing_wallet_login_information_at_file(wallet_file, &id2, &password).unwrap();
|
||||
load_existing_wallet_login_information_at_file(wallet_file.clone(), &id2, &password).unwrap();
|
||||
let StoredAccount::Mnemonic(ref acc2) = loaded_account;
|
||||
assert_eq!(&dummy_account2, acc2.mnemonic());
|
||||
assert_eq!(&different_hd_path, acc2.hd_path());
|
||||
|
||||
// Fails to delete non-existent id in the wallet
|
||||
let id3 = WalletAccountId::new("phony".to_string());
|
||||
assert!(matches!(
|
||||
remove_wallet_login_information_at_file(wallet_file.clone(), &id3),
|
||||
Err(BackendError::NoSuchIdInWallet),
|
||||
));
|
||||
|
||||
// Delete the second account
|
||||
remove_wallet_login_information_at_file(wallet_file.clone(), &id2).unwrap();
|
||||
|
||||
// The 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;
|
||||
assert_eq!(&dummy_account1, acc1.mnemonic());
|
||||
assert_eq!(&cosmos_hd_path, acc1.hd_path());
|
||||
|
||||
// Delete the first account
|
||||
assert!(wallet_file.exists());
|
||||
remove_wallet_login_information_at_file(wallet_file.clone(), &id1).unwrap();
|
||||
|
||||
// The file should now be removed
|
||||
assert!(!wallet_file.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_stored_wallet() {
|
||||
const SAVED_WALLET: &str = "src/wallet_storage/test-data/saved-wallet.json";
|
||||
let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET);
|
||||
|
||||
let wallet = load_existing_wallet_at_file(wallet_file).unwrap();
|
||||
|
||||
let cosmos_hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
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());
|
||||
|
||||
assert!(!wallet.password_can_decrypt_all(&bad_password));
|
||||
assert!(wallet.password_can_decrypt_all(&password));
|
||||
|
||||
let account1 = wallet.decrypt_account(&id1, &password).unwrap();
|
||||
let account2 = wallet.decrypt_account(&id2, &password).unwrap();
|
||||
|
||||
let expected_account1 = bip39::Mnemonic::from_str("country mean universe text phone begin deputy reject result good cram illness common cluster proud swamp digital patrol spread bar face december base kick").unwrap();
|
||||
let expected_account2 = bip39::Mnemonic::from_str("home mansion start quiz dress decide hint second dragon sunny juice always steak real minimum art rival skin draw total pulp foot goddess agent").unwrap();
|
||||
|
||||
assert_eq!(account1.mnemonic(), &expected_account1);
|
||||
assert_eq!(account2.mnemonic(), &expected_account2);
|
||||
|
||||
let StoredAccount::Mnemonic(ref mnemonic1) = account1;
|
||||
assert_eq!(mnemonic1.mnemonic(), &expected_account1);
|
||||
assert_eq!(mnemonic1.hd_path(), &cosmos_hd_path);
|
||||
|
||||
let StoredAccount::Mnemonic(ref mnemonic2) = account2;
|
||||
assert_eq!(mnemonic2.mnemonic(), &expected_account2);
|
||||
assert_eq!(mnemonic2.hd_path(), &cosmos_hd_path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -19,6 +21,12 @@ impl AsRef<str> for WalletAccountId {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for WalletAccountId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
// simple wrapper for String that will get zeroized on drop
|
||||
#[derive(Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"version": 1,
|
||||
"accounts": [
|
||||
{
|
||||
"id": "first",
|
||||
"account": {
|
||||
"ciphertext": "icnpxLmr/H7FIIOaEf7DYNLuM6uhh7poEppXpYCllQD33TjY+8eLtVvhEQmjX60IQeFOd+1JCcrHa2B12vlBAYlfM4gBxA6d2ZJ8+Dw/vNvBNyChiyUx2euV3vPGOs22r/XDBsmEeF40XZcXftQZa2kzYaPnkbP+eiMOIWkcY4FYOEHwx5SxT4VBPZIrVTC3iDalJLWybVbbw/Bc2zbzEXI1ckg4Ccydj95SMil9BiyDpALfZqwlai7I97S+BjmcVxSCsYqFjTkRUHVMjrEr7fWHKU4DIOM=",
|
||||
"salt": "CtnbfkxTybqz0U4cPHW2jQ==",
|
||||
"iv": "77ZROU6dAMttEWwS"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "second",
|
||||
"account": {
|
||||
"ciphertext": "nsqZHdQFlskglc5izKgnr8sBwdMmd82h2Rnjdos9EUca3cqkUdFYEjZDsK8OGR3GZ9alLTNt/1U97Rvvr2HPAWbzl23FW2YXaLTA6yj6ZwQK5w0MYE061NYbcxNHuzT9f5aQWkGULAk4RWb5t8eUX7y/NdJr3tA5xuGOLhooTfBB98/4RpupDsYGZp1DPC/GMFppOA3GmKs9bacZm805Bhfq5mwhXab1SjJQpFHMHisCMhxo/oLqulKML1tQMetBdqDTjJmPpdUnd1mi",
|
||||
"salt": "J2TMLjKv4dkZ/kXso9FGhg==",
|
||||
"iv": "CTqqoMa4LetvBKCP"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "nym-wallet",
|
||||
"version": "1.0.1"
|
||||
"version": "1.0.2"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
@@ -46,7 +46,12 @@
|
||||
}
|
||||
},
|
||||
"updater": {
|
||||
"active": false
|
||||
"active": true,
|
||||
"endpoints": [
|
||||
"https://nymtech.net/.wellknown/wallet/updater.json"
|
||||
],
|
||||
"dialog": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENCNzQ2M0E5N0VFODE2NApSV1JrZ2U2WE9rYTNETTg1OTBKdE5uWUEra0hML2syOVUvQ2lxZmFZRzZ1T3NWbGM0eVRzUTVhVwo="
|
||||
},
|
||||
"allowlist": {
|
||||
"window": {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React, { useMemo, createContext, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { TLoginType } from 'src/pages/sign-in/types';
|
||||
import { Account, Network, TCurrency, TMixnodeBondDetails } from '../types';
|
||||
import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance';
|
||||
import { config } from '../../config';
|
||||
import { getMixnodeBondDetails, selectNetwork, signInWithMnemonic, signOut } from '../requests';
|
||||
import { getMixnodeBondDetails, selectNetwork, signInWithMnemonic, signInWithPassword, signOut } from '../requests';
|
||||
import { currencyMap } from '../utils';
|
||||
import { Console } from '../utils/console';
|
||||
|
||||
@@ -33,12 +34,15 @@ 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;
|
||||
handleShowValidatorSettings: () => void;
|
||||
handleShowAdmin: () => void;
|
||||
logIn: (mnemonic: string) => void;
|
||||
logIn: (opts: { type: 'mnemonic' | 'password'; value: string }) => void;
|
||||
signInWithPassword: (password: string) => void;
|
||||
logOut: () => void;
|
||||
};
|
||||
|
||||
@@ -93,17 +97,24 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
refreshAccount();
|
||||
}, [network]);
|
||||
|
||||
const logIn = async (mnemonic: string) => {
|
||||
const logIn = async ({ type, value }: { type: TLoginType; value: string }) => {
|
||||
if (value.length === 0) {
|
||||
setError(`A ${type} must be provided`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await signInWithMnemonic(mnemonic || '');
|
||||
await getBondDetails();
|
||||
if (type === 'mnemonic') {
|
||||
await signInWithMnemonic(value);
|
||||
} else {
|
||||
await signInWithPassword(value);
|
||||
}
|
||||
setNetwork('MAINNET');
|
||||
history.push('/balance');
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
setError(e as string);
|
||||
} finally {
|
||||
history.push('/balance');
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -142,6 +153,9 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
showValidatorSettings,
|
||||
network,
|
||||
currency,
|
||||
setIsLoading,
|
||||
setError,
|
||||
signInWithPassword,
|
||||
switchNetwork,
|
||||
getBondDetails,
|
||||
handleShowSettings,
|
||||
|
||||
@@ -3,13 +3,14 @@ import ReactDOM from 'react-dom';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import { Routes } from './routes';
|
||||
import { AppRoutes, SignInRoutes } from './routes';
|
||||
import { ClientContext, ClientContextProvider } from './context/main';
|
||||
import { ApplicationLayout } from './layouts';
|
||||
import { Admin, Welcome, Settings, ValidatorSettings } from './pages';
|
||||
import { Admin, Settings, ValidatorSettings } from './pages';
|
||||
import { ErrorFallback } from './components';
|
||||
import { NymWalletTheme, WelcomeTheme } from './theme';
|
||||
import { maximizeWindow } from './utils';
|
||||
import { SignInProvider } from './pages/sign-in/context';
|
||||
|
||||
const App = () => {
|
||||
const { clientDetails } = useContext(ClientContext);
|
||||
@@ -20,7 +21,9 @@ const App = () => {
|
||||
|
||||
return !clientDetails ? (
|
||||
<WelcomeTheme>
|
||||
<Welcome />
|
||||
<SignInProvider>
|
||||
<SignInRoutes />
|
||||
</SignInProvider>
|
||||
</WelcomeTheme>
|
||||
) : (
|
||||
<NymWalletTheme>
|
||||
@@ -28,7 +31,7 @@ const App = () => {
|
||||
<Settings />
|
||||
<ValidatorSettings />
|
||||
<Admin />
|
||||
<Routes />
|
||||
<AppRoutes />
|
||||
</ApplicationLayout>
|
||||
</NymWalletTheme>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ export * from './delegate';
|
||||
export * from './internal-docs';
|
||||
export * from './receive';
|
||||
export * from './send';
|
||||
export * from './welcome';
|
||||
export * from './sign-in';
|
||||
export * from './settings';
|
||||
export * from './unbond';
|
||||
export * from './undelegate';
|
||||
|
||||
@@ -16,6 +16,9 @@ export const SendConfirmation = ({
|
||||
isLoading: boolean;
|
||||
}) => {
|
||||
const { userBalance, currency, network } = useContext(ClientContext);
|
||||
|
||||
if (!data && !error && !isLoading) return null;
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
|
||||
@@ -128,13 +128,9 @@ 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={{
|
||||
@@ -154,7 +150,16 @@ export const SendWizard = () => {
|
||||
color="primary"
|
||||
disableElevation
|
||||
data-testid="button"
|
||||
onClick={activeStep === 0 ? handleNextStep : activeStep === 1 ? handleSend : handleFinish}
|
||||
onClick={() => {
|
||||
switch (activeStep) {
|
||||
case 0:
|
||||
return handleNextStep();
|
||||
case 1:
|
||||
return handleSend();
|
||||
default:
|
||||
return 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' ? (
|
||||
<Typography sx={{ color: 'error.main', fontWeight: 600 }}>Node update failed</Typography>
|
||||
) : (
|
||||
<Fee feeType="UpdateMixnodeConfig" />
|
||||
)}
|
||||
{nodeUpdateResponse === 'failed' && (
|
||||
<Typography sx={{ color: 'error.main', fontWeight: 600 }}>Node update failed</Typography>
|
||||
)}
|
||||
{!nodeUpdateResponse && <Fee feeType="UpdateMixnodeConfig" />}
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
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>
|
||||
);
|
||||
+1
-1
@@ -6,7 +6,7 @@ export const Title = ({ title }: { title: string }) => (
|
||||
);
|
||||
|
||||
export const Subtitle = ({ subtitle }: { subtitle: string }) => (
|
||||
<Typography sx={{ color: 'common.white', textAlign: 'center', maxWidth: 400 }}>{subtitle}</Typography>
|
||||
<Typography sx={{ color: 'common.white', textAlign: 'center', maxWidth: 450 }}>{subtitle}</Typography>
|
||||
);
|
||||
|
||||
export const SubtitleSlick = ({ subtitle }: { subtitle: string }) => (
|
||||
+4
@@ -2,3 +2,7 @@ export * from './heading';
|
||||
export * from './word-tiles';
|
||||
export * from './render-page';
|
||||
export * from './password-strength';
|
||||
export * from './error';
|
||||
export * from './textfields';
|
||||
export * from './step';
|
||||
export * from './page-layout';
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { Stack, Box } from '@mui/material';
|
||||
import { NymWordmark } from '@nymproject/react';
|
||||
import { Step } from './step';
|
||||
|
||||
export const PageLayout: React.FC = ({ children }) => (
|
||||
<Box
|
||||
sx={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'auto',
|
||||
bgcolor: 'nym.background.dark',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
margin: 'auto',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={3} alignItems="center" sx={{ width: 1080 }}>
|
||||
<NymWordmark width={75} />
|
||||
<Step />
|
||||
{children}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
+29
-8
@@ -5,8 +5,8 @@ import { LinearProgress, Stack, Typography, Box } from '@mui/material';
|
||||
|
||||
type TStrength = 'weak' | 'medium' | 'strong' | 'init';
|
||||
|
||||
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 strong = /^(?=.*[a-z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/;
|
||||
const medium = /^(((?=.*[a-z]))|((?=.*[a-z])(?=.*[0-9]))|((?=.*[0-9])))(?=.{6,})/;
|
||||
|
||||
const colorMap = {
|
||||
init: 'inherit' as 'inherit',
|
||||
@@ -41,7 +41,24 @@ const getTextColor = (strength: TStrength) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const PasswordStrength = ({ password }: { password: string }) => {
|
||||
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;
|
||||
}) => {
|
||||
const [strength, setStrength] = useState<TStrength>('init');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -62,13 +79,17 @@ export const PasswordStrength = ({ password }: { password: string }) => {
|
||||
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={strength === 'strong' ? 100 : strength === 'medium' ? 50 : 0}
|
||||
/>
|
||||
<LinearProgress variant="determinate" color={colorMap[strength]} value={getPasswordStrength(strength)} />
|
||||
<Box display="flex" alignItems="center">
|
||||
<LockOutlined sx={{ fontSize: 15, color: getTextColor(strength) }} />
|
||||
<Typography variant="caption" sx={{ ml: 0.5, color: getTextColor(strength) }}>
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
export const Step = () => {
|
||||
const location = useLocation();
|
||||
|
||||
const mapPage = useCallback(() => {
|
||||
switch (location.pathname) {
|
||||
case '/create-mnemonic':
|
||||
return { value: 1, type: 'account', total: 3 };
|
||||
case '/verify-mnemonic':
|
||||
return { value: 2, type: 'account', total: 3 };
|
||||
case '/create-password':
|
||||
return { value: 3, type: 'account', total: 3 };
|
||||
case '/confirm-mnemonic':
|
||||
return { value: 1, type: 'account password', total: 2 };
|
||||
case '/connect-password':
|
||||
return { value: 2, type: 'account password', total: 2 };
|
||||
default:
|
||||
return { value: 0, type: '', total: 0 };
|
||||
}
|
||||
}, [location.pathname]);
|
||||
|
||||
if (mapPage().value === 0) {
|
||||
return null;
|
||||
}
|
||||
const { value, type, total } = mapPage();
|
||||
return <Typography sx={{ color: 'grey.400' }}>{`Create ${type}. Step ${value}/${total}`}</Typography>;
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, IconButton, Link, Stack, TextField } 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>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{/* currently unused */}
|
||||
{showForgottenPassword && (
|
||||
<Link
|
||||
underline="none"
|
||||
variant="body2"
|
||||
component="div"
|
||||
sx={{ mt: 1, textAlign: 'right', color: 'info.main', cursor: 'pointer' }}
|
||||
href="/forgotten-password"
|
||||
>
|
||||
Forgotten password?
|
||||
</Link>
|
||||
)}
|
||||
</Box>
|
||||
{error && <Error message={error} />}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
+7
-2
@@ -7,17 +7,19 @@ export const WordTile = ({
|
||||
index,
|
||||
disabled,
|
||||
onClick,
|
||||
button,
|
||||
}: {
|
||||
mnemonicWord: string;
|
||||
index?: number;
|
||||
disabled?: boolean;
|
||||
onClick?: boolean;
|
||||
button?: boolean;
|
||||
}) => (
|
||||
<Card
|
||||
variant="outlined"
|
||||
sx={{
|
||||
background: '#151A2C',
|
||||
border: '1px solid #3A4053',
|
||||
background: button ? '#151A2C' : 'transparent',
|
||||
border: button ? '1px solid #3A4053' : 'none',
|
||||
cursor: onClick ? 'pointer' : 'default',
|
||||
opacity: disabled ? 0.2 : 1,
|
||||
}}
|
||||
@@ -40,10 +42,12 @@ export const WordTiles = ({
|
||||
mnemonicWords,
|
||||
showIndex,
|
||||
onClick,
|
||||
buttons,
|
||||
}: {
|
||||
mnemonicWords?: TMnemonicWords;
|
||||
showIndex?: boolean;
|
||||
onClick?: ({ name, index }: { name: string; index: number }) => void;
|
||||
buttons?: boolean;
|
||||
}) => {
|
||||
if (mnemonicWords) {
|
||||
return (
|
||||
@@ -55,6 +59,7 @@ export const WordTiles = ({
|
||||
index={showIndex ? index : undefined}
|
||||
onClick={!!onClick}
|
||||
disabled={disabled}
|
||||
button={buttons}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { createContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { createMnemonic } 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>;
|
||||
setPassword: (paswd: string) => void;
|
||||
resetState: () => 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 history = useHistory();
|
||||
|
||||
const generateMnemonic = async () => {
|
||||
const mnemonicPhrase = await createMnemonic();
|
||||
setMnemonic(mnemonicPhrase);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
history.push('/welcome');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (mnemonic.length > 0) {
|
||||
const mnemonicArray = mnemonicToArray(mnemonic);
|
||||
setMnemonicWords(mnemonicArray);
|
||||
} else {
|
||||
setMnemonicWords([]);
|
||||
}
|
||||
}, [mnemonic]);
|
||||
|
||||
const resetState = () => {
|
||||
setPassword('');
|
||||
setMnemonic('');
|
||||
};
|
||||
|
||||
return (
|
||||
<SignInContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
error,
|
||||
password,
|
||||
mnemonic,
|
||||
mnemonicWords,
|
||||
setError,
|
||||
setMnemonic,
|
||||
generateMnemonic,
|
||||
setPassword,
|
||||
resetState,
|
||||
}),
|
||||
[error, password, mnemonic, mnemonicWords],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SignInContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './pages';
|
||||
@@ -0,0 +1,51 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button, Stack } from '@mui/material';
|
||||
import { validateMnemonic } from 'src/requests';
|
||||
import { MnemonicInput, Subtitle } from '../components';
|
||||
import { SignInContext } from '../context';
|
||||
|
||||
export const ConfirmMnemonic = () => {
|
||||
const { error, setError, setMnemonic, mnemonic } = useContext(SignInContext);
|
||||
const [localMnemonic, setLocalMnemonic] = useState(mnemonic);
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
setError(undefined);
|
||||
}, [localMnemonic]);
|
||||
|
||||
return (
|
||||
<Stack spacing={2}>
|
||||
<Subtitle subtitle="Enter the mnemonic you wish to create a password for" />
|
||||
<MnemonicInput mnemonic={localMnemonic} onUpdateMnemonic={(mnc) => setLocalMnemonic(mnc)} error={error} />
|
||||
<Button
|
||||
size="large"
|
||||
variant="contained"
|
||||
fullWidth
|
||||
onClick={async () => {
|
||||
const isValid = await validateMnemonic(localMnemonic);
|
||||
if (isValid) {
|
||||
setMnemonic(localMnemonic);
|
||||
history.push('/connect-password');
|
||||
} else {
|
||||
setError('The mnemonic provided is not valid. Please check the mnemonic');
|
||||
}
|
||||
}}
|
||||
disabled={localMnemonic.length === 0}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
color="inherit"
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
setMnemonic('');
|
||||
history.goBack();
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button, CircularProgress, FormControl, Stack } from '@mui/material';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { Subtitle, Title, PasswordStrength } from '../components';
|
||||
import { PasswordInput } from '../components/textfields';
|
||||
import { SignInContext } from '../context';
|
||||
import { createPassword } from '../../../requests';
|
||||
|
||||
export const ConnectPassword = () => {
|
||||
const [confirmedPassword, setConfirmedPassword] = useState<string>('');
|
||||
const [isStrongPassword, setIsStrongPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { mnemonic, password, setPassword, resetState } = useContext(SignInContext);
|
||||
const history = useHistory();
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const storePassword = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await createPassword({ mnemonic, password });
|
||||
resetState();
|
||||
enqueueSnackbar('Password successfully created', { variant: 'success' });
|
||||
history.push('/sign-in-password');
|
||||
} catch (e) {
|
||||
enqueueSnackbar(e as string, { variant: 'error' });
|
||||
setIsLoading(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}
|
||||
>
|
||||
{isLoading ? <CircularProgress size={25} /> : 'Create password'}
|
||||
</Button>
|
||||
<Button
|
||||
size="large"
|
||||
color="inherit"
|
||||
onClick={() => {
|
||||
setPassword('');
|
||||
history.goBack();
|
||||
}}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Stack>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Alert, Button, Stack, Typography } from '@mui/material';
|
||||
import { Check, ContentCopySharp } from '@mui/icons-material';
|
||||
import { useClipboard } from 'use-clipboard-copy';
|
||||
import { WordTiles } from '../components';
|
||||
import { SignInContext } from '../context';
|
||||
|
||||
export const CreateMnemonic = () => {
|
||||
const { mnemonic, mnemonicWords, generateMnemonic, resetState } = useContext(SignInContext);
|
||||
const history = useHistory();
|
||||
|
||||
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={() => history.push('/verify-mnemonic')}
|
||||
sx={{ width: 250 }}
|
||||
disabled={!copied}
|
||||
>
|
||||
I saved my mnemonic
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
resetState();
|
||||
history.goBack();
|
||||
}}
|
||||
color="inherit"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button, FormControl, Stack } from '@mui/material';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { Subtitle, Title, PasswordStrength } from '../components';
|
||||
import { PasswordInput } from '../components/textfields';
|
||||
import { SignInContext } from '../context';
|
||||
import { createPassword } from '../../../requests';
|
||||
|
||||
export const CreatePassword = () => {
|
||||
const { password, setPassword, resetState } = useContext(SignInContext);
|
||||
const [confirmedPassword, setConfirmedPassword] = useState<string>('');
|
||||
const [isStrongPassword, setIsStrongPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { mnemonic } = useContext(SignInContext);
|
||||
const history = useHistory();
|
||||
|
||||
const handleSkip = () => {
|
||||
setPassword('');
|
||||
history.push('/sign-in-mnemonic');
|
||||
};
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const storePassword = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await createPassword({ mnemonic, password });
|
||||
enqueueSnackbar('Password successfully created', { variant: 'success' });
|
||||
resetState();
|
||||
history.push('/sign-in-password');
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
enqueueSnackbar(e as string, { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable react/no-unused-prop-types */
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Box, Button, Stack, Typography } from '@mui/material';
|
||||
import { SubtitleSlick, Title } from '../components';
|
||||
|
||||
export const ExistingAccount = () => {
|
||||
const history = useHistory();
|
||||
return (
|
||||
<>
|
||||
<Title title="Welcome to Nym" />
|
||||
<SubtitleSlick subtitle="NEXT GENERATION OF PRIVACY" />
|
||||
<Stack spacing={2} sx={{ width: 300 }}>
|
||||
<Button variant="contained" size="large" onClick={() => history.push('/sign-in-mnemonic')} fullWidth>
|
||||
Sign in with mnemonic
|
||||
</Button>
|
||||
<Typography sx={{ textAlign: 'center', fontWeight: 600 }}>or</Typography>
|
||||
<Button variant="contained" size="large" fullWidth onClick={() => history.push('/sign-in-password')}>
|
||||
Sign in with password
|
||||
</Button>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Button color="inherit" onClick={() => history.goBack()}>
|
||||
Back
|
||||
</Button>
|
||||
<Button color="info" onClick={() => history.push('/confirm-mnemonic')}>
|
||||
Create a password
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './welcome';
|
||||
export * from './create-mnemonic';
|
||||
export * from './verify-mnemonic';
|
||||
export * from './create-password';
|
||||
export * from './existing-account';
|
||||
export * from './signin-mnemonic';
|
||||
export * from './signin-password';
|
||||
export * from './connect-password';
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { Box, Button, FormControl, LinearProgress, Stack } from '@mui/material';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { MnemonicInput, Subtitle } from '../components';
|
||||
import { ClientContext } from '../../../context/main';
|
||||
|
||||
export const SignInMnemonic = () => {
|
||||
const [mnemonic, setMnemonic] = useState('');
|
||||
const { setError, logIn, error, isLoading } = useContext(ClientContext);
|
||||
const history = useHistory();
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<Box width="25%">
|
||||
<LinearProgress variant="indeterminate" />
|
||||
</Box>
|
||||
);
|
||||
|
||||
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);
|
||||
history.push('/existing-account');
|
||||
}}
|
||||
fullWidth
|
||||
sx={{ color: 'common.white', border: '1px solid white', '&:hover': { border: '1px solid white' } }}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Stack>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button, LinearProgress, FormControl, Stack, Box } from '@mui/material';
|
||||
import { PasswordInput, Subtitle } from '../components';
|
||||
import { ClientContext } from '../../../context/main';
|
||||
|
||||
export const SignInPassword = () => {
|
||||
const [password, setPassword] = useState('');
|
||||
const { setError, logIn, error, isLoading } = useContext(ClientContext);
|
||||
const history = useHistory();
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<Box width="25%">
|
||||
<LinearProgress variant="indeterminate" />
|
||||
</Box>
|
||||
);
|
||||
|
||||
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);
|
||||
history.push('/existing-account');
|
||||
}}
|
||||
fullWidth
|
||||
sx={{ color: 'common.white', border: '1px solid white', '&:hover': { border: '1px solid white' } }}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
</Stack>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
+21
-19
@@ -1,22 +1,21 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button } from '@mui/material';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button, Stack } from '@mui/material';
|
||||
import { HiddenWords, Subtitle, Title, WordTiles } from '../components';
|
||||
import { THiddenMnemonicWord, THiddenMnemonicWords, TMnemonicWord, TMnemonicWords } from '../types';
|
||||
import { randomNumberBetween } from '../../../utils';
|
||||
import { SignInContext } from '../context';
|
||||
|
||||
const numberOfRandomWords = 4;
|
||||
const numberOfRandomWords = 6;
|
||||
|
||||
export const VerifyMnemonic = ({
|
||||
mnemonicWords,
|
||||
onComplete,
|
||||
}: {
|
||||
mnemonicWords?: TMnemonicWords;
|
||||
onComplete: () => void;
|
||||
}) => {
|
||||
export const VerifyMnemonic = () => {
|
||||
const [randomWords, setRandomWords] = useState<TMnemonicWords>();
|
||||
const [hiddenRandomWords, setHiddenRandomWords] = useState<THiddenMnemonicWords>();
|
||||
const [currentSelection, setCurrentSelection] = useState(0);
|
||||
|
||||
const { mnemonicWords } = useContext(SignInContext);
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
if (mnemonicWords) {
|
||||
const newRandomWords = getRandomEntriesFromArray<TMnemonicWord>(mnemonicWords, numberOfRandomWords);
|
||||
@@ -48,16 +47,19 @@ export const VerifyMnemonic = ({
|
||||
<WordTiles
|
||||
mnemonicWords={randomWords}
|
||||
onClick={currentSelection !== numberOfRandomWords ? revealWord : undefined}
|
||||
buttons
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
sx={{ width: 300 }}
|
||||
size="large"
|
||||
disabled={currentSelection !== numberOfRandomWords}
|
||||
onClick={onComplete}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<Stack spacing={3} sx={{ width: 300 }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
fullWidth
|
||||
size="large"
|
||||
disabled={currentSelection !== numberOfRandomWords}
|
||||
onClick={() => history.push('/create-password')}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/* eslint-disable react/no-unused-prop-types */
|
||||
import React from 'react';
|
||||
import { Button, Stack } from '@mui/material';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { SubtitleSlick, Title } from '../components';
|
||||
|
||||
export const WelcomeContent: React.FC<{}> = () => {
|
||||
const history = useHistory();
|
||||
|
||||
return (
|
||||
<>
|
||||
<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={() => history.push('/existing-account')}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
<Button
|
||||
fullWidth
|
||||
color="inherit"
|
||||
disableElevation
|
||||
size="large"
|
||||
onClick={() => history.push('/create-mnemonic')}
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
export type TPages =
|
||||
| 'welcome'
|
||||
| 'create account'
|
||||
| 'create mnemonic'
|
||||
| 'verify mnemonic'
|
||||
| 'create password'
|
||||
| 'existing account'
|
||||
| 'select network'
|
||||
| 'legacy create account';
|
||||
| 'legacy create account'
|
||||
| 'sign in with mnemonic'
|
||||
| 'sign in with password';
|
||||
|
||||
export type TMnemonicWord = {
|
||||
name: string;
|
||||
@@ -17,3 +19,5 @@ export type TMnemonicWords = TMnemonicWord[];
|
||||
export type THiddenMnemonicWord = { hidden: boolean } & TMnemonicWord;
|
||||
|
||||
export type THiddenMnemonicWords = THiddenMnemonicWord[];
|
||||
|
||||
export type TLoginType = 'mnemonic' | 'password';
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Alert, Button, Card, CardActions, CardContent, CardHeader, Stack, Typography } from '@mui/material';
|
||||
import { createMnemonic } from '../../requests';
|
||||
import { CopyToClipboard } from '../../components';
|
||||
import { TPages } from './types';
|
||||
|
||||
export const CreateAccountContent: React.FC<{ page: TPages; showSignIn: () => void }> = ({ page, showSignIn }) => {
|
||||
const [mnemonic, setMnemonic] = useState<string>();
|
||||
const [error, setError] = useState<Error>();
|
||||
|
||||
const handleCreateMnemonic = async () => {
|
||||
setError(undefined);
|
||||
try {
|
||||
const newMnemonic = await createMnemonic();
|
||||
setMnemonic(newMnemonic);
|
||||
} catch (e: any) {
|
||||
setError(e);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handleCreateMnemonic();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Stack spacing={4} alignItems="center" sx={{ width: 700 }} id={page}>
|
||||
<Typography sx={{ color: 'common.white' }} variant="h4">
|
||||
Congratulations
|
||||
</Typography>
|
||||
<Typography sx={{ color: 'common.white' }} variant="h6">
|
||||
Account setup complete!
|
||||
</Typography>
|
||||
<Alert severity="info" variant="outlined" sx={{ color: 'info.light' }} data-testid="mnemonic-warning">
|
||||
<Typography>Please store your mnemonic in a safe place. You will need it to access your account!</Typography>
|
||||
</Alert>
|
||||
<Card variant="outlined" sx={{ bgcolor: 'transparent', p: 2, borderColor: 'common.white' }}>
|
||||
<CardHeader sx={{ color: 'common.white' }} title="Mnemonic" />
|
||||
<CardContent sx={{ color: 'common.white' }} data-testid="mnemonic-phrase">
|
||||
{mnemonic}
|
||||
</CardContent>
|
||||
<CardActions sx={{ justifyContent: 'flex-end' }}>
|
||||
<CopyToClipboard text={mnemonic || ''} light />
|
||||
</CardActions>
|
||||
</Card>
|
||||
{error && (
|
||||
<Alert severity="error" variant="outlined">
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
<Button variant="contained" onClick={showSignIn} data-testid="sign-in-button" size="large" sx={{ width: 360 }}>
|
||||
Sign in
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { NymLogo } from '@nymproject/react';
|
||||
import { Alert, Button, CircularProgress, Grid, Stack, Typography } from '@mui/material';
|
||||
import { ClientContext } from '../../context/main';
|
||||
|
||||
export const SignInContent: React.FC = () => {
|
||||
const [mnemonic] = useState<string>('');
|
||||
const [inputError, setInputError] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { logIn } = useContext(ClientContext);
|
||||
|
||||
const handleSignIn = async (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
setIsLoading(true);
|
||||
setInputError(undefined);
|
||||
|
||||
try {
|
||||
await logIn(mnemonic || '');
|
||||
setIsLoading(false);
|
||||
} catch (error: any) {
|
||||
setIsLoading(false);
|
||||
setInputError(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack spacing={3} alignItems="center" sx={{ width: '80%' }}>
|
||||
<NymLogo width={50} />
|
||||
<Typography sx={{ color: 'common.white', fontWeight: 600 }}>Welcome to NYM</Typography>
|
||||
<Typography variant="caption" sx={{ color: 'grey.800', textTransform: 'uppercase', letterSpacing: 4 }}>
|
||||
Next generation of privacy
|
||||
</Typography>
|
||||
<Grid container direction="column" spacing={2}>
|
||||
<Grid item>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={isLoading}
|
||||
endIcon={isLoading && <CircularProgress size={20} />}
|
||||
disableElevation
|
||||
size="large"
|
||||
onClick={handleSignIn}
|
||||
type="submit"
|
||||
>
|
||||
Create Account
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button fullWidth variant="outlined" size="large">
|
||||
Use Existing Account
|
||||
</Button>
|
||||
</Grid>
|
||||
{inputError && (
|
||||
<Grid item sx={{ mt: 1 }}>
|
||||
<Alert severity="error" variant="outlined" data-testid="error" sx={{ color: 'error.light' }}>
|
||||
{inputError}
|
||||
</Alert>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,81 +0,0 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
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';
|
||||
|
||||
// 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 [mnemonicWords, setMnemonicWords] = useState<TMnemonicWords>();
|
||||
|
||||
const { isLoading } = useContext(ClientContext);
|
||||
|
||||
// useEffect(() => {
|
||||
// const mnemonicArray = mnemonicToArray(testMnemonic)
|
||||
// setMnemonicWords(mnemonicArray)
|
||||
// }, [])
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'auto',
|
||||
bgcolor: 'nym.background.dark',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
margin: 'auto',
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<CircularProgress size={72} />
|
||||
) : (
|
||||
<Stack spacing={3} alignItems="center" sx={{ width: 1080 }}>
|
||||
<NymLogo width={75} />
|
||||
<RenderPage page={page}>
|
||||
<WelcomeContent
|
||||
onUseExisting={() => setPage('existing account')}
|
||||
onCreateAccountComplete={() => setPage('legacy create account')}
|
||||
page="welcome"
|
||||
/>
|
||||
|
||||
<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>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
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';
|
||||
|
||||
export const CreatePassword = () => {
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [confirmedPassword, setConfirmedPassword] = useState<string>();
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [showConfirmedPassword, setShowConfirmedPassword] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<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,55 +0,0 @@
|
||||
/* eslint-disable react/no-unused-prop-types */
|
||||
import React, { useContext, useState } from 'react';
|
||||
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; 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 (
|
||||
<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={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,5 +0,0 @@
|
||||
export * from './welcome-content';
|
||||
export * from './mnemonic-words';
|
||||
export * from './verify-mnemonic';
|
||||
export * from './create-password';
|
||||
export * from './existing-account';
|
||||
@@ -1,34 +0,0 @@
|
||||
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,42 +0,0 @@
|
||||
/* eslint-disable react/no-unused-prop-types */
|
||||
import React from 'react';
|
||||
import { Button, Stack } from '@mui/material';
|
||||
import { SubtitleSlick, Title } from '../components';
|
||||
import { TPages } from '../types';
|
||||
|
||||
export const WelcomeContent: React.FC<{
|
||||
page: TPages;
|
||||
onUseExisting: () => void;
|
||||
onCreateAccountComplete: () => void;
|
||||
}> = ({ onUseExisting, onCreateAccountComplete }) => (
|
||||
<>
|
||||
<Title title="Welcome to NYM" />
|
||||
<SubtitleSlick subtitle="Next generation of privacy" />
|
||||
<Stack spacing={3} sx={{ width: 300 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disableElevation
|
||||
size="large"
|
||||
onClick={onCreateAccountComplete}
|
||||
>
|
||||
Create Account
|
||||
</Button>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
@@ -1,14 +1,10 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { Account, TCreateAccount } from '../types';
|
||||
import { Account } from '../types';
|
||||
|
||||
export const createAccount = async (): Promise<TCreateAccount> => {
|
||||
const res: TCreateAccount = await invoke('create_new_account');
|
||||
return res;
|
||||
};
|
||||
export const createMnemonic = async (): Promise<string> => invoke('create_new_mnemonic');
|
||||
|
||||
export const createMnemonic = async (): Promise<string> => {
|
||||
const res: string = await invoke('create_new_mnemonic');
|
||||
return res;
|
||||
export const createPassword = async ({ mnemonic, password }: { mnemonic: string; password: string }): Promise<void> => {
|
||||
await invoke('create_password', { mnemonic, password });
|
||||
};
|
||||
|
||||
export const signInWithMnemonic = async (mnemonic: string): Promise<Account> => {
|
||||
@@ -16,6 +12,16 @@ export const signInWithMnemonic = async (mnemonic: string): Promise<Account> =>
|
||||
return res;
|
||||
};
|
||||
|
||||
export const validateMnemonic = async (mnemonic: string): Promise<boolean> => {
|
||||
const res: boolean = await invoke('validate_mnemonic', { mnemonic });
|
||||
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');
|
||||
};
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { Bond, Balance, Delegate, InternalDocs, Receive, Send, Unbond, Undelegate } from '../pages';
|
||||
|
||||
export const AppRoutes = () => (
|
||||
<Switch>
|
||||
<Route path="/balance">
|
||||
<Balance />
|
||||
</Route>
|
||||
<Route path="/send">
|
||||
<Send />
|
||||
</Route>
|
||||
<Route path="/receive">
|
||||
<Receive />
|
||||
</Route>
|
||||
<Route path="/bond">
|
||||
<Bond />
|
||||
</Route>
|
||||
<Route path="/unbond">
|
||||
<Unbond />
|
||||
</Route>
|
||||
<Route path="/delegate">
|
||||
<Delegate />
|
||||
</Route>
|
||||
<Route path="/undelegate">
|
||||
<Undelegate />
|
||||
</Route>
|
||||
<Route path="/docs">
|
||||
<InternalDocs />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
@@ -1,33 +1,2 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { Balance } from '../pages/balance';
|
||||
import { Bond, Delegate, InternalDocs, Receive, Send, Unbond, Undelegate } from '../pages';
|
||||
|
||||
export const Routes = () => (
|
||||
<Switch>
|
||||
<Route path="/balance">
|
||||
<Balance />
|
||||
</Route>
|
||||
<Route path="/send">
|
||||
<Send />
|
||||
</Route>
|
||||
<Route path="/receive">
|
||||
<Receive />
|
||||
</Route>
|
||||
<Route path="/bond">
|
||||
<Bond />
|
||||
</Route>
|
||||
<Route path="/unbond">
|
||||
<Unbond />
|
||||
</Route>
|
||||
<Route path="/delegate">
|
||||
<Delegate />
|
||||
</Route>
|
||||
<Route path="/undelegate">
|
||||
<Undelegate />
|
||||
</Route>
|
||||
<Route path="/docs">
|
||||
<InternalDocs />
|
||||
</Route>
|
||||
</Switch>
|
||||
);
|
||||
export * from './app';
|
||||
export * from './sign-in';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router-dom';
|
||||
import { PageLayout } from 'src/pages/sign-in/components';
|
||||
import {
|
||||
CreateMnemonic,
|
||||
CreatePassword,
|
||||
ExistingAccount,
|
||||
SignInMnemonic,
|
||||
SignInPassword,
|
||||
VerifyMnemonic,
|
||||
WelcomeContent,
|
||||
ConnectPassword,
|
||||
} from 'src/pages/sign-in/pages';
|
||||
import { ConfirmMnemonic } from 'src/pages/sign-in/pages/confirm-mnemonic';
|
||||
|
||||
export const SignInRoutes = () => (
|
||||
<PageLayout>
|
||||
<Switch>
|
||||
<Route path="/welcome">
|
||||
<WelcomeContent />
|
||||
</Route>
|
||||
<Route path="/existing-account">
|
||||
<ExistingAccount />
|
||||
</Route>
|
||||
<Route path="/create-mnemonic">
|
||||
<CreateMnemonic />
|
||||
</Route>
|
||||
<Route path="/verify-mnemonic">
|
||||
<VerifyMnemonic />
|
||||
</Route>
|
||||
<Route path="/create-password">
|
||||
<CreatePassword />
|
||||
</Route>
|
||||
<Route path="/sign-in-mnemonic">
|
||||
<SignInMnemonic />
|
||||
</Route>
|
||||
<Route path="/sign-in-password">
|
||||
<SignInPassword />
|
||||
</Route>
|
||||
<Route path="/confirm-mnemonic">
|
||||
<ConfirmMnemonic />
|
||||
</Route>
|
||||
<Route path="/connect-password">
|
||||
<ConnectPassword />
|
||||
</Route>
|
||||
</Switch>
|
||||
</PageLayout>
|
||||
);
|
||||
@@ -8,6 +8,7 @@ import { NymWalletThemeWithMode } from './NymWalletTheme';
|
||||
/**
|
||||
* Provides the theme for the Network Explorer by reacting to the light/dark mode choice stored in the app context.
|
||||
*/
|
||||
|
||||
export const NymWalletTheme: React.FC = ({ children }) => {
|
||||
const { mode } = useContext(ClientContext);
|
||||
return <NymWalletThemeWithMode mode={mode}>{children}</NymWalletThemeWithMode>;
|
||||
|
||||
@@ -114,7 +114,7 @@ mod test {
|
||||
¶ms,
|
||||
"1234".to_string(),
|
||||
VOUCHER_INFO.to_string(),
|
||||
tx_hash.clone(),
|
||||
tx_hash,
|
||||
identity::PrivateKey::from_base58_string(
|
||||
identity::KeyPair::new(&mut rng)
|
||||
.private_key()
|
||||
|
||||
@@ -139,7 +139,7 @@ async fn signed_before() {
|
||||
¶ms,
|
||||
"1234".to_string(),
|
||||
VOUCHER_INFO.to_string(),
|
||||
tx_hash.clone(),
|
||||
tx_hash,
|
||||
identity::PrivateKey::from_base58_string(
|
||||
identity::KeyPair::new(&mut rng)
|
||||
.private_key()
|
||||
@@ -335,7 +335,7 @@ async fn blind_sign_correct() {
|
||||
¶ms,
|
||||
"1234".to_string(),
|
||||
VOUCHER_INFO.to_string(),
|
||||
tx_hash.clone(),
|
||||
tx_hash,
|
||||
identity::PrivateKey::from_base58_string(
|
||||
identity::KeyPair::new(&mut rng)
|
||||
.private_key()
|
||||
|
||||
@@ -139,7 +139,7 @@ impl RewardedSetUpdater {
|
||||
.insert_rewarding_report(rewarding_report)
|
||||
.await?;
|
||||
|
||||
Ok(self.generate_reward_messages(&to_reward).await?)
|
||||
self.generate_reward_messages(&to_reward).await
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
|
||||
Reference in New Issue
Block a user