Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09fbbaed49 | |||
| d6bb0979d0 | |||
| fa1d47e941 | |||
| 44ec6d6bc8 | |||
| 6d47046a38 | |||
| 5cfd09cd99 | |||
| 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
|
||||
|
||||
@@ -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
+528
-253
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -124,7 +124,7 @@ impl NymApiTopologyProvider {
|
||||
// TODO: we really should be getting ACTIVE gateways only
|
||||
let gateways_fut = self
|
||||
.validator_client
|
||||
.get_all_basic_entry_assigned_nodes_v2();
|
||||
.get_all_basic_entry_assigned_nodes_with_metadata();
|
||||
|
||||
let (rewarded_set, mixnodes_res, gateways_res) =
|
||||
futures::try_join!(rewarded_set_fut, mixnodes_fut, gateways_fut)
|
||||
|
||||
@@ -107,7 +107,10 @@ pub async fn gateways_for_init<R: Rng>(
|
||||
|
||||
log::debug!("Fetching list of gateways from: {nym_api}");
|
||||
|
||||
let gateways = client.get_all_basic_entry_assigned_nodes_v2().await?.nodes;
|
||||
let gateways = client
|
||||
.get_all_basic_entry_assigned_nodes_with_metadata()
|
||||
.await?
|
||||
.nodes;
|
||||
info!("nym api reports {} gateways", gateways.len());
|
||||
|
||||
log::trace!("Gateways: {:#?}", gateways);
|
||||
|
||||
@@ -28,6 +28,7 @@ pub struct Config {
|
||||
pub maximum_reconnection_backoff: Duration,
|
||||
pub initial_connection_timeout: Duration,
|
||||
pub maximum_connection_buffer_size: usize,
|
||||
pub use_legacy_packet_encoding: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -36,12 +37,14 @@ impl Config {
|
||||
maximum_reconnection_backoff: Duration,
|
||||
initial_connection_timeout: Duration,
|
||||
maximum_connection_buffer_size: usize,
|
||||
use_legacy_packet_encoding: bool,
|
||||
) -> Self {
|
||||
Config {
|
||||
initial_reconnection_backoff,
|
||||
maximum_reconnection_backoff,
|
||||
initial_connection_timeout,
|
||||
maximum_connection_buffer_size,
|
||||
use_legacy_packet_encoding,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,7 +270,12 @@ impl SendWithoutResponse for Client {
|
||||
fn send_without_response(&self, packet: MixPacket) -> io::Result<()> {
|
||||
let address = packet.next_hop_address();
|
||||
trace!("Sending packet to {address}");
|
||||
let framed_packet = FramedNymPacket::from(packet);
|
||||
|
||||
// TODO: optimisation for the future: rather than constantly using legacy encoding,
|
||||
// once we're addressing by node_id (and thus have full node info here),
|
||||
// we could simply infer supported encoding based on their version
|
||||
let framed_packet =
|
||||
FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding);
|
||||
|
||||
let Some(sender) = self.active_connections.get_mut(&address) else {
|
||||
// there was never a connection to begin with
|
||||
@@ -328,6 +336,7 @@ mod tests {
|
||||
maximum_reconnection_backoff: Duration::from_millis(300_000),
|
||||
initial_connection_timeout: Duration::from_millis(1_500),
|
||||
maximum_connection_buffer_size: 128,
|
||||
use_legacy_packet_encoding: false,
|
||||
},
|
||||
NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut rng)),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -471,12 +471,12 @@ impl NymApiClient {
|
||||
pub async fn get_all_basic_entry_assigned_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
self.get_all_basic_entry_assigned_nodes_v2()
|
||||
self.get_all_basic_entry_assigned_nodes_with_metadata()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
}
|
||||
|
||||
pub async fn get_all_basic_entry_assigned_nodes_v2(
|
||||
pub async fn get_all_basic_entry_assigned_nodes_with_metadata(
|
||||
&self,
|
||||
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
collect_paged_skimmed_v2!(self, get_basic_entry_assigned_nodes_v2)
|
||||
|
||||
@@ -389,7 +389,6 @@ pub trait NymApiClientExt: ApiClient {
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
"entry-gateways",
|
||||
"all",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -131,7 +131,8 @@ impl NoiseConfig {
|
||||
}
|
||||
|
||||
// Only for phased update
|
||||
//SW This can lead to some troubles if two nodes shares the same IP and one support Noise but not the other. This in only for the progressive update though and there is no workaround
|
||||
//SW This can lead to some troubles if two nodes share the same IP and one support Noise but not the other.
|
||||
// This in only for the progressive update though and there is no workaround
|
||||
pub(crate) fn get_noise_support(&self, ip_addr: IpAddr) -> Option<NoiseVersion> {
|
||||
let plain_ip_support = self.network.support.inner.load().get(&ip_addr).copied();
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ use bytes::{BufMut, BytesMut};
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use nym_sphinx_params::key_rotation::SphinxKeyRotation;
|
||||
use nym_sphinx_params::packet_sizes::PacketSize;
|
||||
use nym_sphinx_params::packet_version::{PacketVersion, CURRENT_PACKET_VERSION};
|
||||
use nym_sphinx_params::packet_version::{
|
||||
PacketVersion, CURRENT_PACKET_VERSION, LEGACY_PACKET_VERSION,
|
||||
};
|
||||
use nym_sphinx_params::PacketType;
|
||||
use nym_sphinx_types::NymPacket;
|
||||
|
||||
@@ -19,26 +21,25 @@ pub struct FramedNymPacket {
|
||||
pub(crate) packet: NymPacket,
|
||||
}
|
||||
|
||||
impl From<MixPacket> for FramedNymPacket {
|
||||
fn from(packet: MixPacket) -> Self {
|
||||
let typ = packet.packet_type();
|
||||
let rot = packet.key_rotation();
|
||||
FramedNymPacket::new(packet.into_packet(), typ, rot)
|
||||
}
|
||||
}
|
||||
|
||||
impl FramedNymPacket {
|
||||
pub fn new(
|
||||
packet: NymPacket,
|
||||
packet_type: PacketType,
|
||||
key_rotation: SphinxKeyRotation,
|
||||
use_legacy_packet_encoding: bool,
|
||||
) -> Self {
|
||||
// If this fails somebody is using the library in a super incorrect way, because they
|
||||
// already managed to somehow create a sphinx packet
|
||||
let packet_size = PacketSize::get_type(packet.len()).unwrap();
|
||||
|
||||
let packet_version = if use_legacy_packet_encoding {
|
||||
LEGACY_PACKET_VERSION
|
||||
} else {
|
||||
PacketVersion::new()
|
||||
};
|
||||
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::new(),
|
||||
packet_version,
|
||||
packet_size,
|
||||
key_rotation,
|
||||
packet_type,
|
||||
@@ -47,6 +48,12 @@ impl FramedNymPacket {
|
||||
FramedNymPacket { header, packet }
|
||||
}
|
||||
|
||||
pub fn from_mix_packet(packet: MixPacket, use_legacy_packet_encoding: bool) -> Self {
|
||||
let typ = packet.packet_type();
|
||||
let rot = packet.key_rotation();
|
||||
FramedNymPacket::new(packet.into_packet(), typ, rot, use_legacy_packet_encoding)
|
||||
}
|
||||
|
||||
pub fn header(&self) -> Header {
|
||||
self.header
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use thiserror::Error;
|
||||
// - packet_version (starting with v1.1.0)
|
||||
// - packet_size indicator
|
||||
// - packet_type
|
||||
// - sphinx key rotation (starting with v1.12.0/v1.13.0 - either Cheddar or Dolcelatte release)
|
||||
// - sphinx key rotation (starting with v1.13.0 - the Dolcelatte release)
|
||||
|
||||
// it also just so happens that the only valid values for packet_size indicator include values 1-6
|
||||
// therefore if we receive byte `7` (or larger than that) we'll know we received a versioned packet,
|
||||
@@ -22,6 +22,9 @@ pub const CURRENT_PACKET_VERSION_NUMBER: u8 = KEY_ROTATION_VERSION_NUMBER;
|
||||
pub const CURRENT_PACKET_VERSION: PacketVersion =
|
||||
PacketVersion::unchecked(CURRENT_PACKET_VERSION_NUMBER);
|
||||
|
||||
pub const LEGACY_PACKET_VERSION: PacketVersion =
|
||||
PacketVersion::unchecked(INITIAL_PACKET_VERSION_NUMBER);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct PacketVersion(u8);
|
||||
|
||||
|
||||
@@ -79,7 +79,9 @@ pub async fn current_network_topology_async(
|
||||
let metadata = mixnodes_res.metadata;
|
||||
let mixnodes = mixnodes_res.nodes;
|
||||
|
||||
let gateways_res = api_client.get_all_basic_entry_assigned_nodes_v2().await?;
|
||||
let gateways_res = api_client
|
||||
.get_all_basic_entry_assigned_nodes_with_metadata()
|
||||
.await?;
|
||||
if gateways_res.metadata != metadata {
|
||||
console_warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata);
|
||||
return Err(WasmCoreError::UnavailableNetworkTopology);
|
||||
|
||||
@@ -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/
|
||||
@@ -419,6 +419,13 @@ impl<R, S> FreshHandler<R, S> {
|
||||
return Ok(INITIAL_PROTOCOL_VERSION);
|
||||
};
|
||||
|
||||
// #####
|
||||
// On backwards compat:
|
||||
// Currently it is the case that gateways will understand all previous protocol versions
|
||||
// and will downgrade accordingly, but this will now always be the case.
|
||||
// For example, once we remove downgrade on legacy auth, anything below version 4 will be rejected
|
||||
// #####
|
||||
|
||||
// a v2 gateway will understand v1 requests, but v1 client will not understand v2 responses
|
||||
if client_protocol_version == 1 {
|
||||
return Ok(1);
|
||||
@@ -434,6 +441,11 @@ impl<R, S> FreshHandler<R, S> {
|
||||
return Ok(3);
|
||||
}
|
||||
|
||||
// a v5 gateway will understand v4 requests (key-rotation)
|
||||
if client_protocol_version == 4 {
|
||||
return Ok(4);
|
||||
}
|
||||
|
||||
// we can't handle clients with higher protocol than ours
|
||||
// (perhaps we could try to negotiate downgrade on our end? sounds like a nice future improvement)
|
||||
if client_protocol_version <= CURRENT_PROTOCOL_VERSION {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -90,8 +90,8 @@ impl KeyRotationController {
|
||||
async fn handle_contract_cache_update(&mut self) {
|
||||
let updated = self.get_contract_data().await;
|
||||
|
||||
info!(
|
||||
"current rotation: {}",
|
||||
debug!(
|
||||
"current key rotation: {}",
|
||||
updated
|
||||
.key_rotation_state
|
||||
.key_rotation_id(updated.interval.current_epoch_absolute_id())
|
||||
|
||||
@@ -101,7 +101,7 @@ pub(crate) async fn mixnodes_basic_active(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/entry-gateways/all",
|
||||
path = "/entry-gateways",
|
||||
context_path = "/v2/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
@@ -124,7 +124,7 @@ pub(crate) async fn entry_gateways_basic_all(
|
||||
tag = "Unstable Nym Nodes",
|
||||
get,
|
||||
params(NodesParams),
|
||||
path = "/exit-gateways/all",
|
||||
path = "/exit-gateways",
|
||||
context_path = "/v2/unstable/nym-nodes/skimmed",
|
||||
responses(
|
||||
(status = 200, content(
|
||||
|
||||
@@ -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'"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user