Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eccb3af10e | |||
| a7e6844b7d | |||
| db77591bb8 | |||
| 0f29c1466c | |||
| 296b4b834b | |||
| cc02fc56de | |||
| 44d74af159 | |||
| eb47d4c3ac | |||
| 40b4670d80 | |||
| 79603d61d7 | |||
| e8e9a70ef4 | |||
| 0c52ee89c8 | |||
| f324d45721 | |||
| 470e88f46c | |||
| 42a5016822 | |||
| 579cff358d | |||
| f95dda0f2f | |||
| fc666fb984 | |||
| 1264fd9bfb | |||
| 3e8451f292 | |||
| 53f4582202 | |||
| c7c6dcab65 | |||
| 3422c49e85 | |||
| deee0b8e14 | |||
| 7243cb57b5 | |||
| 0276bd7b0b | |||
| 457759bb57 | |||
| de0f8ee2d3 | |||
| ebf97ece9b | |||
| 50a55f4bfb | |||
| 4ee5c6457b | |||
| d7b5fce7aa | |||
| c3d9c1131b | |||
| 5bdfb1ba5c | |||
| 94e51f0047 | |||
| f313e95e2f | |||
| 2b13ac99b4 | |||
| ef220882d4 | |||
| 59e26178ee | |||
| 0d420fb0a5 | |||
| fce195fdba | |||
| 554b1eb022 | |||
| 5b67403fb9 | |||
| 3a528d3b89 | |||
| 466bb97bc7 | |||
| 0d78416454 | |||
| e9bb9792ab | |||
| 88d4a9b111 | |||
| b323c62a6e | |||
| d511aac301 |
@@ -5,7 +5,6 @@ on:
|
||||
paths:
|
||||
- 'clients/**'
|
||||
- 'common/**'
|
||||
- 'explorer-api/**'
|
||||
- 'gateway/**'
|
||||
- 'integrations/**'
|
||||
- 'nym-api/**'
|
||||
@@ -39,7 +38,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ arc-ubuntu-22.04, custom-windows-11, custom-runner-mac-m1 ]
|
||||
os: [ arc-ubuntu-22.04, custom-windows-11, custom-macos-15 ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -31,6 +31,7 @@ jobs:
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.87
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
|
||||
|
||||
@@ -66,7 +66,6 @@ jobs:
|
||||
with:
|
||||
name: my-artifact
|
||||
path: |
|
||||
target/release/explorer-api
|
||||
target/release/nym-client
|
||||
target/release/nym-socks5-client
|
||||
target/release/nym-api
|
||||
@@ -75,14 +74,13 @@ jobs:
|
||||
target/release/nymvisor
|
||||
target/release/nym-node
|
||||
retention-days: 30
|
||||
|
||||
|
||||
- id: create-release
|
||||
name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
target/release/explorer-api
|
||||
target/release/nym-client
|
||||
target/release/nym-socks5-client
|
||||
target/release/nym-api
|
||||
|
||||
@@ -40,7 +40,6 @@ validator-config
|
||||
validator-api-config.toml
|
||||
dist
|
||||
storybook-static
|
||||
envs/qwerty.env
|
||||
.parcel-cache
|
||||
**/.DS_Store
|
||||
cpu-cycles/libcpucycles/build
|
||||
|
||||
@@ -4,6 +4,30 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.11-cheddar] (2025-06-10)
|
||||
|
||||
- No autoremoval of peers ([#5831])
|
||||
- Set cached storage counters to 0 ([#5812])
|
||||
- hack: temporarily use next.config.js instead of next.config.ts ([#5805])
|
||||
- chore: resolve 1.87 clippy warnings ([#5802])
|
||||
- Nym Statistics API ([#5800])
|
||||
- QoL: RequestPath trait for http-api-client ([#5788])
|
||||
- Fix contains ticketbook function that always returned true ([#5787])
|
||||
- swap a decode into a fromrow to please future postgres feature ([#5785])
|
||||
- Make address cache configurable ([#5784])
|
||||
- Track wireguard credential retries ([#5783])
|
||||
|
||||
[#5831]: https://github.com/nymtech/nym/pull/5831
|
||||
[#5812]: https://github.com/nymtech/nym/pull/5812
|
||||
[#5805]: https://github.com/nymtech/nym/pull/5805
|
||||
[#5802]: https://github.com/nymtech/nym/pull/5802
|
||||
[#5800]: https://github.com/nymtech/nym/pull/5800
|
||||
[#5788]: https://github.com/nymtech/nym/pull/5788
|
||||
[#5787]: https://github.com/nymtech/nym/pull/5787
|
||||
[#5785]: https://github.com/nymtech/nym/pull/5785
|
||||
[#5784]: https://github.com/nymtech/nym/pull/5784
|
||||
[#5783]: https://github.com/nymtech/nym/pull/5783
|
||||
|
||||
## [2025.10-brie] (2025-05-27)
|
||||
|
||||
- Backport PR 5779 ([#5801])
|
||||
|
||||
Generated
+12
-25
@@ -2550,7 +2550,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "extension-storage"
|
||||
version = "1.3.0-rc.0"
|
||||
version = "1.4.0-rc.0"
|
||||
dependencies = [
|
||||
"bip39",
|
||||
"console_error_panic_hook",
|
||||
@@ -4795,7 +4795,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.59"
|
||||
version = "1.1.61"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -5082,24 +5082,9 @@ dependencies = [
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-bity-integration"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmrs",
|
||||
"eyre",
|
||||
"k256",
|
||||
"nym-cli-commands",
|
||||
"nym-validator-client 0.1.0",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.1.56"
|
||||
version = "1.1.58"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -5181,7 +5166,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.1.56"
|
||||
version = "1.1.58"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -6453,7 +6438,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.57"
|
||||
version = "1.1.59"
|
||||
dependencies = [
|
||||
"addr",
|
||||
"anyhow",
|
||||
@@ -6503,7 +6488,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node"
|
||||
version = "1.12.0"
|
||||
version = "1.14.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -6664,12 +6649,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-api"
|
||||
version = "3.0.0-rc.1"
|
||||
version = "3.0.0"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
"bip39",
|
||||
"celes",
|
||||
"clap",
|
||||
"cosmwasm-std",
|
||||
"envy",
|
||||
@@ -7026,7 +7012,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.56"
|
||||
version = "1.1.58"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -7451,10 +7437,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-statistics-api"
|
||||
version = "0.1.1"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
"axum-client-ip",
|
||||
"axum-extra",
|
||||
"celes",
|
||||
"clap",
|
||||
@@ -7981,7 +7968,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nymvisor"
|
||||
version = "0.1.21"
|
||||
version = "0.1.23"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
|
||||
@@ -101,7 +101,6 @@ members = [
|
||||
"common/wireguard-types",
|
||||
"documentation/autodoc",
|
||||
"gateway",
|
||||
"integrations/bity",
|
||||
"nym-api",
|
||||
"nym-api/nym-api-requests",
|
||||
"nym-browser-extension/storage",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.56"
|
||||
version = "1.1.58"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.56"
|
||||
version = "1.1.58"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
edition = "2021"
|
||||
|
||||
@@ -16,8 +16,8 @@ async fn main() {
|
||||
let prefix = "n";
|
||||
let denom: Denom = "unym".parse().unwrap();
|
||||
let signer_mnemonic: bip39::Mnemonic = "<MNEMONIC WITH FUNDS HERE>".parse().unwrap();
|
||||
let validator = "https://qwerty-validator.qa.nymte.ch";
|
||||
let to_address: AccountId = "n19kdst4srf76xgwe55jg32mpcpcyf6aqgp6qrdk".parse().unwrap();
|
||||
let validator = "https://rpc.sandbox.nymtech.net";
|
||||
let to_address: AccountId = "n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa".parse().unwrap();
|
||||
|
||||
let signer = DirectSecp256k1HdWallet::from_mnemonic(prefix, signer_mnemonic);
|
||||
let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone();
|
||||
|
||||
@@ -60,7 +60,7 @@ pub async fn send(args: Args, client: &SigningClient) {
|
||||
"Nodesguru: https://nym.explorers.guru/transaction/{}",
|
||||
&res.hash
|
||||
);
|
||||
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
|
||||
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
|
||||
println!("Transaction result code: {}", &res.tx_result.code.value());
|
||||
println!("Transaction hash: {}", &res.hash);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ pub async fn send_multiple(args: Args, client: &SigningClient) {
|
||||
"Nodesguru: https://nym.explorers.guru/transaction/{}",
|
||||
&res.hash
|
||||
);
|
||||
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
|
||||
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
|
||||
println!("Transaction result code: {}", &res.tx_result.code.value());
|
||||
println!("Transaction hash: {}", &res.hash);
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ pub async fn execute(args: Args, client: SigningClient) {
|
||||
&res.transaction_hash
|
||||
);
|
||||
println!(
|
||||
"Mintscan: https://www.mintscan.io/nyx/txs/{}",
|
||||
"Mintscan: https://ping.pub/nyx/tx/{}",
|
||||
&res.transaction_hash
|
||||
);
|
||||
println!("Transaction hash: {}", &res.transaction_hash);
|
||||
|
||||
@@ -40,6 +40,10 @@ impl BandwidthStorageManager {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_bandwidth(&self) -> ClientBandwidth {
|
||||
self.client_bandwidth.clone()
|
||||
}
|
||||
|
||||
pub async fn available_bandwidth(&self) -> i64 {
|
||||
self.client_bandwidth.available().await
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ impl ClientBandwidth {
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) async fn available(&self) -> i64 {
|
||||
pub async fn available(&self) -> i64 {
|
||||
self.inner.read().await.bandwidth.bytes
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,10 @@ pub enum PeerControlRequest {
|
||||
key: Key,
|
||||
response_tx: oneshot::Sender<QueryBandwidthControlResponse>,
|
||||
},
|
||||
GetClientBandwidth {
|
||||
key: Key,
|
||||
response_tx: oneshot::Sender<GetClientBandwidthControlResponse>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct AddPeerControlResponse {
|
||||
@@ -65,6 +69,10 @@ pub struct QueryBandwidthControlResponse {
|
||||
pub bandwidth_data: Option<RemainingBandwidthData>,
|
||||
}
|
||||
|
||||
pub struct GetClientBandwidthControlResponse {
|
||||
pub client_bandwidth: Option<ClientBandwidth>,
|
||||
}
|
||||
|
||||
pub struct PeerController {
|
||||
storage: GatewayStorage,
|
||||
|
||||
@@ -267,6 +275,14 @@ impl PeerController {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn handle_get_client_bandwidth(&self, key: &Key) -> Option<ClientBandwidth> {
|
||||
if let Some(Some(bandwidth_storage_manager)) = self.bw_storage_managers.get(key) {
|
||||
Some(bandwidth_storage_manager.read().await.client_bandwidth())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_metrics(&self, new_host: &Host) {
|
||||
let now = SystemTime::now();
|
||||
const ACTIVITY_THRESHOLD: Duration = Duration::from_secs(180);
|
||||
@@ -388,6 +404,10 @@ impl PeerController {
|
||||
response_tx.send(QueryBandwidthControlResponse { success: false, bandwidth_data: None }).ok();
|
||||
}
|
||||
}
|
||||
Some(PeerControlRequest::GetClientBandwidth { key, response_tx }) => {
|
||||
let client_bandwidth = self.handle_get_client_bandwidth(&key).await;
|
||||
response_tx.send(GetClientBandwidthControlResponse { client_bandwidth }).ok();
|
||||
}
|
||||
None => {
|
||||
log::trace!("PeerController [main loop]: stopping since channel closed");
|
||||
break;
|
||||
|
||||
@@ -13,12 +13,10 @@ use nym_gateway_storage::models::WireguardPeer;
|
||||
use nym_task::TaskClient;
|
||||
use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tokio_stream::{wrappers::IntervalStream, StreamExt};
|
||||
|
||||
pub(crate) type SharedBandwidthStorageManager = Arc<RwLock<BandwidthStorageManager>>;
|
||||
const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60); // 1 hour
|
||||
|
||||
pub struct PeerHandle {
|
||||
public_key: Key,
|
||||
@@ -28,7 +26,6 @@ pub struct PeerHandle {
|
||||
request_tx: mpsc::Sender<PeerControlRequest>,
|
||||
timeout_check_interval: IntervalStream,
|
||||
task_client: TaskClient,
|
||||
startup_timestamp: SystemTime,
|
||||
}
|
||||
|
||||
impl PeerHandle {
|
||||
@@ -53,7 +50,6 @@ impl PeerHandle {
|
||||
request_tx,
|
||||
timeout_check_interval,
|
||||
task_client,
|
||||
startup_timestamp: SystemTime::now(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,26 +69,49 @@ impl PeerHandle {
|
||||
Ok(success)
|
||||
}
|
||||
|
||||
async fn active_peer(
|
||||
&mut self,
|
||||
storage_peer: &WireguardPeer,
|
||||
kernel_peer: &Peer,
|
||||
) -> Result<bool, Error> {
|
||||
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
|
||||
if kernel_peer.last_handshake.is_none()
|
||||
&& SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER
|
||||
{
|
||||
let success = self.remove_peer().await?;
|
||||
self.peer_storage_manager.remove_peer();
|
||||
tracing::debug!(
|
||||
"Peer {} has not been active for more then {} seconds, removing it",
|
||||
kernel_peer.public_key.to_string(),
|
||||
AUTO_REMOVE_AFTER.as_secs()
|
||||
fn compute_spent_bandwidth(kernel_peer: &Peer, storage_peer: &WireguardPeer) -> Option<u64> {
|
||||
let storage_peer_rx_bytes = u64::try_from(storage_peer.rx_bytes)
|
||||
.inspect_err(|e| tracing::error!("Storage rx bytes could not be converted: {e}"))
|
||||
.ok()?;
|
||||
let storage_peer_tx_bytes = u64::try_from(storage_peer.tx_bytes)
|
||||
.inspect_err(|e| tracing::error!("Storage tx bytes could not be converted: {e}"))
|
||||
.ok()?;
|
||||
|
||||
let kernel_total = kernel_peer
|
||||
.rx_bytes
|
||||
.checked_add(kernel_peer.tx_bytes)
|
||||
.or_else(|| {
|
||||
tracing::error!(
|
||||
"Overflow on kernel adding bytes: {} + {}",
|
||||
kernel_peer.rx_bytes,
|
||||
kernel_peer.tx_bytes
|
||||
);
|
||||
return Ok(!success);
|
||||
}
|
||||
let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes)
|
||||
.checked_sub(storage_peer.rx_bytes as u64 + storage_peer.tx_bytes as u64)
|
||||
None
|
||||
})?;
|
||||
let storage_total = storage_peer_rx_bytes
|
||||
.checked_add(storage_peer_tx_bytes)
|
||||
.or_else(|| {
|
||||
tracing::error!("Overflow on storage adding bytes: {storage_peer_rx_bytes} + {storage_peer_tx_bytes}");
|
||||
None
|
||||
})?;
|
||||
|
||||
kernel_total.checked_sub(storage_total).or_else(|| {
|
||||
tracing::error!("Overflow on spent bandwidth subtraction: kernel - storage = {kernel_total} - {storage_total}");
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
async fn active_peer(&mut self, kernel_peer: &Peer) -> Result<bool, Error> {
|
||||
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
|
||||
log::debug!(
|
||||
"Peer {:?} not in storage anymore, shutting down handle",
|
||||
self.public_key
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
|
||||
let spent_bandwidth = Self::compute_spent_bandwidth(kernel_peer, &storage_peer)
|
||||
.unwrap_or_else(|| {
|
||||
// if gateway restarted, the kernel values restart from 0
|
||||
// and we should restart from 0 in storage as well
|
||||
@@ -100,8 +119,10 @@ impl PeerHandle {
|
||||
self.peer_storage_manager.peer_information.as_mut()
|
||||
{
|
||||
peer_information.force_sync = true;
|
||||
peer_information.peer.rx_bytes = kernel_peer.rx_bytes;
|
||||
peer_information.peer.tx_bytes = kernel_peer.tx_bytes;
|
||||
}
|
||||
kernel_peer.rx_bytes + kernel_peer.tx_bytes
|
||||
0
|
||||
})
|
||||
.try_into()
|
||||
.map_err(|_| Error::InconsistentConsumedBytes)?;
|
||||
@@ -124,14 +145,6 @@ impl PeerHandle {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER {
|
||||
log::debug!(
|
||||
"Peer {} has been present for 30 days, removing it",
|
||||
self.public_key
|
||||
);
|
||||
let success = self.remove_peer().await?;
|
||||
return Ok(!success);
|
||||
}
|
||||
let spent_bandwidth = kernel_peer.rx_bytes + kernel_peer.tx_bytes;
|
||||
if spent_bandwidth >= BANDWIDTH_CAP_PER_DAY {
|
||||
log::debug!(
|
||||
@@ -158,14 +171,7 @@ impl PeerHandle {
|
||||
// the host information hasn't beed updated yet
|
||||
return Ok(true);
|
||||
};
|
||||
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
|
||||
log::debug!(
|
||||
"Peer {:?} not in storage anymore, shutting down handle",
|
||||
self.public_key
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
if !self.active_peer(&storage_peer, &kernel_peer).await? {
|
||||
if !self.active_peer(&kernel_peer).await? {
|
||||
log::debug!(
|
||||
"Peer {:?} is not active anymore, shutting down handle",
|
||||
self.public_key
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
version: "3.7"
|
||||
|
||||
x-network: &NETWORK
|
||||
BECH32_PREFIX: nymt
|
||||
DENOM: nymt
|
||||
STAKE_DENOM: nyxt
|
||||
WASMD_VERSION: v0.27.0
|
||||
|
||||
services:
|
||||
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_nyxd:/root/.nyxd"
|
||||
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_nyxd:/root/.nyxd"
|
||||
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"
|
||||
|
||||
# service to update geoip binary database, for explorer-api
|
||||
geoipupdate:
|
||||
container_name: geoipupdate
|
||||
image: maxmindinc/geoipupdate
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID}
|
||||
GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY}
|
||||
GEOIPUPDATE_EDITION_IDS: ${GEOIPUPDATE_EDITION_IDS}
|
||||
GEOIPUPDATE_FREQUENCY: ${GEOIPUPDATE_FREQUENCY}
|
||||
networks:
|
||||
- geoipupdate
|
||||
volumes:
|
||||
- ${GEOIP_DB_DIRECTORY}:/usr/share/GeoIP
|
||||
|
||||
volumes:
|
||||
genesis_volume:
|
||||
genesis_nyxd:
|
||||
secondary_nyxd:
|
||||
|
||||
# contract_volume:
|
||||
# mongo_data:
|
||||
|
||||
networks:
|
||||
geoipupdate:
|
||||
localnet:
|
||||
driver: bridge
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 172.168.10.0/25
|
||||
@@ -13,10 +13,6 @@ Stake saturation is a node reputation done in a form of self bond or stakers del
|
||||
> **rewarded_set_size = active_set_size + standby_set_size**
|
||||
</Callout>
|
||||
|
||||
{/*
|
||||
With current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM, staking target of <span style={{display: 'inline-block'}}><StakingTarget /></span> NYM, divided by the sum of nodes in the [rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params), <b>the stake saturation level is <span style={{display: 'inline-block'}}><StakeSaturation /></span> NYM per node.</b>
|
||||
*/}
|
||||
|
||||
With current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM, staking target of <span style={{display: 'inline-block'}}><StakingSupply /></span> NYM, divided by the sum of nodes in the [rewarded set](https://validator.nymtech.net/api/v1/epoch/reward_params), <b>the stake saturation level is <span style={{display: 'inline-block'}}><StakeSaturation /></span> NYM per node.</b>
|
||||
|
||||
Node stake saturation is a value between `0` and `1` following this logic.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
},
|
||||
"mixmining_reserve": {
|
||||
"denom": "unym",
|
||||
"amount": "188691142067690"
|
||||
"amount": "187227500568584"
|
||||
},
|
||||
"vesting_tokens": {
|
||||
"denom": "unym",
|
||||
@@ -13,6 +13,6 @@
|
||||
},
|
||||
"circulating_supply": {
|
||||
"denom": "unym",
|
||||
"amount": "811308857932310"
|
||||
"amount": "812772499431416"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
811_308_857
|
||||
812_772_499
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
5_241
|
||||
5_200
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1_034_081
|
||||
1_037_131
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
50.0%
|
||||
30.63%
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
405_654_428
|
||||
248_911_577
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
248_179_643
|
||||
248_911_577
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
| **Item** | **Description** | **Amount in NYM** |
|
||||
|:-------------------|:------------------------------------------------------|--------------------:|
|
||||
| Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 |
|
||||
| Mixmining Reserve | Tokens releasing for operators rewards | 188_691_142 |
|
||||
| Mixmining Reserve | Tokens releasing for operators rewards | 187_227_500 |
|
||||
| Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 811_308_857 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 1_034_081 |
|
||||
| Circulating Supply | Amount of unlocked tokens | 812_772_499 |
|
||||
| Stake Saturation | Optimal size of node self-bond + delegation | 1_037_131 |
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"interval": {
|
||||
"reward_pool": "188691142067690.265566584946684804",
|
||||
"staking_supply": "248179643769563.241524591796100959",
|
||||
"staking_supply_scale_factor": "0.5",
|
||||
"epoch_reward_budget": "5241420612.991396265738470741",
|
||||
"stake_saturation_point": "1034081849039.84683968579915042",
|
||||
"reward_pool": "187227500568584.066152911712126276",
|
||||
"staking_supply": "248911577950871.129740670788161327",
|
||||
"staking_supply_scale_factor": "0.30625",
|
||||
"epoch_reward_budget": "5200763904.682890726469769781",
|
||||
"stake_saturation_point": "1037131574795.296373919461617338",
|
||||
"sybil_resistance": "0.3",
|
||||
"active_set_work_factor": "10",
|
||||
"interval_pool_emission": "0.02"
|
||||
|
||||
@@ -1 +1 @@
|
||||
Wednesday, May 28th 2025, 11:39:51 UTC
|
||||
Friday, June 6th 2025, 09:33:10 UTC
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"chain-registry": "^1.19.0",
|
||||
"cosmjs-types": "^0.9.0",
|
||||
"lucide-react": "^0.438.0",
|
||||
"next": "^14.2.15",
|
||||
"next": "^15.2.4",
|
||||
"nextra": "2",
|
||||
"nextra-theme-docs": "2",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -104,6 +104,7 @@ To get a full comprehension of [node operators rewards](tokenomics/mixnet-reward
|
||||
```
|
||||
|
||||
#### Supply
|
||||
|
||||
<br />
|
||||
<b>Circulating supply is <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM.</b>
|
||||
|
||||
@@ -118,15 +119,10 @@ A number of aimed NYM tokens to be staked in the network. The staking target a i
|
||||
> **staking_target = staking_supply_scale_factor \* circulating_supply**
|
||||
</Callout>
|
||||
|
||||
{/*
|
||||
Staking supply scale factor is currently it's set to be <span style={{display: 'inline-block'}}><StakingScaleFactor /></span>.
|
||||
Staking supply scale factor is currently at <span style={{display: 'inline-block'}}><StakingScaleFactor /></span>.
|
||||
|
||||
The value of this variable can is changed from time to time to optimize the metrics of the network. With a current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM and staking supply scale factor <span style={{display: 'inline-block'}}><StakingScaleFactor /></span>, <b>the staking target is <span style={{display: 'inline-block'}}><StakingTarget /></span> NYM.</b>
|
||||
*/}
|
||||
|
||||
Staking supply scale factor is currently at 30.5%.
|
||||
|
||||
The value of this variable is changed from time to time to optimize the metrics of the network. With a current circulating supply of <span style={{display: 'inline-block'}}><CirculatingSupply /></span> NYM and staking supply scale factor 30.5%, <b>the staking target is <span style={{display: 'inline-block'}}><StakingSupply /></span> NYM.</b>
|
||||
|
||||
#### Stake saturation
|
||||
|
||||
|
||||
Generated
+1631
-500
File diff suppressed because it is too large
Load Diff
@@ -140,6 +140,7 @@ def get_nested_value(response, args):
|
||||
|
||||
def _return_percent_annotation(value):
|
||||
value = float(value) * 100
|
||||
value = round(value, 2)
|
||||
value = f"{value}%"
|
||||
return value
|
||||
|
||||
@@ -269,7 +270,7 @@ def parser_main():
|
||||
action="store_true",
|
||||
help="A multiplier of staking supply scale factor and circulating supply"
|
||||
)
|
||||
|
||||
|
||||
parser_calculate.add_argument(
|
||||
"-s", "--separator",
|
||||
type=str,
|
||||
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
CONFIGURED=true
|
||||
|
||||
RUST_LOG=info
|
||||
RUST_BACKTRACE=1
|
||||
NETWORK_NAME=qa
|
||||
BECH32_PREFIX=n
|
||||
MIX_DENOM=unym
|
||||
MIX_DENOM_DISPLAY=nym
|
||||
STAKE_DENOM=unyx
|
||||
STAKE_DENOM_DISPLAY=nyx
|
||||
DENOMS_EXPONENT=6
|
||||
|
||||
MIXNET_CONTRACT_ADDRESS=n1hm4y6fzgxgu688jgf7ek66px6xkrtmn3gyk8fax3eawhp68c2d5qujz296
|
||||
ECASH_CONTRACT_ADDRESS=n13xspq62y9gq6nueqmywxcdv2yep4p6nzv98w2889k25v3nhdy2dq2rkrk7
|
||||
GROUP_CONTRACT_ADDRESS=n13l7rwuwktklrwskc7m6lv70zws07en85uma28j7dxwsz9y5hvvhspl7a2t
|
||||
MULTISIG_CONTRACT_ADDRESS=n138c9pyf7f3hyx0j3t6vmsz7ultnw2wj0lu6hzndep9z5grgq9haqlc25k0
|
||||
COCONUT_DKG_CONTRACT_ADDRESS=n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq6834wq2a6dtr
|
||||
VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj
|
||||
REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39
|
||||
|
||||
EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api/
|
||||
NYXD=https://rpc.qa-validator.qa.nymte.ch
|
||||
NYXD_WS=wss://rpc.qa-validator.qa.nymte.ch/websocket
|
||||
NYM_API=https://qa-nym-api.qa.nymte.ch/api/
|
||||
NYM_VPN_API=https://nym-vpn-api-git-deploy-qa-nyx-network-staging.vercel.app/api/
|
||||
@@ -1,21 +0,0 @@
|
||||
[package]
|
||||
name = "nym-bity-integration"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.56"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
k256 = { workspace = true, features = ["ecdsa", "sha256"] }
|
||||
eyre = { workspace = true }
|
||||
|
||||
cosmrs = { workspace = true }
|
||||
|
||||
nym-cli-commands = { path = "../../common/commands" }
|
||||
nym-validator-client = { path = "../../common/client-libs/validator-client" }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
@@ -1,53 +0,0 @@
|
||||
# Buy NYM with Bity
|
||||
|
||||
This crate allows Bity to verify orders for purchasing NYM tokens. The same crate is used by the wallet to sign orders for purchases.
|
||||
|
||||
## Signing
|
||||
|
||||
The Nym Wallet user will sign an order message provided by Bity to create a signed order with the following fields:
|
||||
|
||||
```
|
||||
account_id: n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf
|
||||
message: "This is the order message from Bity"
|
||||
order signature:
|
||||
{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
```
|
||||
|
||||
The `signature` field of the order contains a JSON representation of:
|
||||
|
||||
- the Cosmos address of the signer (`account_id`)
|
||||
- the Cosmos public key
|
||||
- a hex string digest of the Bity order message signed by the user
|
||||
|
||||
Note: the `signature_as_hex` is not in recoverable form (e.g. allows recovering the public key from the signature in `secp256k1`). This is why the public key is supplied along with the account id, as the prefix cannot be recovered.
|
||||
|
||||
## Verification
|
||||
|
||||
Verification has been wrapped up into taking a single struct that can be parsed from JSON:
|
||||
|
||||
```
|
||||
{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"message": "This is the order message from Bity",
|
||||
"signature": << ORDER SIGNATURE JSON GOES HERE >>
|
||||
}
|
||||
```
|
||||
|
||||
The following will be checked:
|
||||
|
||||
- the `account_id` supplied matches:
|
||||
- the account id derived from the public key
|
||||
- the account id field in the order signature JSON
|
||||
- the account id is for Nym mainnet
|
||||
- the signature is for the message
|
||||
- all data structures parse correctly
|
||||
- nested structs
|
||||
- account ids
|
||||
- Cosmos public keys
|
||||
@@ -1,220 +0,0 @@
|
||||
pub mod order;
|
||||
pub mod sign;
|
||||
pub mod verify;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::order::{Order, OrderSignature};
|
||||
use crate::{sign, verify};
|
||||
use cosmrs::AccountId;
|
||||
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
|
||||
use std::str::FromStr;
|
||||
|
||||
fn get_order(prefix: &str) -> anyhow::Result<(OrderSignature, String)> {
|
||||
let mnemonic = "crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove";
|
||||
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.parse()?);
|
||||
|
||||
let accounts = wallet.try_derive_accounts()?;
|
||||
|
||||
let message = "This is the order message from Bity";
|
||||
let signature = sign::sign_order(&wallet, &accounts[0], message.to_string())?;
|
||||
|
||||
Ok((signature, message.to_string()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_happy_path() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("n")?;
|
||||
|
||||
let account_id = AccountId::from_str("n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf").unwrap();
|
||||
assert_eq!(account_id.to_string(), signature.account_id.to_string());
|
||||
|
||||
println!("Order signature:");
|
||||
println!("{}", ::serde_json::to_string_pretty(&signature)?);
|
||||
|
||||
Ok(verify::verify_order(Order {
|
||||
account_id,
|
||||
signature,
|
||||
message,
|
||||
})?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_fails_on_non_mainnet_address() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("nymt")?;
|
||||
|
||||
println!("Order signature:");
|
||||
println!("{}", ::serde_json::to_string_pretty(&signature)?);
|
||||
|
||||
let res = verify::verify_order(Order {
|
||||
account_id: signature.clone().account_id,
|
||||
signature,
|
||||
message,
|
||||
});
|
||||
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_fails_on_non_mainnet_address_variant2() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("atom")?;
|
||||
|
||||
println!("Order signature:");
|
||||
println!("{}", ::serde_json::to_string_pretty(&signature)?);
|
||||
|
||||
let res = verify::verify_order(Order {
|
||||
account_id: signature.clone().account_id,
|
||||
signature,
|
||||
message,
|
||||
});
|
||||
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_change_account_id() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("n")?;
|
||||
|
||||
let OrderSignature {
|
||||
signature_as_hex,
|
||||
public_key,
|
||||
account_id: _,
|
||||
} = signature;
|
||||
|
||||
// use a different account id to the one that signed the order
|
||||
let account_id = AccountId::from_str("n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es").unwrap();
|
||||
|
||||
let res = verify::verify_order(Order {
|
||||
account_id: account_id.clone(),
|
||||
signature: OrderSignature {
|
||||
account_id,
|
||||
signature_as_hex,
|
||||
public_key,
|
||||
},
|
||||
message,
|
||||
});
|
||||
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_change_signature() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("n")?;
|
||||
|
||||
let OrderSignature {
|
||||
signature_as_hex: _,
|
||||
public_key,
|
||||
account_id,
|
||||
} = signature;
|
||||
|
||||
let res = verify::verify_order(Order {
|
||||
account_id: account_id.clone(),
|
||||
signature: OrderSignature {
|
||||
account_id,
|
||||
signature_as_hex: "this is not the signature you were looking for".to_string(),
|
||||
public_key,
|
||||
},
|
||||
message,
|
||||
});
|
||||
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_with_json_happy_path() -> anyhow::Result<()> {
|
||||
let json_order = r#"{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"message": "This is the order message from Bity",
|
||||
"signature": {
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
}"#;
|
||||
let order: Order = serde_json::from_str(json_order)?;
|
||||
|
||||
Ok(verify::verify_order(order)?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_with_json_bad_signature() -> anyhow::Result<()> {
|
||||
let json_order = r#"{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"message": "A different message to the one signed",
|
||||
"signature": {
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
}"#;
|
||||
let order: Order = serde_json::from_str(json_order)?;
|
||||
let res = verify::verify_order(order);
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_with_json_bad_account_id() -> anyhow::Result<()> {
|
||||
let json_order = r#"{
|
||||
"account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es",
|
||||
"message": "This is the order message from Bity",
|
||||
"signature": {
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
}"#;
|
||||
let order: Order = serde_json::from_str(json_order)?;
|
||||
let res = verify::verify_order(order);
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_with_json_bad_account_id_variation_2() -> anyhow::Result<()> {
|
||||
let json_order = r#"{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"message": "This is the order message from Bity",
|
||||
"signature": {
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es",
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
}"#;
|
||||
let order: Order = serde_json::from_str(json_order)?;
|
||||
let res = verify::verify_order(order);
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
use cosmrs::crypto::PublicKey;
|
||||
use cosmrs::AccountId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct OrderSignature {
|
||||
pub public_key: PublicKey,
|
||||
pub account_id: AccountId,
|
||||
pub signature_as_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Order {
|
||||
pub account_id: AccountId,
|
||||
pub message: String,
|
||||
pub signature: OrderSignature,
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
use crate::order::OrderSignature;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
|
||||
use nym_validator_client::signing::signer::OfflineSigner;
|
||||
use nym_validator_client::signing::AccountData;
|
||||
|
||||
/// Signs an order message to purchase Nym with Bity
|
||||
pub fn sign_order(
|
||||
wallet: &DirectSecp256k1HdWallet,
|
||||
signer: &AccountData,
|
||||
message: String,
|
||||
) -> Result<OrderSignature, NyxdError> {
|
||||
Ok(OrderSignature {
|
||||
account_id: signer.address().clone(),
|
||||
public_key: signer.public_key(),
|
||||
signature_as_hex: wallet
|
||||
.sign_raw_with_account(signer, message.into_bytes())?
|
||||
.to_string(),
|
||||
})
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use nym_cli_commands::validator::signature::helpers::secp256k1_verify_with_public_key;
|
||||
|
||||
use crate::order::Order;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum VerifyOrderError {
|
||||
#[error("{source}")]
|
||||
K256Error {
|
||||
#[from]
|
||||
source: k256::ecdsa::Error,
|
||||
},
|
||||
#[error("{source}")]
|
||||
ErrorReport {
|
||||
#[from]
|
||||
source: eyre::Report,
|
||||
},
|
||||
#[error("Account id does not match public key")]
|
||||
AccountIdDoesNotMatchPubKey,
|
||||
#[error("Unsupported key type. Only secp256k1 is currently supported.")]
|
||||
UnsupportedKeyType,
|
||||
#[error("Signature error - {0}")]
|
||||
SignatureError(k256::ecdsa::signature::Error),
|
||||
#[error("Account id is not a Nyx mainnet account")]
|
||||
AccountIdPrefixIncorrect,
|
||||
}
|
||||
|
||||
/// Verifies an order
|
||||
pub fn verify_order(order: Order) -> Result<(), VerifyOrderError> {
|
||||
let account_id = order.signature.public_key.account_id("n")?;
|
||||
|
||||
if order.signature.account_id.prefix() != "n" || order.account_id.prefix() != "n" {
|
||||
return Err(VerifyOrderError::AccountIdPrefixIncorrect);
|
||||
}
|
||||
|
||||
// the account id in the order must match the account id derived from the public key
|
||||
if account_id != order.signature.account_id {
|
||||
return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey);
|
||||
}
|
||||
|
||||
// the user provided account id in the order must match the derived account id
|
||||
if account_id != order.account_id || account_id != order.signature.account_id {
|
||||
return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey);
|
||||
}
|
||||
|
||||
if order.signature.public_key.type_url() != cosmrs::crypto::PublicKey::SECP256K1_TYPE_URL {
|
||||
return Err(VerifyOrderError::UnsupportedKeyType);
|
||||
}
|
||||
|
||||
match secp256k1_verify_with_public_key(
|
||||
&order.signature.public_key.to_bytes(),
|
||||
order.signature.signature_as_hex,
|
||||
order.message,
|
||||
) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(VerifyOrderError::SignatureError(e)),
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
[package]
|
||||
name = "nym-api"
|
||||
license = "GPL-3.0"
|
||||
version = "1.1.59"
|
||||
version = "1.1.61"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
|
||||
@@ -13,13 +13,10 @@ pub(crate) fn circulating_supply_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", axum::routing::get(get_full_circulating_supply))
|
||||
.route(
|
||||
"/total-supply-value",
|
||||
"/circulating-supply-value",
|
||||
axum::routing::get(get_circulating_supply),
|
||||
)
|
||||
.route(
|
||||
"/circulating-supply-value",
|
||||
axum::routing::get(get_total_supply),
|
||||
)
|
||||
.route("/total-supply-value", axum::routing::get(get_total_supply))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"@nymproject/eslint-config-react-typescript"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.eslint.json"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
# environment
|
||||
.env.dev
|
||||
|
||||
# error logs
|
||||
yarn-error.log
|
||||
dist/
|
||||
build/
|
||||
**/*.wasm
|
||||
**/*.js.map
|
||||
.env*
|
||||
!.env.example
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
16
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
RPC_URL=
|
||||
VALIDATOR_URL=
|
||||
PREFIX=
|
||||
MIXNET_CONTRACT_ADDRESS=
|
||||
VESTING_CONTRACT_ADDRESS=
|
||||
DENOM=
|
||||
BLOCK_EXPLORER_URL=
|
||||
+108
-23
@@ -1,40 +1,125 @@
|
||||
# Nym Browser Extension
|
||||
# Nym Browser Extension Storage
|
||||
|
||||
The Nym browser extension lets you access your Nym wallet via the browser.
|
||||
A WebAssembly-based storage component for securely managing mnemonics in extensions. This component provides encrypted storage functionality using IndexedDB with password-based encryption.
|
||||
|
||||
## Getting started
|
||||
## Overview
|
||||
|
||||
You will need:
|
||||
This storage component is built in Rust and compiled to WebAssembly, providing:
|
||||
|
||||
- NodeJS (use `nvm install` to automatically install the correct version)
|
||||
- `npm`
|
||||
- `yarn`
|
||||
- **Secure mnemonic storage**: Password-encrypted storage of BIP39 mnemonics
|
||||
- **IndexedDB integration**: Browser-native persistent storage
|
||||
- **Multiple account support**: Store and manage multiple mnemonic phrases with custom names
|
||||
- **Type-safe API**: Promise-based JavaScript API with proper error handling
|
||||
|
||||
> **Note**: This project is part of a mono repo, so you will need to build the shared packages before starting. And any time they change, you'll need to rebuild them.
|
||||
## Getting Started
|
||||
|
||||
From the [root of the repository](../README.md) run the following to build shared packages:
|
||||
### Prerequisites
|
||||
|
||||
```
|
||||
yarn
|
||||
yarn build
|
||||
- Rust (latest stable)
|
||||
- `wasm-pack` tool for building WebAssembly
|
||||
- Node.js (for the demo server)
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
cd storage
|
||||
make wasm-pack
|
||||
```
|
||||
|
||||
From the `nym-browser-extension` directory of the `nym` monorepo, run:
|
||||
This will compile the Rust code to WebAssembly and generate the necessary JavaScript bindings.
|
||||
|
||||
`yarn dev` to run the extension in dev mode.
|
||||
### Example Usage
|
||||
|
||||
You can then open a browser to http://localhost:9000 and start development.
|
||||
See the [internal-dev example](./storage/internal-dev/index.js) for complete usage examples.
|
||||
|
||||
OR
|
||||
Basic usage:
|
||||
|
||||
`yarn build` to build the extension.
|
||||
```javascript
|
||||
import init, { ExtensionStorage, set_panic_hook } from "@nymproject/extension-storage"
|
||||
|
||||
The extension will build to the `nym-browser-extension/dist` directory.
|
||||
// Initialize the WASM module first
|
||||
await init();
|
||||
|
||||
## Load extension
|
||||
// Set up better error handling
|
||||
set_panic_hook();
|
||||
|
||||
To load the extension into a Chrome browser
|
||||
// Create storage instance with password
|
||||
const storage = await new ExtensionStorage("your-secure-password");
|
||||
|
||||
- Go to `settings > extensions > manage extensions`
|
||||
- Select `Load unpacked`
|
||||
- Select the `nym-browser-extension/dist`
|
||||
// Store a mnemonic
|
||||
const mnemonic = "your twenty four word mnemonic phrase goes here...";
|
||||
await storage.store_mnemonic("my-wallet", mnemonic);
|
||||
|
||||
// Read a mnemonic
|
||||
const retrievedMnemonic = await storage.read_mnemonic("my-wallet");
|
||||
|
||||
// Check if a mnemonic exists
|
||||
const exists = await storage.has_mnemonic("my-wallet");
|
||||
|
||||
// Get all stored mnemonic keys
|
||||
const allKeys = await storage.get_all_mnemonic_keys();
|
||||
|
||||
// Remove a mnemonic
|
||||
await storage.remove_mnemonic("my-wallet");
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
To run the internal development example:
|
||||
|
||||
```bash
|
||||
cd storage
|
||||
|
||||
make demo
|
||||
|
||||
# Option 2: Manual server setup
|
||||
cd internal-dev && node serve.js
|
||||
|
||||
# Then open http://localhost:8000 in your browser
|
||||
```
|
||||
|
||||
**Note**: The demo requires a server that properly serves WASM files with `application/wasm` MIME type. The Node.js server is recommended as it handles MIME types more reliably.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Initialization
|
||||
- `init()` - **Required**: Initialize the WASM module before using other functions
|
||||
|
||||
### Constructor
|
||||
- `new ExtensionStorage(password: string)` - Creates a new storage instance with the given password
|
||||
|
||||
### Static Methods
|
||||
- `ExtensionStorage.exists()` - Check if storage database exists
|
||||
|
||||
### Instance Methods
|
||||
- `store_mnemonic(name: string, mnemonic: string)` - Store a mnemonic with the given name
|
||||
- `read_mnemonic(name: string)` - Retrieve a mnemonic by name (returns null if not found)
|
||||
- `has_mnemonic(name: string)` - Check if a mnemonic with the given name exists
|
||||
- `get_all_mnemonic_keys()` - Get all stored mnemonic names
|
||||
- `remove_mnemonic(name: string)` - Remove a mnemonic by name
|
||||
|
||||
### Error Handling
|
||||
- `set_panic_hook()` - Set up better stack traces for Rust panics in development
|
||||
|
||||
## Security Features
|
||||
|
||||
- **Password-based encryption**: All data is encrypted using the provided password
|
||||
- **BIP39 validation**: Mnemonics are validated before storage
|
||||
- **Secure memory handling**: Sensitive data is zeroed from memory when no longer needed
|
||||
- **Browser sandbox**: Runs within the browser's security model
|
||||
|
||||
## Architecture
|
||||
|
||||
The storage component consists of:
|
||||
|
||||
- **Rust core** (`src/storage.rs`): Main storage implementation with encryption
|
||||
- **WASM bindings** (`src/lib.rs`): WebAssembly interface layer
|
||||
- **Error handling** (`src/error.rs`): Comprehensive error types
|
||||
- **Build configuration** (`Cargo.toml`, `Makefile`): Build and dependency management
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **WASM Initialization**: Always call `await init()` before using any other functions
|
||||
2. **MIME Types**: The demo requires a server that properly serves WASM files
|
||||
3. **Browser Compatibility**: Requires modern browsers with WebAssembly support
|
||||
4. **Module Loading**: Uses ES modules - ensure your build system supports them
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
{
|
||||
"name": "@nym/browser-extension",
|
||||
"version": "0.1.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "yarn webpack serve --config webpack.dev.js",
|
||||
"build": "yarn preinstall && webpack build --progress --config webpack.prod.js",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"lint:ts": "tsc --noEmit",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"storybook:build": "build-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.7.0",
|
||||
"@emotion/styled": "^11.7.0",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.15",
|
||||
"@mui/system": "^5.11.15",
|
||||
"@nymproject/mui-theme": "^1.0.0",
|
||||
"@nymproject/nym-validator-client": ">=1.2.0-rc.0 || 1",
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"@nymproject/types": "^1.0.0",
|
||||
"@nymproject/extension-storage": ">=1.2.0-rc.0 || 1",
|
||||
"@storybook/react": "^6.5.16",
|
||||
"big.js": "^6.2.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.3",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nymproject/eslint-config-react-typescript": "^1.0.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
|
||||
"@svgr/webpack": "^6.1.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/big.js": "^6.1.6",
|
||||
"@types/crypto-js": "4.1.1",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^18.16.1",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"dotenv-webpack": "^8.0.1",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-root-import": "^1.0.4",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^26.1.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.29.2",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-storybook": "^0.5.12",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"jest": "^27.1.0",
|
||||
"mini-css-extract-plugin": "^2.2.2",
|
||||
"prettier": "^2.8.7",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-refresh-typescript": "^2.0.8",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-loader": "^9.4.2",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.6.2",
|
||||
"url-loader": "^4.1.1",
|
||||
"util": "^0.12.5",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.5.0",
|
||||
"webpack-favicons": "^1.3.8",
|
||||
"webpack-merge": "^5.8.0"
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NymBrowserExtThemeWithMode } from './theme/NymBrowserExtensionTheme';
|
||||
import { AppRoutes } from './routes';
|
||||
import { AppLayout } from './layouts/AppLayout';
|
||||
import { AppProvider } from './context';
|
||||
|
||||
export const App = () => (
|
||||
<NymBrowserExtThemeWithMode mode="light">
|
||||
<AppProvider>
|
||||
<AppLayout>
|
||||
<AppRoutes />
|
||||
</AppLayout>
|
||||
</AppProvider>
|
||||
</NymBrowserExtThemeWithMode>
|
||||
);
|
||||
@@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Avatar, ListItem, ListItemAvatar, ListItemButton, ListItemText } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppContext } from 'src/context';
|
||||
import { AccountActions } from './Actions';
|
||||
|
||||
const AccountItem = ({
|
||||
accountName,
|
||||
disabled,
|
||||
onSelect,
|
||||
}: {
|
||||
accountName: string;
|
||||
disabled: boolean;
|
||||
onSelect: () => void;
|
||||
}) => (
|
||||
<ListItem disableGutters disablePadding secondaryAction={<AccountActions accountName={accountName} />} divider>
|
||||
<ListItemButton onClick={onSelect} disabled={disabled}>
|
||||
<ListItemAvatar>
|
||||
<Avatar>{accountName[0]}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={accountName} secondary={disabled && '(Selected)'} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
export const AccountList = () => {
|
||||
const navigate = useNavigate();
|
||||
const { accounts, selectAccount, selectedAccount } = useAppContext();
|
||||
|
||||
const handleSelectAccount = async (accountName: string) => {
|
||||
await selectAccount(accountName);
|
||||
navigate('/user/balance');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{accounts.map((accountName) => (
|
||||
<AccountItem
|
||||
disabled={selectedAccount === accountName}
|
||||
accountName={accountName}
|
||||
key={accountName}
|
||||
onSelect={() => handleSelectAccount(accountName)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,55 +0,0 @@
|
||||
import React from 'react';
|
||||
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material';
|
||||
import { MoreVert, VisibilityOutlined } from '@mui/icons-material';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
type ActionType = {
|
||||
title: string;
|
||||
Icon: React.ReactNode;
|
||||
onSelect: () => void;
|
||||
};
|
||||
|
||||
const ActionItem = ({ action }: { action: ActionType }) => (
|
||||
<MenuItem dense onClick={action.onSelect}>
|
||||
<ListItemIcon>{action.Icon}</ListItemIcon>
|
||||
<ListItemText>{action.title}</ListItemText>
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
export const AccountActions = ({ accountName }: { accountName: string }) => {
|
||||
const { setShowSeedForAccount } = useAppContext();
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const actions: Array<ActionType> = [
|
||||
{
|
||||
title: 'View seed phrase',
|
||||
Icon: <VisibilityOutlined />,
|
||||
onSelect: () => {
|
||||
setShowSeedForAccount(accountName);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={handleClick}>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
<Menu anchorEl={anchorEl} id="account-menu" open={open} onClose={handleClose} onClick={handleClose}>
|
||||
{actions.map((action) => (
|
||||
<ActionItem action={action} key={action.title} />
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,68 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Card, CardContent, Typography } from '@mui/material';
|
||||
import { PasswordInput } from '@nymproject/react/textfields/Password';
|
||||
import { ExtensionStorage } from '@nymproject/extension-storage';
|
||||
import { Button, ConfirmationModal } from 'src/components/ui';
|
||||
|
||||
const ShowSeedButton = ({ handleShowSeedPhrase }: { handleShowSeedPhrase: () => void }) => (
|
||||
<Button fullWidth variant="contained" onClick={handleShowSeedPhrase}>
|
||||
Show seed phrase
|
||||
</Button>
|
||||
);
|
||||
|
||||
const DoneButton = ({ onDone }: { onDone: () => void }) => (
|
||||
<Button fullWidth variant="contained" onClick={onDone}>
|
||||
Done
|
||||
</Button>
|
||||
);
|
||||
|
||||
const Seed = ({ seed }: { seed: string }) => (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography>{seed}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export const ViewSeedPhrase = ({ accountName, onDone }: { accountName: string; onDone: () => void }) => {
|
||||
const [seed, setSeed] = useState<string>();
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const handleShowSeedPhrase = async () => {
|
||||
try {
|
||||
const storage = await new ExtensionStorage(password);
|
||||
const accountSeed = await storage.read_mnemonic(accountName);
|
||||
setSeed(accountSeed);
|
||||
} catch (e) {
|
||||
setError('Could not retrieve seed phrase. Please check your password');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
open
|
||||
onClose={onDone}
|
||||
title={seed ? 'Account seed phrase' : 'Password'}
|
||||
subtitle={seed ? '' : 'Enter your account password'}
|
||||
ConfirmButton={
|
||||
seed ? <DoneButton onDone={onDone} /> : <ShowSeedButton handleShowSeedPhrase={handleShowSeedPhrase} />
|
||||
}
|
||||
>
|
||||
{seed ? (
|
||||
<Seed seed={seed} />
|
||||
) : (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
error={error}
|
||||
password={password}
|
||||
onUpdatePassword={(pw: string) => {
|
||||
setPassword(pw);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</ConfirmationModal>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './Accounts';
|
||||
export * from './Actions';
|
||||
export * from './ViewSeedPhrase';
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { Stack } from '@mui/system';
|
||||
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
export const Address = () => {
|
||||
const { client } = useAppContext();
|
||||
|
||||
return (
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography fontWeight={700}>Address</Typography>
|
||||
<ClientAddress withCopy address={client?.address || ''} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
export const Balance = () => {
|
||||
const { balance, fiatBalance, currency, getBalance } = useAppContext();
|
||||
|
||||
useEffect(() => {
|
||||
getBalance();
|
||||
}, []);
|
||||
|
||||
const fiat = fiatBalance ? `~ ${Intl.NumberFormat().format(fiatBalance)} ${currency.toUpperCase()}` : '-';
|
||||
|
||||
return (
|
||||
<Stack alignItems="center" gap={1}>
|
||||
<Typography sx={{ color: 'grey.600' }}>Available</Typography>
|
||||
<Typography variant="h4" textAlign="center">
|
||||
{balance} NYM
|
||||
</Typography>
|
||||
<Typography sx={{ color: 'grey.600' }}>{fiat}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from './accounts';
|
||||
export * from './address';
|
||||
export * from './balance';
|
||||
export * from './receive';
|
||||
export * from './send';
|
||||
export * from './ui';
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { useAppContext } from 'src/context';
|
||||
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
|
||||
import { Close } from '@mui/icons-material';
|
||||
|
||||
export const ReceiveModal = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
|
||||
const { client } = useAppContext();
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
|
||||
<DialogTitle>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography fontWeight={700}>Receive</Typography>
|
||||
<IconButton size="small" onClick={onClose} sx={{ padding: 0 }}>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack gap={1} alignItems="center">
|
||||
<Card elevation={3} sx={{ my: 2, width: 200 }}>
|
||||
<CardContent sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<QRCodeSVG value={client?.address || ''} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Typography variant="body2" fontWeight={700}>
|
||||
Your Nym address
|
||||
</Typography>
|
||||
<ClientAddress address={client?.address || ''} withCopy smallIcons />
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './ReceiveModal';
|
||||
@@ -1,30 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { ConfirmationModal, Button } from 'src/components/ui';
|
||||
|
||||
export const SendConfirmationModal = ({
|
||||
amount,
|
||||
txUrl,
|
||||
onConfirm,
|
||||
}: {
|
||||
amount: string;
|
||||
txUrl: string;
|
||||
onConfirm: () => void;
|
||||
}) => (
|
||||
<ConfirmationModal
|
||||
open
|
||||
fullWidth
|
||||
title="You sent"
|
||||
ConfirmButton={
|
||||
<Button fullWidth variant="contained" size="large" onClick={onConfirm}>
|
||||
Done
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h6">{amount}</Typography>
|
||||
<Link href={txUrl} target="_blank" sx={{ ml: 1 }} text="View on blockchain" />
|
||||
</Box>
|
||||
</ConfirmationModal>
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
export * from './SendConfirmationModal';
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { AppBar as MUIAppBar } from '@mui/material/';
|
||||
import Box from '@mui/material/Box';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
|
||||
export const AppBar = ({ Action }: { Action: React.ReactNode }) => (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<MUIAppBar position="static" elevation={0} sx={{ bgcolor: 'rgba(103, 80, 164, 0.14)' }}>
|
||||
<Toolbar variant="dense">{Action}</Toolbar>
|
||||
</MUIAppBar>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default AppBar;
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ArrowBackIosRounded } from '@mui/icons-material';
|
||||
import { IconButton } from '@mui/material';
|
||||
|
||||
export const BackButton = ({ onBack }: { onBack?: () => void }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
if (onBack) {
|
||||
onBack();
|
||||
} else {
|
||||
navigate(-1);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton size="small" onClick={handleClick}>
|
||||
<ArrowBackIosRounded fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Button as MUIButton, ButtonProps } from '@mui/material';
|
||||
|
||||
export const Button = (props: ButtonProps) => (
|
||||
<MUIButton {...props} disableElevation sx={{ textTransform: 'initial' }} />
|
||||
);
|
||||
@@ -1,6 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NymLogoBW } from '@nymproject/react/logo/NymLogoBW';
|
||||
|
||||
export const Logo = ({ small }: { small?: boolean }) => (
|
||||
<NymLogoBW width={small ? '37.5px' : '75px'} height={small ? '37.5px' : '75px'} />
|
||||
);
|
||||
@@ -1,20 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { Logo } from '../Logo';
|
||||
import { Title } from '../Title';
|
||||
|
||||
export const LogoWithText = ({
|
||||
logoSmall,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
logoSmall?: boolean;
|
||||
title: string;
|
||||
description?: string;
|
||||
}) => (
|
||||
<Stack alignItems="center" justifyContent="center" gap={3}>
|
||||
<Logo small={logoSmall} />
|
||||
<Title>{title}</Title>
|
||||
<Typography sx={{ color: 'grey.700', textAlign: 'center' }}>{description}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
@@ -1,55 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { AccountBalanceWalletRounded, AccountCircleRounded, ArrowDownwardRounded } from '@mui/icons-material';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const menuSchema = [
|
||||
{
|
||||
title: 'Accounts',
|
||||
Icon: <AccountCircleRounded />,
|
||||
path: '/user/accounts',
|
||||
},
|
||||
{
|
||||
title: 'Balance',
|
||||
Icon: <AccountBalanceWalletRounded />,
|
||||
path: '/user/balance',
|
||||
},
|
||||
{
|
||||
title: 'Send',
|
||||
Icon: <ArrowDownwardRounded />,
|
||||
path: '/user/send',
|
||||
},
|
||||
];
|
||||
|
||||
export const MenuDrawer = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
|
||||
const list = () => (
|
||||
<Box sx={{ width: 250 }} role="presentation" onClick={() => {}}>
|
||||
<List>
|
||||
{menuSchema.map(({ title, Icon, path }) => (
|
||||
<Link to={path} style={{ textDecoration: 'none', color: 'unset' }} key={title}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton>
|
||||
<ListItemIcon>{Icon}</ListItemIcon>
|
||||
<ListItemText primary={title} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</Link>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Drawer anchor="left" open={open} onClose={onClose}>
|
||||
{list()}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Breakpoint,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
SxProps,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Button } from '../Button';
|
||||
|
||||
export interface ErrorModalProps {
|
||||
open: boolean;
|
||||
children?: React.ReactNode;
|
||||
title: React.ReactNode | string;
|
||||
subtitle?: React.ReactNode | string;
|
||||
sx?: SxProps;
|
||||
fullWidth?: boolean;
|
||||
maxWidth?: Breakpoint;
|
||||
backdropProps?: object;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ErrorModal = ({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
sx,
|
||||
fullWidth,
|
||||
maxWidth,
|
||||
backdropProps,
|
||||
}: ErrorModalProps) => {
|
||||
const Title = (
|
||||
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle &&
|
||||
(typeof subtitle === 'string' ? (
|
||||
<Typography fontWeight={400} variant="subtitle1" fontSize={12} sx={{ color: 'grey.400' }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
) : (
|
||||
subtitle
|
||||
))}
|
||||
</DialogTitle>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="responsive-dialog-title"
|
||||
maxWidth={maxWidth || 'sm'}
|
||||
sx={{ textAlign: 'center', ...sx }}
|
||||
fullWidth={fullWidth}
|
||||
BackdropProps={backdropProps}
|
||||
PaperComponent={Paper}
|
||||
PaperProps={{ elevation: 0 }}
|
||||
>
|
||||
{Title}
|
||||
<DialogContent>{children}</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 3 }}>
|
||||
<Button variant="contained" size="large" fullWidth onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material';
|
||||
|
||||
const modalStyle: SxProps = {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 300,
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
borderRadius: '16px',
|
||||
p: 4,
|
||||
};
|
||||
|
||||
export const LoadingModal = ({ sx, backdropProps }: { sx?: SxProps; backdropProps?: object }) => (
|
||||
<Modal open BackdropProps={backdropProps}>
|
||||
<Box sx={{ border: (t) => `1px solid ${t.palette.grey[500]}`, ...modalStyle, ...sx }} textAlign="center">
|
||||
<Stack spacing={4} direction="row" alignItems="center">
|
||||
<CircularProgress />
|
||||
<Typography sx={{ color: 'text.primary' }}>Please wait...</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Breakpoint,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
SxProps,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
|
||||
export interface ConfirmationModalProps {
|
||||
open: boolean;
|
||||
children?: React.ReactNode;
|
||||
title: React.ReactNode | string;
|
||||
subtitle?: React.ReactNode | string;
|
||||
ConfirmButton: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
fullWidth?: boolean;
|
||||
maxWidth?: Breakpoint;
|
||||
backdropProps?: object;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ConfirmationModal = ({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
ConfirmButton,
|
||||
sx,
|
||||
fullWidth,
|
||||
maxWidth,
|
||||
backdropProps,
|
||||
}: ConfirmationModalProps) => {
|
||||
const Title = (
|
||||
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle &&
|
||||
(typeof subtitle === 'string' ? (
|
||||
<Typography fontWeight={400} variant="subtitle1" fontSize={12} sx={{ color: 'grey.400' }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
) : (
|
||||
subtitle
|
||||
))}
|
||||
</DialogTitle>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="responsive-dialog-title"
|
||||
maxWidth={maxWidth || 'sm'}
|
||||
sx={{ textAlign: 'center', ...sx }}
|
||||
fullWidth={fullWidth}
|
||||
BackdropProps={backdropProps}
|
||||
PaperComponent={Paper}
|
||||
PaperProps={{ elevation: 0 }}
|
||||
>
|
||||
{Title}
|
||||
<DialogContent>{children}</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './Modal';
|
||||
export * from './LoadingModal';
|
||||
export * from './ErrorModal';
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const FONT_WEIGHT = 400;
|
||||
|
||||
export const Title = ({ children }: { children: string }) => (
|
||||
<Typography variant="h5" fontWeight={FONT_WEIGHT}>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
@@ -1,8 +0,0 @@
|
||||
export * from './AppBar';
|
||||
export * from './Button';
|
||||
export * from './BackButton';
|
||||
export * from './Logo';
|
||||
export * from './LogoWithText';
|
||||
export * from './MenuDrawer';
|
||||
export * from './Modal';
|
||||
export * from './Title';
|
||||
@@ -1,8 +0,0 @@
|
||||
export const config = {
|
||||
rpcUrl: process.env.RPC_URL || '',
|
||||
validatorUrl: process.env.VALIDATOR_URL || '',
|
||||
prefix: process.env.PREFIX || '',
|
||||
mixnetContractAddress: process.env.MIXNET_CONTRACT_ADDRESS || '',
|
||||
vestingContractAddress: process.env.VESTING_CONTRACT_ADDRESS || '',
|
||||
denom: process.env.DENOM || '',
|
||||
};
|
||||
@@ -1,109 +0,0 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import ValidatorClient from '@nymproject/nym-validator-client';
|
||||
import { ExtensionStorage } from '@nymproject/extension-storage';
|
||||
import { connectToValidator } from 'src/validator-client';
|
||||
import { unymToNym } from 'src/utils/coin';
|
||||
import { Currency, getTokenPrice } from 'src/utils/price';
|
||||
|
||||
type TAppContext = {
|
||||
client?: ValidatorClient;
|
||||
accounts: string[];
|
||||
balance?: string;
|
||||
fiatBalance?: number;
|
||||
denom: 'NYM';
|
||||
minorDenom: 'unym';
|
||||
currency: Currency;
|
||||
showSeedForAccount?: string;
|
||||
selectedAccount: string;
|
||||
storage?: ExtensionStorage;
|
||||
selectAccount: (accountName: string) => Promise<void>;
|
||||
setAccounts: (accounts: string[]) => void;
|
||||
setShowSeedForAccount: (accountName?: string) => void;
|
||||
handleUnlockWallet: (password: string) => void;
|
||||
getBalance: () => void;
|
||||
};
|
||||
|
||||
type TBalanceInNYMs = string;
|
||||
|
||||
const DEFAULT_ACCOUNT_NAME = 'Default account';
|
||||
|
||||
const AppContext = React.createContext({} as TAppContext);
|
||||
|
||||
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [client, setClient] = useState<ValidatorClient>();
|
||||
const [selectedAccount, setSelected] = useState<string>(DEFAULT_ACCOUNT_NAME);
|
||||
const [balance, setBalance] = useState<TBalanceInNYMs>();
|
||||
const [fiatBalance, setFiatBalance] = useState<number>();
|
||||
const [accounts, setAccounts] = useState<string[]>([]);
|
||||
const [showSeedForAccount, setShowSeedForAccount] = useState<string>();
|
||||
const [storage, setStorage] = useState<ExtensionStorage>();
|
||||
|
||||
const denom = 'NYM';
|
||||
const minorDenom = 'unym';
|
||||
const currency = 'gbp';
|
||||
|
||||
const handleUnlockWallet = async (password: string) => {
|
||||
const store = await new ExtensionStorage(password);
|
||||
const mnemonic = await store.read_mnemonic(DEFAULT_ACCOUNT_NAME);
|
||||
const userAccounts = await store.get_all_mnemonic_keys();
|
||||
const clientFromMnemonic = await connectToValidator(mnemonic);
|
||||
|
||||
setStorage(store);
|
||||
setAccounts(userAccounts);
|
||||
setClient(clientFromMnemonic);
|
||||
};
|
||||
|
||||
const selectAccount = async (accountName: string) => {
|
||||
const mnemonic = await storage!.read_mnemonic(accountName);
|
||||
const clientFromMnemonic = await connectToValidator(mnemonic);
|
||||
setSelected(accountName);
|
||||
setClient(clientFromMnemonic);
|
||||
};
|
||||
|
||||
const getFiatBalance = async (bal: number) => {
|
||||
const tokenPrice = await getTokenPrice('nym', currency);
|
||||
const fiatBal = tokenPrice.nym.gbp * bal;
|
||||
return fiatBal;
|
||||
};
|
||||
|
||||
const getBalance = async () => {
|
||||
const bal = await client?.getBalance(client.address);
|
||||
if (bal) {
|
||||
const nym = unymToNym(Number(bal.amount));
|
||||
const fiat = await getFiatBalance(nym);
|
||||
setFiatBalance(fiat);
|
||||
setBalance(nym.toString());
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (client) {
|
||||
getBalance();
|
||||
}
|
||||
}, [client]);
|
||||
|
||||
const value = useMemo<TAppContext>(
|
||||
() => ({
|
||||
client,
|
||||
accounts,
|
||||
balance,
|
||||
fiatBalance,
|
||||
currency,
|
||||
denom,
|
||||
minorDenom,
|
||||
selectedAccount,
|
||||
storage,
|
||||
handleUnlockWallet,
|
||||
getBalance,
|
||||
setShowSeedForAccount,
|
||||
showSeedForAccount,
|
||||
setAccounts,
|
||||
selectAccount,
|
||||
}),
|
||||
[client, accounts, balance, fiatBalance, denom, minorDenom, selectedAccount, showSeedForAccount, storage],
|
||||
);
|
||||
|
||||
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAppContext = () => React.useContext(AppContext);
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './app';
|
||||
export * from './send';
|
||||
export * from './register';
|
||||
@@ -1,71 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ExtensionStorage } from '@nymproject/extension-storage';
|
||||
|
||||
const RegisterContext = React.createContext({} as TRegisterContext);
|
||||
|
||||
type TRegisterContext = {
|
||||
userPassword: string;
|
||||
userMnemonic: string;
|
||||
accountName: string;
|
||||
checkAccountName: () => Promise<boolean>;
|
||||
setUserPassword: (password: string) => void;
|
||||
setUserMnemonic: (mnemonic: string) => void;
|
||||
setAccountName: (name: string) => void;
|
||||
createAccount: (args: { mnemonic: string; password: string; accName: string }) => Promise<void>;
|
||||
importAccount: () => Promise<string[]>;
|
||||
resetState: () => void;
|
||||
};
|
||||
|
||||
export const RegisterContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [userPassword, setUserPassword] = useState('');
|
||||
const [userMnemonic, setUserMnemonic] = useState('');
|
||||
const [accountName, setAccountName] = useState('');
|
||||
|
||||
const resetState = () => {
|
||||
setUserMnemonic('');
|
||||
setUserPassword('');
|
||||
setAccountName('');
|
||||
};
|
||||
|
||||
const createAccount = async ({
|
||||
mnemonic,
|
||||
password,
|
||||
accName,
|
||||
}: {
|
||||
mnemonic: string;
|
||||
password: string;
|
||||
accName: string;
|
||||
}) => {
|
||||
const storage = await new ExtensionStorage(password);
|
||||
await storage.store_mnemonic(accName, mnemonic);
|
||||
};
|
||||
|
||||
const importAccount = async () => {
|
||||
const storage = await new ExtensionStorage(userPassword);
|
||||
await storage.store_mnemonic(accountName, userMnemonic);
|
||||
const accounts = await storage.get_all_mnemonic_keys();
|
||||
return accounts;
|
||||
};
|
||||
|
||||
const checkAccountName = async () => true;
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
userPassword,
|
||||
setUserPassword,
|
||||
userMnemonic,
|
||||
accountName,
|
||||
setAccountName,
|
||||
setUserMnemonic,
|
||||
createAccount,
|
||||
checkAccountName,
|
||||
importAccount,
|
||||
resetState,
|
||||
}),
|
||||
[userPassword, userMnemonic, accountName],
|
||||
);
|
||||
|
||||
return <RegisterContext.Provider value={value}>{children}</RegisterContext.Provider>;
|
||||
};
|
||||
|
||||
export const useRegisterContext = () => React.useContext(RegisterContext);
|
||||
@@ -1,112 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { DecCoin } from '@nymproject/types';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { nymToUnym } from 'src/utils/coin';
|
||||
import { TTransaction } from 'src/types';
|
||||
import { Fee, useGetFee } from 'src/hooks/useGetFee';
|
||||
import { createFeeObject } from 'src/utils/fee';
|
||||
import { useAppContext } from './app';
|
||||
|
||||
type TSendContext = {
|
||||
address?: string;
|
||||
amount?: DecCoin;
|
||||
transaction?: TTransaction;
|
||||
fee?: Fee;
|
||||
handleChangeAddress: (address?: string) => void;
|
||||
handleChangeAmount: (amount?: DecCoin) => void;
|
||||
handleSend: () => void;
|
||||
resetTx: () => void;
|
||||
onDone: () => void;
|
||||
handleGetFee: (address: string, amount: string) => Promise<void>;
|
||||
};
|
||||
|
||||
const SendContext = React.createContext({} as TSendContext);
|
||||
|
||||
export const SendProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [address, setAddress] = useState<string>();
|
||||
const [amount, setAmount] = useState<DecCoin>();
|
||||
const [transaction, setTransaction] = useState<TTransaction>();
|
||||
|
||||
const { client, minorDenom } = useAppContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleChangeAddress = (_address?: string) => setAddress(_address);
|
||||
|
||||
const handleChangeAmount = (_amount?: DecCoin) => setAmount(_amount);
|
||||
|
||||
const { getFee, fee } = useGetFee();
|
||||
|
||||
const handleGetFee = async (addressVal: string, amountVal: string) => {
|
||||
const unym = nymToUnym(Number(amountVal));
|
||||
|
||||
if (client) {
|
||||
// client loses its 'this' context when passing the method
|
||||
// TODO find a better way of doing this.
|
||||
getFee(client.simulateSend.bind(client), {
|
||||
signingAddress: client.address,
|
||||
from: client.address,
|
||||
to: addressVal,
|
||||
amount: [{ amount: unym.toString(), denom: minorDenom }],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = async () => {
|
||||
setTransaction({ status: 'loading', type: 'send' });
|
||||
let unyms;
|
||||
|
||||
if (!Number(amount?.amount)) {
|
||||
setTransaction({ status: 'error', type: 'send', message: 'Amount is not a valid number' });
|
||||
}
|
||||
|
||||
if (amount) {
|
||||
unyms = nymToUnym(Number(amount.amount));
|
||||
}
|
||||
|
||||
if (client && address && unyms) {
|
||||
try {
|
||||
const response = await client.send(
|
||||
address,
|
||||
[{ amount: unyms.toString(), denom: minorDenom }],
|
||||
createFeeObject(fee?.unym),
|
||||
);
|
||||
|
||||
setTransaction({ status: 'success', type: 'send', txHash: response?.transactionHash });
|
||||
} catch (e) {
|
||||
setTransaction({
|
||||
status: 'error',
|
||||
type: 'send',
|
||||
message: e instanceof Error ? e.message : 'Error making send transaction. Please try again',
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const resetTx = () => {
|
||||
setTransaction(undefined);
|
||||
};
|
||||
|
||||
const onDone = () => {
|
||||
navigate('/user/balance');
|
||||
};
|
||||
|
||||
const value = useMemo<TSendContext>(
|
||||
() => ({
|
||||
address,
|
||||
amount,
|
||||
transaction,
|
||||
fee,
|
||||
handleChangeAddress,
|
||||
handleChangeAmount,
|
||||
handleSend,
|
||||
resetTx,
|
||||
onDone,
|
||||
handleGetFee,
|
||||
}),
|
||||
[address, amount, transaction, fee],
|
||||
);
|
||||
|
||||
return <SendContext.Provider value={value}>{children}</SendContext.Provider>;
|
||||
};
|
||||
|
||||
export const useSendContext = () => React.useContext(SendContext);
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export const useCreatePassword = () => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isSafePassword, setIsSafePassword] = useState(false);
|
||||
const [hasReadTerms, setHasReadTerms] = useState(false);
|
||||
|
||||
const canProceed = isSafePassword && hasReadTerms && password === confirmPassword;
|
||||
|
||||
return {
|
||||
password,
|
||||
setPassword,
|
||||
confirmPassword,
|
||||
setConfirmPassword,
|
||||
setIsSafePassword,
|
||||
canProceed,
|
||||
setHasReadTerms,
|
||||
hasReadTerms,
|
||||
};
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import Big from 'big.js';
|
||||
import { useState } from 'react';
|
||||
import { unymToNym } from 'src/utils/coin';
|
||||
|
||||
export type Fee = { nym: number; unym: number };
|
||||
|
||||
export function useGetFee() {
|
||||
const [fee, setFee] = useState<Fee>();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
async function getFee<T>(txReq: (args: T) => Promise<number | undefined>, args: T) {
|
||||
setError(undefined);
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
const txFee = await txReq(args);
|
||||
|
||||
if (txFee) {
|
||||
const feeWithMultiplyer = Big(txFee).mul(1);
|
||||
console.log(fee);
|
||||
|
||||
const txFeeInNyms = unymToNym(feeWithMultiplyer);
|
||||
|
||||
setFee({ nym: Number(txFeeInNyms), unym: Number(feeWithMultiplyer) });
|
||||
}
|
||||
|
||||
if (!txFee) {
|
||||
setError('Unable to calculate fee');
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(`Unable to get estimated fee: ${e}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return { fee, getFee, isLoading, error };
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Nym browser extension</title>
|
||||
</head>
|
||||
<body style="width: 360px; height: 600px">
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
|
||||
const rootDomElem = document.getElementById('root');
|
||||
|
||||
if (rootDomElem) {
|
||||
const root = createRoot(rootDomElem);
|
||||
root.render(<App />);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import { Container } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
export const AppLayout = ({ children }: { children: React.ReactNode }) => (
|
||||
<Container maxWidth="xs" disableGutters sx={{ height: '100vh' }}>
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { LogoWithText } from 'src/components/ui';
|
||||
|
||||
const layoutStyle = {
|
||||
height: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr',
|
||||
gridTemplateRows: 'repeat(3, 1fr)',
|
||||
gridColumnGap: '0px',
|
||||
gridRowGap: '0px',
|
||||
p: 2,
|
||||
};
|
||||
|
||||
export const CenteredLogoLayout = ({
|
||||
title,
|
||||
description,
|
||||
Actions,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
Actions: React.ReactNode;
|
||||
}) => (
|
||||
<Box sx={layoutStyle}>
|
||||
<Box />
|
||||
<LogoWithText title={title} description={description} />
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'flex-end' }}>{Actions}</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -1,38 +0,0 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Box, IconButton } from '@mui/material';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import { AppBar, BackButton, MenuDrawer } from 'src/components/ui';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
const layoutStyle = {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr',
|
||||
gridTemplateRows: '50px 1fr',
|
||||
gridColumnGap: '0px',
|
||||
gridRowGap: '0px',
|
||||
};
|
||||
|
||||
export const PageLayout = ({ children, onBack }: { children: React.ReactNode; onBack?: () => void }) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const MenuAction = useCallback(
|
||||
() => (
|
||||
<IconButton onClick={() => setMenuOpen(true)}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
const Action = location.pathname.includes('balance') ? MenuAction : BackButton;
|
||||
|
||||
return (
|
||||
<Box sx={layoutStyle}>
|
||||
<AppBar Action={<Action onBack={onBack} />} />
|
||||
<MenuDrawer open={menuOpen} onClose={() => setMenuOpen(false)} />
|
||||
<Box sx={{ p: 2 }}>{children}</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { BackButton, LogoWithText } from 'src/components/ui';
|
||||
|
||||
const layoutStyle = {
|
||||
height: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr',
|
||||
gridTemplaterows: '1fr 2fr 1fr',
|
||||
gridColumnGap: '0px',
|
||||
gridRowGap: '0px',
|
||||
position: 'relative',
|
||||
p: 2,
|
||||
};
|
||||
|
||||
export const TopLogoLayout = ({
|
||||
title,
|
||||
description,
|
||||
children,
|
||||
Actions,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
Actions: React.ReactNode;
|
||||
}) => (
|
||||
<Box sx={layoutStyle}>
|
||||
<Box sx={{ position: 'absolute', top: 16, left: 16 }}>
|
||||
<BackButton />
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'center' }}>
|
||||
<LogoWithText logoSmall title={title} description={description} />
|
||||
</Box>
|
||||
<Box>{children}</Box>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-end', width: '100%' }}>{Actions}</Box>
|
||||
</Box>
|
||||
);
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './AppLayout';
|
||||
export * from './CenteredLogo';
|
||||
export * from './TopLogo';
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "Nym browser extension",
|
||||
"description": "Nym browser extension - Wallet & credentials",
|
||||
"version": "0.1.0",
|
||||
"manifest_version": 3,
|
||||
"action": {
|
||||
"default_popup": "index.html",
|
||||
"default_title": "Nym - Browser extension"
|
||||
},
|
||||
"icons": {
|
||||
"16": "favicon-16x16.png",
|
||||
"32": "favicon-32x32.png",
|
||||
"48": "favicon-48x48.png"
|
||||
},
|
||||
"content_security_policy": {
|
||||
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { PageLayout } from 'src/layouts/PageLayout';
|
||||
import { Stack } from '@mui/material';
|
||||
import { Add, ArrowDownward } from '@mui/icons-material';
|
||||
import { AccountList, Button } from 'src/components';
|
||||
import { ViewSeedPhrase } from 'src/components/accounts/ViewSeedPhrase';
|
||||
import { useAppContext, useRegisterContext } from 'src/context';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export const Accounts = () => {
|
||||
const { showSeedForAccount, setShowSeedForAccount } = useAppContext();
|
||||
const { resetState } = useRegisterContext();
|
||||
|
||||
useEffect(() => {
|
||||
resetState();
|
||||
}, []);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleAddAccount = () => navigate(`${location.pathname}/add-account`);
|
||||
|
||||
const handleImportAccount = () => navigate(`${location.pathname}/import-account`);
|
||||
|
||||
const onBack = () => navigate('/user/balance');
|
||||
|
||||
return (
|
||||
<PageLayout onBack={onBack}>
|
||||
{showSeedForAccount && (
|
||||
<ViewSeedPhrase accountName={showSeedForAccount} onDone={() => setShowSeedForAccount(undefined)} />
|
||||
)}
|
||||
<AccountList />
|
||||
<Stack gap={1} alignItems="start" sx={{ mt: 2 }}>
|
||||
<Button startIcon={<Add />} onClick={handleAddAccount}>
|
||||
Add account
|
||||
</Button>
|
||||
<Button startIcon={<ArrowDownward />} onClick={handleImportAccount}>
|
||||
Import account
|
||||
</Button>
|
||||
</Stack>
|
||||
</PageLayout>
|
||||
);
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRegisterContext } from 'src/context';
|
||||
import { SeedPhraseTemplate } from 'src/pages/templates';
|
||||
|
||||
export const AddAccount = () => {
|
||||
const { setUserMnemonic } = useRegisterContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const onNext = (seedPhrase: string) => {
|
||||
setUserMnemonic(seedPhrase);
|
||||
navigate('/user/accounts/name-account');
|
||||
};
|
||||
|
||||
return <SeedPhraseTemplate onNext={onNext} />;
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SetupCompleteTemplate } from 'src/pages/templates/Complete';
|
||||
|
||||
export const SetupComplete = () => {
|
||||
const navigate = useNavigate();
|
||||
const handleOnDone = () => {
|
||||
navigate('/user/accounts');
|
||||
};
|
||||
|
||||
return (
|
||||
<SetupCompleteTemplate title="You're all set!" description="Account successfully imported" onDone={handleOnDone} />
|
||||
);
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { PasswordInput } from '@nymproject/react/textfields/Password';
|
||||
import { Button } from 'src/components';
|
||||
import { useAppContext, useRegisterContext } from 'src/context';
|
||||
import { TopLogoLayout } from 'src/layouts';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const ConfirmPassword = () => {
|
||||
const { setAccounts } = useAppContext();
|
||||
const { userPassword, setUserPassword, importAccount } = useRegisterContext();
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleOnComplete = async () => {
|
||||
try {
|
||||
const accounts = await importAccount();
|
||||
setAccounts(accounts);
|
||||
navigate('/user/accounts/complete');
|
||||
} catch (e) {
|
||||
setError('Incorrect password. Please try again');
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (password: string) => {
|
||||
setError(undefined);
|
||||
setUserPassword(password);
|
||||
};
|
||||
|
||||
return (
|
||||
<TopLogoLayout
|
||||
title="Confirm password"
|
||||
description="Confirm password to import account"
|
||||
Actions={
|
||||
<Button fullWidth variant="contained" size="large" onClick={handleOnComplete} disabled={!!error}>
|
||||
Confirm
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<PasswordInput value={userPassword} onUpdatePassword={onChange} error={error} />
|
||||
</TopLogoLayout>
|
||||
);
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRegisterContext } from 'src/context/register';
|
||||
import { ImportAccountTemplate } from '../templates';
|
||||
|
||||
export const ImportAccount = () => {
|
||||
const { userMnemonic, setUserMnemonic } = useRegisterContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleOnNext = () => {
|
||||
navigate('/user/accounts/name-account');
|
||||
};
|
||||
|
||||
return (
|
||||
<ImportAccountTemplate userMnemonic={userMnemonic} onChangeUserMnemonic={setUserMnemonic} onNext={handleOnNext} />
|
||||
);
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import { TextField } from '@mui/material';
|
||||
import { Button } from 'src/components';
|
||||
import { useRegisterContext } from 'src/context/register';
|
||||
import { TopLogoLayout } from 'src/layouts';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
export const NameAccount = () => {
|
||||
const { accountName, setAccountName } = useRegisterContext();
|
||||
const { storage } = useAppContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const handleNext = async () => {
|
||||
const accountNameExists = await storage?.has_mnemonic(accountName);
|
||||
if (accountNameExists) {
|
||||
setError('Account name already exists. Please choose another account name');
|
||||
} else {
|
||||
navigate('/user/accounts/confirm-password');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TopLogoLayout
|
||||
title="Name account"
|
||||
description="Give your account a unique name"
|
||||
Actions={
|
||||
<Button fullWidth variant="contained" size="large" onClick={handleNext} disabled={!!error}>
|
||||
Next
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
value={accountName}
|
||||
onChange={(e) => {
|
||||
setError(undefined);
|
||||
setAccountName(e.target.value);
|
||||
}}
|
||||
error={!!error}
|
||||
helperText={error}
|
||||
/>
|
||||
</TopLogoLayout>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
export * from './Accounts';
|
||||
export * from './AddAccount';
|
||||
export * from './Complete';
|
||||
export * from './ConfirmPassword';
|
||||
export * from './ImportAccount';
|
||||
export * from './NameAccount';
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { Box } from '@mui/system';
|
||||
import { TopLogoLayout } from 'src/layouts';
|
||||
|
||||
const steps = [
|
||||
'Make sure you have your mnemonic saved',
|
||||
'Uninstal Nym extension wallet',
|
||||
'Reinstal Nym extension wallet',
|
||||
'Import your account using seed phrase',
|
||||
'Create new password',
|
||||
];
|
||||
|
||||
export const ForgotPassword = () => (
|
||||
<TopLogoLayout title="Forgot password" description="Follow instructions below" Actions={<div />}>
|
||||
<Box sx={{ px: 2 }}>
|
||||
{steps.map((step, index) => (
|
||||
<Typography sx={{ color: 'grey.700', my: 3 }} key={step} variant="body2">
|
||||
{`${index + 1}. ${step}`}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
</TopLogoLayout>
|
||||
);
|
||||
@@ -1,69 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Stack, TextField } from '@mui/material';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { Button } from 'src/components/ui';
|
||||
import { CenteredLogoLayout } from 'src/layouts/CenteredLogo';
|
||||
import { useAppContext } from 'src/context';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { validationSchema } from './validationSchema';
|
||||
|
||||
export const Login = () => {
|
||||
const { handleUnlockWallet } = useAppContext();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm({ resolver: zodResolver(validationSchema), defaultValues: { password: '' } });
|
||||
|
||||
const onSubmit = async (data: { password: string }) => {
|
||||
try {
|
||||
await handleUnlockWallet(data.password);
|
||||
} catch (e) {
|
||||
setError('password', { message: 'Incorrect password. Please try again.' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CenteredLogoLayout
|
||||
title="Privacy crypto wallet"
|
||||
Actions={
|
||||
<Stack gap={1} width="100%" justifyContent="flex-end">
|
||||
<TextField
|
||||
{...register('password')}
|
||||
placeholder="Password"
|
||||
type="password"
|
||||
sx={{ mb: 3 }}
|
||||
helperText={errors.password?.message}
|
||||
error={!!errors.password}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleSubmit(onSubmit)}
|
||||
disabled={isSubmitting}
|
||||
variant="contained"
|
||||
disableElevation
|
||||
size="large"
|
||||
fullWidth
|
||||
>
|
||||
{isSubmitting ? 'Loading..' : 'Unlock'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disableElevation
|
||||
size="large"
|
||||
fullWidth
|
||||
color="primary"
|
||||
onClick={() => navigate(`${location.pathname}/forgot-password`)}
|
||||
>
|
||||
Forgot password?
|
||||
</Button>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './Login';
|
||||
export * from './ForgotPassword';
|
||||
@@ -1,5 +0,0 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const validationSchema = z.object({
|
||||
password: z.string().min(1, { message: 'Required' }),
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user