Compare commits
147 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d6a81d9213 | |||
| a6db5fe704 | |||
| fe57d08f3e | |||
| ae29b2300c | |||
| 7b98d62f96 | |||
| 6abe95ed61 | |||
| db743578a9 | |||
| ba7f535cb7 | |||
| fd4f5b319c | |||
| 6045f57612 | |||
| 878cb3f0e5 | |||
| 686c0de5eb | |||
| 5ce2e0c8bb | |||
| 68e120dbf4 | |||
| 1362fcdbfa | |||
| 3281f10443 | |||
| 2409f85e31 | |||
| 37c06f338f | |||
| 8a37df64b5 | |||
| 20828d0d28 | |||
| 1e3c8ed3e0 | |||
| 830dbcecfc | |||
| a0d3144837 | |||
| a4988e3547 | |||
| 0b585a15b7 | |||
| 86a93a71e9 | |||
| 967692bf88 | |||
| 662b4d2fff | |||
| add4747e99 | |||
| 242733f144 | |||
| 0cc1926636 | |||
| 0f54c073a7 | |||
| 3bfce128a6 | |||
| 1b3e79e84d | |||
| e94f99211f | |||
| 003ea095cc | |||
| 74d93df74b | |||
| 0c66cc7393 | |||
| 82abfa5c5c | |||
| f7486f0490 | |||
| ee5f0f5808 | |||
| 6cb25cf1fb | |||
| db01c245d9 | |||
| 53347bec67 | |||
| 252385688d | |||
| cbcef9fbcd | |||
| e27fc82524 | |||
| f661bf0446 | |||
| 1d22f35c82 | |||
| fa41fe62c4 | |||
| 94a006a725 | |||
| 98cbd2509c | |||
| d2d99ca5c9 | |||
| 9e4904ff37 | |||
| bf9db4128d | |||
| bce86235c7 | |||
| 423f6bfb55 | |||
| 38fcbb7f2e | |||
| 1785b10d91 | |||
| 7771c5d3d9 | |||
| d6206a04bd | |||
| 83a7f6577b | |||
| 97c6567139 | |||
| a1534d23af | |||
| bcbda85477 | |||
| 54e95d795e | |||
| 11c0e79725 | |||
| 4525be9871 | |||
| b83d4ca1a3 | |||
| bfb868bfc7 | |||
| 7b00282b27 | |||
| 54194c03e1 | |||
| 87c2a317d5 | |||
| be92171fec | |||
| 04eef83c15 | |||
| 25cc7dbebf | |||
| beeb67e9c2 | |||
| ec19de6fa3 | |||
| 575845af38 | |||
| b6b757436e | |||
| eda69447de | |||
| 0f6f47c5ac | |||
| b57c17e5af | |||
| f2fa221489 | |||
| 0e4787f078 | |||
| 9b0b961d43 | |||
| ac72e20447 | |||
| 571fd5cb93 | |||
| 7cfaf6fa1e | |||
| 6e9eab4edb | |||
| 0812378fdd | |||
| ecb27e2cc2 | |||
| a1961dbc2f | |||
| ef4af0a1db | |||
| 5930ec1f18 | |||
| 72c7049fca | |||
| 92cbe651de | |||
| b44b074af7 | |||
| 7a0dff5f00 | |||
| 6685b129bb | |||
| e0a80c777e | |||
| ab019266cc | |||
| 92fcae9a37 | |||
| 413e2662ff | |||
| e026a532dd | |||
| a1e0087760 | |||
| b15cc094ea | |||
| 7adee63ebe | |||
| 1a5580229b | |||
| df4c6493d4 | |||
| 30a41261ea | |||
| 6d874cc34a | |||
| 7e356ea3b3 | |||
| f7be9e7e6f | |||
| 05374393ef | |||
| 580656c002 | |||
| a7caf97b73 | |||
| 091507b6d8 | |||
| 71b5bc9e71 | |||
| 8bbffb6a88 | |||
| 31c4fc6807 | |||
| 66b9b13edc | |||
| 1b945ae918 | |||
| be9b83a87d | |||
| ba1fb17908 | |||
| 5446874ebe | |||
| afac630a77 | |||
| 3250d6982e | |||
| ecdbe1a6fb | |||
| 6ba79ee924 | |||
| 2bebb4b0c2 | |||
| 81b7d49624 | |||
| f118a0c854 | |||
| db6ecaaecb | |||
| bd13aa6f35 | |||
| 0cad7f635d | |||
| a5e6032393 | |||
| 4edc0700a1 | |||
| 019a04c0fc | |||
| 2e7b8e911f | |||
| 50f4699c95 | |||
| 3265df019a | |||
| 902721bda3 | |||
| 585724fc79 | |||
| c6578384a8 | |||
| 3b245e16db | |||
| 6d0e2cf491 |
@@ -0,0 +1,72 @@
|
||||
name: Continuous integration on dispatch
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [ self-hosted, custom-linux-exoscale ]
|
||||
# Enable sccache via environment variable
|
||||
env:
|
||||
RUSTC_WRAPPER: /home/ubuntu/.cargo/bin/sccache
|
||||
steps:
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
|
||||
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Build all binaries
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --workspace
|
||||
|
||||
- name: Run all tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --workspace --all-features
|
||||
|
||||
- name: Check formatting
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
name: Clippy checks
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features
|
||||
|
||||
- name: Run clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --workspace -- -D warnings
|
||||
|
||||
- name: Build all binaries with coconut enabled
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
args: --workspace --features=coconut
|
||||
|
||||
- name: Run all tests with coconut enabled
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --workspace --features=coconut
|
||||
|
||||
- name: Run clippy with coconut enabled
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --features=coconut -- -D warnings
|
||||
@@ -75,6 +75,12 @@ jobs:
|
||||
command: clippy
|
||||
args: --workspace --all-targets -- -D warnings
|
||||
|
||||
- name: Reclaim some disk space (because Windows is being annoying)
|
||||
uses: actions-rs/cargo@v1
|
||||
if: ${{ matrix.os == 'windows-latest' }}
|
||||
with:
|
||||
command: clean
|
||||
|
||||
# COCONUT stuff
|
||||
- name: Build all binaries with coconut enabled
|
||||
uses: actions-rs/cargo@v1
|
||||
|
||||
@@ -4,19 +4,31 @@
|
||||
|
||||
### Added
|
||||
|
||||
- wallet: require password to switch accounts
|
||||
- wallet: add simple CLI tool for decrypting and recovering the wallet file.
|
||||
- wallet: added support for multiple accounts ([#1265])
|
||||
- wallet: the wallet backend learned how to keep track of validator name, either hardcoded or by querying the status endpoint.
|
||||
- mixnet-contract: Replace all naked `-` with `saturating_sub`.
|
||||
- validator-api: add Swagger to document the REST API ([#1249]).
|
||||
- all: added network compilation target to `--help` (or `--version`) commands ([#1256]).
|
||||
- network-requester: send traffic statistics from all network requesters and receive it in a special network-requester that aggregates the data and exposes it via a rest API ([#1267], [#1278]).
|
||||
|
||||
### Fixed
|
||||
|
||||
- vesting-contract: replaced `checked_sub` with `saturating_sub` to fix the underflow in `get_vesting_tokens` ([#1275])
|
||||
- mixnet-contract: removed `expect` in `query_delegator_reward` and queries containing invalid proxy address should now return a more human-readable error ([#1257])
|
||||
- mixnet-contract: Under certain circumstances nodes could not be unbonded ([#1255](https://github.com/nymtech/nym/issues/1255)) ([#1258])
|
||||
- mixnode, gateway: attempting to determine reconnection backoff to persistently failing mixnode could result in a crash ([#1260])
|
||||
|
||||
[#1258]: https://github.com/nymtech/nym/pull/1258
|
||||
[#1249]: https://github.com/nymtech/nym/pull/1249
|
||||
[#1256]: https://github.com/nymtech/nym/pull/1256
|
||||
[#1257]: https://github.com/nymtech/nym/pull/1257
|
||||
[#1260]: https://github.com/nymtech/nym/pull/1260
|
||||
[#1265]: https://github.com/nymtech/nym/pull/1265
|
||||
[#1267]: https://github.com/nymtech/nym/pull/1267
|
||||
[#1275]: https://github.com/nymtech/nym/pull/1275
|
||||
[#1278]: https://github.com/nymtech/nym/pull/1278
|
||||
|
||||
## [nym-wallet-v1.0.4](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.4) (2022-05-04)
|
||||
|
||||
|
||||
Generated
+14
-2
@@ -648,6 +648,7 @@ dependencies = [
|
||||
name = "config"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
"handlebars",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
@@ -1062,6 +1063,8 @@ dependencies = [
|
||||
"pemstore",
|
||||
"rand 0.7.3",
|
||||
"rand_chacha 0.2.2",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"subtle-encoding",
|
||||
"x25519-dalek",
|
||||
]
|
||||
@@ -1426,6 +1429,7 @@ version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d5c4b5e5959dc2c2b89918d8e2cc40fcdd623cef026ed09d2f0ee05199dc8e4"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"signature",
|
||||
]
|
||||
|
||||
@@ -1439,6 +1443,7 @@ dependencies = [
|
||||
"ed25519",
|
||||
"rand 0.7.3",
|
||||
"serde",
|
||||
"serde_bytes",
|
||||
"sha2",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -3164,18 +3169,24 @@ dependencies = [
|
||||
name = "nym-network-requester"
|
||||
version = "1.0.1"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"clap 2.34.0",
|
||||
"dirs",
|
||||
"futures",
|
||||
"ipnetwork",
|
||||
"log",
|
||||
"network-defaults",
|
||||
"nymsphinx",
|
||||
"ordered-buffer",
|
||||
"pretty_env_logger",
|
||||
"proxy-helpers",
|
||||
"publicsuffix",
|
||||
"rand 0.7.3",
|
||||
"rocket",
|
||||
"serde",
|
||||
"socks5-requests",
|
||||
"sqlx",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"websocket-requests",
|
||||
@@ -4827,9 +4838,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_bytes"
|
||||
version = "0.11.5"
|
||||
version = "0.11.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16ae07dd2f88a366f15bd0632ba725227018c69a1c8550a927324f8eb8368bb9"
|
||||
checksum = "212e73464ebcde48d723aa02eb270ba62eff38a9b732df31f33f1b4e145f3a54"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -5218,6 +5229,7 @@ dependencies = [
|
||||
"bitflags",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"chrono",
|
||||
"crc",
|
||||
"crossbeam-queue",
|
||||
"either",
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ use proxy_helpers::connection_controller::{
|
||||
};
|
||||
use proxy_helpers::proxy_runner::ProxyRunner;
|
||||
use rand::RngCore;
|
||||
use socks5_requests::{ConnectionId, RemoteAddress, Request};
|
||||
use socks5_requests::{ConnectionId, Message, RemoteAddress, Request};
|
||||
use std::io;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
@@ -224,8 +224,9 @@ impl SocksClient {
|
||||
|
||||
async fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) {
|
||||
let req = Request::new_connect(self.connection_id, remote_address, self.self_address);
|
||||
let msg = Message::Request(req);
|
||||
|
||||
let input_message = InputMessage::new_fresh(self.service_provider, req.into_bytes(), false);
|
||||
let input_message = InputMessage::new_fresh(self.service_provider, msg.into_bytes(), false);
|
||||
self.input_sender.unbounded_send(input_message).unwrap();
|
||||
}
|
||||
|
||||
@@ -252,7 +253,8 @@ impl SocksClient {
|
||||
)
|
||||
.run(move |conn_id, read_data, socket_closed| {
|
||||
let provider_request = Request::new_send(conn_id, read_data, socket_closed);
|
||||
InputMessage::new_fresh(recipient, provider_request.into_bytes(), false)
|
||||
let provider_message = Message::Request(provider_request);
|
||||
InputMessage::new_fresh(recipient, provider_message.into_bytes(), false)
|
||||
})
|
||||
.await
|
||||
.into_inner();
|
||||
|
||||
@@ -133,20 +133,14 @@ impl Client {
|
||||
if current_attempt == 0 {
|
||||
None
|
||||
} else {
|
||||
// according to https://github.com/tokio-rs/tokio/issues/1953 there's an undocumented
|
||||
// limit of tokio delay of about 2 years.
|
||||
// let's ensure our delay is always on a sane side of being maximum 1 hour.
|
||||
let maximum_sane_delay = Duration::from_secs(60 * 60);
|
||||
let exp = 2_u32.checked_pow(current_attempt);
|
||||
let backoff = exp
|
||||
.and_then(|exp| self.config.initial_reconnection_backoff.checked_mul(exp))
|
||||
.unwrap_or(self.config.maximum_reconnection_backoff);
|
||||
|
||||
Some(std::cmp::min(
|
||||
maximum_sane_delay,
|
||||
std::cmp::min(
|
||||
self.config
|
||||
.initial_reconnection_backoff
|
||||
.checked_mul(2_u32.pow(current_attempt))
|
||||
.unwrap_or(self.config.maximum_reconnection_backoff),
|
||||
self.config.maximum_reconnection_backoff,
|
||||
),
|
||||
backoff,
|
||||
self.config.maximum_reconnection_backoff,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -254,3 +248,45 @@ impl SendWithoutResponse for Client {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn dummy_client() -> Client {
|
||||
Client::new(Config {
|
||||
initial_reconnection_backoff: Duration::from_millis(10_000),
|
||||
maximum_reconnection_backoff: Duration::from_millis(300_000),
|
||||
initial_connection_timeout: Duration::from_millis(1_500),
|
||||
maximum_connection_buffer_size: 128,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn determining_backoff_works_regardless_of_attempt() {
|
||||
let client = dummy_client();
|
||||
assert!(client.determine_backoff(0).is_none());
|
||||
assert!(client.determine_backoff(1).is_some());
|
||||
assert!(client.determine_backoff(2).is_some());
|
||||
assert_eq!(
|
||||
client.determine_backoff(16).unwrap(),
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
assert_eq!(
|
||||
client.determine_backoff(32).unwrap(),
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
assert_eq!(
|
||||
client.determine_backoff(1024).unwrap(),
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
assert_eq!(
|
||||
client.determine_backoff(65536).unwrap(),
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
assert_eq!(
|
||||
client.determine_backoff(u32::MAX).unwrap(),
|
||||
client.config.maximum_reconnection_backoff
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,6 +324,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
|
||||
Ok(ExecuteResult {
|
||||
logs: parse_raw_logs(tx_res.tx_result.log)?,
|
||||
data: tx_res.tx_result.data,
|
||||
transaction_hash: tx_res.hash,
|
||||
gas_info,
|
||||
})
|
||||
@@ -364,6 +365,7 @@ pub trait SigningCosmWasmClient: CosmWasmClient {
|
||||
|
||||
Ok(ExecuteResult {
|
||||
logs: parse_raw_logs(tx_res.tx_result.log)?,
|
||||
data: tx_res.tx_result.data,
|
||||
transaction_hash: tx_res.hash,
|
||||
gas_info,
|
||||
})
|
||||
|
||||
@@ -25,6 +25,7 @@ use cosmrs::proto::cosmwasm::wasm::v1::{
|
||||
CodeInfoResponse, ContractCodeHistoryEntry as ProtoContractCodeHistoryEntry,
|
||||
ContractCodeHistoryOperationType, ContractInfo as ProtoContractInfo,
|
||||
};
|
||||
use cosmrs::tendermint::abci::Data;
|
||||
use cosmrs::tendermint::{abci, chain};
|
||||
use cosmrs::tx::{AccountNumber, Gas, SequenceNumber};
|
||||
use cosmrs::{tx, AccountId, Any, Coin};
|
||||
@@ -672,6 +673,8 @@ pub struct MigrateResult {
|
||||
pub struct ExecuteResult {
|
||||
pub logs: Vec<Log>,
|
||||
|
||||
pub data: Data,
|
||||
|
||||
/// Transaction hash (might be used as transaction ID)
|
||||
pub transaction_hash: tx::Hash,
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cfg-if = "1.0.0"
|
||||
handlebars = "3.0.1"
|
||||
humantime-serde = "1.0"
|
||||
log = "0.4"
|
||||
|
||||
@@ -69,14 +69,16 @@ pub trait NymConfig: Default + Serialize + DeserializeOwned {
|
||||
let location = custom_location
|
||||
.unwrap_or_else(|| self.config_directory().join(Self::config_file_name()));
|
||||
|
||||
fs::write(location.clone(), templated_config)?;
|
||||
|
||||
#[cfg(unix)]
|
||||
let mut perms = fs::metadata(location.clone())?.permissions();
|
||||
#[cfg(unix)]
|
||||
perms.set_mode(0o600);
|
||||
#[cfg(unix)]
|
||||
fs::set_permissions(location, perms)?;
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(unix)] {
|
||||
fs::write(location.clone(), templated_config)?;
|
||||
let mut perms = fs::metadata(location.clone())?.permissions();
|
||||
perms.set_mode(0o600);
|
||||
fs::set_permissions(location, perms)?;
|
||||
} else {
|
||||
fs::write(location, templated_config)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ cipher = { version = "0.4.3", optional = true }
|
||||
x25519-dalek = { version = "1.1", optional = true }
|
||||
ed25519-dalek = { version = "1.0", optional = true }
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"], optional = true }
|
||||
serde_bytes = { version = "0.11.6", optional = true }
|
||||
serde_crate = { version = "1.0", optional = true, default_features = false, package = "serde" }
|
||||
subtle-encoding = { version = "0.5", features = ["bech32-preview"]}
|
||||
|
||||
# internal
|
||||
@@ -30,6 +32,7 @@ config = { path="../../common/config" }
|
||||
rand_chacha = "0.2"
|
||||
|
||||
[features]
|
||||
serde = ["serde_crate", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
|
||||
asymmetric = ["x25519-dalek", "ed25519-dalek"]
|
||||
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array"]
|
||||
symmetric = ["aes", "ctr", "cipher", "generic-array"]
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
use pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
#[cfg(feature = "rand")]
|
||||
use rand::{CryptoRng, RngCore};
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
/// Size of a X25519 private key
|
||||
@@ -127,6 +129,28 @@ impl PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for PublicKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'d> Deserialize<'d> for PublicKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'d>,
|
||||
{
|
||||
Ok(PublicKey(x25519_dalek::PublicKey::deserialize(
|
||||
deserializer,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl PemStorableKey for PublicKey {
|
||||
type Error = KeyRecoveryError;
|
||||
|
||||
@@ -143,7 +167,6 @@ impl PemStorableKey for PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PrivateKey(x25519_dalek::StaticSecret);
|
||||
|
||||
impl Display for PrivateKey {
|
||||
@@ -187,6 +210,28 @@ impl PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for PrivateKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'d> Deserialize<'d> for PrivateKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'d>,
|
||||
{
|
||||
Ok(PrivateKey(x25519_dalek::StaticSecret::deserialize(
|
||||
deserializer,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl PemStorableKey for PrivateKey {
|
||||
type Error = KeyRecoveryError;
|
||||
|
||||
|
||||
@@ -10,6 +10,13 @@ use pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::de::Error as SerdeError;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
#[cfg(feature = "serde")]
|
||||
use serde_bytes::{ByteBuf as SerdeByteBuf, Bytes as SerdeBytes};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Ed25519RecoveryError {
|
||||
MalformedBytes(SignatureError),
|
||||
@@ -40,6 +47,7 @@ impl fmt::Display for Ed25519RecoveryError {
|
||||
impl std::error::Error for Ed25519RecoveryError {}
|
||||
|
||||
/// Keypair for usage in ed25519 EdDSA.
|
||||
#[derive(Debug)]
|
||||
pub struct KeyPair {
|
||||
private_key: PrivateKey,
|
||||
public_key: PublicKey,
|
||||
@@ -135,6 +143,28 @@ impl PublicKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for PublicKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'d> Deserialize<'d> for PublicKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'d>,
|
||||
{
|
||||
Ok(PublicKey(ed25519_dalek::PublicKey::deserialize(
|
||||
deserializer,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl PemStorableKey for PublicKey {
|
||||
type Error = Ed25519RecoveryError;
|
||||
|
||||
@@ -200,6 +230,28 @@ impl PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for PrivateKey {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.0.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'d> Deserialize<'d> for PrivateKey {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'d>,
|
||||
{
|
||||
Ok(PrivateKey(ed25519_dalek::SecretKey::deserialize(
|
||||
deserializer,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl PemStorableKey for PrivateKey {
|
||||
type Error = Ed25519RecoveryError;
|
||||
|
||||
@@ -216,7 +268,7 @@ impl PemStorableKey for PrivateKey {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Signature(ed25519_dalek::Signature);
|
||||
|
||||
impl Signature {
|
||||
@@ -237,3 +289,24 @@ impl Signature {
|
||||
Ok(Signature(ed25519_dalek::Signature::from_bytes(bytes)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl Serialize for Signature {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
SerdeBytes::new(&self.to_bytes()).serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
impl<'d> Deserialize<'d> for Signature {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'d>,
|
||||
{
|
||||
let bytes = <SerdeByteBuf>::deserialize(deserializer)?;
|
||||
Signature::from_bytes(bytes.as_ref()).map_err(SerdeError::custom)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,5 +29,5 @@ pub use blake3;
|
||||
#[cfg(feature = "symmetric")]
|
||||
pub use ctr;
|
||||
|
||||
// TODO: this function uses all three modules: asymmetric crypto, symmetric crypto and derives key...,
|
||||
// so I don't know where to put it...
|
||||
#[cfg(feature = "serde")]
|
||||
extern crate serde_crate as serde;
|
||||
|
||||
@@ -56,6 +56,10 @@ impl Network {
|
||||
self.details().rewarding_validator_address
|
||||
}
|
||||
|
||||
pub fn stats_provider_network_address(&self) -> &str {
|
||||
self.details().stats_provider_network_address
|
||||
}
|
||||
|
||||
pub fn validators(&self) -> impl Iterator<Item = &ValidatorDetails> {
|
||||
self.details().validators.iter()
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ cfg_if::cfg_if! {
|
||||
if #[cfg(network = "mainnet")] {
|
||||
pub const DEFAULT_NETWORK: all::Network = all::Network::MAINNET;
|
||||
pub const DENOM: &str = mainnet::DENOM;
|
||||
pub const STAKE_DENOM: &str = mainnet::STAKE_DENOM;
|
||||
|
||||
pub const ETH_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_CONTRACT_ADDRESS;
|
||||
pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = mainnet::_ETH_ERC20_CONTRACT_ADDRESS;
|
||||
@@ -25,6 +26,7 @@ cfg_if::cfg_if! {
|
||||
} else if #[cfg(network = "qa")] {
|
||||
pub const DEFAULT_NETWORK: all::Network = all::Network::QA;
|
||||
pub const DENOM: &str = qa::DENOM;
|
||||
pub const STAKE_DENOM: &str = qa::STAKE_DENOM;
|
||||
|
||||
pub const ETH_CONTRACT_ADDRESS: [u8; 20] = qa::_ETH_CONTRACT_ADDRESS;
|
||||
pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = qa::_ETH_ERC20_CONTRACT_ADDRESS;
|
||||
@@ -32,6 +34,7 @@ cfg_if::cfg_if! {
|
||||
} else if #[cfg(network = "sandbox")] {
|
||||
pub const DEFAULT_NETWORK: all::Network = all::Network::SANDBOX;
|
||||
pub const DENOM: &str = sandbox::DENOM;
|
||||
pub const STAKE_DENOM: &str = sandbox::STAKE_DENOM;
|
||||
|
||||
pub const ETH_CONTRACT_ADDRESS: [u8; 20] = sandbox::_ETH_CONTRACT_ADDRESS;
|
||||
pub const ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = sandbox::_ETH_ERC20_CONTRACT_ADDRESS;
|
||||
@@ -49,6 +52,7 @@ pub struct DefaultNetworkDetails<'a> {
|
||||
vesting_contract_address: &'a str,
|
||||
bandwidth_claim_contract_address: &'a str,
|
||||
rewarding_validator_address: &'a str,
|
||||
stats_provider_network_address: &'a str,
|
||||
validators: Vec<ValidatorDetails>,
|
||||
}
|
||||
|
||||
@@ -60,6 +64,7 @@ static MAINNET_DEFAULTS: Lazy<DefaultNetworkDetails<'static>> =
|
||||
vesting_contract_address: mainnet::VESTING_CONTRACT_ADDRESS,
|
||||
bandwidth_claim_contract_address: mainnet::BANDWIDTH_CLAIM_CONTRACT_ADDRESS,
|
||||
rewarding_validator_address: mainnet::REWARDING_VALIDATOR_ADDRESS,
|
||||
stats_provider_network_address: mainnet::STATS_PROVIDER_CLIENT_ADDRESS,
|
||||
validators: mainnet::validators(),
|
||||
});
|
||||
|
||||
@@ -71,6 +76,7 @@ static SANDBOX_DEFAULTS: Lazy<DefaultNetworkDetails<'static>> =
|
||||
vesting_contract_address: sandbox::VESTING_CONTRACT_ADDRESS,
|
||||
bandwidth_claim_contract_address: sandbox::BANDWIDTH_CLAIM_CONTRACT_ADDRESS,
|
||||
rewarding_validator_address: sandbox::REWARDING_VALIDATOR_ADDRESS,
|
||||
stats_provider_network_address: sandbox::STATS_PROVIDER_CLIENT_ADDRESS,
|
||||
validators: sandbox::validators(),
|
||||
});
|
||||
|
||||
@@ -81,6 +87,7 @@ static QA_DEFAULTS: Lazy<DefaultNetworkDetails<'static>> = Lazy::new(|| DefaultN
|
||||
vesting_contract_address: qa::VESTING_CONTRACT_ADDRESS,
|
||||
bandwidth_claim_contract_address: qa::BANDWIDTH_CLAIM_CONTRACT_ADDRESS,
|
||||
rewarding_validator_address: qa::REWARDING_VALIDATOR_ADDRESS,
|
||||
stats_provider_network_address: qa::STATS_PROVIDER_CLIENT_ADDRESS,
|
||||
validators: qa::validators(),
|
||||
});
|
||||
|
||||
@@ -101,6 +108,13 @@ impl ValidatorDetails {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_name(nymd_url: &str, api_url: Option<&str>) -> Self {
|
||||
ValidatorDetails {
|
||||
nymd_url: nymd_url.to_string(),
|
||||
api_url: api_url.map(ToString::to_string),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nymd_url(&self) -> Url {
|
||||
self.nymd_url
|
||||
.parse()
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::ValidatorDetails;
|
||||
|
||||
pub(crate) const BECH32_PREFIX: &str = "n";
|
||||
pub const DENOM: &str = "unym";
|
||||
pub const STAKE_DENOM: &str = "unyx";
|
||||
|
||||
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str =
|
||||
"n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g";
|
||||
@@ -18,9 +19,11 @@ pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
|
||||
hex_literal::hex!("0000000000000000000000000000000000000000");
|
||||
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
|
||||
|
||||
pub(crate) const STATS_PROVIDER_CLIENT_ADDRESS: &str = "3V3me68qkEYNNShSQ5yLkrzC8rUJmcmtrTFbLKPqytEZ.7dGmnRAheEozNeGAsp9LXM8oPgS5YgJraNmYguj2t7Bn@BNjYZPxzcJwczXHHgBxCAyVJKxN6LPteDRrKapxWmexv";
|
||||
|
||||
pub(crate) fn validators() -> Vec<ValidatorDetails> {
|
||||
vec![ValidatorDetails::new(
|
||||
"https://rpc.nyx.nodes.guru/",
|
||||
Some("https://validator.nymtech.net/api"),
|
||||
Some("https://validator.nymtech.net/api/"),
|
||||
)]
|
||||
}
|
||||
|
||||
@@ -3,22 +3,27 @@
|
||||
|
||||
use crate::ValidatorDetails;
|
||||
|
||||
pub(crate) const BECH32_PREFIX: &str = "nymt";
|
||||
pub const DENOM: &str = "unymt";
|
||||
pub(crate) const BECH32_PREFIX: &str = "n";
|
||||
pub const DENOM: &str = "unym";
|
||||
pub const STAKE_DENOM: &str = "unyx";
|
||||
|
||||
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt17x6pt4msccvawgxjeg5nmnygttu56tftg5l6j3";
|
||||
pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt1t4dmskxea0avvrj8xtmu66hv7dkyg9s8059t3c";
|
||||
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str =
|
||||
"n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep";
|
||||
pub(crate) const VESTING_CONTRACT_ADDRESS: &str =
|
||||
"n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav";
|
||||
pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str =
|
||||
"nymt17p9rzwnnfxcjp32un9ug7yhhzgtkhvl9f8xzkv";
|
||||
"n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
|
||||
pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] =
|
||||
hex_literal::hex!("0000000000000000000000000000000000000000");
|
||||
pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
|
||||
hex_literal::hex!("0000000000000000000000000000000000000000");
|
||||
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "nymt1dn52nx8wv9wkqmrvj6tcmdzh4es6jt8tr7f6j9";
|
||||
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq";
|
||||
|
||||
pub(crate) const STATS_PROVIDER_CLIENT_ADDRESS: &str = "BLFPkyQ68xtR3TmrUWJZUKJF4SVwJR23wzQEmLHi2QcZ.5zms2X4ANsgY1VB4iC9kTqvbsHWmWUNSuvTtYr4Cp5qT@ExyJVqTSrgHTwzXm2r9RawfF5qYpvZjSVN2dLTs6bnWH";
|
||||
|
||||
pub(crate) fn validators() -> Vec<ValidatorDetails> {
|
||||
vec![ValidatorDetails::new(
|
||||
"https://qa-validator.nymtech.net",
|
||||
Some("https://qa-validator.nymtech.net/api"),
|
||||
Some("https://qa-validator-api.nymtech.net/api"),
|
||||
)]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::ValidatorDetails;
|
||||
|
||||
pub(crate) const BECH32_PREFIX: &str = "nymt";
|
||||
pub const DENOM: &str = "unymt";
|
||||
pub const STAKE_DENOM: &str = "unyxt";
|
||||
|
||||
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = "nymt1ghd753shjuwexxywmgs4xz7x2q732vcnstz02j";
|
||||
pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "nymt14ejqjyq8um4p3xfqj74yld5waqljf88fn549lh";
|
||||
@@ -16,6 +17,8 @@ pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
|
||||
hex_literal::hex!("E8883BAeF3869e14E4823F46662e81D4F7d2A81F");
|
||||
pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "nymt1jh0s6qu6tuw9ut438836mmn7f3f2wencrnmdj4";
|
||||
|
||||
pub(crate) const STATS_PROVIDER_CLIENT_ADDRESS: &str = "HqYWvCcB4sswYiyMj5Q8H5oc71kLf96vfrLK3npM7stH.CoeC5dcqurgdxr5zcgU77nZBSBCc8ntCiwUivQ9TX3KT@E3mvZTHQCdBvhfr178Swx9g4QG3kkRUun7YnToLMcMbM";
|
||||
|
||||
pub(crate) fn validators() -> Vec<ValidatorDetails> {
|
||||
vec![ValidatorDetails::new(
|
||||
"https://sandbox-validator.nymtech.net",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
pub mod msg;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
pub use msg::*;
|
||||
pub use request::*;
|
||||
pub use response::*;
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::request::{Request, RequestError};
|
||||
use crate::response::{Response, ResponseError};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MessageError {
|
||||
Request(RequestError),
|
||||
Response(ResponseError),
|
||||
NoData,
|
||||
UnknownMessageType,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for MessageError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MessageError::Request(r) => write!(f, "{}", r),
|
||||
MessageError::Response(r) => write!(f, "{:?}", r),
|
||||
MessageError::NoData => write!(f, "no data provided"),
|
||||
MessageError::UnknownMessageType => write!(f, "unknown message type received"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Message {
|
||||
Request(Request),
|
||||
Response(Response),
|
||||
}
|
||||
|
||||
impl Message {
|
||||
const REQUEST_FLAG: u8 = 0;
|
||||
const RESPONSE_FLAG: u8 = 1;
|
||||
|
||||
pub fn try_from_bytes(b: &[u8]) -> Result<Message, MessageError> {
|
||||
if b.is_empty() {
|
||||
return Err(MessageError::NoData);
|
||||
}
|
||||
|
||||
if b[0] == Self::REQUEST_FLAG {
|
||||
Request::try_from_bytes(&b[1..])
|
||||
.map(Message::Request)
|
||||
.map_err(MessageError::Request)
|
||||
} else if b[0] == Self::RESPONSE_FLAG {
|
||||
Response::try_from_bytes(&b[1..])
|
||||
.map(Message::Response)
|
||||
.map_err(MessageError::Response)
|
||||
} else {
|
||||
Err(MessageError::UnknownMessageType)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
match self {
|
||||
Self::Request(r) => std::iter::once(Self::REQUEST_FLAG)
|
||||
.chain(r.into_bytes().iter().cloned())
|
||||
.collect(),
|
||||
Self::Response(r) => std::iter::once(Self::RESPONSE_FLAG)
|
||||
.chain(r.into_bytes().iter().cloned())
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+1
@@ -227,6 +227,7 @@ dependencies = [
|
||||
name = "config"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"handlebars",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::rewards::helpers;
|
||||
use crate::support::helpers::is_authorized;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::{Addr, Api, Coin, DepsMut, Env, MessageInfo, Order, Response, Storage, Uint128};
|
||||
use cw_storage_plus::Bound;
|
||||
use mixnet_contract_common::events::{
|
||||
new_compound_delegator_reward_event, new_compound_operator_reward_event,
|
||||
new_mix_operator_rewarding_event, new_not_found_mix_operator_rewarding_event,
|
||||
@@ -114,9 +115,13 @@ pub fn calculate_operator_reward(
|
||||
let accumulated_rewards = mixnodes()
|
||||
.changelog()
|
||||
.prefix(bond.identity())
|
||||
.keys(storage, None, None, Order::Ascending)
|
||||
.keys(
|
||||
storage,
|
||||
Some(Bound::exclusive(last_claimed_height)),
|
||||
None,
|
||||
Order::Ascending,
|
||||
)
|
||||
.filter_map(|height| height.ok())
|
||||
.filter(|height| last_claimed_height <= *height)
|
||||
.fold(
|
||||
Ok(Uint128::zero()),
|
||||
|acc, height| -> Result<Uint128, ContractError> {
|
||||
@@ -284,32 +289,46 @@ pub fn calculate_delegator_reward(
|
||||
|
||||
// Get delegations newer then last_claimed_height, it would be nice to also fold this into the iteration bellow but it should be ok for now, as
|
||||
// I doubt folks refresh their delegations often
|
||||
let delegations = delegations_storage::delegations()
|
||||
let mut delegations = delegations_storage::delegations()
|
||||
.prefix((mix_identity.to_string(), key))
|
||||
.range(storage, None, None, Order::Descending)
|
||||
.range(
|
||||
storage,
|
||||
Some(Bound::exclusive(last_claimed_height)),
|
||||
None,
|
||||
Order::Descending,
|
||||
)
|
||||
.filter_map(|record| record.ok())
|
||||
.filter(|(height, _)| last_claimed_height <= *height)
|
||||
.map(|(_, delegation)| delegation)
|
||||
.collect::<Vec<Delegation>>();
|
||||
|
||||
// Accumulate outside of the loop to gain some speed, on a log of checkpoints
|
||||
let mut delegation_at_height = Uint128::zero();
|
||||
|
||||
// This is a bit gnarly, but we want to avoid loading all heights, the loading mixnodes, so we're doing it all in the iterator
|
||||
let accumulated_rewards = mixnodes()
|
||||
.changelog()
|
||||
.prefix(mix_identity)
|
||||
.keys(storage, None, None, Order::Ascending)
|
||||
.keys(
|
||||
storage,
|
||||
Some(Bound::exclusive(last_claimed_height)),
|
||||
None,
|
||||
Order::Ascending,
|
||||
)
|
||||
.filter_map(|height| height.ok())
|
||||
// Get all checkpoints greater then last claimed delegation height
|
||||
.filter(|height| last_claimed_height <= *height)
|
||||
.fold(
|
||||
Ok(Uint128::zero()),
|
||||
|acc, height| -> Result<Uint128, ContractError> {
|
||||
let accumulated_reward = acc?;
|
||||
let delegation_at_height = delegations
|
||||
delegation_at_height = delegations
|
||||
.iter()
|
||||
.filter(|d| d.block_height <= height)
|
||||
.fold(Uint128::zero(), |total, delegation| {
|
||||
.fold(delegation_at_height, |total, delegation| {
|
||||
total + delegation.amount.amount
|
||||
});
|
||||
// Drop what we've processed
|
||||
// This should be replaced with drain_filter once it stabilizes
|
||||
delegations.retain(|d| d.block_height > height);
|
||||
// debug_with_visibility(
|
||||
// api,
|
||||
// format!("delegation at height {} - {}", height, delegation_at_height),
|
||||
|
||||
@@ -14,24 +14,22 @@ impl VestingAccount for Account {
|
||||
env: &Env,
|
||||
storage: &dyn Storage,
|
||||
) -> Result<Coin, ContractError> {
|
||||
// Returns 0 in case of underflow.
|
||||
// Returns 0 in case of underflow. Which is fine, as the amount of pledged and delegated tokens can be larger then vesting_coins due to rewards and vesting periods expiring
|
||||
Ok(Coin {
|
||||
amount: Uint128::new(
|
||||
self.get_vesting_coins(block_time, env)?
|
||||
.amount
|
||||
.u128()
|
||||
.checked_sub(
|
||||
.saturating_sub(
|
||||
self.get_delegated_vesting(block_time, env, storage)?
|
||||
.amount
|
||||
.u128(),
|
||||
)
|
||||
.ok_or(ContractError::Underflow)?
|
||||
.checked_sub(
|
||||
.saturating_sub(
|
||||
self.get_pledged_vesting(block_time, env, storage)?
|
||||
.amount
|
||||
.u128(),
|
||||
)
|
||||
.ok_or(ContractError::Underflow)?,
|
||||
),
|
||||
),
|
||||
denom: DENOM.to_string(),
|
||||
})
|
||||
|
||||
+116
-100
@@ -1,106 +1,122 @@
|
||||
version: '3.7'
|
||||
x-bech32-prefix: &BECH32_PREFIX
|
||||
nymt
|
||||
x-wasmd-version: &WASMD_VERSION
|
||||
v0.21.0
|
||||
x-wasmd-commit-hash: &WASMD_COMMIT_HASH
|
||||
1d436638af7cacb5aeeb7248b57b085c64f3ae35
|
||||
|
||||
x-network: &NETWORK
|
||||
BECH32_PREFIX: nymt
|
||||
DENOM: nymt
|
||||
STAKE_DENOM: nyxt
|
||||
WASMD_VERSION: v0.26.0
|
||||
WASMD_COMMIT_HASH: dc5ef6fe84f0a5e3b0894692a18cc48fb5b00adf
|
||||
|
||||
services:
|
||||
genesis_validator:
|
||||
build:
|
||||
context: docker/validator
|
||||
args:
|
||||
BECH32_PREFIX: *BECH32_PREFIX
|
||||
WASMD_VERSION: *WASMD_VERSION
|
||||
WASMD_COMMIT_HASH: *WASMD_COMMIT_HASH
|
||||
image: validator:latest
|
||||
ports:
|
||||
- "26657:26657"
|
||||
- "1317:1317"
|
||||
container_name: genesis_validator
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume"
|
||||
environment:
|
||||
BECH32_PREFIX: *BECH32_PREFIX
|
||||
WASMD_VERSION: *WASMD_VERSION
|
||||
command: ["genesis"]
|
||||
secondary_validator:
|
||||
build:
|
||||
context: docker/validator
|
||||
args:
|
||||
BECH32_PREFIX: *BECH32_PREFIX
|
||||
WASMD_VERSION: *WASMD_VERSION
|
||||
image: validator:latest
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume:ro"
|
||||
environment:
|
||||
BECH32_PREFIX: *BECH32_PREFIX
|
||||
WASMD_VERSION: *WASMD_VERSION
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
command: ["secondary"]
|
||||
mixnet_contract:
|
||||
build: docker/mixnet_contract
|
||||
image: contract:latest
|
||||
volumes:
|
||||
- ".:/nym"
|
||||
vesting_contract:
|
||||
build: docker/vesting_contract
|
||||
image: vesting_contract:latest
|
||||
volumes:
|
||||
- ".:/nym"
|
||||
contract_uploader:
|
||||
build: docker/typescript_client
|
||||
image: contract_uploader:typescript
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume:ro"
|
||||
- "contract_volume:/contract_volume"
|
||||
- ".:/nym"
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
- "secondary_validator"
|
||||
- "mixnet_contract"
|
||||
environment:
|
||||
BECH32_PREFIX: *BECH32_PREFIX
|
||||
mnemonic_echo:
|
||||
build: docker/mnemonic_echo
|
||||
image: mnemonic_echo:latest
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume:ro"
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
genesis_validator:
|
||||
build:
|
||||
context: docker/validator
|
||||
args: *NETWORK
|
||||
image: validator:latest
|
||||
ports:
|
||||
- "26657:26657"
|
||||
- "1317:1317"
|
||||
container_name: genesis_validator
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume"
|
||||
- "genesis_nymd:/root/.nymd"
|
||||
environment: *NETWORK
|
||||
networks:
|
||||
localnet:
|
||||
ipv4_address: 172.168.10.2
|
||||
command: [ "genesis" ]
|
||||
secondary_validator:
|
||||
build:
|
||||
context: docker/validator
|
||||
args: *NETWORK
|
||||
image: validator:latest
|
||||
ports:
|
||||
- "36657:26657"
|
||||
- "2317:1317"
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume"
|
||||
- "secondary_nymd:/root/.nymd"
|
||||
environment: *NETWORK
|
||||
networks:
|
||||
localnet:
|
||||
ipv4_address: 172.168.10.3
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
command: [ "secondary" ]
|
||||
# mixnet_contract:
|
||||
# build: docker/mixnet_contract
|
||||
# image: contract:latest
|
||||
# volumes:
|
||||
# - ".:/nym"
|
||||
# vesting_contract:
|
||||
# build: docker/vesting_contract
|
||||
# image: vesting_contract:latest
|
||||
# volumes:
|
||||
# - ".:/nym"
|
||||
# contract_uploader:
|
||||
# build: docker/typescript_client
|
||||
# image: contract_uploader:typescript
|
||||
# volumes:
|
||||
# - "genesis_volume:/genesis_volume:ro"
|
||||
# - "contract_volume:/contract_volume"
|
||||
# - ".:/nym"
|
||||
# depends_on:
|
||||
# - "genesis_validator"
|
||||
# - "secondary_validator"
|
||||
# - "mixnet_contract"
|
||||
# environment:
|
||||
# BECH32_PREFIX: *BECH32_PREFIX
|
||||
mnemonic_echo:
|
||||
build: docker/mnemonic_echo
|
||||
image: mnemonic_echo:latest
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume:ro"
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
- "secondary_validator"
|
||||
|
||||
mongo:
|
||||
image: mongo:latest
|
||||
command:
|
||||
- --storageEngine=wiredTiger
|
||||
volumes:
|
||||
- mongo_data:/data/db
|
||||
block_explorer:
|
||||
build:
|
||||
context: https://github.com/forbole/big-dipper.git#v0.41.x-7
|
||||
image: block_explorer:v0.41.x-7
|
||||
ports:
|
||||
- "3080:3000"
|
||||
depends_on:
|
||||
- "mongo"
|
||||
environment:
|
||||
ROOT_URL: ${APP_ROOT_URL:-http://localhost}
|
||||
MONGO_URL: mongodb://mongo:27017/meteor
|
||||
PORT: 3000
|
||||
METEOR_SETTINGS: ${METEOR_SETTINGS}
|
||||
explorer:
|
||||
build:
|
||||
context: docker/explorer
|
||||
image: explorer:latest
|
||||
ports:
|
||||
- "3040:3000"
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
- "block_explorer"
|
||||
# mongo:
|
||||
# image: mongo:latest
|
||||
# command:
|
||||
# - --storageEngine=wiredTiger
|
||||
# volumes:
|
||||
# - mongo_data:/data/db
|
||||
# block_explorer:
|
||||
# build:
|
||||
# context: https://github.com/forbole/big-dipper.git#v0.41.x-7
|
||||
# image: block_explorer:v0.41.x-7
|
||||
# ports:
|
||||
# - "3080:3000"
|
||||
# depends_on:
|
||||
# - "mongo"
|
||||
# environment:
|
||||
# ROOT_URL: ${APP_ROOT_URL:-http://localhost}
|
||||
# MONGO_URL: mongodb://mongo:27017/meteor
|
||||
# PORT: 3000
|
||||
# METEOR_SETTINGS: ${METEOR_SETTINGS}
|
||||
# explorer:
|
||||
# build:
|
||||
# context: docker/explorer
|
||||
# image: explorer:latest
|
||||
# ports:
|
||||
# - "3040:3000"
|
||||
# depends_on:
|
||||
# - "genesis_validator"
|
||||
# - "block_explorer"
|
||||
|
||||
volumes:
|
||||
genesis_volume:
|
||||
contract_volume:
|
||||
mongo_data:
|
||||
genesis_volume:
|
||||
genesis_nymd:
|
||||
secondary_nymd:
|
||||
|
||||
# contract_volume:
|
||||
# mongo_data:
|
||||
|
||||
|
||||
networks:
|
||||
localnet:
|
||||
driver: bridge
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 172.168.10.0/25
|
||||
@@ -1,9 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Wait for the mnemonic to be generated
|
||||
# Wait for the mnemonic(s) to be generated
|
||||
while ! [ -s /genesis_volume/genesis_mnemonic ]; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "This is the current mnemonic:"
|
||||
while ! [ -s /genesis_volume/secondary_mnemonic ]; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "This is the current genesis mnemonic:"
|
||||
cat /genesis_volume/genesis_mnemonic
|
||||
|
||||
echo "This is the current secondary mnemonic:"
|
||||
cat /genesis_volume/secondary_mnemonic
|
||||
|
||||
@@ -6,17 +6,29 @@ PASSPHRASE=passphrase
|
||||
cd /root
|
||||
|
||||
if [ "$1" = "genesis" ]; then
|
||||
if [ ! -d "/root/.nymd" ]; then
|
||||
if [ ! -f "/root/.nymd/config/genesis.json" ]; then
|
||||
./nymd init nymnet --chain-id nymnet 2> /dev/null
|
||||
sed -i 's/minimum-gas-prices = ""/minimum-gas-prices = "0.025u'"${BECH32_PREFIX}"'"/' /root/.nymd/config/app.toml
|
||||
# staking/governance token is hardcoded in config, change this
|
||||
sed -i "s/\"stake\"/\"u${STAKE_DENOM}\"/" /root/.nymd/config/genesis.json
|
||||
sed -i 's/minimum-gas-prices = ""/minimum-gas-prices = "0.025u'"${DENOM}"'"/' /root/.nymd/config/app.toml
|
||||
sed -i '0,/enable = false/s//enable = true/g' /root/.nymd/config/app.toml
|
||||
sed -i 's/cors_allowed_origins = \[\]/cors_allowed_origins = \["*"\]/' /root/.nymd/config/config.toml
|
||||
sed -i 's/create_empty_blocks = true/create_empty_blocks = false/' /root/.nymd/config/config.toml
|
||||
sed -i 's/laddr = "tcp:\/\/127.0.0.1:26657"/laddr = "tcp:\/\/0.0.0.0:26657"/' /root/.nymd/config/config.toml
|
||||
yes "${PASSPHRASE}" | ./nymd keys add node_admin 2>&1 >/dev/null | tail -n 1 > /genesis_volume/genesis_mnemonic
|
||||
ADDRESS=$(yes "${PASSPHRASE}" | ./nymd keys show node_admin -a)
|
||||
yes "${PASSPHRASE}" | ./nymd add-genesis-account "${ADDRESS}" 1000000000000000u${BECH32_PREFIX},1000000000000000stake
|
||||
yes "${PASSPHRASE}" | ./nymd gentx node_admin 1000000000stake --chain-id nymnet 2> /dev/null
|
||||
|
||||
# create accounts
|
||||
yes "${PASSPHRASE}" | ./nymd keys add node_admin 2>&1 >/dev/null | tail -n 1 > /root/.nymd/mnemonic
|
||||
yes "${PASSPHRASE}" | ./nymd keys add secondary 2>&1 >/dev/null | tail -n 1 > /root/.nymd/secondary_mnemonic
|
||||
cp /root/.nymd/mnemonic /genesis_volume/genesis_mnemonic
|
||||
cp /root/.nymd/secondary_mnemonic /genesis_volume/secondary_mnemonic
|
||||
|
||||
# add genesis accounts with some initial tokens
|
||||
GENESIS_ADDRESS=$(yes "${PASSPHRASE}" | ./nymd keys show node_admin -a)
|
||||
SECONDARY_ADDRESS=$(yes "${PASSPHRASE}" | ./nymd keys show secondary -a)
|
||||
yes "${PASSPHRASE}" | ./nymd add-genesis-account "${GENESIS_ADDRESS}" 1000000000000000u"${DENOM}",1000000000000000u"${STAKE_DENOM}"
|
||||
yes "${PASSPHRASE}" | ./nymd add-genesis-account "${SECONDARY_ADDRESS}" 1000000000000000u"${DENOM}",1000000000000000u"${STAKE_DENOM}"
|
||||
|
||||
yes "${PASSPHRASE}" | ./nymd gentx node_admin 1000000000u"${STAKE_DENOM}" --chain-id nymnet 2> /dev/null
|
||||
./nymd collect-gentxs 2> /dev/null
|
||||
./nymd validate-genesis > /dev/null
|
||||
cp /root/.nymd/config/genesis.json /genesis_volume/genesis.json
|
||||
@@ -26,7 +38,7 @@ if [ "$1" = "genesis" ]; then
|
||||
fi
|
||||
./nymd start
|
||||
elif [ "$1" = "secondary" ]; then
|
||||
if [ ! -d "/root/.nymd" ]; then
|
||||
if [ ! -f "/root/.nymd/config/genesis.json" ]; then
|
||||
./nymd init nymnet --chain-id nym-secondary 2> /dev/null
|
||||
|
||||
# Wait until the genesis node writes the genesis.json to the shared volume
|
||||
@@ -34,16 +46,27 @@ elif [ "$1" = "secondary" ]; then
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# wait for the actual validator to start up
|
||||
sleep 5
|
||||
|
||||
cp /genesis_volume/genesis.json /root/.nymd/config/genesis.json
|
||||
GENESIS_PEER=$(cat /root/.nymd/config/genesis.json | grep '"memo"' | cut -d'"' -f 4)
|
||||
GENESIS_IP=$(cat /root/.nymd/config/genesis.json | grep '"memo"' | cut -d'@' -f2 | cut -d: -f1)
|
||||
sed -i 's/persistent_peers = ""/persistent_peers = "'"${GENESIS_PEER}"'"/' /root/.nymd/config/config.toml
|
||||
sed -i 's/minimum-gas-prices = ""/minimum-gas-prices = "0.025u'"${BECH32_PREFIX}"'"/' /root/.nymd/config/app.toml
|
||||
sed -i '0,/enable = false/s//enable = true/g' /root/.nymd/config/app.toml
|
||||
sed -i 's/cors_allowed_origins = \[\]/cors_allowed_origins = \["*"\]/' /root/.nymd/config/config.toml
|
||||
sed -i 's/create_empty_blocks = true/create_empty_blocks = false/' /root/.nymd/config/config.toml
|
||||
sed -i 's/laddr = "tcp:\/\/127.0.0.1:26657"/laddr = "tcp:\/\/0.0.0.0:26657"/' /root/.nymd/config/config.toml
|
||||
yes "${PASSPHRASE}" | ./nymd keys add node_admin 2> mnemonic > /dev/null
|
||||
|
||||
# import mnemonic generated by the genesis validator (have a local copy for ease of use)
|
||||
cp /genesis_volume/secondary_mnemonic /root/.nymd/mnemonic
|
||||
{ cat /root/.nymd/mnemonic; echo "${PASSPHRASE}"; echo "${PASSPHRASE}"; } | ./nymd keys add node_admin --recover #> /dev/null
|
||||
./nymd validate-genesis > /dev/null
|
||||
|
||||
# create validator
|
||||
# don't even ask about those sleeps...
|
||||
{ echo "${PASSPHRASE}"; sleep 10; yes; sleep 10; } | ./nymd tx staking create-validator --amount=10000000u"${STAKE_DENOM}" --fees 100000u"${DENOM}" --pubkey="$(./nymd tendermint show-validator)" --moniker="secondary" --commission-rate="0.10" --commission-max-rate="0.20" --commission-max-change-rate="0.01" --min-self-delegation="1" --chain-id=nymnet --from=node_admin -b async --node http://"${GENESIS_IP}":26657
|
||||
else
|
||||
echo "Validator already initialized, starting with the existing configuration."
|
||||
echo "If you want to re-init the validator, destroy the existing container"
|
||||
|
||||
@@ -98,7 +98,7 @@ export const BondBreakdownTable: React.FC = () => {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell align="left">Pledge total</TableCell>
|
||||
<TableCell align="left">Self</TableCell>
|
||||
<TableCell align="left" data-testid="pledge-total-amount">
|
||||
{bonds.pledges}
|
||||
</TableCell>
|
||||
|
||||
@@ -12,6 +12,7 @@ export type MixnodeRowType = {
|
||||
host: string;
|
||||
layer: string;
|
||||
profit_percentage: string;
|
||||
avg_uptime: string;
|
||||
};
|
||||
|
||||
export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] {
|
||||
@@ -35,5 +36,6 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
|
||||
host: item?.mix_node?.host || '',
|
||||
layer: item?.layer || '',
|
||||
profit_percentage: `${profitPercentage}%`,
|
||||
avg_uptime: `${item.avg_uptime}%` || '-',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -231,6 +231,23 @@ export const PageMixnodes: React.FC = () => {
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'avg_uptime',
|
||||
headerName: 'Average Uptime',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Average Uptime" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 160,
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnode/${params.row.identity_key}`}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePageSize = (event: SelectChangeEvent<string>) => {
|
||||
|
||||
@@ -83,6 +83,7 @@ export interface MixNodeResponseItem {
|
||||
two_letter_iso_country_code: string;
|
||||
};
|
||||
mix_node: MixNode;
|
||||
avg_uptime: number;
|
||||
}
|
||||
|
||||
export type MixNodeResponse = MixNodeResponseItem[];
|
||||
|
||||
Generated
+96
-6
@@ -75,9 +75,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.55"
|
||||
version = "1.0.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd"
|
||||
checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
@@ -87,7 +87,18 @@ checksum = "25df3c03f1040d0069fcd3907e24e36d59f9b6fa07ba49be0eb25a794f036ba7"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"password-hash",
|
||||
"password-hash 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argon2"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a27e27b63e4a34caee411ade944981136fdfa535522dc9944d6700196cbd899f"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"blake2",
|
||||
"password-hash 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -619,6 +630,45 @@ dependencies = [
|
||||
"generic-array 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags",
|
||||
"clap_derive",
|
||||
"clap_lex",
|
||||
"indexmap",
|
||||
"lazy_static",
|
||||
"strsim 0.10.0",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "3.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c"
|
||||
dependencies = [
|
||||
"heck 0.4.0",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloudabi"
|
||||
version = "0.0.3"
|
||||
@@ -694,6 +744,7 @@ dependencies = [
|
||||
name = "config"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"handlebars",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
@@ -2949,7 +3000,7 @@ name = "nym_wallet"
|
||||
version = "1.0.4"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"argon2",
|
||||
"argon2 0.3.4",
|
||||
"base64",
|
||||
"bip39",
|
||||
"cfg-if",
|
||||
@@ -2965,6 +3016,7 @@ dependencies = [
|
||||
"itertools",
|
||||
"log",
|
||||
"mixnet-contract-common",
|
||||
"once_cell",
|
||||
"pretty_env_logger",
|
||||
"rand 0.6.5",
|
||||
"reqwest",
|
||||
@@ -3106,6 +3158,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
|
||||
|
||||
[[package]]
|
||||
name = "pairing"
|
||||
version = "0.20.0"
|
||||
@@ -3182,6 +3240,17 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-hash"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e029e94abc8fb0065241c308f1ac6bc8d20f450e8f7c5f0b25cd9b8d526ba294"
|
||||
dependencies = [
|
||||
"base64ct",
|
||||
"rand_core 0.6.3",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.6"
|
||||
@@ -4260,9 +4329,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.79"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95"
|
||||
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
|
||||
dependencies = [
|
||||
"itoa 1.0.1",
|
||||
"ryu",
|
||||
@@ -5049,6 +5118,12 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
|
||||
|
||||
[[package]]
|
||||
name = "thin-slice"
|
||||
version = "0.1.1"
|
||||
@@ -5498,6 +5573,21 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wallet-recovery-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
"argon2 0.4.0",
|
||||
"base64",
|
||||
"bip39",
|
||||
"clap",
|
||||
"log",
|
||||
"pretty_env_logger",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.0"
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
[workspace]
|
||||
members = ["src-tauri"]
|
||||
|
||||
resolver = "2"
|
||||
members = [
|
||||
"src-tauri",
|
||||
"wallet-recovery-cli",
|
||||
]
|
||||
|
||||
@@ -42,7 +42,9 @@
|
||||
"react-hook-form": "^7.14.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"semver": "^6.3.0",
|
||||
"string-to-color": "^2.2.2",
|
||||
"use-clipboard-copy": "^0.2.0",
|
||||
"uuid": "^8.3.2",
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -71,6 +73,7 @@
|
||||
"@types/react-router": "^5.1.18",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/semver": "^7.3.8",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
|
||||
@@ -28,6 +28,7 @@ eyre = "0.6.5"
|
||||
futures = "0.3.15"
|
||||
itertools = "0.10"
|
||||
log = "0.4"
|
||||
once_cell = "1.7.2"
|
||||
pretty_env_logger = "0.4"
|
||||
rand = "0.6.5"
|
||||
reqwest = "0.11.9"
|
||||
|
||||
@@ -57,7 +57,7 @@ pub struct NetworkConfig {
|
||||
|
||||
// Additional user provided validators.
|
||||
// It is an option for the purpuse of file serialization.
|
||||
validator_urls: Option<Vec<ValidatorUrl>>,
|
||||
validator_urls: Option<Vec<ValidatorConfigEntry>>,
|
||||
}
|
||||
|
||||
impl Default for Base {
|
||||
@@ -89,7 +89,7 @@ impl Default for NetworkConfig {
|
||||
}
|
||||
|
||||
impl NetworkConfig {
|
||||
fn validators(&self) -> impl Iterator<Item = &ValidatorUrl> {
|
||||
fn validators(&self) -> impl Iterator<Item = &ValidatorConfigEntry> {
|
||||
self.validator_urls.iter().flat_map(|v| v.iter())
|
||||
}
|
||||
}
|
||||
@@ -192,7 +192,7 @@ impl Config {
|
||||
pub fn get_base_validators(
|
||||
&self,
|
||||
network: WalletNetwork,
|
||||
) -> impl Iterator<Item = ValidatorUrl> + '_ {
|
||||
) -> impl Iterator<Item = ValidatorConfigEntry> + '_ {
|
||||
self.base.networks.validators(network.into()).map(|v| {
|
||||
v.clone()
|
||||
.try_into()
|
||||
@@ -203,7 +203,7 @@ impl Config {
|
||||
pub fn get_configured_validators(
|
||||
&self,
|
||||
network: WalletNetwork,
|
||||
) -> impl Iterator<Item = ValidatorUrl> + '_ {
|
||||
) -> impl Iterator<Item = ValidatorConfigEntry> + '_ {
|
||||
self
|
||||
.networks
|
||||
.get(&network.as_key())
|
||||
@@ -272,7 +272,7 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_selected_validator_nymd_url(&self, network: &WalletNetwork) -> Option<Url> {
|
||||
pub fn get_selected_validator_nymd_url(&self, network: WalletNetwork) -> Option<Url> {
|
||||
self
|
||||
.networks
|
||||
.get(&network.as_key())
|
||||
@@ -286,7 +286,7 @@ impl Config {
|
||||
.and_then(|config| config.selected_api_url.clone())
|
||||
}
|
||||
|
||||
pub fn add_validator_url(&mut self, url: ValidatorUrl, network: WalletNetwork) {
|
||||
pub fn add_validator_url(&mut self, url: ValidatorConfigEntry, network: WalletNetwork) {
|
||||
if let Some(network_config) = self.networks.get_mut(&network.as_key()) {
|
||||
if let Some(ref mut urls) = network_config.validator_urls {
|
||||
urls.push(url);
|
||||
@@ -304,7 +304,7 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_validator_url(&mut self, url: ValidatorUrl, network: WalletNetwork) {
|
||||
pub fn remove_validator_url(&mut self, url: ValidatorConfigEntry, network: WalletNetwork) {
|
||||
if let Some(network_config) = self.networks.get_mut(&network.as_key()) {
|
||||
if let Some(ref mut urls) = network_config.validator_urls {
|
||||
// Removes duplicates too if there are any
|
||||
@@ -325,17 +325,19 @@ where
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub struct ValidatorUrl {
|
||||
pub struct ValidatorConfigEntry {
|
||||
pub nymd_url: Url,
|
||||
pub nymd_name: Option<String>,
|
||||
pub api_url: Option<Url>,
|
||||
}
|
||||
|
||||
impl TryFrom<ValidatorDetails> for ValidatorUrl {
|
||||
impl TryFrom<ValidatorDetails> for ValidatorConfigEntry {
|
||||
type Error = BackendError;
|
||||
|
||||
fn try_from(validator: ValidatorDetails) -> Result<Self, Self::Error> {
|
||||
Ok(ValidatorUrl {
|
||||
Ok(ValidatorConfigEntry {
|
||||
nymd_url: validator.nymd_url.parse()?,
|
||||
nymd_name: None,
|
||||
api_url: match &validator.api_url {
|
||||
Some(url) => Some(url.parse()?),
|
||||
None => None,
|
||||
@@ -344,12 +346,13 @@ impl TryFrom<ValidatorDetails> for ValidatorUrl {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<network_config::Validator> for ValidatorUrl {
|
||||
impl TryFrom<network_config::Validator> for ValidatorConfigEntry {
|
||||
type Error = BackendError;
|
||||
|
||||
fn try_from(validator: network_config::Validator) -> Result<Self, Self::Error> {
|
||||
Ok(ValidatorUrl {
|
||||
Ok(ValidatorConfigEntry {
|
||||
nymd_url: validator.nymd_url.parse()?,
|
||||
nymd_name: validator.nymd_name,
|
||||
api_url: match &validator.api_url {
|
||||
Some(url) => Some(url.parse()?),
|
||||
None => None,
|
||||
@@ -358,14 +361,21 @@ impl TryFrom<network_config::Validator> for ValidatorUrl {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ValidatorUrl {
|
||||
impl fmt::Display for ValidatorConfigEntry {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s1 = format!("nymd_url: {}", self.nymd_url);
|
||||
let name = self.nymd_name.as_ref().map(|name| format!(" ({})", name));
|
||||
let s2 = self
|
||||
.api_url
|
||||
.as_ref()
|
||||
.map(|url| format!(", api_url: {}", url));
|
||||
write!(f, " {}{},", s1, s2.unwrap_or_default())
|
||||
write!(
|
||||
f,
|
||||
" {}{}{},",
|
||||
s1,
|
||||
name.unwrap_or_default(),
|
||||
s2.unwrap_or_default()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,13 +384,13 @@ impl fmt::Display for ValidatorUrl {
|
||||
pub struct OptionalValidators {
|
||||
// User supplied additional validator urls in addition to the hardcoded ones.
|
||||
// These are separate fields, rather than a map, to force the serialization order.
|
||||
mainnet: Option<Vec<ValidatorUrl>>,
|
||||
sandbox: Option<Vec<ValidatorUrl>>,
|
||||
qa: Option<Vec<ValidatorUrl>>,
|
||||
mainnet: Option<Vec<ValidatorConfigEntry>>,
|
||||
sandbox: Option<Vec<ValidatorConfigEntry>>,
|
||||
qa: Option<Vec<ValidatorConfigEntry>>,
|
||||
}
|
||||
|
||||
impl OptionalValidators {
|
||||
pub fn validators(&self, network: WalletNetwork) -> impl Iterator<Item = &ValidatorUrl> {
|
||||
pub fn validators(&self, network: WalletNetwork) -> impl Iterator<Item = &ValidatorConfigEntry> {
|
||||
match network {
|
||||
WalletNetwork::MAINNET => self.mainnet.as_ref(),
|
||||
WalletNetwork::SANDBOX => self.sandbox.as_ref(),
|
||||
@@ -422,16 +432,19 @@ mod tests {
|
||||
selected_api_url: Some("https://my_api_url.com".parse().unwrap()),
|
||||
|
||||
validator_urls: Some(vec![
|
||||
ValidatorUrl {
|
||||
ValidatorConfigEntry {
|
||||
nymd_url: "https://foo".parse().unwrap(),
|
||||
nymd_name: Some("FooName".to_string()),
|
||||
api_url: None,
|
||||
},
|
||||
ValidatorUrl {
|
||||
ValidatorConfigEntry {
|
||||
nymd_url: "https://bar".parse().unwrap(),
|
||||
nymd_name: None,
|
||||
api_url: Some("https://bar/api".parse().unwrap()),
|
||||
},
|
||||
ValidatorUrl {
|
||||
ValidatorConfigEntry {
|
||||
nymd_url: "https://baz".parse().unwrap(),
|
||||
nymd_name: None,
|
||||
api_url: Some("https://baz/api".parse().unwrap()),
|
||||
},
|
||||
]),
|
||||
@@ -458,6 +471,7 @@ selected_api_url = 'https://my_api_url.com/'
|
||||
|
||||
[[validator_urls]]
|
||||
nymd_url = 'https://foo/'
|
||||
nymd_name = 'FooName'
|
||||
|
||||
[[validator_urls]]
|
||||
nymd_url = 'https://bar/'
|
||||
@@ -469,6 +483,39 @@ api_url = 'https://baz/api'
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_to_json() {
|
||||
let config = test_config();
|
||||
let netconfig = &config.networks[&WalletNetwork::MAINNET.as_key()];
|
||||
println!("{}", serde_json::to_string_pretty(netconfig).unwrap());
|
||||
assert_eq!(
|
||||
serde_json::to_string_pretty(netconfig).unwrap(),
|
||||
r#"{
|
||||
"version": 1,
|
||||
"selected_nymd_url": null,
|
||||
"selected_api_url": "https://my_api_url.com/",
|
||||
"validator_urls": [
|
||||
{
|
||||
"nymd_url": "https://foo/",
|
||||
"nymd_name": "FooName",
|
||||
"api_url": null
|
||||
},
|
||||
{
|
||||
"nymd_url": "https://bar/",
|
||||
"nymd_name": null,
|
||||
"api_url": "https://bar/api"
|
||||
},
|
||||
{
|
||||
"nymd_url": "https://baz/",
|
||||
"nymd_name": null,
|
||||
"api_url": "https://baz/api"
|
||||
}
|
||||
]
|
||||
}"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_and_deserialize_to_toml() {
|
||||
let config = test_config();
|
||||
@@ -513,6 +560,6 @@ api_url = 'https://baz/api'
|
||||
.next()
|
||||
.and_then(|v| v.api_url)
|
||||
.unwrap();
|
||||
assert_eq!(api_url.as_ref(), "https://validator.nymtech.net/api",);
|
||||
assert_eq!(api_url.as_ref(), "https://validator.nymtech.net/api/",);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,12 +83,22 @@ pub enum BackendError {
|
||||
WalletFileAlreadyExists,
|
||||
#[error("The wallet file is not found")]
|
||||
WalletFileNotFound,
|
||||
#[error("Account ID not found in wallet")]
|
||||
NoSuchIdInWallet,
|
||||
#[error("Account ID already found in wallet")]
|
||||
IdAlreadyExistsInWallet,
|
||||
#[error("Login ID not found in wallet")]
|
||||
WalletNoSuchLoginId,
|
||||
#[error("Account ID not found in wallet login")]
|
||||
WalletNoSuchAccountIdInWalletLogin,
|
||||
#[error("Login ID already found in wallet")]
|
||||
WalletLoginIdAlreadyExists,
|
||||
#[error("Account ID already found in wallet login")]
|
||||
WalletAccountIdAlreadyExistsInWalletLogin,
|
||||
#[error("Adding a different password to the wallet not currently supported")]
|
||||
WalletDifferentPasswordDetected,
|
||||
#[error("Unexpted mnemonic account for login")]
|
||||
WalletUnexpectedMnemonicAccount,
|
||||
#[error("Unexpted multiple account entry for login")]
|
||||
WalletUnexpectedMultipleAccounts,
|
||||
#[error("Failed to derive address from mnemonic")]
|
||||
FailedToDeriveAddress,
|
||||
}
|
||||
|
||||
impl Serialize for BackendError {
|
||||
|
||||
@@ -35,15 +35,20 @@ fn main() {
|
||||
tauri::Builder::default()
|
||||
.manage(Arc::new(RwLock::new(State::default())))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
mixnet::account::add_account_for_password,
|
||||
mixnet::account::connect_with_mnemonic,
|
||||
mixnet::account::create_new_account,
|
||||
mixnet::account::create_new_mnemonic,
|
||||
mixnet::account::create_password,
|
||||
mixnet::account::does_password_file_exist,
|
||||
mixnet::account::get_balance,
|
||||
mixnet::account::list_accounts,
|
||||
mixnet::account::logout,
|
||||
mixnet::account::remove_account_for_password,
|
||||
mixnet::account::remove_password,
|
||||
mixnet::account::show_mnemonic_for_account_in_password,
|
||||
mixnet::account::sign_in_with_password,
|
||||
mixnet::account::sign_in_with_password_and_account_id,
|
||||
mixnet::account::switch_network,
|
||||
mixnet::account::validate_mnemonic,
|
||||
mixnet::admin::get_contract_settings,
|
||||
|
||||
@@ -9,18 +9,30 @@ use serde::{Deserialize, Serialize};
|
||||
use std::{fmt, sync::Arc};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
// When the UI queries validator urls we use this type
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurls.ts"))]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ValidatorUrls {
|
||||
pub urls: Vec<String>,
|
||||
pub urls: Vec<ValidatorUrl>,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurl.ts"))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ValidatorUrl {
|
||||
pub url: String,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
// The type used when adding or removing validators, effectively the input.
|
||||
// NOTE: we should consider if we want to split this up
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/validatorurls.ts"))]
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Validator {
|
||||
pub nymd_url: String,
|
||||
pub nymd_name: Option<String>,
|
||||
pub api_url: Option<String>,
|
||||
}
|
||||
|
||||
@@ -42,10 +54,7 @@ pub async fn get_validator_nymd_urls(
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<ValidatorUrls, BackendError> {
|
||||
let state = state.read().await;
|
||||
let urls: Vec<String> = state
|
||||
.get_nymd_urls(network)
|
||||
.map(|url| url.to_string())
|
||||
.collect();
|
||||
let urls: Vec<ValidatorUrl> = state.get_nymd_urls(network).collect();
|
||||
Ok(ValidatorUrls { urls })
|
||||
}
|
||||
|
||||
@@ -55,10 +64,7 @@ pub async fn get_validator_api_urls(
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<ValidatorUrls, BackendError> {
|
||||
let state = state.read().await;
|
||||
let urls: Vec<String> = state
|
||||
.get_api_urls(network)
|
||||
.map(|url| url.to_string())
|
||||
.collect();
|
||||
let urls: Vec<ValidatorUrl> = state.get_api_urls(network).collect();
|
||||
Ok(ValidatorUrls { urls })
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ use crate::error::BackendError;
|
||||
use crate::network::Network as WalletNetwork;
|
||||
use crate::network_config;
|
||||
use crate::nymd_client;
|
||||
use crate::state::State;
|
||||
use crate::wallet_storage::{self, DEFAULT_WALLET_ACCOUNT_ID};
|
||||
use crate::state::{State, WalletAccountIds};
|
||||
use crate::wallet_storage::{self, DEFAULT_LOGIN_ID};
|
||||
|
||||
use bip39::{Language, Mnemonic};
|
||||
use config::defaults::all::Network;
|
||||
@@ -21,6 +21,7 @@ use std::sync::Arc;
|
||||
use strum::IntoEnumIterator;
|
||||
use tokio::sync::RwLock;
|
||||
use url::Url;
|
||||
use validator_client::nymd::wallet::{AccountData, DirectSecp256k1HdWallet};
|
||||
|
||||
use validator_client::{nymd::SigningNymdClient, Client};
|
||||
|
||||
@@ -51,6 +52,14 @@ pub struct CreatedAccount {
|
||||
mnemonic: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/createdaccount.ts"))]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct AccountEntry {
|
||||
id: String,
|
||||
address: String,
|
||||
}
|
||||
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(test, ts(export, export_to = "../src/types/rust/balance.ts"))]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@@ -107,9 +116,8 @@ pub async fn create_new_account(
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_new_mnemonic() -> Result<String, BackendError> {
|
||||
let rand_mnemonic = random_mnemonic();
|
||||
Ok(rand_mnemonic.to_string())
|
||||
pub fn create_new_mnemonic() -> String {
|
||||
random_mnemonic().to_string()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -169,7 +177,7 @@ async fn _connect_with_mnemonic(
|
||||
for network in WalletNetwork::iter() {
|
||||
log::debug!(
|
||||
"List of validators for {network}: [\n{}\n]",
|
||||
state.get_validators(network).format(",\n")
|
||||
state.get_config_validator_entries(network).format(",\n")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -221,6 +229,10 @@ async fn _connect_with_mnemonic(
|
||||
};
|
||||
|
||||
// Register all the clients
|
||||
{
|
||||
let mut w_state = state.write().await;
|
||||
w_state.logout();
|
||||
}
|
||||
for client in clients {
|
||||
let network: WalletNetwork = client.network.into();
|
||||
let mut w_state = state.write().await;
|
||||
@@ -268,7 +280,7 @@ fn create_clients(
|
||||
) -> Result<Vec<Client<SigningNymdClient>>, BackendError> {
|
||||
let mut clients = Vec::new();
|
||||
for network in WalletNetwork::iter() {
|
||||
let nymd_url = if let Some(url) = config.get_selected_validator_nymd_url(&network) {
|
||||
let nymd_url = if let Some(url) = config.get_selected_validator_nymd_url(network) {
|
||||
log::debug!("Using selected nymd_url for {network}: {url}");
|
||||
url.clone()
|
||||
} else {
|
||||
@@ -353,18 +365,18 @@ pub fn does_password_file_exist() -> Result<bool, BackendError> {
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn create_password(mnemonic: String, password: String) -> Result<(), BackendError> {
|
||||
pub fn create_password(mnemonic: &str, password: String) -> Result<(), BackendError> {
|
||||
if does_password_file_exist()? {
|
||||
return Err(BackendError::WalletFileAlreadyExists);
|
||||
}
|
||||
log::info!("Creating password");
|
||||
|
||||
let mnemonic = Mnemonic::from_str(&mnemonic)?;
|
||||
let mnemonic = Mnemonic::from_str(mnemonic)?;
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
// Currently we only support a single, default, login id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password);
|
||||
wallet_storage::store_wallet_login_information(mnemonic, hd_path, id, &password)
|
||||
wallet_storage::store_login_with_multiple_accounts(mnemonic, hd_path, login_id, &password)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -375,15 +387,301 @@ pub async fn sign_in_with_password(
|
||||
log::info!("Signing in with password");
|
||||
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password);
|
||||
let stored_account = wallet_storage::load_existing_wallet_login_information(&id, &password)?;
|
||||
_connect_with_mnemonic(stored_account.mnemonic().clone(), state).await
|
||||
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
|
||||
|
||||
let mnemonic = extract_first_mnemonic(&stored_login)?;
|
||||
let first_login_id_when_converting = login_id.into();
|
||||
set_state_with_all_accounts(stored_login, first_login_id_when_converting, state.clone()).await?;
|
||||
|
||||
_connect_with_mnemonic(mnemonic, state).await
|
||||
}
|
||||
|
||||
fn extract_first_mnemonic(
|
||||
stored_login: &wallet_storage::StoredLogin,
|
||||
) -> Result<Mnemonic, BackendError> {
|
||||
let mnemonic = match stored_login {
|
||||
wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(),
|
||||
wallet_storage::StoredLogin::Multiple(ref accounts) => {
|
||||
// Login using the first account in the list
|
||||
accounts
|
||||
.get_accounts()
|
||||
.next()
|
||||
.ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)?
|
||||
.mnemonic()
|
||||
.clone()
|
||||
}
|
||||
};
|
||||
|
||||
Ok(mnemonic)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn sign_in_with_password_and_account_id(
|
||||
account_id: &str,
|
||||
password: &str,
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<Account, BackendError> {
|
||||
log::info!("Signing in with password");
|
||||
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password.to_string());
|
||||
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
|
||||
|
||||
let mnemonic = extract_mnemonic(&stored_login, &account_id)?;
|
||||
let first_login_id_when_converting = login_id.into();
|
||||
set_state_with_all_accounts(stored_login, first_login_id_when_converting, state.clone()).await?;
|
||||
|
||||
_connect_with_mnemonic(mnemonic, state).await
|
||||
}
|
||||
|
||||
fn extract_mnemonic(
|
||||
stored_login: &wallet_storage::StoredLogin,
|
||||
account_id: &wallet_storage::AccountId,
|
||||
) -> Result<Mnemonic, BackendError> {
|
||||
let mnemonic = match stored_login {
|
||||
wallet_storage::StoredLogin::Mnemonic(_) => {
|
||||
return Err(BackendError::WalletNoSuchAccountIdInWalletLogin);
|
||||
}
|
||||
wallet_storage::StoredLogin::Multiple(ref accounts) => accounts
|
||||
.get_account(account_id)
|
||||
.ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)?
|
||||
.mnemonic()
|
||||
.clone(),
|
||||
};
|
||||
Ok(mnemonic)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn remove_password() -> Result<(), BackendError> {
|
||||
log::info!("Removing password");
|
||||
let id = wallet_storage::WalletAccountId::new(DEFAULT_WALLET_ACCOUNT_ID.to_string());
|
||||
wallet_storage::remove_wallet_login_information(&id)
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
wallet_storage::remove_login(&login_id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn add_account_for_password(
|
||||
mnemonic: &str,
|
||||
password: &str,
|
||||
account_id: &str,
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<AccountEntry, BackendError> {
|
||||
log::info!("Adding account for the current password: {account_id}");
|
||||
let mnemonic = Mnemonic::from_str(mnemonic)?;
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
// Currently we only support a single, default, login id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password.to_string());
|
||||
|
||||
wallet_storage::append_account_to_login(
|
||||
mnemonic.clone(),
|
||||
hd_path,
|
||||
login_id.clone(),
|
||||
account_id.clone(),
|
||||
&password,
|
||||
)?;
|
||||
|
||||
let address = {
|
||||
let state = state.read().await;
|
||||
let network: Network = state.current_network().into();
|
||||
derive_address(mnemonic, network.bech32_prefix())?.to_string()
|
||||
};
|
||||
|
||||
// Re-read all the acccounts from the wallet to reset the state, rather than updating it
|
||||
// incrementally
|
||||
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
|
||||
// NOTE: since we are appending, this id shouldn't be needed, but setting the state is supposed
|
||||
// to be a general function
|
||||
let first_id_when_converting = login_id.into();
|
||||
set_state_with_all_accounts(stored_login, first_id_when_converting, state).await?;
|
||||
|
||||
Ok(AccountEntry {
|
||||
id: account_id.to_string(),
|
||||
address,
|
||||
})
|
||||
}
|
||||
|
||||
// The first `AccoundId` when converting is the `LoginId` for the entry that was loaded.
|
||||
async fn set_state_with_all_accounts(
|
||||
stored_login: wallet_storage::StoredLogin,
|
||||
first_id_when_converting: wallet_storage::AccountId,
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<(), BackendError> {
|
||||
log::trace!("Set state with accounts:");
|
||||
let all_accounts: Vec<_> = stored_login
|
||||
.unwrap_into_multiple_accounts(first_id_when_converting)
|
||||
.into_accounts()
|
||||
.collect();
|
||||
|
||||
for account in &all_accounts {
|
||||
log::trace!("account: {:?}", account.id());
|
||||
}
|
||||
|
||||
let all_account_ids: Vec<WalletAccountIds> = all_accounts
|
||||
.iter()
|
||||
.map(|account| {
|
||||
let mnemonic = account.mnemonic();
|
||||
let addresses: HashMap<WalletNetwork, cosmrs::AccountId> = WalletNetwork::iter()
|
||||
.map(|network| {
|
||||
let config_network: Network = network.into();
|
||||
(
|
||||
network,
|
||||
derive_address(mnemonic.clone(), config_network.bech32_prefix()).unwrap(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
WalletAccountIds {
|
||||
id: account.id().clone(),
|
||||
addresses,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut w_state = state.write().await;
|
||||
w_state.set_all_accounts(all_account_ids);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn remove_account_for_password(
|
||||
password: &str,
|
||||
account_id: &str,
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<(), BackendError> {
|
||||
log::info!("Removing account: {account_id}");
|
||||
// Currently we only support a single, default, id in the wallet
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id.to_string());
|
||||
let password = wallet_storage::UserPassword::new(password.to_string());
|
||||
wallet_storage::remove_account_from_login(&login_id, &account_id, &password)?;
|
||||
|
||||
// Load to reset the internal state
|
||||
let stored_login = wallet_storage::load_existing_login(&login_id, &password)?;
|
||||
// NOTE: Since we removed from a multi-account login, this id shouldn't be needed, but setting
|
||||
// the state is supposed to be a general function
|
||||
let first_account_id_when_converting = login_id.into();
|
||||
set_state_with_all_accounts(stored_login, first_account_id_when_converting, state).await
|
||||
}
|
||||
|
||||
fn derive_address(
|
||||
mnemonic: bip39::Mnemonic,
|
||||
prefix: &str,
|
||||
) -> Result<cosmrs::AccountId, BackendError> {
|
||||
DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic)?
|
||||
.try_derive_accounts()?
|
||||
.first()
|
||||
.map(AccountData::address)
|
||||
.cloned()
|
||||
.ok_or(BackendError::FailedToDeriveAddress)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_accounts(
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
) -> Result<Vec<AccountEntry>, BackendError> {
|
||||
log::trace!("Listing accounts");
|
||||
let state = state.read().await;
|
||||
let network = state.current_network();
|
||||
|
||||
let all_accounts = state
|
||||
.get_all_accounts()
|
||||
.map(|account| AccountEntry {
|
||||
id: account.id.to_string(),
|
||||
address: account.addresses[&network].to_string(),
|
||||
})
|
||||
.map(|account| {
|
||||
log::trace!("{:?}", account);
|
||||
account
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(all_accounts)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn show_mnemonic_for_account_in_password(
|
||||
account_id: String,
|
||||
password: String,
|
||||
) -> Result<String, BackendError> {
|
||||
log::info!("Getting mnemonic for: {account_id}");
|
||||
let login_id = wallet_storage::LoginId::new(DEFAULT_LOGIN_ID.to_string());
|
||||
let account_id = wallet_storage::AccountId::new(account_id);
|
||||
let password = wallet_storage::UserPassword::new(password);
|
||||
let mnemonic = _show_mnemonic_for_account_in_password(&login_id, &account_id, &password)?;
|
||||
Ok(mnemonic.to_string())
|
||||
}
|
||||
|
||||
fn _show_mnemonic_for_account_in_password(
|
||||
login_id: &wallet_storage::LoginId,
|
||||
account_id: &wallet_storage::AccountId,
|
||||
password: &wallet_storage::UserPassword,
|
||||
) -> Result<bip39::Mnemonic, BackendError> {
|
||||
let stored_account = wallet_storage::load_existing_login(login_id, password)?;
|
||||
let mnemonic = match stored_account {
|
||||
wallet_storage::StoredLogin::Mnemonic(ref account) => account.mnemonic().clone(),
|
||||
wallet_storage::StoredLogin::Multiple(ref accounts) => {
|
||||
for account in accounts.get_accounts() {
|
||||
log::debug!("{:?}", account);
|
||||
}
|
||||
accounts
|
||||
.get_account(account_id)
|
||||
.ok_or(BackendError::WalletNoSuchAccountIdInWalletLogin)?
|
||||
.mnemonic()
|
||||
.clone()
|
||||
}
|
||||
};
|
||||
Ok(mnemonic)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::wallet_storage::{
|
||||
self,
|
||||
account_data::{MnemonicAccount, WalletAccount},
|
||||
};
|
||||
|
||||
// This decryptes a stored wallet file using the same procedure as when signing in. Most tests
|
||||
// related to the encryped wallet storage is in `wallet_storage`.
|
||||
#[test]
|
||||
fn decrypt_stored_wallet_for_sign_in() {
|
||||
const SAVED_WALLET: &str = "src/wallet_storage/test-data/saved-wallet.json";
|
||||
let wallet_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(SAVED_WALLET);
|
||||
let login_id = wallet_storage::LoginId::new("first".to_string());
|
||||
let account_id = wallet_storage::AccountId::new("first".to_string());
|
||||
let password = wallet_storage::UserPassword::new("password".to_string());
|
||||
let hd_path: DerivationPath = COSMOS_DERIVATION_PATH.parse().unwrap();
|
||||
|
||||
let stored_login =
|
||||
wallet_storage::load_existing_login_at_file(&wallet_file, &login_id, &password).unwrap();
|
||||
let mnemonic = extract_first_mnemonic(&stored_login).unwrap();
|
||||
|
||||
let expected_mnemonic = bip39::Mnemonic::from_str("country mean universe text phone begin deputy reject result good cram illness common cluster proud swamp digital patrol spread bar face december base kick").unwrap();
|
||||
assert_eq!(mnemonic, expected_mnemonic);
|
||||
|
||||
let all_accounts: Vec<_> = stored_login
|
||||
.unwrap_into_multiple_accounts(account_id.clone())
|
||||
.into_accounts()
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
all_accounts,
|
||||
vec![WalletAccount::new(
|
||||
account_id,
|
||||
MnemonicAccount::new(expected_mnemonic, hd_path),
|
||||
)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_stored_wallet_multiple_for_sign_in() {
|
||||
// WIP(JON): same as above but with file containing multiple accounts
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::config::{Config, OptionalValidators, ValidatorUrl};
|
||||
use crate::error::BackendError;
|
||||
use crate::network::Network;
|
||||
use crate::{config, network_config};
|
||||
|
||||
use strum::IntoEnumIterator;
|
||||
use validator_client::nymd::SigningNymdClient;
|
||||
use validator_client::Client;
|
||||
|
||||
use itertools::Itertools;
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::sync::RwLock;
|
||||
use url::Url;
|
||||
|
||||
@@ -14,6 +15,16 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
// Some hardcoded metadata overrides
|
||||
static METADATA_OVERRIDES: Lazy<Vec<(Url, ValidatorMetadata)>> = Lazy::new(|| {
|
||||
vec![(
|
||||
"https://rpc.nyx.nodes.guru/".parse().unwrap(),
|
||||
ValidatorMetadata {
|
||||
name: Some("Nodes.Guru".to_string()),
|
||||
},
|
||||
)]
|
||||
});
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn load_config_from_files(
|
||||
state: tauri::State<'_, Arc<RwLock<State>>>,
|
||||
@@ -31,12 +42,26 @@ pub async fn save_config_to_files(
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct State {
|
||||
config: Config,
|
||||
config: config::Config,
|
||||
signing_clients: HashMap<Network, Client<SigningNymdClient>>,
|
||||
current_network: Network,
|
||||
|
||||
// All the accounts the we get from decrypting the wallet. We hold on to these for being able to
|
||||
// switch accounts on-the-fly
|
||||
all_accounts: Vec<WalletAccountIds>,
|
||||
|
||||
/// Validators that have been fetched dynamically, probably during startup.
|
||||
fetched_validators: OptionalValidators,
|
||||
fetched_validators: config::OptionalValidators,
|
||||
|
||||
/// We fetch (and cache) some metadata, such as names, when available
|
||||
validator_metadata: HashMap<Url, ValidatorMetadata>,
|
||||
}
|
||||
|
||||
pub(crate) struct WalletAccountIds {
|
||||
// The wallet account id
|
||||
pub id: crate::wallet_storage::AccountId,
|
||||
// The set of corresponding network identities derived from the mnemonic
|
||||
pub addresses: HashMap<Network, cosmrs::AccountId>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
@@ -72,13 +97,13 @@ impl State {
|
||||
.ok_or(BackendError::ClientNotInitialized)
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &Config {
|
||||
pub fn config(&self) -> &config::Config {
|
||||
&self.config
|
||||
}
|
||||
|
||||
/// Load configuration from files. If unsuccessful we just log it and move on.
|
||||
pub fn load_config_files(&mut self) {
|
||||
self.config = Config::load_from_files();
|
||||
self.config = config::Config::load_from_files();
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
@@ -98,6 +123,14 @@ impl State {
|
||||
self.current_network
|
||||
}
|
||||
|
||||
pub(crate) fn set_all_accounts(&mut self, all_accounts: Vec<WalletAccountIds>) {
|
||||
self.all_accounts = all_accounts
|
||||
}
|
||||
|
||||
pub(crate) fn get_all_accounts(&self) -> impl Iterator<Item = &WalletAccountIds> {
|
||||
self.all_accounts.iter()
|
||||
}
|
||||
|
||||
pub fn logout(&mut self) {
|
||||
self.signing_clients = HashMap::new();
|
||||
}
|
||||
@@ -106,37 +139,98 @@ impl State {
|
||||
/// 1. from the configuration file
|
||||
/// 2. provided remotely
|
||||
/// 3. hardcoded fallback
|
||||
pub fn get_validators(&self, network: Network) -> impl Iterator<Item = ValidatorUrl> + '_ {
|
||||
/// The format is the config backend format, which is flat due to serialization preference.
|
||||
pub fn get_config_validator_entries(
|
||||
&self,
|
||||
network: Network,
|
||||
) -> impl Iterator<Item = config::ValidatorConfigEntry> + '_ {
|
||||
let validators_in_config = self.config.get_configured_validators(network);
|
||||
let fetched_validators = self.fetched_validators.validators(network).cloned();
|
||||
let default_validators = self.config.get_base_validators(network);
|
||||
|
||||
validators_in_config
|
||||
// All the validators, in decending list of priority
|
||||
let validators = validators_in_config
|
||||
.chain(fetched_validators)
|
||||
.chain(default_validators)
|
||||
.unique()
|
||||
.unique_by(|v| (v.nymd_url.clone(), v.api_url.clone()));
|
||||
|
||||
// Annotate with dynamic metadata
|
||||
validators.map(|v| {
|
||||
let metadata = self.validator_metadata.get(&v.nymd_url);
|
||||
let name = v
|
||||
.nymd_name
|
||||
.or_else(|| metadata.and_then(|m| m.name.clone()));
|
||||
config::ValidatorConfigEntry {
|
||||
nymd_url: v.nymd_url,
|
||||
nymd_name: name,
|
||||
api_url: v.api_url,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_nymd_urls(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
|
||||
self.get_validators(network).into_iter().map(|v| v.nymd_url)
|
||||
}
|
||||
|
||||
pub fn get_api_urls(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
|
||||
pub fn get_nymd_urls_only(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
|
||||
self
|
||||
.get_validators(network)
|
||||
.get_config_validator_entries(network)
|
||||
.into_iter()
|
||||
.map(|v| v.nymd_url)
|
||||
}
|
||||
|
||||
pub fn get_api_urls_only(&self, network: Network) -> impl Iterator<Item = Url> + '_ {
|
||||
self
|
||||
.get_config_validator_entries(network)
|
||||
.into_iter()
|
||||
.filter_map(|v| v.api_url)
|
||||
}
|
||||
|
||||
/// Get the list of validator nymd urls in the network config format, suitable for passing on to
|
||||
/// the UI
|
||||
pub fn get_nymd_urls(
|
||||
&self,
|
||||
network: Network,
|
||||
) -> impl Iterator<Item = network_config::ValidatorUrl> + '_ {
|
||||
self
|
||||
.get_config_validator_entries(network)
|
||||
.into_iter()
|
||||
.map(|v| network_config::ValidatorUrl {
|
||||
url: v.nymd_url.to_string(),
|
||||
name: v.nymd_name,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the list of validator-api urls in the network config format, suitable for passing on to
|
||||
/// the UI
|
||||
pub fn get_api_urls(
|
||||
&self,
|
||||
network: Network,
|
||||
) -> impl Iterator<Item = network_config::ValidatorUrl> + '_ {
|
||||
self
|
||||
.get_config_validator_entries(network)
|
||||
.into_iter()
|
||||
.filter_map(|v| {
|
||||
v.api_url.map(|u| network_config::ValidatorUrl {
|
||||
url: u.to_string(),
|
||||
name: None,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_all_nymd_urls(&self) -> HashMap<Network, Vec<Url>> {
|
||||
Network::iter()
|
||||
.flat_map(|network| self.get_nymd_urls(network).map(move |url| (network, url)))
|
||||
.flat_map(|network| {
|
||||
self
|
||||
.get_nymd_urls_only(network)
|
||||
.map(move |url| (network, url))
|
||||
})
|
||||
.into_group_map()
|
||||
}
|
||||
|
||||
pub fn get_all_api_urls(&self) -> HashMap<Network, Vec<Url>> {
|
||||
Network::iter()
|
||||
.flat_map(|network| self.get_api_urls(network).map(move |url| (network, url)))
|
||||
.flat_map(|network| {
|
||||
self
|
||||
.get_api_urls_only(network)
|
||||
.map(move |url| (network, url))
|
||||
})
|
||||
.into_group_map()
|
||||
}
|
||||
|
||||
@@ -154,11 +248,71 @@ impl State {
|
||||
.get(crate::config::REMOTE_SOURCE_OF_VALIDATOR_URLS.to_string())
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
self.fetched_validators = serde_json::from_str(&response.text().await?)?;
|
||||
log::debug!("Received validator urls: \n{}", self.fetched_validators);
|
||||
|
||||
self.refresh_validator_status().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn refresh_validator_status(&mut self) -> Result<(), BackendError> {
|
||||
log::debug!("Refreshing validator status");
|
||||
|
||||
// All urls for all networks
|
||||
let nymd_urls = self
|
||||
.get_all_nymd_urls()
|
||||
.into_iter()
|
||||
.flat_map(|(_, urls)| urls.into_iter());
|
||||
|
||||
// Fetch status for all urls
|
||||
let responses = fetch_status_for_urls(nymd_urls).await?;
|
||||
|
||||
// Update the stored metadata
|
||||
self.apply_responses(responses)?;
|
||||
|
||||
// Override some overrides for usability
|
||||
self.apply_metadata_override(METADATA_OVERRIDES.to_vec());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_responses(
|
||||
&mut self,
|
||||
responses: Vec<Result<(Url, String), reqwest::Error>>,
|
||||
) -> Result<(), BackendError> {
|
||||
for response in responses.into_iter().flatten() {
|
||||
let json: serde_json::Value = serde_json::from_str(&response.1)?;
|
||||
let moniker = &json["result"]["node_info"]["moniker"];
|
||||
log::debug!("Fetched moniker for: {}: {}", response.0, moniker);
|
||||
|
||||
// Insert into metadata map
|
||||
if let Some(ref mut m) = self.validator_metadata.get_mut(&response.0) {
|
||||
m.name = Some(moniker.to_string());
|
||||
} else {
|
||||
self.validator_metadata.insert(
|
||||
response.0,
|
||||
ValidatorMetadata {
|
||||
name: Some(moniker.to_string()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_metadata_override(&mut self, metadata_overrides: Vec<(Url, ValidatorMetadata)>) {
|
||||
for (url, metadata) in metadata_overrides {
|
||||
log::debug!("Overriding (some) metadata for: {url}");
|
||||
if let Some(m) = self.validator_metadata.get_mut(&url) {
|
||||
m.name = metadata.name;
|
||||
} else {
|
||||
self.validator_metadata.insert(url, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_validator_nymd_url(
|
||||
&mut self,
|
||||
url: &str,
|
||||
@@ -183,15 +337,41 @@ impl State {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_validator_url(&mut self, url: ValidatorUrl, network: Network) {
|
||||
pub fn add_validator_url(&mut self, url: config::ValidatorConfigEntry, network: Network) {
|
||||
self.config.add_validator_url(url, network);
|
||||
}
|
||||
|
||||
pub fn remove_validator_url(&mut self, url: ValidatorUrl, network: Network) {
|
||||
pub fn remove_validator_url(&mut self, url: config::ValidatorConfigEntry, network: Network) {
|
||||
self.config.remove_validator_url(url, network)
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_status_for_urls(
|
||||
nymd_urls: impl Iterator<Item = Url>,
|
||||
) -> Result<Vec<Result<(Url, String), reqwest::Error>>, BackendError> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(3))
|
||||
.build()?;
|
||||
|
||||
let responses = futures::future::join_all(nymd_urls.into_iter().map(|url| {
|
||||
let client = &client;
|
||||
let status_url = url.join("status").unwrap_or_else(|_| url.clone());
|
||||
async move {
|
||||
let resp = client.get(status_url).send().await?;
|
||||
resp.text().await.map(|text| (url, text))
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
|
||||
Ok(responses)
|
||||
}
|
||||
|
||||
// Validator metadata that can by dynamically populated
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ValidatorMetadata {
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! client {
|
||||
($state:ident) => {
|
||||
@@ -223,31 +403,36 @@ mod tests {
|
||||
let _api_urls = state.get_api_urls(Network::MAINNET).collect::<Vec<_>>();
|
||||
|
||||
state.add_validator_url(
|
||||
ValidatorUrl {
|
||||
config::ValidatorConfigEntry {
|
||||
nymd_url: "http://nymd_url.com".parse().unwrap(),
|
||||
nymd_name: Some("NymdUrl".to_string()),
|
||||
api_url: Some("http://nymd_url.com/api".parse().unwrap()),
|
||||
},
|
||||
Network::MAINNET,
|
||||
);
|
||||
|
||||
state.add_validator_url(
|
||||
ValidatorUrl {
|
||||
config::ValidatorConfigEntry {
|
||||
nymd_url: "http://foo.com".parse().unwrap(),
|
||||
nymd_name: None,
|
||||
api_url: None,
|
||||
},
|
||||
Network::MAINNET,
|
||||
);
|
||||
|
||||
state.add_validator_url(
|
||||
ValidatorUrl {
|
||||
config::ValidatorConfigEntry {
|
||||
nymd_url: "http://bar.com".parse().unwrap(),
|
||||
nymd_name: None,
|
||||
api_url: None,
|
||||
},
|
||||
Network::MAINNET,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
state.get_nymd_urls(Network::MAINNET).collect::<Vec<_>>(),
|
||||
state
|
||||
.get_nymd_urls_only(Network::MAINNET)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
"http://nymd_url.com/".parse().unwrap(),
|
||||
"http://foo.com".parse().unwrap(),
|
||||
@@ -256,10 +441,12 @@ mod tests {
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
state.get_api_urls(Network::MAINNET).collect::<Vec<_>>(),
|
||||
state
|
||||
.get_api_urls_only(Network::MAINNET)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
"http://nymd_url.com/api".parse().unwrap(),
|
||||
"https://validator.nymtech.net/api".parse().unwrap(),
|
||||
"https://validator.nymtech.net/api/".parse().unwrap(),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// The wallet storage is a single json file, containing multiple entries. These are referred to as
|
||||
// Logins, and has a plaintext id tag attached.
|
||||
//
|
||||
// Each encrypted login contains either a single account, or a list of multiple accounts.
|
||||
//
|
||||
// NOTE: A not insignificant amount of complexity comes from being able to handle both these cases,
|
||||
// instead of, for example, converting a single account to a list of multiple accounts with a single
|
||||
// entry. This also avoids resaving the wallet file when opening a file created with an earlier
|
||||
// version of the wallet.
|
||||
//
|
||||
// In the future we might want to simplify by dropping the support for a single account entry,
|
||||
// instead treating as muliple accounts with one entry.
|
||||
|
||||
use cosmrs::bip32::DerivationPath;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zeroize::Zeroize;
|
||||
@@ -8,15 +21,16 @@ use zeroize::Zeroize;
|
||||
use crate::error::BackendError;
|
||||
|
||||
use super::encryption::EncryptedData;
|
||||
use super::password::WalletAccountId;
|
||||
use super::password::{AccountId, LoginId};
|
||||
use super::UserPassword;
|
||||
|
||||
const CURRENT_WALLET_FILE_VERSION: u32 = 1;
|
||||
|
||||
/// The wallet, stored as a serialized json file.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct StoredWallet {
|
||||
version: u32,
|
||||
accounts: Vec<EncryptedAccount>,
|
||||
accounts: Vec<EncryptedLogin>,
|
||||
}
|
||||
|
||||
impl StoredWallet {
|
||||
@@ -25,16 +39,52 @@ impl StoredWallet {
|
||||
self.version
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.accounts.is_empty()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn len(&self) -> usize {
|
||||
self.accounts.len()
|
||||
}
|
||||
|
||||
pub fn remove_account(&mut self, id: &WalletAccountId) -> Option<EncryptedAccount> {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.accounts.is_empty()
|
||||
}
|
||||
|
||||
pub fn add_encrypted_login(&mut self, new_login: EncryptedLogin) -> Result<(), BackendError> {
|
||||
if self.get_encrypted_login(&new_login.id).is_ok() {
|
||||
return Err(BackendError::WalletLoginIdAlreadyExists);
|
||||
}
|
||||
self.accounts.push(new_login);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_encrypted_login(&self, id: &LoginId) -> Result<&EncryptedData<StoredLogin>, BackendError> {
|
||||
self
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|account| &account.id == id)
|
||||
.map(|account| &account.account)
|
||||
.ok_or(BackendError::WalletNoSuchLoginId)
|
||||
}
|
||||
|
||||
fn get_encrypted_login_mut(&mut self, id: &LoginId) -> Result<&mut EncryptedLogin, BackendError> {
|
||||
self
|
||||
.accounts
|
||||
.iter_mut()
|
||||
.find(|account| &account.id == id)
|
||||
.ok_or(BackendError::WalletNoSuchLoginId)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn get_encrypted_login_by_index(&self, index: usize) -> Option<&EncryptedLogin> {
|
||||
self.accounts.get(index)
|
||||
}
|
||||
|
||||
pub fn replace_encrypted_login(&mut self, new_login: EncryptedLogin) -> Result<(), BackendError> {
|
||||
let login = self.get_encrypted_login_mut(&new_login.id)?;
|
||||
*login = new_login;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_encrypted_login(&mut self, id: &LoginId) -> Option<EncryptedLogin> {
|
||||
if let Some(index) = self.accounts.iter().position(|account| &account.id == id) {
|
||||
log::info!("Removing from wallet file: {id}");
|
||||
Some(self.accounts.remove(index))
|
||||
@@ -44,43 +94,15 @@ impl StoredWallet {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn encrypted_account_by_index(&self, index: usize) -> Option<&EncryptedAccount> {
|
||||
self.accounts.get(index)
|
||||
}
|
||||
|
||||
fn encrypted_account(
|
||||
pub fn decrypt_login(
|
||||
&self,
|
||||
id: &WalletAccountId,
|
||||
) -> Result<&EncryptedData<StoredAccount>, BackendError> {
|
||||
self
|
||||
.accounts
|
||||
.iter()
|
||||
.find(|account| &account.id == id)
|
||||
.map(|account| &account.account)
|
||||
.ok_or(BackendError::NoSuchIdInWallet)
|
||||
}
|
||||
|
||||
pub fn add_encrypted_account(
|
||||
&mut self,
|
||||
new_account: EncryptedAccount,
|
||||
) -> Result<(), BackendError> {
|
||||
if self.encrypted_account(&new_account.id).is_ok() {
|
||||
return Err(BackendError::IdAlreadyExistsInWallet);
|
||||
}
|
||||
self.accounts.push(new_account);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn decrypt_account(
|
||||
&self,
|
||||
id: &WalletAccountId,
|
||||
id: &LoginId,
|
||||
password: &UserPassword,
|
||||
) -> Result<StoredAccount, BackendError> {
|
||||
self.encrypted_account(id)?.decrypt_struct(password)
|
||||
) -> Result<StoredLogin, BackendError> {
|
||||
self.get_encrypted_login(id)?.decrypt_struct(password)
|
||||
}
|
||||
|
||||
pub fn decrypt_all(&self, password: &UserPassword) -> Result<Vec<StoredAccount>, BackendError> {
|
||||
pub fn decrypt_all(&self, password: &UserPassword) -> Result<Vec<StoredLogin>, BackendError> {
|
||||
self
|
||||
.accounts
|
||||
.iter()
|
||||
@@ -102,39 +124,176 @@ impl Default for StoredWallet {
|
||||
}
|
||||
}
|
||||
|
||||
/// Each entry in the stored wallet file. An id field in plaintext and an encrypted stored login.
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub(crate) struct EncryptedAccount {
|
||||
pub id: WalletAccountId,
|
||||
pub account: EncryptedData<StoredAccount>,
|
||||
pub(crate) struct EncryptedLogin {
|
||||
pub id: LoginId,
|
||||
pub account: EncryptedData<StoredLogin>,
|
||||
}
|
||||
|
||||
// future-proofing
|
||||
impl EncryptedLogin {
|
||||
pub(crate) fn encrypt(
|
||||
id: LoginId,
|
||||
login: &StoredLogin,
|
||||
password: &UserPassword,
|
||||
) -> Result<Self, BackendError> {
|
||||
Ok(EncryptedLogin {
|
||||
id,
|
||||
account: super::encryption::encrypt_struct(login, password)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A stored login is either a account, such as a mnemonic, or a list of multiple accounts where
|
||||
/// each has an inner id. Future proofed for having private key backed accounts.
|
||||
#[derive(Serialize, Deserialize, Debug, Zeroize)]
|
||||
#[serde(untagged)]
|
||||
#[zeroize(drop)]
|
||||
pub(crate) enum StoredAccount {
|
||||
pub(crate) enum StoredLogin {
|
||||
Mnemonic(MnemonicAccount),
|
||||
// PrivateKey(PrivateKeyAccount)
|
||||
Multiple(MultipleAccounts),
|
||||
}
|
||||
|
||||
impl StoredAccount {
|
||||
pub(crate) fn new_mnemonic_backed_account(
|
||||
mnemonic: bip39::Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
) -> StoredAccount {
|
||||
StoredAccount::Mnemonic(MnemonicAccount { mnemonic, hd_path })
|
||||
impl StoredLogin {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn as_mnemonic_account(&self) -> Option<&MnemonicAccount> {
|
||||
match self {
|
||||
StoredLogin::Mnemonic(mn) => Some(mn),
|
||||
StoredLogin::Multiple(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
// If we add accounts backed by something that is not a mnemonic, this should probably be changed
|
||||
// to return `Option<..>`.
|
||||
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn as_multiple_accounts(&self) -> Option<&MultipleAccounts> {
|
||||
match self {
|
||||
StoredAccount::Mnemonic(account) => account.mnemonic(),
|
||||
StoredLogin::Mnemonic(_) => None,
|
||||
StoredLogin::Multiple(accounts) => Some(accounts),
|
||||
}
|
||||
}
|
||||
|
||||
// Return the login as multiple accounts, and if there is only a single mnemonic backed account,
|
||||
// return a set containing only the single account paired with the account id passed as function
|
||||
// argument.
|
||||
pub(crate) fn unwrap_into_multiple_accounts(self, id: AccountId) -> MultipleAccounts {
|
||||
match self {
|
||||
StoredLogin::Mnemonic(ref account) => vec![WalletAccount::new(id, account.clone())].into(),
|
||||
StoredLogin::Multiple(ref accounts) => accounts.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
/// Multiple stored accounts, each entry having an id and a data field.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Zeroize, PartialEq, Eq)]
|
||||
pub(crate) struct MultipleAccounts {
|
||||
accounts: Vec<WalletAccount>,
|
||||
}
|
||||
|
||||
impl MultipleAccounts {
|
||||
pub(crate) fn new() -> Self {
|
||||
MultipleAccounts {
|
||||
accounts: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_accounts(&self) -> impl Iterator<Item = &WalletAccount> {
|
||||
self.accounts.iter()
|
||||
}
|
||||
|
||||
pub(crate) fn get_account(&self, id: &AccountId) -> Option<&WalletAccount> {
|
||||
self.accounts.iter().find(|account| &account.id == id)
|
||||
}
|
||||
|
||||
pub(crate) fn into_accounts(self) -> impl Iterator<Item = WalletAccount> {
|
||||
self.accounts.into_iter()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.accounts.len()
|
||||
}
|
||||
|
||||
pub(crate) fn is_empty(&self) -> bool {
|
||||
self.accounts.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn add(
|
||||
&mut self,
|
||||
id: AccountId,
|
||||
mnemonic: bip39::Mnemonic,
|
||||
hd_path: DerivationPath,
|
||||
) -> Result<(), BackendError> {
|
||||
if self.get_account(&id).is_some() {
|
||||
Err(BackendError::WalletAccountIdAlreadyExistsInWalletLogin)
|
||||
} else {
|
||||
self.accounts.push(WalletAccount::new(
|
||||
id,
|
||||
MnemonicAccount::new(mnemonic, hd_path),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&mut self, id: &AccountId) -> Result<(), BackendError> {
|
||||
if self.get_account(id).is_none() {
|
||||
return Err(BackendError::WalletNoSuchAccountIdInWalletLogin);
|
||||
}
|
||||
self.accounts.retain(|accounts| &accounts.id != id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<WalletAccount>> for MultipleAccounts {
|
||||
fn from(accounts: Vec<WalletAccount>) -> MultipleAccounts {
|
||||
Self { accounts }
|
||||
}
|
||||
}
|
||||
|
||||
/// An entry in the list of stored accounts
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Zeroize, PartialEq, Eq)]
|
||||
pub(crate) struct WalletAccount {
|
||||
id: AccountId,
|
||||
account: AccountData,
|
||||
}
|
||||
|
||||
impl WalletAccount {
|
||||
pub(crate) fn new(id: AccountId, mnemonic_account: MnemonicAccount) -> Self {
|
||||
Self {
|
||||
id,
|
||||
account: AccountData::Mnemonic(mnemonic_account),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn id(&self) -> &AccountId {
|
||||
&self.id
|
||||
}
|
||||
|
||||
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
|
||||
match self.account {
|
||||
AccountData::Mnemonic(ref account) => account.mnemonic(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn hd_path(&self) -> &DerivationPath {
|
||||
match self.account {
|
||||
AccountData::Mnemonic(ref account) => account.hd_path(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An account usually is a mnemonic account, but in the future it might be backed by a private
|
||||
/// key.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Zeroize, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
#[zeroize(drop)]
|
||||
enum AccountData {
|
||||
Mnemonic(MnemonicAccount),
|
||||
// PrivateKey(PrivateKeyAccount)
|
||||
}
|
||||
|
||||
/// An account backed by a unique mnemonic.
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct MnemonicAccount {
|
||||
mnemonic: bip39::Mnemonic,
|
||||
#[serde(with = "display_hd_path")]
|
||||
@@ -142,11 +301,15 @@ pub(crate) struct MnemonicAccount {
|
||||
}
|
||||
|
||||
impl MnemonicAccount {
|
||||
pub(crate) fn new(mnemonic: bip39::Mnemonic, hd_path: DerivationPath) -> Self {
|
||||
Self { mnemonic, hd_path }
|
||||
}
|
||||
|
||||
pub(crate) fn mnemonic(&self) -> &bip39::Mnemonic {
|
||||
&self.mnemonic
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[cfg(test)]
|
||||
pub(crate) fn hd_path(&self) -> &DerivationPath {
|
||||
&self.hd_path
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,22 +6,75 @@ use std::fmt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub(crate) struct WalletAccountId(String);
|
||||
// The `LoginId` is the top level id in the wallet file, and is not stored encrypted
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Zeroize)]
|
||||
pub(crate) struct LoginId(String);
|
||||
|
||||
impl WalletAccountId {
|
||||
pub(crate) fn new(id: String) -> WalletAccountId {
|
||||
WalletAccountId(id)
|
||||
impl LoginId {
|
||||
pub(crate) fn new(id: String) -> LoginId {
|
||||
LoginId(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for WalletAccountId {
|
||||
impl AsRef<str> for LoginId {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for WalletAccountId {
|
||||
impl From<String> for LoginId {
|
||||
fn from(id: String) -> Self {
|
||||
Self::new(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for LoginId {
|
||||
fn from(id: &str) -> Self {
|
||||
Self::new(id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for LoginId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
// For each encrypted login, we can have multiple encrypted accounts.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Zeroize)]
|
||||
pub(crate) struct AccountId(String);
|
||||
|
||||
impl AccountId {
|
||||
pub(crate) fn new(id: String) -> AccountId {
|
||||
AccountId(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for AccountId {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for AccountId {
|
||||
fn from(id: String) -> Self {
|
||||
Self::new(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for AccountId {
|
||||
fn from(id: &str) -> Self {
|
||||
Self::new(id.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LoginId> for AccountId {
|
||||
fn from(login_id: LoginId) -> Self {
|
||||
Self::new(login_id.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AccountId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"accounts": [
|
||||
{
|
||||
"id": "default",
|
||||
"account": {
|
||||
"ciphertext": "cq5w4W5ex5eFFRcqLG+824XyUAUoYmrRY3NGw/rue6/mLoQKQE/07+BxzRuKjyYFasC1HBPg41KJwp2IY+/7+80rB9aXPpaKVLUcG9U40qgCw66WhgxTrXOnrt5toefpSTBL7f9N/PVwpuumfAgD9CS0ioB7/9Qoea7nYKkextGX15ex26B/ndQddvUkQ4gx+Vq7OLymv4l+nkdZ2nKMja349zd/BjnzPBB68/iIjyYlivVjtQ7FRbvpNRj6Mjg4905wGlO7bTpkw+RGiaGK4pK8fTWz8gAKr8GYoXPD",
|
||||
"salt": "SPVGdbVyoEayD4ZzM4I+Jg==",
|
||||
"iv": "tjpn/tRjD1gty+fQ"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"accounts": [
|
||||
{
|
||||
"id": "default",
|
||||
"account": {
|
||||
"ciphertext": "3MDgoU2i5QMc9r80yPeq2AMk5wpkke0tXum5NsOE5NcFciF+aHLQW0dvXbGszap1y3nN4+YZD3cgGrmtKh/cChqRGJDkniaxdf3XPHh9RkiWXw2KSHHeyGrFY0INJeiky1ZtUFhWhopcHJWSnfCmVC15YFpnM5xOKpITjHAFhGt98MaYR+mS+3zoUFrjYbaZRh2TR2lFWsbR8YU1uaTYqJZ1HX1PBCub6aS3vjQm0Fwa+hAtR/gMymXJc5qtruTO4NbqYtMj3Z9eIgoVB+56SLAXlIF1Uo1pjvV0mx1hWNNiIc10ujF/wl/nnKF6icOcmrfm9XhOtsvUYBsE/wAIJZw3LKXgSX+hJbOl+zLAwJZK1xiL8n/nM1IJZDn+Wu6z0OzRaj9S7T16+brMw1oaqjk56saM8n5z725fizJj+ur6gnPBWnoyHPaCHgHdB2PKQNY0ZlwRM6dVncRaEWQDLAboyMq3FXxK9UbusNcDFpYw6bdnuJlNVf6y9yxwyvkUrt5YtgfkLyoW42z1PVtVWsV9P8eE/A/tnYjXf34xvba3K8Y1/3DTi7uuydNrSR/XhA+pevz68VWCbY+j746Yi8Lz7altePphkjfJAezodobKvMplXzqInopIWNovyemw/+1E7WZbkQIOAXg1WC1+Y/df+dffRGuGRdDerfRLmA5XLej1M/wE3WQ7b9KwlAo6XJ4hnQKwyDCqYP/ButBXW1AOnnZpCq59gGbiccZJsTMZB4OP95yFPgz8//IeDgma2PDixVmDEp0SGHhN7dlSoNa5eoglblqzJu/TcTA6jmQFA3ef0GiA3QzBjmyB4bz0bFybh8XA1brVIVlsjRwXb3/UYaVqsP6Hy1QDUpZofXIJs5lK0hUd0ECdaNFXXgHd25ifPocp09WLFyK92H6i3ABDZ7pu3b4lTUt6kHt6LTVsKkyylmYf2iMHnCcmfy4uxGTXxRjPjMgKL8pd++OZ3q62jLBuoTjgdj6pccwDvD+NYQ2FFeHmBzxyTLqUyKltYiyFlJHWLKOcXyeDHzRhHic+e/wn3VhM3NdrvtqYWA9m72Ye1L1I7VX7KatGurG6CeiFiY5xHxxpLT7dF0fJ7uxRye4JnRyYQuU7iK72qCKjgYjwjCIha4qPi5Q/x6S+uVe7yX5Eb73L3eB+IlkyW9wPHmSOcE4GpbMU96tK8xoxT0T9eQlj050GDnJ/oI2XHfZTs1bIxsjfZqW03g==",
|
||||
"salt": "wXR3RnPmsoA3ncrixIvaUw==",
|
||||
"iv": "/Zjn1OXsLJhA43n/"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -18,4 +18,4 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Avatar } from '@mui/material';
|
||||
import stc from 'string-to-color';
|
||||
import { TAccount } from 'src/types';
|
||||
|
||||
export const AccountAvatar = ({ name }: Pick<TAccount, 'name'>) => (
|
||||
<Avatar sx={{ bgcolor: stc(name), width: 35, height: 35 }}>{name?.split('')[0]}</Avatar>
|
||||
);
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Box, ListItem, ListItemAvatar, ListItemButton, ListItemText, Tooltip, Typography } from '@mui/material';
|
||||
import { useClipboard } from 'use-clipboard-copy';
|
||||
import { AccountsContext } from 'src/context';
|
||||
import { AccountAvatar } from './AccountAvatar';
|
||||
|
||||
export const AccountItem = ({
|
||||
name,
|
||||
address,
|
||||
onSelectAccount,
|
||||
}: {
|
||||
name: string;
|
||||
address: string;
|
||||
onSelectAccount: () => void;
|
||||
}) => {
|
||||
const { selectedAccount, setDialogToDisplay, setAccountMnemonic } = useContext(AccountsContext);
|
||||
const { copy, copied } = useClipboard({ copiedTimeout: 1000 });
|
||||
return (
|
||||
<ListItem
|
||||
disablePadding
|
||||
disableGutters
|
||||
sx={selectedAccount?.id === name ? { bgcolor: 'rgba(33, 208, 115, 0.1)' } : {}}
|
||||
>
|
||||
<ListItemButton disableRipple onClick={onSelectAccount}>
|
||||
<ListItemAvatar sx={{ minWidth: 0, mr: 2 }}>
|
||||
<AccountAvatar name={name} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
secondary={
|
||||
<Box>
|
||||
<Tooltip title={copied ? 'Copied!' : `Click to copy address ${address}`}>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="body2"
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
copy(address);
|
||||
}}
|
||||
sx={{ '&:hover': { color: 'grey.900' } }}
|
||||
>
|
||||
{address}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
<Box sx={{ mt: 0.5 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="span"
|
||||
sx={{ textDecoration: 'underline', mb: 0.5, '&:hover': { color: 'primary.main' } }}
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
setDialogToDisplay('Mnemonic');
|
||||
setAccountMnemonic((accountMnemonic) => ({ ...accountMnemonic, accountName: name }));
|
||||
}}
|
||||
>
|
||||
Show mnemonic
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
{/* edit and remove accounts todo */}
|
||||
{/* <ListItemIcon>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleAccountToEdit(name);
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</ListItemIcon> */}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { Button } from '@mui/material';
|
||||
import { AccountEntry } from 'src/types';
|
||||
import { AccountAvatar } from './AccountAvatar';
|
||||
|
||||
export const AccountOverview = ({ account, onClick }: { account: AccountEntry; onClick: () => void }) => (
|
||||
<Button
|
||||
startIcon={<AccountAvatar name={account.id} />}
|
||||
sx={{ color: 'nym.text.dark' }}
|
||||
onClick={onClick}
|
||||
disableRipple
|
||||
>
|
||||
{account.id}
|
||||
</Button>
|
||||
);
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { AccountsContext, AppContext } from 'src/context';
|
||||
import { EditAccountModal } from './modals/EditAccountModal';
|
||||
import { AddAccountModal } from './modals/AddAccountModal';
|
||||
import { AccountsModal } from './modals/AccountsModal';
|
||||
import { MnemonicModal } from './modals/MnemonicModal';
|
||||
import { AccountOverview } from './AccountOverview';
|
||||
import { MultiAccountHowTo } from './MultiAccountHowTo';
|
||||
|
||||
export const Accounts = () => {
|
||||
const { accounts, selectedAccount, setDialogToDisplay } = useContext(AccountsContext);
|
||||
|
||||
return accounts && selectedAccount ? (
|
||||
<>
|
||||
<AccountOverview account={selectedAccount} onClick={() => setDialogToDisplay('Accounts')} />
|
||||
<AccountsModal />
|
||||
<AddAccountModal />
|
||||
<EditAccountModal />
|
||||
<MnemonicModal />
|
||||
</>
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const SingleAccount = () => {
|
||||
const [showHowToDialog, setShowHowToDialog] = useState(false);
|
||||
const { clientDetails } = useContext(AppContext);
|
||||
return (
|
||||
<>
|
||||
<AccountOverview
|
||||
account={{ id: 'Account 1', address: clientDetails?.client_address || '' }}
|
||||
onClick={() => setShowHowToDialog(true)}
|
||||
/>
|
||||
<MultiAccountHowTo show={showHowToDialog} handleClose={() => setShowHowToDialog(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { Alert, Box, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
|
||||
import { Close } from '@mui/icons-material';
|
||||
|
||||
const passwordCreationSteps = [
|
||||
'Log out',
|
||||
'When signing in, select “Sign in with mnemonic”',
|
||||
'On the next screen click “Create a password for your account”',
|
||||
'Sign in to wallet with your new password',
|
||||
'Now you can create multiple accounts',
|
||||
];
|
||||
|
||||
export const MultiAccountHowTo = ({ show, handleClose }: { show: boolean; handleClose: () => void }) => (
|
||||
<Dialog open={show} onClose={handleClose} fullWidth hideBackdrop>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">Multi accounts</Typography>
|
||||
<IconButton onClick={handleClose}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: 'grey.600' }}>
|
||||
How to set up multiple accounts
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack spacing={2}>
|
||||
<Alert severity="warning" icon={false}>
|
||||
<Typography>In order to create multiple accounts your wallet needs a password.</Typography>
|
||||
<Typography>Follow steps below to create password.</Typography>
|
||||
</Alert>
|
||||
<Typography>How to create a password for your account</Typography>
|
||||
{passwordCreationSteps.map((step, index) => (
|
||||
<Typography key={step}>{`${index + 1}. ${step}`}</Typography>
|
||||
))}
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
@@ -0,0 +1,16 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { AccountsProvider, AppContext } from 'src/context';
|
||||
import { Accounts, SingleAccount } from './Accounts';
|
||||
|
||||
export const MultiAccounts = () => {
|
||||
const { loginType } = useContext(AppContext);
|
||||
|
||||
if (loginType === 'password') {
|
||||
return (
|
||||
<AccountsProvider>
|
||||
<Accounts />
|
||||
</AccountsProvider>
|
||||
);
|
||||
}
|
||||
return <SingleAccount />;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
export const accounts = [
|
||||
{
|
||||
id: 'Account 1',
|
||||
address: 'n107wsxkj08hycflnkp5ayfg6rt3pt0psm7w2t9r',
|
||||
},
|
||||
{
|
||||
id: 'Account 2',
|
||||
address: 'n1dgp04lqaasnzaww66zwdp6u24smqe7ltuny8vk',
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, IconButton, Typography } from '@mui/material';
|
||||
import { Add, ArrowDownwardSharp, Close } from '@mui/icons-material';
|
||||
import { AccountsContext } from 'src/context';
|
||||
import { AccountItem } from '../AccountItem';
|
||||
import { ConfirmPasswordModal } from './ConfirmPasswordModal';
|
||||
|
||||
export const AccountsModal = () => {
|
||||
const { accounts, dialogToDisplay, setDialogToDisplay, setError, handleSelectAccount, selectedAccount } =
|
||||
useContext(AccountsContext);
|
||||
const [accountToSwitchTo, setAccountToSwitchTo] = useState<string>();
|
||||
|
||||
const handleClose = () => {
|
||||
setDialogToDisplay(undefined);
|
||||
setError(undefined);
|
||||
setAccountToSwitchTo(undefined);
|
||||
};
|
||||
|
||||
if (accountToSwitchTo)
|
||||
return (
|
||||
<ConfirmPasswordModal
|
||||
accountName={accountToSwitchTo}
|
||||
onClose={() => {
|
||||
handleClose();
|
||||
setDialogToDisplay('Accounts');
|
||||
}}
|
||||
onConfirm={async (password) => {
|
||||
const isSuccessful = await handleSelectAccount({ password, accountName: accountToSwitchTo });
|
||||
if (isSuccessful) handleClose();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={dialogToDisplay === 'Accounts'} onClose={handleClose} fullWidth hideBackdrop>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">Accounts</Typography>
|
||||
<IconButton onClick={handleClose}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: 'grey.600' }}>
|
||||
Switch between accounts
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ padding: 0 }}>
|
||||
{accounts?.map(({ id, address }) => (
|
||||
<AccountItem
|
||||
name={id}
|
||||
address={address}
|
||||
key={address}
|
||||
onSelectAccount={() => {
|
||||
if (selectedAccount?.id !== id) {
|
||||
setAccountToSwitchTo(id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3 }}>
|
||||
<Button startIcon={<ArrowDownwardSharp />} onClick={() => setDialogToDisplay('Import')}>
|
||||
Import account
|
||||
</Button>
|
||||
<Button
|
||||
disableElevation
|
||||
variant="contained"
|
||||
startIcon={<Add fontSize="small" />}
|
||||
onClick={() => setDialogToDisplay('Add')}
|
||||
>
|
||||
Add new account
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,238 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { ArrowBackSharp } from '@mui/icons-material';
|
||||
import { useClipboard } from 'use-clipboard-copy';
|
||||
import { createMnemonic, validateMnemonic } from 'src/requests';
|
||||
import { Console } from 'src/utils/console';
|
||||
import { AccountsContext } from 'src/context';
|
||||
import { ConfirmPassword, Mnemonic } from 'src/components';
|
||||
import { MnemonicInput } from 'src/components/textfields';
|
||||
|
||||
const createAccountSteps = [
|
||||
'Copy and save mnemonic for your new account',
|
||||
'Name your new account',
|
||||
'Confirm the password used to login to your wallet',
|
||||
];
|
||||
const importAccountSteps = [
|
||||
'Provide mnemonic of account you want to import',
|
||||
'Name your new account',
|
||||
'Confirm the password used to login to your wallet',
|
||||
];
|
||||
|
||||
const MnemonicStep = ({ mnemonic, onNext }: { mnemonic: string; onNext: () => void }) => {
|
||||
const { copy, copied } = useClipboard({ copiedTimeout: 5000 });
|
||||
return (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<DialogContent>
|
||||
<Mnemonic mnemonic={mnemonic} handleCopy={copy} copied={copied} />
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3, pt: 0 }}>
|
||||
<Button disabled={!copied} fullWidth disableElevation variant="contained" size="large" onClick={onNext}>
|
||||
I saved my mnemonic
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const ImportMnemonic = ({
|
||||
value,
|
||||
onChange,
|
||||
onNext,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onNext: () => void;
|
||||
}) => {
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const handleOnNext = async () => {
|
||||
const isValid = await validateMnemonic(value);
|
||||
if (!isValid) setError('Please enter a valid mnemonic. Mnemonic must have a word count that is a multiple of 6.');
|
||||
else onNext();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogContent>
|
||||
<Typography variant="body1" sx={{ color: 'error.main', my: 2 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
<MnemonicInput
|
||||
mnemonic={value}
|
||||
onUpdateMnemonic={(mnemon) => {
|
||||
onChange(mnemon);
|
||||
setError(undefined);
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3, pt: 0 }}>
|
||||
<Button
|
||||
disabled={value.length === 0}
|
||||
fullWidth
|
||||
disableElevation
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={handleOnNext}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const NameAccount = ({ onNext }: { onNext: (value: string) => void }) => {
|
||||
const [value, setValue] = useState('');
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const nameValidation = /^([a-zA-Z0-9\s]){1,20}$/;
|
||||
|
||||
const handleNext = (accountName: string) => {
|
||||
if (!nameValidation.test(accountName)) {
|
||||
setError('Account name must contain only letters and numbers and be between 1 and 20 characters');
|
||||
} else onNext(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogContent>
|
||||
<Typography variant="body1" sx={{ color: 'error.main', my: 2 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
<TextField
|
||||
placeholder="Account name"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
setValue(e.target.value);
|
||||
setError(undefined);
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3, pt: 0 }}>
|
||||
<Button
|
||||
disabled={!value.length}
|
||||
fullWidth
|
||||
disableElevation
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={() => handleNext(value)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddAccountModal = () => {
|
||||
const [step, setStep] = useState(0);
|
||||
const [data, setData] = useState({
|
||||
mnemonic: '',
|
||||
accountName: '',
|
||||
});
|
||||
|
||||
const { dialogToDisplay, setDialogToDisplay, handleAddAccount, setError, isLoading, error } =
|
||||
useContext(AccountsContext);
|
||||
|
||||
const generateMnemonic = async () => {
|
||||
const mnemon = await createMnemonic();
|
||||
setData((d) => ({ ...d, mnemonic: mnemon }));
|
||||
};
|
||||
|
||||
const resetState = () => {
|
||||
setData({ mnemonic: '', accountName: '' });
|
||||
setStep(0);
|
||||
setError(undefined);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setDialogToDisplay('Accounts');
|
||||
resetState();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogToDisplay === 'Add') generateMnemonic();
|
||||
if (dialogToDisplay === 'Accounts') resetState();
|
||||
}, [dialogToDisplay]);
|
||||
|
||||
useEffect(() => {
|
||||
setError(undefined);
|
||||
}, [step]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={dialogToDisplay === 'Add' || dialogToDisplay === 'Import'}
|
||||
onClose={handleClose}
|
||||
fullWidth
|
||||
hideBackdrop
|
||||
>
|
||||
<DialogTitle sx={{ pb: 0 }}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">{`${dialogToDisplay} new account`}</Typography>
|
||||
<IconButton onClick={() => (step === 0 ? handleClose() : setStep((s) => s - 1))}>
|
||||
<ArrowBackSharp />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography sx={{ mt: 2 }}>
|
||||
{dialogToDisplay === 'Add' ? createAccountSteps[step] : importAccountSteps[step]}
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
{(() => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return dialogToDisplay === 'Add' ? (
|
||||
<MnemonicStep mnemonic={data.mnemonic} onNext={() => setStep((s) => s + 1)} />
|
||||
) : (
|
||||
<ImportMnemonic
|
||||
value={data.mnemonic}
|
||||
onChange={(value) => setData((d) => ({ ...d, mnemonic: value }))}
|
||||
onNext={() => setStep((s) => s + 1)}
|
||||
/>
|
||||
);
|
||||
case 1:
|
||||
return (
|
||||
<NameAccount
|
||||
onNext={(accountName) => {
|
||||
setData((d) => ({ ...d, accountName }));
|
||||
setStep((s) => s + 1);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case 2:
|
||||
return (
|
||||
<ConfirmPassword
|
||||
buttonTitle="Add account"
|
||||
onConfirm={async (password) => {
|
||||
if (data.accountName && data.mnemonic) {
|
||||
try {
|
||||
await handleAddAccount({ accountName: data.accountName, mnemonic: data.mnemonic, password });
|
||||
setStep(0);
|
||||
setDialogToDisplay('Accounts');
|
||||
} catch (e) {
|
||||
Console.error(e as string);
|
||||
}
|
||||
}
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})()}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Box, Dialog, DialogTitle, IconButton, Typography } from '@mui/material';
|
||||
import { ArrowBack } from '@mui/icons-material';
|
||||
import { ConfirmPassword } from 'src/components/ConfirmPassword';
|
||||
import { AccountsContext } from 'src/context';
|
||||
|
||||
export const ConfirmPasswordModal = ({
|
||||
accountName,
|
||||
onClose,
|
||||
onConfirm,
|
||||
}: {
|
||||
accountName?: string;
|
||||
onClose: () => void;
|
||||
onConfirm: (password: string) => Promise<void>;
|
||||
}) => {
|
||||
const { isLoading, error } = useContext(AccountsContext);
|
||||
|
||||
return (
|
||||
<Dialog open={Boolean(accountName)} onClose={onClose} fullWidth hideBackdrop>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">Switch account</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<ArrowBack />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: 'grey.600' }}>
|
||||
Confirm password
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<ConfirmPassword onConfirm={onConfirm} error={error} isLoading={isLoading} buttonTitle="Switch account" />
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Close } from '@mui/icons-material';
|
||||
import { AccountsContext } from 'src/context';
|
||||
|
||||
export const EditAccountModal = () => {
|
||||
const [accountName, setAccountName] = useState('');
|
||||
|
||||
const { accountToEdit, dialogToDisplay, setDialogToDisplay, handleEditAccount } = useContext(AccountsContext);
|
||||
|
||||
useEffect(() => {
|
||||
setAccountName(accountToEdit ? accountToEdit?.id : '');
|
||||
}, [accountToEdit]);
|
||||
|
||||
return (
|
||||
<Dialog open={dialogToDisplay === 'Edit'} onClose={() => setDialogToDisplay('Accounts')} fullWidth hideBackdrop>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">Edit account name</Typography>
|
||||
<IconButton onClick={() => setDialogToDisplay('Accounts')}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: 'grey.600' }}>
|
||||
New wallet address
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ p: 0 }}>
|
||||
<Box sx={{ px: 3, mt: 1 }}>
|
||||
<TextField
|
||||
label="Account name"
|
||||
fullWidth
|
||||
value={accountName}
|
||||
onChange={(e) => setAccountName(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
disableElevation
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
if (accountToEdit) {
|
||||
handleEditAccount({ ...accountToEdit, id: accountName });
|
||||
setDialogToDisplay('Accounts');
|
||||
}
|
||||
}}
|
||||
disabled={!accountName?.length}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Close } from '@mui/icons-material';
|
||||
import { AccountsContext } from 'src/context';
|
||||
|
||||
export const ImportAccountModal = () => {
|
||||
const [mnemonic, setMnemonic] = useState('');
|
||||
|
||||
const { dialogToDisplay, setDialogToDisplay, handleImportAccount } = useContext(AccountsContext);
|
||||
|
||||
const handleClose = () => {
|
||||
setMnemonic('');
|
||||
setDialogToDisplay('Accounts');
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={dialogToDisplay === 'Import'} onClose={handleClose} fullWidth hideBackdrop>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">Import account</Typography>
|
||||
<IconButton onClick={handleClose}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: 'grey.600' }}>
|
||||
Provide mnemonic of account you want to import
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ p: 0 }}>
|
||||
<Box sx={{ px: 3, mt: 1 }}>
|
||||
<TextField
|
||||
placeholder="Paste or type your mnemonic here"
|
||||
fullWidth
|
||||
value={mnemonic}
|
||||
onChange={(e) => setMnemonic(e.target.value)}
|
||||
autoFocus
|
||||
multiline
|
||||
rows={3}
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
disableElevation
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={() => handleImportAccount({ id: '', address: '' })}
|
||||
disabled={!mnemonic.length}
|
||||
>
|
||||
Import account
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { ArrowBackSharp } from '@mui/icons-material';
|
||||
import { AccountsContext } from 'src/context';
|
||||
import { useClipboard } from 'use-clipboard-copy';
|
||||
import { PasswordInput, Mnemonic } from 'src/components';
|
||||
|
||||
export const MnemonicModal = () => {
|
||||
const [password, setPassword] = useState('');
|
||||
|
||||
const { copy, copied } = useClipboard({ copiedTimeout: 5000 });
|
||||
|
||||
const {
|
||||
dialogToDisplay,
|
||||
setDialogToDisplay,
|
||||
accountMnemonic,
|
||||
setAccountMnemonic,
|
||||
handleGetAccountMnemonic,
|
||||
error,
|
||||
setError,
|
||||
isLoading,
|
||||
} = useContext(AccountsContext);
|
||||
|
||||
const handleClose = () => {
|
||||
setAccountMnemonic({ value: undefined, accountName: undefined });
|
||||
setError(undefined);
|
||||
setDialogToDisplay('Accounts');
|
||||
setPassword('');
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={dialogToDisplay === 'Mnemonic'} onClose={handleClose} fullWidth hideBackdrop>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">Display mnemonic</Typography>
|
||||
<IconButton onClick={handleClose}>
|
||||
<ArrowBackSharp />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: 'grey.600' }}>
|
||||
{`Display mnemonic for: ${accountMnemonic?.accountName}`}
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ p: 0 }}>
|
||||
<Box sx={{ px: 3, mt: 1 }}>
|
||||
{error && (
|
||||
<Typography variant="body1" sx={{ color: 'error.main', mb: 2 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
{!accountMnemonic.value ? (
|
||||
<>
|
||||
<Typography sx={{ mb: 2 }}>Enter the password used to login to your wallet</Typography>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
password={password}
|
||||
onUpdatePassword={(pswrd) => setPassword(pswrd)}
|
||||
autoFocus
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Mnemonic mnemonic={accountMnemonic.value} handleCopy={copy} copied={copied} />
|
||||
)}
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3 }}>
|
||||
{!accountMnemonic.value && (
|
||||
<Button
|
||||
disableRipple
|
||||
disabled={!password.length || isLoading}
|
||||
fullWidth
|
||||
disableElevation
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={async () => {
|
||||
if (accountMnemonic?.accountName) {
|
||||
setError(undefined);
|
||||
await handleGetAccountMnemonic({ password, accountName: accountMnemonic?.accountName });
|
||||
}
|
||||
}}
|
||||
endIcon={isLoading && <CircularProgress size={20} />}
|
||||
>
|
||||
Display mnemonic
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { MockAccountsProvider } from 'src/context/mocks/accounts';
|
||||
import { Accounts } from '../Accounts';
|
||||
|
||||
export default {
|
||||
title: 'Wallet / Multi Account',
|
||||
component: Accounts,
|
||||
} as ComponentMeta<typeof Accounts>;
|
||||
|
||||
export const Default: ComponentStory<typeof Accounts> = () => (
|
||||
<Box display="flex" alignContent="center">
|
||||
<MockAccountsProvider>
|
||||
<Accounts />
|
||||
</MockAccountsProvider>
|
||||
</Box>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export type TDialog = 'Accounts' | 'Add' | 'Edit' | 'Import';
|
||||
@@ -1,21 +1,28 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { AppBar as MuiAppBar, Grid, IconButton, Toolbar } from '@mui/material';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Logout } from '@mui/icons-material';
|
||||
import TerminalIcon from '@mui/icons-material/Terminal';
|
||||
import { ClientContext } from '../context/main';
|
||||
import { AppContext } from '../context/main';
|
||||
import { NetworkSelector } from './NetworkSelector';
|
||||
import { Node as NodeIcon } from '../svg-icons/node';
|
||||
import { MultiAccounts } from './Accounts';
|
||||
import { config } from '../../config';
|
||||
|
||||
export const AppBar = () => {
|
||||
const { showSettings, logOut, handleShowSettings, handleShowTerminal, appEnv } = useContext(ClientContext);
|
||||
|
||||
const { showSettings, logOut, handleShowSettings, handleShowTerminal, appEnv } = useContext(AppContext);
|
||||
const history = useHistory();
|
||||
return (
|
||||
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent' }}>
|
||||
<Toolbar disableGutters>
|
||||
<Grid container justifyContent="space-between" alignItems="center" flexWrap="nowrap">
|
||||
<Grid item>
|
||||
<NetworkSelector />
|
||||
<Grid item container alignItems="center" spacing={1}>
|
||||
<Grid item>
|
||||
<MultiAccounts />
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<NetworkSelector />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item container justifyContent="flex-end" md={12} lg={5} spacing={2}>
|
||||
{(appEnv?.SHOW_TERMINAL || config.IS_DEV_MODE) && (
|
||||
@@ -35,7 +42,14 @@ export const AppBar = () => {
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<IconButton size="small" onClick={logOut} sx={{ color: 'nym.background.dark' }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={async () => {
|
||||
await logOut();
|
||||
history.push('/');
|
||||
}}
|
||||
sx={{ color: 'nym.background.dark' }}
|
||||
>
|
||||
<Logout fontSize="small" />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { Box } from '@mui/material';
|
||||
import { ClientAddressDisplay } from './ClientAddress';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { FC, useContext } from 'react';
|
||||
import { Box, Typography, Tooltip } from '@mui/material';
|
||||
import { ClientContext } from '../context/main';
|
||||
import { AppContext } from '../context/main';
|
||||
import { CopyToClipboard } from './CopyToClipboard';
|
||||
import { splice } from '../utils';
|
||||
|
||||
@@ -53,6 +53,6 @@ export const ClientAddressDisplay: FC<ClientAddressProps & { address?: string }>
|
||||
);
|
||||
|
||||
export const ClientAddress: FC<ClientAddressProps> = ({ ...props }) => {
|
||||
const { clientDetails } = useContext(ClientContext);
|
||||
const { clientDetails } = useContext(AppContext);
|
||||
return <ClientAddressDisplay {...props} address={clientDetails?.client_address} />;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, CircularProgress, DialogActions, DialogContent, Typography } from '@mui/material';
|
||||
import { PasswordInput } from './textfields';
|
||||
|
||||
export const ConfirmPassword = ({
|
||||
error,
|
||||
isLoading,
|
||||
onConfirm,
|
||||
buttonTitle,
|
||||
}: {
|
||||
error?: string;
|
||||
isLoading?: boolean;
|
||||
buttonTitle: string;
|
||||
onConfirm: (password: string) => void;
|
||||
}) => {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogContent>
|
||||
<Typography variant="body1" sx={{ color: 'error.main', my: 2 }}>
|
||||
{error}
|
||||
</Typography>
|
||||
|
||||
<PasswordInput
|
||||
password={value}
|
||||
onUpdatePassword={(pswrd) => setValue(pswrd)}
|
||||
placeholder="Confirm password"
|
||||
autoFocus
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3, pt: 0 }}>
|
||||
<Button
|
||||
disabled={!value.length || isLoading}
|
||||
fullWidth
|
||||
disableElevation
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={() => onConfirm(value)}
|
||||
endIcon={isLoading && <CircularProgress size={20} />}
|
||||
>
|
||||
{buttonTitle}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +1,8 @@
|
||||
import React from 'react';
|
||||
import { FallbackProps } from 'react-error-boundary';
|
||||
import { Alert, AlertTitle, Button } from '@mui/material';
|
||||
import { Alert } from '@mui/material';
|
||||
|
||||
export const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => (
|
||||
<div>
|
||||
<Alert severity="error" data-testid="error-message">
|
||||
<AlertTitle>{error.name}</AlertTitle>
|
||||
{error.message}
|
||||
</Alert>
|
||||
<Alert severity="error" data-testid="stack-trace">
|
||||
<AlertTitle>Stack trace</AlertTitle>
|
||||
{error.stack}
|
||||
</Alert>
|
||||
<Button onClick={resetErrorBoundary}>Back to safety</Button>
|
||||
</div>
|
||||
export const Error = ({ message }: { message: string }) => (
|
||||
<Alert severity="error" variant="outlined" data-testid="error" sx={{ color: 'error.light', width: '100%' }}>
|
||||
{message}
|
||||
</Alert>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import { FallbackProps } from 'react-error-boundary';
|
||||
import { Alert, AlertTitle, Button } from '@mui/material';
|
||||
|
||||
export const ErrorFallback = ({ error, resetErrorBoundary }: FallbackProps) => (
|
||||
<div>
|
||||
<Alert severity="error" data-testid="error-message">
|
||||
<AlertTitle>{error.name}</AlertTitle>
|
||||
{error.message}
|
||||
</Alert>
|
||||
<Alert severity="error" data-testid="stack-trace">
|
||||
<AlertTitle>Stack trace</AlertTitle>
|
||||
{error.stack}
|
||||
</Alert>
|
||||
<Button onClick={resetErrorBoundary}>Back to safety</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -2,11 +2,11 @@ import React, { useState, useEffect, useContext } from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { Operation } from '../types';
|
||||
import { getGasFee } from '../requests';
|
||||
import { ClientContext } from '../context/main';
|
||||
import { AppContext } from '../context/main';
|
||||
|
||||
export const Fee = ({ feeType }: { feeType: Operation }) => {
|
||||
const [fee, setFee] = useState<string>();
|
||||
const { currency } = useContext(ClientContext);
|
||||
const { currency } = useContext(AppContext);
|
||||
|
||||
const getFee = async () => {
|
||||
const res = await getGasFee(feeType);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { Box, LinearProgress, Stack } from '@mui/material';
|
||||
import { NymWordmark } from '@nymproject/react';
|
||||
import { AuthTheme } from 'src/theme';
|
||||
|
||||
export const LoadingPage = () => (
|
||||
<AuthTheme>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'auto',
|
||||
bgcolor: 'nym.background.dark',
|
||||
zIndex: 2000,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
margin: 'auto',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={3} alignItems="center" sx={{ width: 1080 }}>
|
||||
<NymWordmark width={75} fill="white" />
|
||||
<Box width="25%">
|
||||
<LinearProgress variant="indeterminate" color="primary" />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
</AuthTheme>
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { Alert, Button, Stack, TextField, Typography } from '@mui/material';
|
||||
import { Check, ContentCopySharp } from '@mui/icons-material';
|
||||
|
||||
export const Mnemonic = ({
|
||||
mnemonic,
|
||||
copied,
|
||||
handleCopy,
|
||||
}: {
|
||||
mnemonic: string;
|
||||
copied: boolean;
|
||||
handleCopy: (text?: string) => void;
|
||||
}) => (
|
||||
<Stack spacing={2} alignItems="center">
|
||||
<Alert severity="warning" icon={false} sx={{ display: 'block' }}>
|
||||
<Typography sx={{ textAlign: 'center' }}>
|
||||
Below is your 24 word mnemonic, make sure to store it in a safe place for accessing your wallet in the future
|
||||
</Typography>
|
||||
</Alert>
|
||||
<TextField multiline rows={3} value={mnemonic} fullWidth />
|
||||
|
||||
<Button
|
||||
color="inherit"
|
||||
disableElevation
|
||||
size="large"
|
||||
onClick={() => {
|
||||
handleCopy(mnemonic);
|
||||
}}
|
||||
sx={{
|
||||
width: 250,
|
||||
}}
|
||||
endIcon={!copied ? <ContentCopySharp /> : <Check color="success" />}
|
||||
>
|
||||
Copy mnemonic
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
@@ -2,7 +2,7 @@ import React, { useContext, useEffect } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
|
||||
import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material';
|
||||
import { ClientContext } from '../context/main';
|
||||
import { AppContext } from '../context/main';
|
||||
import { Bond, Delegate, Unbond, Undelegate } from '../svg-icons';
|
||||
|
||||
const routesSchema = [
|
||||
@@ -44,7 +44,7 @@ const routesSchema = [
|
||||
];
|
||||
|
||||
export const Nav = () => {
|
||||
const { isAdminAddress, handleShowAdmin } = useContext(ClientContext);
|
||||
const { isAdminAddress, handleShowAdmin } = useContext(AppContext);
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import { Button, List, ListItem, ListItemIcon, ListItemText, ListSubheader, Popover } from '@mui/material';
|
||||
import { ArrowDropDown, CheckSharp } from '@mui/icons-material';
|
||||
import { ClientContext } from '../context/main';
|
||||
import { AppContext } from '../context/main';
|
||||
import { config } from '../../config';
|
||||
import { Network } from '../types';
|
||||
|
||||
@@ -23,7 +23,7 @@ const NetworkItem: React.FC<{ title: string; isSelected: boolean; onSelect: () =
|
||||
);
|
||||
|
||||
export const NetworkSelector = () => {
|
||||
const { network, switchNetwork } = useContext(ClientContext);
|
||||
const { network, switchNetwork } = useContext(AppContext);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
|
||||
@@ -14,10 +14,11 @@ export const NymCard: React.FC<{
|
||||
title: string | React.ReactElement;
|
||||
subheader?: string;
|
||||
Action?: React.ReactNode;
|
||||
Icon?: any;
|
||||
Icon?: React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
}> = ({ title, subheader, Action, Icon, noPadding, children }) => (
|
||||
<Card variant="outlined" sx={{ overflow: 'auto' }}>
|
||||
borderless?: boolean;
|
||||
}> = ({ title, subheader, Action, Icon, noPadding, borderless, children }) => (
|
||||
<Card variant="outlined" sx={{ overflow: 'auto', ...(borderless && { border: 'none', dropShadow: 'none' }) }}>
|
||||
<CardHeader
|
||||
sx={{ p: 3, color: 'nym.background.dark' }}
|
||||
title={<Title title={title} Icon={Icon} />}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
export const Title: React.FC<{ title: string | React.ReactNode; Icon: any }> = ({ title, Icon }) => (
|
||||
export const Title: React.FC<{ title: string | React.ReactNode; Icon?: React.ReactNode }> = ({ title, Icon }) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
{Icon && <Icon sx={{ mr: 1 }} />}{' '}
|
||||
{Icon}
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { FormControl, InputLabel, ListItemText, MenuItem, Select, SelectChangeEvent, Typography } from '@mui/material';
|
||||
import { ClientContext } from '../context/main';
|
||||
import { AppContext } from '../context/main';
|
||||
|
||||
type TPoolOption = 'balance' | 'locked';
|
||||
|
||||
@@ -12,7 +12,7 @@ export const TokenPoolSelector: React.FC<{ disabled: boolean; onSelect: (pool: T
|
||||
const {
|
||||
userBalance: { tokenAllocation, balance, fetchBalance, fetchTokenAllocation },
|
||||
currency,
|
||||
} = useContext(ClientContext);
|
||||
} = useContext(AppContext);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
export * from './Error';
|
||||
export * from './CopyToClipboard';
|
||||
export * from './NymCard';
|
||||
export * from './Nav';
|
||||
export * from './NodeTypeSelector';
|
||||
export * from './RequestStatus';
|
||||
export * from './NoClientError';
|
||||
export * from './SuccessResponse';
|
||||
export * from './TransactionDetails';
|
||||
export * from './NymLogo';
|
||||
export * from './Fee';
|
||||
export * from './AppBar';
|
||||
export * from './NetworkSelector';
|
||||
export * from './ClientAddress';
|
||||
export * from './ConfirmPassword';
|
||||
export * from './CopyToClipboard';
|
||||
export * from './ErrorFallback';
|
||||
export * from './Fee';
|
||||
export * from './InfoToolTip';
|
||||
export * from './LoadingPage';
|
||||
export * from './Mnemonic';
|
||||
export * from './Nav';
|
||||
export * from './NetworkSelector';
|
||||
export * from './NoClientError';
|
||||
export * from './NodeTypeSelector';
|
||||
export * from './NymCard';
|
||||
export * from './NymLogo';
|
||||
export * from './RequestStatus';
|
||||
export * from './SuccessResponse';
|
||||
export * from './textfields';
|
||||
export * from './Title';
|
||||
export * from './TokenPoolSelector';
|
||||
export * from './TransactionDetails';
|
||||
|
||||
+5
-3
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, IconButton, Stack, TextField } from '@mui/material';
|
||||
import { Visibility, VisibilityOff } from '@mui/icons-material';
|
||||
import { Error } from './error';
|
||||
import { Error } from './Error';
|
||||
|
||||
export const MnemonicInput: React.FC<{
|
||||
mnemonic: string;
|
||||
@@ -36,10 +36,11 @@ export const MnemonicInput: React.FC<{
|
||||
export const PasswordInput: React.FC<{
|
||||
password: string;
|
||||
error?: string;
|
||||
label: string;
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
autoFocus?: boolean;
|
||||
onUpdatePassword: (password: string) => void;
|
||||
}> = ({ password, label, error, autoFocus, onUpdatePassword }) => {
|
||||
}> = ({ password, label, placeholder, error, autoFocus, onUpdatePassword }) => {
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
return (
|
||||
@@ -47,6 +48,7 @@ export const PasswordInput: React.FC<{
|
||||
<Box>
|
||||
<TextField
|
||||
label={label}
|
||||
placeholder={placeholder}
|
||||
fullWidth
|
||||
value={password}
|
||||
onChange={(e) => onUpdatePassword(e.target.value)}
|
||||
@@ -0,0 +1,139 @@
|
||||
import React, { createContext, Dispatch, SetStateAction, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { AccountEntry } from 'src/types';
|
||||
import { addAccount as addAccountRequest, showMnemonicForAccount } from 'src/requests';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { AppContext } from './main';
|
||||
|
||||
type TAccounts = {
|
||||
accounts?: AccountEntry[];
|
||||
selectedAccount?: AccountEntry;
|
||||
accountToEdit?: AccountEntry;
|
||||
dialogToDisplay?: TAccountsDialog;
|
||||
isLoading: boolean;
|
||||
error?: string;
|
||||
accountMnemonic: TAccountMnemonic;
|
||||
setError: Dispatch<SetStateAction<string | undefined>>;
|
||||
setAccountMnemonic: Dispatch<SetStateAction<TAccountMnemonic>>;
|
||||
handleAddAccount: (data: { accountName: string; mnemonic: string; password: string }) => void;
|
||||
setDialogToDisplay: (dialog?: TAccountsDialog) => void;
|
||||
handleSelectAccount: (data: { accountName: string; password: string }) => Promise<boolean>;
|
||||
handleAccountToEdit: (accountId: string) => void;
|
||||
handleEditAccount: (account: AccountEntry) => void;
|
||||
handleImportAccount: (account: AccountEntry) => void;
|
||||
handleGetAccountMnemonic: (data: { password: string; accountName: string }) => void;
|
||||
};
|
||||
|
||||
export type TAccountsDialog = 'Accounts' | 'Add' | 'Edit' | 'Import' | 'Mnemonic';
|
||||
export type TAccountMnemonic = { value?: string; accountName?: string };
|
||||
|
||||
export const AccountsContext = createContext({} as TAccounts);
|
||||
|
||||
export const AccountsProvider: React.FC = ({ children }) => {
|
||||
const [accounts, setAccounts] = useState<AccountEntry[]>([]);
|
||||
const [selectedAccount, setSelectedAccount] = useState<AccountEntry>();
|
||||
const [accountToEdit, setAccountToEdit] = useState<AccountEntry>();
|
||||
const [dialogToDisplay, setDialogToDisplay] = useState<TAccountsDialog>();
|
||||
const [accountMnemonic, setAccountMnemonic] = useState<TAccountMnemonic>({
|
||||
value: undefined,
|
||||
accountName: undefined,
|
||||
});
|
||||
const [error, setError] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { onAccountChange, storedAccounts } = useContext(AppContext);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const handleAddAccount = async ({
|
||||
accountName,
|
||||
mnemonic,
|
||||
password,
|
||||
}: {
|
||||
accountName: string;
|
||||
mnemonic: string;
|
||||
password: string;
|
||||
}) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const newAccount = await addAccountRequest({
|
||||
accountName,
|
||||
mnemonic,
|
||||
password,
|
||||
});
|
||||
setAccounts((accs) => [...accs, newAccount]);
|
||||
enqueueSnackbar('New account created', { variant: 'success' });
|
||||
} catch (e) {
|
||||
setError(`Error adding account: ${e}`);
|
||||
throw new Error();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
const handleEditAccount = (account: AccountEntry) =>
|
||||
setAccounts((accs) => accs?.map((acc) => (acc.address === account.address ? account : acc)));
|
||||
|
||||
const handleImportAccount = (account: AccountEntry) => setAccounts((accs) => [...(accs ? [...accs] : []), account]);
|
||||
|
||||
const handleAccountToEdit = (accountName: string) =>
|
||||
setAccountToEdit(accounts?.find((acc) => acc.id === accountName));
|
||||
|
||||
const handleSelectAccount = async ({ accountName, password }: { accountName: string; password: string }) => {
|
||||
try {
|
||||
await onAccountChange({ accountId: accountName, password });
|
||||
const match = accounts?.find((acc) => acc.id === accountName);
|
||||
setSelectedAccount(match);
|
||||
return true;
|
||||
} catch (e) {
|
||||
setError('Error switching account. Please check your password');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleGetAccountMnemonic = async ({ password, accountName }: { password: string; accountName: string }) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const mnemonic = await showMnemonicForAccount({ password, accountName });
|
||||
setAccountMnemonic({ value: mnemonic, accountName });
|
||||
} catch (e) {
|
||||
setError(e as string);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (storedAccounts) {
|
||||
setAccounts(storedAccounts);
|
||||
}
|
||||
|
||||
if (storedAccounts && !selectedAccount) {
|
||||
setSelectedAccount(storedAccounts[0]);
|
||||
}
|
||||
}, [storedAccounts]);
|
||||
|
||||
return (
|
||||
<AccountsContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
error,
|
||||
setError,
|
||||
accounts,
|
||||
selectedAccount,
|
||||
accountToEdit,
|
||||
dialogToDisplay,
|
||||
accountMnemonic,
|
||||
setDialogToDisplay,
|
||||
setAccountMnemonic,
|
||||
isLoading,
|
||||
handleAddAccount,
|
||||
handleEditAccount,
|
||||
handleAccountToEdit,
|
||||
handleSelectAccount,
|
||||
handleImportAccount,
|
||||
handleGetAccountMnemonic,
|
||||
}),
|
||||
[accounts, selectedAccount, accountToEdit, dialogToDisplay, isLoading, error, accountMnemonic],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</AccountsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { createContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { createMnemonic } from 'src/requests';
|
||||
import { TMnemonicWords } from '../types';
|
||||
import { TMnemonicWords } from 'src/pages/auth/types';
|
||||
|
||||
export const SignInContext = createContext({} as TSignInContent);
|
||||
export const AuthContext = createContext({} as TAuthContext);
|
||||
|
||||
export type TSignInContent = {
|
||||
export type TAuthContext = {
|
||||
error?: string;
|
||||
password: string;
|
||||
mnemonic: string;
|
||||
@@ -22,23 +21,17 @@ const mnemonicToArray = (mnemonic: string): TMnemonicWords =>
|
||||
.split(' ')
|
||||
.reduce((a, c: string, index) => [...a, { name: c, index: index + 1, disabled: false }], [] as TMnemonicWords);
|
||||
|
||||
export const SignInProvider: React.FC = ({ children }) => {
|
||||
export const AuthProvider: React.FC = ({ children }) => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [mnemonic, setMnemonic] = useState('');
|
||||
const [mnemonicWords, setMnemonicWords] = useState<TMnemonicWords>([]);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const generateMnemonic = async () => {
|
||||
const mnemonicPhrase = await createMnemonic();
|
||||
setMnemonic(mnemonicPhrase);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
history.push('/welcome');
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (mnemonic.length > 0) {
|
||||
const mnemonicArray = mnemonicToArray(mnemonic);
|
||||
@@ -54,7 +47,7 @@ export const SignInProvider: React.FC = ({ children }) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SignInContext.Provider
|
||||
<AuthContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
error,
|
||||
@@ -71,6 +64,6 @@ export const SignInProvider: React.FC = ({ children }) => {
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SignInContext.Provider>
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './main';
|
||||
export * from './auth';
|
||||
export * from './accounts';
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { useMemo, createContext, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { TLoginType } from 'src/pages/sign-in/types';
|
||||
import { Account, AppEnv, Network, TCurrency, TMixnodeBondDetails } from '../types';
|
||||
import { Account, Network, TCurrency, TMixnodeBondDetails, AccountEntry, AppEnv } from '../types';
|
||||
import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance';
|
||||
import {
|
||||
getMixnodeBondDetails,
|
||||
@@ -10,7 +9,9 @@ import {
|
||||
signInWithMnemonic,
|
||||
signInWithPassword,
|
||||
signOut,
|
||||
switchAccount,
|
||||
getEnv,
|
||||
listAccounts,
|
||||
} from '../requests';
|
||||
import { currencyMap } from '../utils';
|
||||
import { Console } from '../utils/console';
|
||||
@@ -26,10 +27,13 @@ export const urls = (networkName?: Network) =>
|
||||
networkExplorer: `https://${networkName}-explorer.nymtech.net`,
|
||||
};
|
||||
|
||||
type TClientContext = {
|
||||
type TLoginType = 'mnemonic' | 'password';
|
||||
|
||||
type TAppContext = {
|
||||
mode: 'light' | 'dark';
|
||||
appEnv?: AppEnv;
|
||||
clientDetails?: Account;
|
||||
storedAccounts?: AccountEntry[];
|
||||
mixnodeDetails?: TMixnodeBondDetails | null;
|
||||
userBalance: TUseuserBalance;
|
||||
showAdmin: boolean;
|
||||
@@ -40,30 +44,34 @@ type TClientContext = {
|
||||
isLoading: boolean;
|
||||
isAdminAddress: boolean;
|
||||
error?: string;
|
||||
loginType?: TLoginType;
|
||||
setIsLoading: (isLoading: boolean) => void;
|
||||
setError: (value?: string) => void;
|
||||
switchNetwork: (network: Network) => void;
|
||||
getBondDetails: () => Promise<void>;
|
||||
handleShowSettings: () => void;
|
||||
handleShowAdmin: () => void;
|
||||
logIn: (opts: { type: TLoginType; value: string }) => void;
|
||||
handleShowTerminal: () => void;
|
||||
logIn: (opts: { type: 'mnemonic' | 'password'; value: string }) => void;
|
||||
signInWithPassword: (password: string) => void;
|
||||
logOut: () => void;
|
||||
onAccountChange: ({ accountId, password }: { accountId: string; password: string }) => void;
|
||||
};
|
||||
|
||||
export const ClientContext = createContext({} as TClientContext);
|
||||
export const AppContext = createContext({} as TAppContext);
|
||||
|
||||
export const ClientContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [appEnv, setAppEnv] = useState<AppEnv>();
|
||||
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [clientDetails, setClientDetails] = useState<Account>();
|
||||
const [storedAccounts, setStoredAccounts] = useState<AccountEntry[]>();
|
||||
const [mixnodeDetails, setMixnodeDetails] = useState<TMixnodeBondDetails | null>();
|
||||
const [network, setNetwork] = useState<Network | undefined>();
|
||||
const [appEnv, setAppEnv] = useState<AppEnv>();
|
||||
const [currency, setCurrency] = useState<TCurrency>();
|
||||
const [showAdmin, setShowAdmin] = useState(false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [showTerminal, setShowTerminal] = useState(false);
|
||||
const [mode] = useState<'light' | 'dark'>('light');
|
||||
const [loginType, setLoginType] = useState<'mnemonic' | 'password'>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
@@ -71,6 +79,15 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
const history = useHistory();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const clearState = () => {
|
||||
userBalance.clearAll();
|
||||
setStoredAccounts(undefined);
|
||||
setNetwork(undefined);
|
||||
setError(undefined);
|
||||
setIsLoading(false);
|
||||
setMixnodeDetails(undefined);
|
||||
};
|
||||
|
||||
const loadAccount = async (n: Network) => {
|
||||
try {
|
||||
const client = await selectNetwork(n);
|
||||
@@ -83,6 +100,11 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
}
|
||||
};
|
||||
|
||||
const loadStoredAccounts = async () => {
|
||||
const accounts = await listAccounts();
|
||||
setStoredAccounts(accounts);
|
||||
};
|
||||
|
||||
const getBondDetails = async () => {
|
||||
setMixnodeDetails(undefined);
|
||||
try {
|
||||
@@ -93,19 +115,25 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getEnv().then(setAppEnv);
|
||||
}, []);
|
||||
const refreshAccount = async (_network: Network) => {
|
||||
await loadAccount(_network);
|
||||
if (loginType === 'password') {
|
||||
await loadStoredAccounts();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const refreshAccount = async () => {
|
||||
if (network) {
|
||||
await loadAccount(network);
|
||||
await getBondDetails();
|
||||
await userBalance.fetchBalance();
|
||||
}
|
||||
};
|
||||
refreshAccount();
|
||||
if (!clientDetails) {
|
||||
clearState();
|
||||
history.push('/');
|
||||
}
|
||||
}, [clientDetails]);
|
||||
|
||||
useEffect(() => {
|
||||
if (network) {
|
||||
refreshAccount(network);
|
||||
getEnv().then(setAppEnv);
|
||||
}
|
||||
}, [network]);
|
||||
|
||||
const logIn = async ({ type, value }: { type: TLoginType; value: string }) => {
|
||||
@@ -117,8 +145,10 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
setIsLoading(true);
|
||||
if (type === 'mnemonic') {
|
||||
await signInWithMnemonic(value);
|
||||
setLoginType('mnemonic');
|
||||
} else {
|
||||
await signInWithPassword(value);
|
||||
setLoginType('password');
|
||||
}
|
||||
setNetwork('MAINNET');
|
||||
history.push('/balance');
|
||||
@@ -130,16 +160,26 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
};
|
||||
|
||||
const logOut = async () => {
|
||||
userBalance.clearAll();
|
||||
setClientDetails(undefined);
|
||||
setNetwork(undefined);
|
||||
setError(undefined);
|
||||
setIsLoading(false);
|
||||
setMixnodeDetails(undefined);
|
||||
await signOut();
|
||||
setClientDetails(undefined);
|
||||
enqueueSnackbar('Successfully logged out', { variant: 'success' });
|
||||
};
|
||||
|
||||
const onAccountChange = async ({ accountId, password }: { accountId: string; password: string }) => {
|
||||
if (network) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await switchAccount({ accountId, password });
|
||||
await loadAccount(network);
|
||||
enqueueSnackbar('Account switch success', { variant: 'success', preventDuplicate: true });
|
||||
} catch (e) {
|
||||
throw new Error(`Error swtiching account: ${e}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowAdmin = () => setShowAdmin((show) => !show);
|
||||
const handleShowSettings = () => setShowSettings((show) => !show);
|
||||
const handleShowTerminal = () => setShowTerminal((show) => !show);
|
||||
@@ -153,6 +193,7 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
isLoading,
|
||||
error,
|
||||
clientDetails,
|
||||
storedAccounts,
|
||||
mixnodeDetails,
|
||||
userBalance,
|
||||
showAdmin,
|
||||
@@ -160,6 +201,7 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
showTerminal,
|
||||
network,
|
||||
currency,
|
||||
loginType,
|
||||
setIsLoading,
|
||||
setError,
|
||||
signInWithPassword,
|
||||
@@ -170,8 +212,10 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
handleShowTerminal,
|
||||
logIn,
|
||||
logOut,
|
||||
onAccountChange,
|
||||
}),
|
||||
[
|
||||
loginType,
|
||||
mode,
|
||||
appEnv,
|
||||
isLoading,
|
||||
@@ -181,11 +225,12 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
userBalance,
|
||||
showAdmin,
|
||||
showSettings,
|
||||
showTerminal,
|
||||
network,
|
||||
currency,
|
||||
storedAccounts,
|
||||
showTerminal,
|
||||
],
|
||||
);
|
||||
|
||||
return <ClientContext.Provider value={memoizedValue}>{children}</ClientContext.Provider>;
|
||||
return <AppContext.Provider value={memoizedValue}>{children}</AppContext.Provider>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { AccountEntry } from 'src/types';
|
||||
import { AccountsContext, TAccountMnemonic, TAccountsDialog } from '../accounts';
|
||||
|
||||
export const MockAccountsProvider: React.FC = ({ children }) => {
|
||||
const [accounts, setAccounts] = useState<AccountEntry[]>([{ id: 'Account_1', address: 'abc123' }]);
|
||||
const [selectedAccount, setSelectedAccount] = useState<AccountEntry | undefined>({
|
||||
id: 'Account_1',
|
||||
address: 'abc123',
|
||||
});
|
||||
const [accountToEdit, setAccountToEdit] = useState<AccountEntry>();
|
||||
const [dialogToDisplay, setDialogToDisplay] = useState<TAccountsDialog>();
|
||||
const [accountMnemonic, setAccountMnemonic] = useState<TAccountMnemonic>({
|
||||
value: undefined,
|
||||
accountName: undefined,
|
||||
});
|
||||
const [error, setError] = useState<string>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleAddAccount = async ({ accountName }: { accountName: string; mnemonic: string; password: string }) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
setAccounts((accs) => [...accs, { address: 'abc123', id: accountName }]);
|
||||
setDialogToDisplay('Accounts');
|
||||
} catch (e) {
|
||||
setError(`Error adding account: ${e}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
const handleEditAccount = (account: AccountEntry) =>
|
||||
setAccounts((accs) => accs?.map((acc) => (acc.address === account.address ? account : acc)));
|
||||
|
||||
const handleImportAccount = (account: AccountEntry) => setAccounts((accs) => [...(accs ? [...accs] : []), account]);
|
||||
|
||||
const handleAccountToEdit = (accountName: string) =>
|
||||
setAccountToEdit(accounts?.find((acc) => acc.id === accountName));
|
||||
|
||||
const handleSelectAccount = async ({ accountName }: { accountName: string; password: string }) => {
|
||||
const match = accounts?.find((acc) => acc.id === accountName);
|
||||
setSelectedAccount(match);
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleGetAccountMnemonic = async ({ accountName }: { password: string; accountName: string }) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const mnemonic = 'test mnemonic';
|
||||
setAccountMnemonic({ value: mnemonic, accountName });
|
||||
} catch (e) {
|
||||
setError(e as string);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<AccountsContext.Provider
|
||||
value={useMemo(
|
||||
() => ({
|
||||
error,
|
||||
setError,
|
||||
accounts,
|
||||
selectedAccount,
|
||||
accountToEdit,
|
||||
dialogToDisplay,
|
||||
accountMnemonic,
|
||||
setDialogToDisplay,
|
||||
setAccountMnemonic,
|
||||
isLoading,
|
||||
handleAddAccount,
|
||||
handleEditAccount,
|
||||
handleAccountToEdit,
|
||||
handleSelectAccount,
|
||||
handleImportAccount,
|
||||
handleGetAccountMnemonic,
|
||||
}),
|
||||
[accounts, selectedAccount, accountToEdit, dialogToDisplay, isLoading, error, accountMnemonic],
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</AccountsContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { Console } from '../utils/console';
|
||||
import { ClientContext } from '../context/main';
|
||||
import { AppContext } from '../context/main';
|
||||
import { checkGatewayOwnership, checkMixnodeOwnership, getVestingPledgeInfo } from '../requests';
|
||||
import { EnumNodeType, TNodeOwnership } from '../types';
|
||||
|
||||
@@ -11,7 +11,7 @@ const initial = {
|
||||
};
|
||||
|
||||
export const useCheckOwnership = () => {
|
||||
const { clientDetails } = useContext(ClientContext);
|
||||
const { clientDetails } = useContext(AppContext);
|
||||
|
||||
const [ownership, setOwnership] = useState<TNodeOwnership>(initial);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
@@ -92,9 +92,7 @@ export const useGetBalance = (address?: string): TUseuserBalance => {
|
||||
} catch (err) {
|
||||
setError(err as string);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
+22
-43
@@ -1,60 +1,39 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import { AppRoutes, SignInRoutes } from './routes';
|
||||
import { ClientContext, ClientContextProvider } from './context/main';
|
||||
import { ApplicationLayout } from './layouts';
|
||||
import { Admin, Settings } from './pages';
|
||||
import { Routes } from './routes';
|
||||
import { AppProvider } from './context/main';
|
||||
import { ErrorFallback } from './components';
|
||||
import { NymWalletTheme, WelcomeTheme } from './theme';
|
||||
import { NymWalletTheme } from './theme';
|
||||
import { maximizeWindow } from './utils';
|
||||
import { SignInProvider } from './pages/sign-in/context';
|
||||
import { Terminal } from './pages/terminal';
|
||||
|
||||
const App = () => {
|
||||
const { clientDetails } = useContext(ClientContext);
|
||||
|
||||
useEffect(() => {
|
||||
maximizeWindow();
|
||||
}, []);
|
||||
|
||||
return !clientDetails ? (
|
||||
<WelcomeTheme>
|
||||
<SignInProvider>
|
||||
<SignInRoutes />
|
||||
</SignInProvider>
|
||||
</WelcomeTheme>
|
||||
) : (
|
||||
<NymWalletTheme>
|
||||
<ApplicationLayout>
|
||||
<Settings />
|
||||
<Admin />
|
||||
<Terminal />
|
||||
<AppRoutes />
|
||||
</ApplicationLayout>
|
||||
</NymWalletTheme>
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<Router>
|
||||
<SnackbarProvider
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<AppProvider>
|
||||
<NymWalletTheme>
|
||||
<Routes />
|
||||
</NymWalletTheme>
|
||||
</AppProvider>
|
||||
</SnackbarProvider>
|
||||
</Router>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
|
||||
const AppWrapper = () => (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
<Router>
|
||||
<SnackbarProvider
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<ClientContextProvider>
|
||||
<App />
|
||||
</ClientContextProvider>
|
||||
</SnackbarProvider>
|
||||
</Router>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
const root = document.getElementById('root');
|
||||
|
||||
ReactDOM.render(<AppWrapper />, root);
|
||||
ReactDOM.render(<App />, root);
|
||||
|
||||
@@ -1,44 +1,52 @@
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { NymWordmark } from '@nymproject/react';
|
||||
import { Box, Container } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { AppBar, Nav } from '../components';
|
||||
import { AppContext } from 'src/context';
|
||||
import { Settings } from 'src/pages';
|
||||
import { AppBar, LoadingPage, Nav } from '../components';
|
||||
|
||||
export const ApplicationLayout: React.FC = ({ children }) => {
|
||||
const theme = useTheme();
|
||||
const { isLoading, showSettings } = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '240px auto',
|
||||
gridTemplateRows: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<>
|
||||
{isLoading && <LoadingPage />}
|
||||
{showSettings && <Settings />}
|
||||
<Box
|
||||
sx={{
|
||||
background: '#121726',
|
||||
overflow: 'auto',
|
||||
py: 4,
|
||||
px: 5,
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '240px auto',
|
||||
gridTemplateRows: '100%',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Box>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<NymWordmark height={14} fill={theme.palette.background.paper} />
|
||||
<Box
|
||||
sx={{
|
||||
background: '#121726',
|
||||
overflow: 'auto',
|
||||
py: 3,
|
||||
px: 5,
|
||||
}}
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Box>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<NymWordmark height={14} fill={theme.palette.background.paper} />
|
||||
</Box>
|
||||
<Nav />
|
||||
</Box>
|
||||
<Nav />
|
||||
</Box>
|
||||
<Container>
|
||||
<AppBar />
|
||||
{children}
|
||||
</Container>
|
||||
</Box>
|
||||
<Container>
|
||||
<AppBar />
|
||||
{children}
|
||||
</Container>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { NymWordmark } from '@nymproject/react';
|
||||
import { Stack, Box } from '@mui/material';
|
||||
import { AppContext } from 'src/context';
|
||||
import { LoadingPage } from 'src/components';
|
||||
import { Step } from '../pages/auth/components/step';
|
||||
|
||||
export const AuthLayout: React.FC = ({ children }) => {
|
||||
const { isLoading } = useContext(AppContext);
|
||||
|
||||
return isLoading ? (
|
||||
<LoadingPage />
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
height: '100vh',
|
||||
width: '100vw',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
overflow: 'auto',
|
||||
bgcolor: 'nym.background.dark',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
margin: 'auto',
|
||||
}}
|
||||
>
|
||||
<Stack spacing={3} alignItems="center" sx={{ width: 1080 }}>
|
||||
<NymWordmark width={75} />
|
||||
<Step />
|
||||
{children}
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Backdrop, Box, Button, CircularProgress, FormControl, Grid, Paper, Slide, TextField } from '@mui/material';
|
||||
|
||||
import { ClientContext } from '../../context/main';
|
||||
import { AppContext } from '../../context/main';
|
||||
import { NymCard } from '../../components';
|
||||
import { getContractParams, setContractParams } from '../../requests';
|
||||
import { TauriContractStateParams } from '../../types';
|
||||
@@ -99,7 +99,7 @@ const AdminForm: React.FC<{
|
||||
};
|
||||
|
||||
export const Admin: React.FC = () => {
|
||||
const { showAdmin, handleShowAdmin } = useContext(ClientContext);
|
||||
const { showAdmin, handleShowAdmin } = useContext(AppContext);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [params, setParams] = useState<TauriContractStateParams>();
|
||||
|
||||
|
||||
-4
@@ -1,8 +1,4 @@
|
||||
export * from './heading';
|
||||
export * from './word-tiles';
|
||||
export * from './render-page';
|
||||
export * from './password-strength';
|
||||
export * from './error';
|
||||
export * from './textfields';
|
||||
export * from './step';
|
||||
export * from './page-layout';
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import { AuthProvider } from 'src/context';
|
||||
import { AuthRoutes } from 'src/routes/auth';
|
||||
|
||||
export const Auth = () => (
|
||||
<AuthProvider>
|
||||
<AuthRoutes />
|
||||
</AuthProvider>
|
||||
);
|
||||
+4
-3
@@ -2,11 +2,12 @@ import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button, Stack } from '@mui/material';
|
||||
import { validateMnemonic } from 'src/requests';
|
||||
import { MnemonicInput, Subtitle } from '../components';
|
||||
import { SignInContext } from '../context';
|
||||
import { MnemonicInput } from 'src/components';
|
||||
import { AuthContext } from 'src/context/auth';
|
||||
import { Subtitle } from '../components';
|
||||
|
||||
export const ConfirmMnemonic = () => {
|
||||
const { error, setError, setMnemonic, mnemonic } = useContext(SignInContext);
|
||||
const { error, setError, setMnemonic, mnemonic } = useContext(AuthContext);
|
||||
const [localMnemonic, setLocalMnemonic] = useState(mnemonic);
|
||||
const history = useHistory();
|
||||
|
||||
+4
-4
@@ -2,17 +2,17 @@ import React, { useContext, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button, CircularProgress, FormControl, Stack } from '@mui/material';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { AuthContext } from 'src/context/auth';
|
||||
import { createPassword } from 'src/requests';
|
||||
import { PasswordInput } from 'src/components';
|
||||
import { Subtitle, Title, PasswordStrength } from '../components';
|
||||
import { PasswordInput } from '../components/textfields';
|
||||
import { SignInContext } from '../context';
|
||||
import { createPassword } from '../../../requests';
|
||||
|
||||
export const ConnectPassword = () => {
|
||||
const [confirmedPassword, setConfirmedPassword] = useState<string>('');
|
||||
const [isStrongPassword, setIsStrongPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { mnemonic, password, setPassword, resetState } = useContext(SignInContext);
|
||||
const { mnemonic, password, setPassword, resetState } = useContext(AuthContext);
|
||||
const history = useHistory();
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
+2
-2
@@ -1,13 +1,13 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Alert, Button, Stack, Typography } from '@mui/material';
|
||||
import { AuthContext } from 'src/context/auth';
|
||||
import { Check, ContentCopySharp } from '@mui/icons-material';
|
||||
import { useClipboard } from 'use-clipboard-copy';
|
||||
import { WordTiles } from '../components';
|
||||
import { SignInContext } from '../context';
|
||||
|
||||
export const CreateMnemonic = () => {
|
||||
const { mnemonic, mnemonicWords, generateMnemonic, resetState } = useContext(SignInContext);
|
||||
const { mnemonic, mnemonicWords, generateMnemonic, resetState } = useContext(AuthContext);
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
+4
-5
@@ -2,18 +2,17 @@ import React, { useContext, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { Button, FormControl, Stack } from '@mui/material';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { AuthContext } from 'src/context/auth';
|
||||
import { createPassword } from 'src/requests';
|
||||
import { PasswordInput } from 'src/components';
|
||||
import { Subtitle, Title, PasswordStrength } from '../components';
|
||||
import { PasswordInput } from '../components/textfields';
|
||||
import { SignInContext } from '../context';
|
||||
import { createPassword } from '../../../requests';
|
||||
|
||||
export const CreatePassword = () => {
|
||||
const { password, setPassword, resetState } = useContext(SignInContext);
|
||||
const { password, setPassword, resetState, mnemonic } = useContext(AuthContext);
|
||||
const [confirmedPassword, setConfirmedPassword] = useState<string>('');
|
||||
const [isStrongPassword, setIsStrongPassword] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const { mnemonic } = useContext(SignInContext);
|
||||
const history = useHistory();
|
||||
|
||||
const handleSkip = () => {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user