move client wasm to wallet dir
This commit is contained in:
@@ -3,7 +3,7 @@ name: Nym Wallet Storybook
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'nym-wallet/**'
|
||||
- 'nym-wallet/**''
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -25,10 +25,9 @@ jobs:
|
||||
toolchain: stable
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
working-directory: clients/webassembly
|
||||
- name: Build WASM
|
||||
run: wasm-pack build
|
||||
working-directory: clients/webassembly
|
||||
run: yarn build:wasm
|
||||
working-directory: nym/nym-wallet
|
||||
- name: Build dependencies
|
||||
run: yarn && yarn build
|
||||
- name: Build storybook
|
||||
|
||||
@@ -42,10 +42,9 @@ jobs:
|
||||
toolchain: stable
|
||||
- name: Install wasm-pack
|
||||
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
||||
working-directory: clients/webassembly
|
||||
- name: Build WASM
|
||||
run: wasm-pack build
|
||||
working-directory: clients/webassembly
|
||||
run: yarn build:wasm
|
||||
working-directory: nym-wallet
|
||||
- name: Install
|
||||
run: yarn
|
||||
- name: Build packages
|
||||
|
||||
Generated
+1290
-142
File diff suppressed because it is too large
Load Diff
@@ -4,5 +4,6 @@ resolver = "2"
|
||||
members = [
|
||||
"src-tauri",
|
||||
"nym-wallet-recovery-cli",
|
||||
"nym-wallet-types"
|
||||
"nym-wallet-types",
|
||||
"nym-client-wasm",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[build]
|
||||
target = "wasm32-unknown-unknown"
|
||||
Generated
+5289
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
[package]
|
||||
name = "nym-client-wasm"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jedrzej Stuczynski <andrew@nymtech.net>"]
|
||||
version = "1.1.1"
|
||||
edition = "2021"
|
||||
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
|
||||
license = "Apache-2.0"
|
||||
repository = "https://github.com/nymtech/nym"
|
||||
description = "A webassembly client which can be used to interact with the the Nym privacy platform. Wasm is used for Sphinx packet generation."
|
||||
rust-version = "1.56"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["console_error_panic_hook"]
|
||||
offline-test = []
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.68"
|
||||
bs58 = "0.4.0"
|
||||
futures = "0.3"
|
||||
js-sys = "0.3"
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
anyhow = "1.0"
|
||||
serde-wasm-bindgen = "0.5"
|
||||
tokio = { version = "1.24.1", features = ["sync"] }
|
||||
url = "2.2"
|
||||
wasm-bindgen = { version = "=0.2.83", features = ["serde-serialize"] }
|
||||
wasm-bindgen-futures = "0.4"
|
||||
thiserror = "1.0.40"
|
||||
zeroize = "1.6.0"
|
||||
|
||||
wasm-timer = { git = "https://github.com/mmsinclair/wasm-timer", rev = "b9d1a54ad514c2f230a026afe0dde341e98cd7b6"}
|
||||
|
||||
# internal
|
||||
nym-node-tester-utils = { path = "../../common/node-tester-utils" }
|
||||
nym-client-core = { path = "../../common/client-core", default-features = false, features = ["wasm"] }
|
||||
nym-bandwidth-controller = { path = "../../common/bandwidth-controller" }
|
||||
nym-coconut-interface = { path = "../../common/coconut-interface" }
|
||||
nym-credentials = { path = "../../common/credentials" }
|
||||
nym-credential-storage = { path = "../../common/credential-storage" }
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "serde"] }
|
||||
nym-sphinx = { path = "../../common/nymsphinx" }
|
||||
nym-sphinx-acknowledgements = { path = "../../common/nymsphinx/acknowledgements", features = ["serde"]}
|
||||
nym-topology = { path = "../../common/topology" }
|
||||
nym-gateway-client = { path = "../../common/client-libs/gateway-client", default-features = false, features = ["wasm"] }
|
||||
nym-validator-client = { path = "../../common/client-libs/validator-client", default-features = false }
|
||||
nym-task = { path = "../../common/task" }
|
||||
wasm-utils = { path = "../../common/wasm-utils", features = ["storage"], default-features = false }
|
||||
# The `console_error_panic_hook` crate provides better debugging of panics by
|
||||
# logging them with `console.error`. This is great for development, but requires
|
||||
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
|
||||
# code size when deploying.
|
||||
console_error_panic_hook = { version = "0.1", optional = true }
|
||||
|
||||
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
|
||||
# compared to the default allocator's ~10K. It is slower than the default
|
||||
# allocator, however.
|
||||
#
|
||||
# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now.
|
||||
wee_alloc = { version = "0.4", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
wasm-bindgen-test = "0.3"
|
||||
|
||||
[package.metadata.wasm-pack.profile.release]
|
||||
wasm-opt = false
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 'z'
|
||||
@@ -0,0 +1,8 @@
|
||||
clippy:
|
||||
cargo clippy --all-features --target wasm32-unknown-unknown -- -D warnings
|
||||
|
||||
test:
|
||||
wasm-pack test --node
|
||||
|
||||
wasm-build:
|
||||
wasm-pack build
|
||||
@@ -0,0 +1,393 @@
|
||||
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// due to expansion of #[wasm_bindgen] macro on `Debug` Config struct
|
||||
#![allow(clippy::drop_non_drop)]
|
||||
// another issue due to #[wasm_bindgen] and `Copy` trait
|
||||
#![allow(clippy::drop_copy)]
|
||||
|
||||
use nym_client_core::config::{
|
||||
Acknowledgements as ConfigAcknowledgements, Config as BaseClientConfig,
|
||||
CoverTraffic as ConfigCoverTraffic, DebugConfig as ConfigDebug,
|
||||
GatewayConnection as ConfigGatewayConnection, ReplySurbs as ConfigReplySurbs,
|
||||
Topology as ConfigTopology, Traffic as ConfigTraffic,
|
||||
};
|
||||
use nym_sphinx::params::{PacketSize, PacketType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Config {
|
||||
pub(crate) base: BaseClientConfig,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Config {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(id: String, validator_server: String, debug: Option<DebugWasm>) -> Self {
|
||||
Config {
|
||||
base: BaseClientConfig::new(id, env!("CARGO_PKG_VERSION").to_string())
|
||||
.with_custom_nyxd(vec![validator_server
|
||||
.parse()
|
||||
.expect("provided url was malformed")])
|
||||
.with_debug_config(debug.map(Into::into).unwrap_or_default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_tester_config<S: Into<String>>(id: S) -> Self {
|
||||
Config {
|
||||
base: BaseClientConfig::new(id.into(), env!("CARGO_PKG_VERSION").to_string())
|
||||
.with_disabled_credentials(true)
|
||||
.with_disabled_cover_traffic(true)
|
||||
.with_disabled_topology_refresh(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct TrafficWasm {
|
||||
/// The parameter of Poisson distribution determining how long, on average,
|
||||
/// sent packet is going to be delayed at any given mix node.
|
||||
/// So for a packet going through three mix nodes, on average, it will take three times this value
|
||||
/// until the packet reaches its destination.
|
||||
pub average_packet_delay_ms: u64,
|
||||
|
||||
/// The parameter of Poisson distribution determining how long, on average,
|
||||
/// it is going to take another 'real traffic stream' message to be sent.
|
||||
/// If no real packets are available and cover traffic is enabled,
|
||||
/// a loop cover message is sent instead in order to preserve the rate.
|
||||
pub message_sending_average_delay_ms: u64,
|
||||
|
||||
/// Controls whether the main packet stream constantly produces packets according to the predefined
|
||||
/// poisson distribution.
|
||||
pub disable_main_poisson_packet_distribution: bool,
|
||||
|
||||
/// Controls whether the sent sphinx packet use the NON-DEFAULT bigger size.
|
||||
pub use_extended_packet_size: bool,
|
||||
|
||||
/// Controls whether the sent packets should use outfox as opposed to the default sphinx.
|
||||
pub use_outfox: bool,
|
||||
}
|
||||
|
||||
impl From<TrafficWasm> for ConfigTraffic {
|
||||
fn from(traffic: TrafficWasm) -> Self {
|
||||
let use_extended_packet_size = traffic
|
||||
.use_extended_packet_size
|
||||
.then(|| PacketSize::ExtendedPacket32);
|
||||
|
||||
let packet_type = if traffic.use_outfox {
|
||||
PacketType::Outfox
|
||||
} else {
|
||||
PacketType::Mix
|
||||
};
|
||||
|
||||
ConfigTraffic {
|
||||
average_packet_delay: Duration::from_millis(traffic.average_packet_delay_ms),
|
||||
message_sending_average_delay: Duration::from_millis(
|
||||
traffic.message_sending_average_delay_ms,
|
||||
),
|
||||
disable_main_poisson_packet_distribution: traffic
|
||||
.disable_main_poisson_packet_distribution,
|
||||
primary_packet_size: PacketSize::RegularPacket,
|
||||
secondary_packet_size: use_extended_packet_size,
|
||||
packet_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigTraffic> for TrafficWasm {
|
||||
fn from(traffic: ConfigTraffic) -> Self {
|
||||
TrafficWasm {
|
||||
average_packet_delay_ms: traffic.average_packet_delay.as_millis() as u64,
|
||||
message_sending_average_delay_ms: traffic.message_sending_average_delay.as_millis()
|
||||
as u64,
|
||||
disable_main_poisson_packet_distribution: traffic
|
||||
.disable_main_poisson_packet_distribution,
|
||||
use_extended_packet_size: traffic.secondary_packet_size.is_some(),
|
||||
use_outfox: traffic.packet_type == PacketType::Outfox,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct CoverTrafficWasm {
|
||||
/// The parameter of Poisson distribution determining how long, on average,
|
||||
/// it is going to take for another loop cover traffic message to be sent.
|
||||
pub loop_cover_traffic_average_delay_ms: u64,
|
||||
|
||||
/// Specifies the ratio of `primary_packet_size` to `secondary_packet_size` used in cover traffic.
|
||||
/// Only applicable if `secondary_packet_size` is enabled.
|
||||
pub cover_traffic_primary_size_ratio: f64,
|
||||
|
||||
/// Controls whether the dedicated loop cover traffic stream should be enabled.
|
||||
/// (and sending packets, on average, every [Self::loop_cover_traffic_average_delay])
|
||||
pub disable_loop_cover_traffic_stream: bool,
|
||||
}
|
||||
|
||||
impl From<CoverTrafficWasm> for ConfigCoverTraffic {
|
||||
fn from(cover_traffic: CoverTrafficWasm) -> Self {
|
||||
ConfigCoverTraffic {
|
||||
loop_cover_traffic_average_delay: Duration::from_millis(
|
||||
cover_traffic.loop_cover_traffic_average_delay_ms,
|
||||
),
|
||||
cover_traffic_primary_size_ratio: cover_traffic.cover_traffic_primary_size_ratio,
|
||||
disable_loop_cover_traffic_stream: cover_traffic.disable_loop_cover_traffic_stream,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigCoverTraffic> for CoverTrafficWasm {
|
||||
fn from(cover_traffic: ConfigCoverTraffic) -> Self {
|
||||
CoverTrafficWasm {
|
||||
loop_cover_traffic_average_delay_ms: cover_traffic
|
||||
.loop_cover_traffic_average_delay
|
||||
.as_millis() as u64,
|
||||
cover_traffic_primary_size_ratio: cover_traffic.cover_traffic_primary_size_ratio,
|
||||
disable_loop_cover_traffic_stream: cover_traffic.disable_loop_cover_traffic_stream,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct GatewayConnectionWasm {
|
||||
/// How long we're willing to wait for a response to a message sent to the gateway,
|
||||
/// before giving up on it.
|
||||
pub gateway_response_timeout_ms: u64,
|
||||
}
|
||||
|
||||
impl From<GatewayConnectionWasm> for ConfigGatewayConnection {
|
||||
fn from(gateway_connection: GatewayConnectionWasm) -> Self {
|
||||
ConfigGatewayConnection {
|
||||
gateway_response_timeout: Duration::from_millis(
|
||||
gateway_connection.gateway_response_timeout_ms,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigGatewayConnection> for GatewayConnectionWasm {
|
||||
fn from(gateway_connection: ConfigGatewayConnection) -> Self {
|
||||
GatewayConnectionWasm {
|
||||
gateway_response_timeout_ms: gateway_connection.gateway_response_timeout.as_millis()
|
||||
as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct AcknowledgementsWasm {
|
||||
/// The parameter of Poisson distribution determining how long, on average,
|
||||
/// sent acknowledgement is going to be delayed at any given mix node.
|
||||
/// So for an ack going through three mix nodes, on average, it will take three times this value
|
||||
/// until the packet reaches its destination.
|
||||
pub average_ack_delay_ms: u64,
|
||||
|
||||
/// Value multiplied with the expected round trip time of an acknowledgement packet before
|
||||
/// it is assumed it was lost and retransmission of the data packet happens.
|
||||
/// In an ideal network with 0 latency, this value would have been 1.
|
||||
pub ack_wait_multiplier: f64,
|
||||
|
||||
/// Value added to the expected round trip time of an acknowledgement packet before
|
||||
/// it is assumed it was lost and retransmission of the data packet happens.
|
||||
/// In an ideal network with 0 latency, this value would have been 0.
|
||||
pub ack_wait_addition_ms: u64,
|
||||
}
|
||||
|
||||
impl From<AcknowledgementsWasm> for ConfigAcknowledgements {
|
||||
fn from(acknowledgements: AcknowledgementsWasm) -> Self {
|
||||
ConfigAcknowledgements {
|
||||
average_ack_delay: Duration::from_millis(acknowledgements.average_ack_delay_ms),
|
||||
ack_wait_multiplier: acknowledgements.ack_wait_multiplier,
|
||||
ack_wait_addition: Duration::from_millis(acknowledgements.ack_wait_addition_ms),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigAcknowledgements> for AcknowledgementsWasm {
|
||||
fn from(acknowledgements: ConfigAcknowledgements) -> Self {
|
||||
AcknowledgementsWasm {
|
||||
average_ack_delay_ms: acknowledgements.average_ack_delay.as_millis() as u64,
|
||||
ack_wait_multiplier: acknowledgements.ack_wait_multiplier,
|
||||
ack_wait_addition_ms: acknowledgements.ack_wait_addition.as_millis() as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct TopologyWasm {
|
||||
/// The uniform delay every which clients are querying the directory server
|
||||
/// to try to obtain a compatible network topology to send sphinx packets through.
|
||||
pub topology_refresh_rate_ms: u64,
|
||||
|
||||
/// During topology refresh, test packets are sent through every single possible network
|
||||
/// path. This timeout determines waiting period until it is decided that the packet
|
||||
/// did not reach its destination.
|
||||
pub topology_resolution_timeout_ms: u64,
|
||||
|
||||
/// Specifies whether the client should not refresh the network topology after obtaining
|
||||
/// the first valid instance.
|
||||
/// Supersedes `topology_refresh_rate_ms`.
|
||||
pub disable_refreshing: bool,
|
||||
}
|
||||
|
||||
impl From<TopologyWasm> for ConfigTopology {
|
||||
fn from(topology: TopologyWasm) -> Self {
|
||||
ConfigTopology {
|
||||
topology_refresh_rate: Duration::from_millis(topology.topology_refresh_rate_ms),
|
||||
topology_resolution_timeout: Duration::from_millis(
|
||||
topology.topology_resolution_timeout_ms,
|
||||
),
|
||||
disable_refreshing: topology.disable_refreshing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigTopology> for TopologyWasm {
|
||||
fn from(topology: ConfigTopology) -> Self {
|
||||
TopologyWasm {
|
||||
topology_refresh_rate_ms: topology.topology_refresh_rate.as_millis() as u64,
|
||||
topology_resolution_timeout_ms: topology.topology_resolution_timeout.as_millis() as u64,
|
||||
disable_refreshing: topology.disable_refreshing,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct ReplySurbsWasm {
|
||||
/// Defines the minimum number of reply surbs the client wants to keep in its storage at all times.
|
||||
/// It can only allow to go below that value if its to request additional reply surbs.
|
||||
pub minimum_reply_surb_storage_threshold: usize,
|
||||
|
||||
/// Defines the maximum number of reply surbs the client wants to keep in its storage at any times.
|
||||
pub maximum_reply_surb_storage_threshold: usize,
|
||||
|
||||
/// Defines the minimum number of reply surbs the client would request.
|
||||
pub minimum_reply_surb_request_size: u32,
|
||||
|
||||
/// Defines the maximum number of reply surbs the client would request.
|
||||
pub maximum_reply_surb_request_size: u32,
|
||||
|
||||
/// Defines the maximum number of reply surbs a remote party is allowed to request from this client at once.
|
||||
pub maximum_allowed_reply_surb_request_size: u32,
|
||||
|
||||
/// Defines maximum amount of time the client is going to wait for reply surbs before explicitly asking
|
||||
/// for more even though in theory they wouldn't need to.
|
||||
pub maximum_reply_surb_rerequest_waiting_period_ms: u64,
|
||||
|
||||
/// Defines maximum amount of time the client is going to wait for reply surbs before
|
||||
/// deciding it's never going to get them and would drop all pending messages
|
||||
pub maximum_reply_surb_drop_waiting_period_ms: u64,
|
||||
|
||||
/// Defines maximum amount of time given reply surb is going to be valid for.
|
||||
/// This is going to be superseded by key rotation once implemented.
|
||||
pub maximum_reply_surb_age_ms: u64,
|
||||
|
||||
/// Defines maximum amount of time given reply key is going to be valid for.
|
||||
/// This is going to be superseded by key rotation once implemented.
|
||||
pub maximum_reply_key_age_ms: u64,
|
||||
}
|
||||
|
||||
impl From<ReplySurbsWasm> for ConfigReplySurbs {
|
||||
fn from(reply_surbs: ReplySurbsWasm) -> Self {
|
||||
ConfigReplySurbs {
|
||||
minimum_reply_surb_storage_threshold: reply_surbs.minimum_reply_surb_storage_threshold,
|
||||
maximum_reply_surb_storage_threshold: reply_surbs.maximum_reply_surb_storage_threshold,
|
||||
minimum_reply_surb_request_size: reply_surbs.minimum_reply_surb_request_size,
|
||||
maximum_reply_surb_request_size: reply_surbs.maximum_reply_surb_request_size,
|
||||
maximum_allowed_reply_surb_request_size: reply_surbs
|
||||
.maximum_allowed_reply_surb_request_size,
|
||||
maximum_reply_surb_rerequest_waiting_period: Duration::from_millis(
|
||||
reply_surbs.maximum_reply_surb_rerequest_waiting_period_ms,
|
||||
),
|
||||
maximum_reply_surb_drop_waiting_period: Duration::from_millis(
|
||||
reply_surbs.maximum_reply_surb_drop_waiting_period_ms,
|
||||
),
|
||||
maximum_reply_surb_age: Duration::from_millis(reply_surbs.maximum_reply_surb_age_ms),
|
||||
maximum_reply_key_age: Duration::from_millis(reply_surbs.maximum_reply_key_age_ms),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigReplySurbs> for ReplySurbsWasm {
|
||||
fn from(reply_surbs: ConfigReplySurbs) -> Self {
|
||||
ReplySurbsWasm {
|
||||
minimum_reply_surb_storage_threshold: reply_surbs.minimum_reply_surb_storage_threshold,
|
||||
maximum_reply_surb_storage_threshold: reply_surbs.maximum_reply_surb_storage_threshold,
|
||||
minimum_reply_surb_request_size: reply_surbs.minimum_reply_surb_request_size,
|
||||
maximum_reply_surb_request_size: reply_surbs.maximum_reply_surb_request_size,
|
||||
maximum_allowed_reply_surb_request_size: reply_surbs
|
||||
.maximum_allowed_reply_surb_request_size,
|
||||
maximum_reply_surb_rerequest_waiting_period_ms: reply_surbs
|
||||
.maximum_reply_surb_rerequest_waiting_period
|
||||
.as_millis() as u64,
|
||||
maximum_reply_surb_drop_waiting_period_ms: reply_surbs
|
||||
.maximum_reply_surb_drop_waiting_period
|
||||
.as_millis() as u64,
|
||||
maximum_reply_surb_age_ms: reply_surbs.maximum_reply_surb_age.as_millis() as u64,
|
||||
maximum_reply_key_age_ms: reply_surbs.maximum_reply_key_age.as_millis() as u64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// just a helper structure to more easily pass through the JS boundary
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct DebugWasm {
|
||||
/// Defines all configuration options related to traffic streams.
|
||||
pub traffic: TrafficWasm,
|
||||
|
||||
/// Defines all configuration options related to cover traffic stream(s).
|
||||
pub cover_traffic: CoverTrafficWasm,
|
||||
|
||||
/// Defines all configuration options related to the gateway connection.
|
||||
pub gateway_connection: GatewayConnectionWasm,
|
||||
|
||||
/// Defines all configuration options related to acknowledgements, such as delays or wait timeouts.
|
||||
pub acknowledgements: AcknowledgementsWasm,
|
||||
|
||||
/// Defines all configuration options related topology, such as refresh rates or timeouts.
|
||||
pub topology: TopologyWasm,
|
||||
|
||||
/// Defines all configuration options related to reply SURBs.
|
||||
pub reply_surbs: ReplySurbsWasm,
|
||||
}
|
||||
|
||||
impl From<DebugWasm> for ConfigDebug {
|
||||
fn from(debug: DebugWasm) -> Self {
|
||||
ConfigDebug {
|
||||
traffic: debug.traffic.into(),
|
||||
cover_traffic: debug.cover_traffic.into(),
|
||||
gateway_connection: debug.gateway_connection.into(),
|
||||
acknowledgements: debug.acknowledgements.into(),
|
||||
topology: debug.topology.into(),
|
||||
reply_surbs: debug.reply_surbs.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ConfigDebug> for DebugWasm {
|
||||
fn from(debug: ConfigDebug) -> Self {
|
||||
DebugWasm {
|
||||
traffic: debug.traffic.into(),
|
||||
cover_traffic: debug.cover_traffic.into(),
|
||||
gateway_connection: debug.gateway_connection.into(),
|
||||
acknowledgements: debug.acknowledgements.into(),
|
||||
topology: debug.topology.into(),
|
||||
reply_surbs: debug.reply_surbs.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn default_debug() -> DebugWasm {
|
||||
ConfigDebug::default().into()
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::WasmClientError;
|
||||
use crate::tester::helpers::WasmTestMessageExt;
|
||||
use crate::tester::{NodeTestMessage, DEFAULT_TEST_PACKETS};
|
||||
use crate::topology::WasmNymTopology;
|
||||
use js_sys::Promise;
|
||||
use nym_client_core::client::base_client::{ClientInput, ClientState};
|
||||
use nym_client_core::client::inbound_messages::InputMessage;
|
||||
use nym_topology::{MixLayer, NymTopology};
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use wasm_utils::{console_log, simple_js_error};
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct NymClientTestRequest {
|
||||
// serialized NodeTestMessage
|
||||
pub(crate) test_msgs: Vec<Vec<u8>>,
|
||||
|
||||
// specially constructed network topology that only contains the target
|
||||
// node on the tested layer
|
||||
pub(crate) testable_topology: NymTopology,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl NymClientTestRequest {
|
||||
pub fn injectable_topology(&self) -> WasmNymTopology {
|
||||
self.testable_topology.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
// defining helper trait as we could directly call the method on the wrapper
|
||||
pub(crate) trait InputSender {
|
||||
fn send_message(&self, message: InputMessage) -> Promise;
|
||||
|
||||
fn send_messages(&self, messages: Vec<InputMessage>) -> Promise;
|
||||
}
|
||||
|
||||
impl InputSender for Arc<ClientInput> {
|
||||
fn send_message(&self, message: InputMessage) -> Promise {
|
||||
let this = Arc::clone(self);
|
||||
future_to_promise(async move {
|
||||
match this.input_sender.send(message).await {
|
||||
Ok(_) => Ok(JsValue::null()),
|
||||
Err(_) => Err(simple_js_error(
|
||||
"InputMessageReceiver has stopped receiving!",
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn send_messages(&self, messages: Vec<InputMessage>) -> Promise {
|
||||
let this = Arc::clone(self);
|
||||
future_to_promise(async move {
|
||||
for message in messages {
|
||||
if this.input_sender.send(message).await.is_err() {
|
||||
return Err(simple_js_error(
|
||||
"InputMessageReceiver has stopped receiving!",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(JsValue::null())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait WasmTopologyExt {
|
||||
/// Changes the current network topology to the provided value.
|
||||
fn change_hardcoded_topology(&self, topology: WasmNymTopology) -> Promise;
|
||||
|
||||
/// Returns the current network topology.
|
||||
fn current_topology(&self) -> Promise;
|
||||
|
||||
/// Checks whether the provided node exists in the known network topology and if so, returns its layer.
|
||||
fn check_for_mixnode_existence(&self, mixnode_identity: String) -> Promise;
|
||||
|
||||
/// Creates a `NymClientTestRequest` with a variant of `this` topology where the target node is the only one on its layer.
|
||||
fn mix_test_request(
|
||||
&self,
|
||||
test_id: u32,
|
||||
mixnode_identity: String,
|
||||
num_test_packets: Option<u32>,
|
||||
) -> Promise;
|
||||
}
|
||||
|
||||
impl WasmTopologyExt for Arc<ClientState> {
|
||||
fn change_hardcoded_topology(&self, topology: WasmNymTopology) -> Promise {
|
||||
let this = Arc::clone(self);
|
||||
future_to_promise(async move {
|
||||
let nym_topology: NymTopology = topology.into();
|
||||
console_log!("changing topology to {nym_topology:?}");
|
||||
this.topology_accessor
|
||||
.manually_change_topology(nym_topology)
|
||||
.await;
|
||||
Ok(JsValue::null())
|
||||
})
|
||||
}
|
||||
|
||||
fn current_topology(&self) -> Promise {
|
||||
let this = Arc::clone(self);
|
||||
future_to_promise(async move {
|
||||
match this.topology_accessor.current_topology().await {
|
||||
Some(topology) => Ok(JsValue::from(WasmNymTopology::from(topology))),
|
||||
None => Err(WasmClientError::UnavailableNetworkTopology.into()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks whether the target mixnode exists in the known network topology and returns its layer.
|
||||
fn check_for_mixnode_existence(&self, mixnode_identity: String) -> Promise {
|
||||
let this = Arc::clone(self);
|
||||
future_to_promise(async move {
|
||||
let Some(current_topology) = this.topology_accessor.current_topology().await else {
|
||||
return Err(WasmClientError::UnavailableNetworkTopology.into())
|
||||
};
|
||||
|
||||
match current_topology.find_mix_by_identity(&mixnode_identity) {
|
||||
None => Err(WasmClientError::NonExistentMixnode { mixnode_identity }.into()),
|
||||
Some(node) => Ok(JsValue::from(MixLayer::from(node.layer))),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn mix_test_request(
|
||||
&self,
|
||||
test_id: u32,
|
||||
mixnode_identity: String,
|
||||
num_test_packets: Option<u32>,
|
||||
) -> Promise {
|
||||
let num_test_packets = num_test_packets.unwrap_or(DEFAULT_TEST_PACKETS);
|
||||
|
||||
let this = Arc::clone(self);
|
||||
future_to_promise(async move {
|
||||
let Some(current_topology) = this.topology_accessor.current_topology().await else {
|
||||
return Err(WasmClientError::UnavailableNetworkTopology.into())
|
||||
};
|
||||
|
||||
let Some(mix) = current_topology.find_mix_by_identity(&mixnode_identity) else {
|
||||
return Err(WasmClientError::NonExistentMixnode { mixnode_identity }.into());
|
||||
};
|
||||
|
||||
let ext = WasmTestMessageExt::new(test_id);
|
||||
let test_msgs = NodeTestMessage::mix_plaintexts(mix, num_test_packets, ext)
|
||||
.map_err(WasmClientError::from)?;
|
||||
|
||||
let mut updated = current_topology.clone();
|
||||
updated.set_mixes_in_layer(mix.layer.into(), vec![mix.to_owned()]);
|
||||
|
||||
Ok(JsValue::from(NymClientTestRequest {
|
||||
test_msgs,
|
||||
testable_topology: updated,
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use self::config::Config;
|
||||
use crate::client::helpers::{InputSender, NymClientTestRequest, WasmTopologyExt};
|
||||
use crate::client::response_pusher::ResponsePusher;
|
||||
use crate::constants::NODE_TESTER_CLIENT_ID;
|
||||
use crate::error::WasmClientError;
|
||||
use crate::helpers::{
|
||||
parse_recipient, parse_sender_tag, setup_from_topology, setup_gateway_from_api,
|
||||
setup_reply_surb_storage_backend,
|
||||
};
|
||||
use crate::storage::traits::FullWasmClientStorage;
|
||||
use crate::storage::ClientStorage;
|
||||
use crate::topology::WasmNymTopology;
|
||||
use js_sys::Promise;
|
||||
use nym_bandwidth_controller::wasm_mockups::{Client as FakeClient, DirectSigningNyxdClient};
|
||||
use nym_client_core::client::base_client::{
|
||||
BaseClientBuilder, ClientInput, ClientOutput, ClientState,
|
||||
};
|
||||
use nym_client_core::client::inbound_messages::InputMessage;
|
||||
use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage;
|
||||
use nym_sphinx::params::PacketType;
|
||||
use nym_task::connections::TransmissionLane;
|
||||
use nym_task::TaskManager;
|
||||
use nym_topology::provider_trait::{HardcodedTopologyProvider, TopologyProvider};
|
||||
use nym_topology::NymTopology;
|
||||
use nym_validator_client::client::IdentityKey;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use wasm_utils::{check_promise_result, console_log, PromisableResult};
|
||||
|
||||
pub mod config;
|
||||
mod helpers;
|
||||
mod response_pusher;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct NymClient {
|
||||
self_address: String,
|
||||
client_input: Arc<ClientInput>,
|
||||
client_state: Arc<ClientState>,
|
||||
|
||||
// keep track of the "old" topology for the purposes of node tester
|
||||
// so that it could be restored after the check is done
|
||||
_full_topology: Option<NymTopology>,
|
||||
|
||||
// even though we don't use graceful shutdowns, other components rely on existence of this struct
|
||||
// and if it's dropped, everything will start going offline
|
||||
_task_manager: TaskManager,
|
||||
|
||||
packet_type: PacketType,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct NymClientBuilder {
|
||||
config: Config,
|
||||
custom_topology: Option<NymTopology>,
|
||||
preferred_gateway: Option<IdentityKey>,
|
||||
|
||||
storage_passphrase: Option<String>,
|
||||
on_message: js_sys::Function,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl NymClientBuilder {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
config: Config,
|
||||
on_message: js_sys::Function,
|
||||
preferred_gateway: Option<IdentityKey>,
|
||||
storage_passphrase: Option<String>,
|
||||
) -> Self {
|
||||
NymClientBuilder {
|
||||
config,
|
||||
custom_topology: None,
|
||||
storage_passphrase,
|
||||
on_message,
|
||||
preferred_gateway,
|
||||
}
|
||||
}
|
||||
|
||||
// no cover traffic
|
||||
// no poisson delay
|
||||
// hardcoded topology
|
||||
// NOTE: you most likely want to use `[NymNodeTester]` instead.
|
||||
pub fn new_tester(
|
||||
topology: WasmNymTopology,
|
||||
on_message: js_sys::Function,
|
||||
gateway: Option<IdentityKey>,
|
||||
) -> Self {
|
||||
if let Some(gateway_id) = &gateway {
|
||||
if !topology.ensure_contains_gateway_id(gateway_id) {
|
||||
panic!("the specified topology does not contain the gateway used by the client")
|
||||
}
|
||||
}
|
||||
|
||||
let full_config = Config::new_tester_config(NODE_TESTER_CLIENT_ID);
|
||||
|
||||
NymClientBuilder {
|
||||
config: full_config,
|
||||
custom_topology: Some(topology.into()),
|
||||
on_message,
|
||||
storage_passphrase: None,
|
||||
preferred_gateway: gateway,
|
||||
}
|
||||
}
|
||||
|
||||
fn start_reconstructed_pusher(client_output: ClientOutput, on_message: js_sys::Function) {
|
||||
ResponsePusher::new(client_output, on_message).start()
|
||||
}
|
||||
|
||||
fn topology_provider(&mut self) -> Option<Box<dyn TopologyProvider + Send + Sync>> {
|
||||
if let Some(hardcoded_topology) = self.custom_topology.take() {
|
||||
Some(Box::new(HardcodedTopologyProvider::new(hardcoded_topology)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn initialise_storage(config: &Config, base_storage: ClientStorage) -> FullWasmClientStorage {
|
||||
FullWasmClientStorage {
|
||||
keys_and_gateway_store: base_storage,
|
||||
reply_storage: setup_reply_surb_storage_backend(config.base.debug.reply_surbs),
|
||||
credential_storage: EphemeralCredentialStorage::default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_client_async(mut self) -> Result<NymClient, WasmClientError> {
|
||||
console_log!("Starting the wasm client");
|
||||
|
||||
let nym_api_endpoints = self.config.base.client.nym_api_urls.clone();
|
||||
|
||||
// TODO: this will have to be re-used for surbs. but this is a problem for another PR.
|
||||
let client_store =
|
||||
ClientStorage::new_async(&self.config.base.client.id, self.storage_passphrase.take())
|
||||
.await?;
|
||||
|
||||
let user_chosen = self.preferred_gateway.clone();
|
||||
|
||||
// if we provided hardcoded topology, get gateway from it, otherwise get it the 'standard' way
|
||||
if let Some(topology) = &self.custom_topology {
|
||||
setup_from_topology(user_chosen, topology, &client_store).await?
|
||||
} else {
|
||||
setup_gateway_from_api(&client_store, user_chosen, &nym_api_endpoints).await?
|
||||
};
|
||||
|
||||
let packet_type = self.config.base.debug.traffic.packet_type;
|
||||
let storage = Self::initialise_storage(&self.config, client_store);
|
||||
let maybe_topology_provider = self.topology_provider();
|
||||
|
||||
let mut base_builder: BaseClientBuilder<_, FullWasmClientStorage> =
|
||||
BaseClientBuilder::<FakeClient<DirectSigningNyxdClient>, _>::new(
|
||||
&self.config.base,
|
||||
storage,
|
||||
None,
|
||||
);
|
||||
if let Some(topology_provider) = maybe_topology_provider {
|
||||
base_builder = base_builder.with_topology_provider(topology_provider);
|
||||
}
|
||||
|
||||
let mut started_client = base_builder.start_base().await?;
|
||||
let self_address = started_client.address.to_string();
|
||||
|
||||
let client_input = started_client.client_input.register_producer();
|
||||
let client_output = started_client.client_output.register_consumer();
|
||||
|
||||
Self::start_reconstructed_pusher(client_output, self.on_message);
|
||||
|
||||
Ok(NymClient {
|
||||
self_address,
|
||||
client_input: Arc::new(client_input),
|
||||
client_state: Arc::new(started_client.client_state),
|
||||
_full_topology: None,
|
||||
_task_manager: started_client.task_manager,
|
||||
packet_type,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn start_client(self) -> Promise {
|
||||
future_to_promise(async move { self.start_client_async().await.into_promise_result() })
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl NymClient {
|
||||
async fn _new(
|
||||
config: Config,
|
||||
on_message: js_sys::Function,
|
||||
preferred_gateway: Option<IdentityKey>,
|
||||
storage_passphrase: Option<String>,
|
||||
) -> Result<NymClient, WasmClientError> {
|
||||
NymClientBuilder::new(config, on_message, preferred_gateway, storage_passphrase)
|
||||
.start_client_async()
|
||||
.await
|
||||
}
|
||||
|
||||
#[wasm_bindgen(constructor)]
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
config: Config,
|
||||
on_message: js_sys::Function,
|
||||
preferred_gateway: Option<IdentityKey>,
|
||||
storage_passphrase: Option<String>,
|
||||
) -> Promise {
|
||||
future_to_promise(async move {
|
||||
Self::_new(config, on_message, preferred_gateway, storage_passphrase)
|
||||
.await
|
||||
.into_promise_result()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn self_address(&self) -> String {
|
||||
self.self_address.clone()
|
||||
}
|
||||
|
||||
pub fn try_construct_test_packet_request(
|
||||
&self,
|
||||
mixnode_identity: String,
|
||||
num_test_packets: Option<u32>,
|
||||
) -> Promise {
|
||||
// TODO: improve the source of rng (i.e. don't make it ephemeral...)
|
||||
let mut ephemeral_rng = OsRng;
|
||||
let test_id = ephemeral_rng.next_u32();
|
||||
self.client_state
|
||||
.mix_test_request(test_id, mixnode_identity, num_test_packets)
|
||||
}
|
||||
|
||||
pub fn change_hardcoded_topology(&self, topology: WasmNymTopology) -> Promise {
|
||||
self.client_state.change_hardcoded_topology(topology)
|
||||
}
|
||||
|
||||
pub fn current_network_topology(&self) -> Promise {
|
||||
self.client_state.current_topology()
|
||||
}
|
||||
|
||||
/// Sends a test packet through the current network topology.
|
||||
/// It's the responsibility of the caller to ensure the correct topology has been injected and
|
||||
/// correct onmessage handlers have been setup.
|
||||
pub fn try_send_test_packets(&mut self, request: NymClientTestRequest) -> Promise {
|
||||
// TOOD: use the premade packets instead
|
||||
console_log!(
|
||||
"Attempting to send {} test packets",
|
||||
request.test_msgs.len()
|
||||
);
|
||||
|
||||
// our address MUST BE valid
|
||||
let recipient = parse_recipient(&self.self_address()).unwrap();
|
||||
|
||||
let lane = TransmissionLane::General;
|
||||
let input_msgs = request
|
||||
.test_msgs
|
||||
.into_iter()
|
||||
.map(|p| InputMessage::new_regular(recipient, p, lane, None))
|
||||
.collect();
|
||||
|
||||
self.client_input.send_messages(input_msgs)
|
||||
}
|
||||
|
||||
/// The simplest message variant where no additional information is attached.
|
||||
/// You're simply sending your `data` to specified `recipient` without any tagging.
|
||||
///
|
||||
/// Ends up with `NymMessage::Plain` variant
|
||||
pub fn send_regular_message(&self, message: Vec<u8>, recipient: String) -> Promise {
|
||||
console_log!(
|
||||
"Attempting to send {:.2} kiB message to {recipient}",
|
||||
message.len() as f64 / 1024.0
|
||||
);
|
||||
|
||||
let recipient = check_promise_result!(parse_recipient(&recipient));
|
||||
|
||||
let lane = TransmissionLane::General;
|
||||
|
||||
let input_msg = InputMessage::new_regular(recipient, message, lane, Some(self.packet_type));
|
||||
self.client_input.send_message(input_msg)
|
||||
}
|
||||
|
||||
/// Creates a message used for a duplex anonymous communication where the recipient
|
||||
/// will never learn of our true identity. This is achieved by carefully sending `reply_surbs`.
|
||||
///
|
||||
/// Note that if reply_surbs is set to zero then
|
||||
/// this variant requires the client having sent some reply_surbs in the past
|
||||
/// (and thus the recipient also knowing our sender tag).
|
||||
///
|
||||
/// Ends up with `NymMessage::Repliable` variant
|
||||
pub fn send_anonymous_message(
|
||||
&self,
|
||||
message: Vec<u8>,
|
||||
recipient: String,
|
||||
reply_surbs: u32,
|
||||
) -> Promise {
|
||||
console_log!(
|
||||
"Attempting to anonymously send {:.2} kiB message to {recipient} while attaching {reply_surbs} replySURBs.",
|
||||
message.len() as f64 / 1024.0
|
||||
);
|
||||
|
||||
let recipient = check_promise_result!(parse_recipient(&recipient));
|
||||
|
||||
let lane = TransmissionLane::General;
|
||||
|
||||
let input_msg = InputMessage::new_anonymous(
|
||||
recipient,
|
||||
message,
|
||||
reply_surbs,
|
||||
lane,
|
||||
Some(self.packet_type),
|
||||
);
|
||||
self.client_input.send_message(input_msg)
|
||||
}
|
||||
|
||||
/// Attempt to use our internally received and stored `ReplySurb` to send the message back
|
||||
/// to specified recipient whilst not knowing its full identity (or even gateway).
|
||||
///
|
||||
/// Ends up with `NymMessage::Reply` variant
|
||||
pub fn send_reply(&self, message: Vec<u8>, recipient_tag: String) -> Promise {
|
||||
console_log!(
|
||||
"Attempting to send {:.2} kiB reply message to {recipient_tag}",
|
||||
message.len() as f64 / 1024.0
|
||||
);
|
||||
|
||||
let sender_tag = check_promise_result!(parse_sender_tag(&recipient_tag));
|
||||
|
||||
let lane = TransmissionLane::General;
|
||||
|
||||
let input_msg = InputMessage::new_reply(sender_tag, message, lane, Some(self.packet_type));
|
||||
self.client_input.send_message(input_msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use futures::channel::mpsc;
|
||||
use futures::StreamExt;
|
||||
use js_sys::Uint8Array;
|
||||
use nym_client_core::client::base_client::ClientOutput;
|
||||
use nym_client_core::client::received_buffer::{
|
||||
ReceivedBufferMessage, ReconstructedMessagesReceiver,
|
||||
};
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
use wasm_utils::console_error;
|
||||
|
||||
pub(crate) struct ResponsePusher {
|
||||
reconstructed_receiver: ReconstructedMessagesReceiver,
|
||||
on_message: js_sys::Function,
|
||||
}
|
||||
|
||||
impl ResponsePusher {
|
||||
pub(crate) fn new(client_output: ClientOutput, on_message: js_sys::Function) -> Self {
|
||||
// register our output
|
||||
let (reconstructed_sender, reconstructed_receiver) = mpsc::unbounded();
|
||||
|
||||
// tell the buffer to start sending stuff to us
|
||||
client_output
|
||||
.received_buffer_request_sender
|
||||
.unbounded_send(ReceivedBufferMessage::ReceiverAnnounce(
|
||||
reconstructed_sender,
|
||||
))
|
||||
.expect("the buffer request failed!");
|
||||
|
||||
ResponsePusher {
|
||||
reconstructed_receiver,
|
||||
on_message,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start(mut self) {
|
||||
spawn_local(async move {
|
||||
let this = JsValue::null();
|
||||
|
||||
while let Some(reconstructed) = self.reconstructed_receiver.next().await {
|
||||
for reconstructed_msg in reconstructed {
|
||||
let (msg, tag) = reconstructed_msg.into_inner();
|
||||
|
||||
let msg_slice: &[u8] = &msg;
|
||||
let array = Uint8Array::from(msg_slice);
|
||||
let arg1 = JsValue::from(array);
|
||||
let arg2 = JsValue::from(tag);
|
||||
self.on_message
|
||||
.call2(&this, &arg1, &arg2)
|
||||
.expect("on binary message failed!");
|
||||
}
|
||||
}
|
||||
|
||||
console_error!("we stopped receiving reconstructed messages!")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub(crate) const NODE_TESTER_ID: &str = "_nym-node-tester";
|
||||
pub(crate) const NODE_TESTER_CLIENT_ID: &str = "_nym-node-tester-client";
|
||||
@@ -0,0 +1,238 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
#[wasm_bindgen(typescript_custom_section)]
|
||||
const TS_DEFS: &'static str = r#"
|
||||
export interface EncodedPayload {
|
||||
mimeType: string,
|
||||
payload: Uint8Array;
|
||||
headers: string,
|
||||
}
|
||||
"#;
|
||||
|
||||
#[wasm_bindgen]
|
||||
extern "C" {
|
||||
#[wasm_bindgen(typescript_type = "EncodedPayload")]
|
||||
pub type IEncodedPayload;
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EncodedPayload {
|
||||
pub mime_type: String,
|
||||
pub payload: Vec<u8>,
|
||||
pub headers: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EncodedPayloadMetadata {
|
||||
pub mime_type: String,
|
||||
pub headers: Option<String>,
|
||||
}
|
||||
|
||||
/// Encode a payload
|
||||
#[wasm_bindgen]
|
||||
pub fn encode_payload(mime_type: String, payload: Vec<u8>) -> Result<Vec<u8>, JsValue> {
|
||||
encode_payload_with_headers(mime_type, payload, None)
|
||||
}
|
||||
|
||||
/// Create a new binary message with a user-specified `kind`, and `headers` as a string.
|
||||
#[wasm_bindgen]
|
||||
pub fn encode_payload_with_headers(
|
||||
mime_type: String,
|
||||
payload: Vec<u8>,
|
||||
headers: Option<String>,
|
||||
) -> Result<Vec<u8>, JsValue> {
|
||||
match serde_json::to_string(&EncodedPayloadMetadata { mime_type, headers }) {
|
||||
Ok(metadata) => {
|
||||
let metadata = metadata.as_bytes().to_vec();
|
||||
let size = (metadata.len() as u64).to_be_bytes().to_vec();
|
||||
Ok(vec![size, metadata, payload].concat())
|
||||
}
|
||||
Err(e) => Err(JsValue::from(JsError::new(
|
||||
format!("Could not encode message: {}", e).as_str(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the `kind` and byte array `payload` from a byte array
|
||||
#[wasm_bindgen]
|
||||
pub fn decode_payload(message: Vec<u8>) -> Result<IEncodedPayload, JsValue> {
|
||||
if message.len() < 8 {
|
||||
return Err(JsValue::from(JsError::new(
|
||||
"Could not parse message, as less than 8 bytes long",
|
||||
)));
|
||||
}
|
||||
|
||||
match parse_payload(&message) {
|
||||
Ok((metadata, payload)) => Ok(serde_wasm_bindgen::to_value(&EncodedPayload {
|
||||
mime_type: metadata.mime_type,
|
||||
payload: payload.to_vec(),
|
||||
headers: metadata.headers,
|
||||
})
|
||||
.unwrap()
|
||||
.unchecked_into::<IEncodedPayload>()),
|
||||
Err(e) => Err(JsValue::from(JsError::new(
|
||||
format!("Could not parse message: {}", e).as_str(),
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_payload(message: &[u8]) -> anyhow::Result<(EncodedPayloadMetadata, &[u8])> {
|
||||
// 1st 8 bytes are the size (as u64 big endian)
|
||||
let mut size = [0u8; 8];
|
||||
size.clone_from_slice(&message[0..8]);
|
||||
let size = u64::from_be_bytes(size) as usize;
|
||||
|
||||
// then the metadata
|
||||
let metadata = String::from_utf8_lossy(&message[8..8 + size]).into_owned();
|
||||
let metadata: EncodedPayloadMetadata = serde_json::from_str(metadata.as_str())?;
|
||||
|
||||
// finally the payload
|
||||
let payload = &message[8 + size..];
|
||||
|
||||
Ok((metadata, payload))
|
||||
}
|
||||
|
||||
/// Try parse a UTF-8 string from an array of bytes
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_utf8_string(payload: Vec<u8>) -> String {
|
||||
String::from_utf8_lossy(&payload).into_owned()
|
||||
}
|
||||
|
||||
/// Converts a UTF-8 string into an array of bytes
|
||||
///
|
||||
/// This method is provided as a replacement for the mess of `atob`
|
||||
/// (https://developer.mozilla.org/en-US/docs/Web/API/atob) helpers provided by browsers and NodeJS.
|
||||
///
|
||||
/// Feel free to use `atob` if you know you won't have problems with polyfills or encoding issues.
|
||||
#[wasm_bindgen]
|
||||
pub fn utf8_string_to_byte_array(message: String) -> Vec<u8> {
|
||||
message.into_bytes()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_encode_payload_with_headers() {
|
||||
let message_as_bytes = encode_payload_with_headers(
|
||||
"text/plain".to_string(),
|
||||
vec![0u8, 1u8, 2u8],
|
||||
Some("test headers".to_string()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// the expected message size
|
||||
let size = message_as_bytes.len();
|
||||
let expected_size = 61;
|
||||
assert_eq!(size, expected_size);
|
||||
|
||||
let expected_header_size = 50usize;
|
||||
assert_eq!(
|
||||
message_as_bytes[0..8],
|
||||
(expected_header_size as u64).to_be_bytes()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
message_as_bytes[8 + expected_header_size..8 + expected_header_size + 3],
|
||||
vec![0u8, 1u8, 2u8]
|
||||
);
|
||||
|
||||
let res = parse_payload(&message_as_bytes).unwrap();
|
||||
|
||||
assert_eq!(res.0.mime_type, "text/plain");
|
||||
assert_eq!(res.0.headers.unwrap(), "test headers".to_string());
|
||||
assert_eq!(res.1, vec![0u8, 1u8, 2u8]);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_encode_payload_with_empty_headers() {
|
||||
let message_as_bytes =
|
||||
encode_payload_with_headers("text/plain".to_string(), vec![0u8, 1u8, 2u8], None)
|
||||
.unwrap();
|
||||
|
||||
// the expected message size
|
||||
let size = message_as_bytes.len();
|
||||
let expected_size = 51;
|
||||
assert_eq!(size, expected_size);
|
||||
|
||||
let expected_header_size = 40usize;
|
||||
assert_eq!(
|
||||
message_as_bytes[0..8],
|
||||
(expected_header_size as u64).to_be_bytes()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
message_as_bytes[8 + expected_header_size..8 + expected_header_size + 3],
|
||||
vec![0u8, 1u8, 2u8]
|
||||
);
|
||||
let res = parse_payload(&message_as_bytes).unwrap();
|
||||
|
||||
assert_eq!(res.0.mime_type, "text/plain");
|
||||
assert_eq!(res.0.headers, None);
|
||||
assert_eq!(res.1, vec![0u8, 1u8, 2u8]);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_encode_payload_with_empty_headers_and_empty_mime_type() {
|
||||
let message_as_bytes =
|
||||
encode_payload_with_headers("".to_string(), vec![0u8, 1u8, 2u8], None).unwrap();
|
||||
|
||||
// the expected message size
|
||||
let size = message_as_bytes.len();
|
||||
let expected_size = 41;
|
||||
assert_eq!(size, expected_size);
|
||||
|
||||
let expected_header_size = 30usize;
|
||||
assert_eq!(
|
||||
message_as_bytes[0..8],
|
||||
(expected_header_size as u64).to_be_bytes()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
message_as_bytes[8 + expected_header_size..8 + expected_header_size + 3],
|
||||
vec![0u8, 1u8, 2u8]
|
||||
);
|
||||
let res = parse_payload(&message_as_bytes).unwrap();
|
||||
|
||||
assert_eq!(res.0.mime_type, "");
|
||||
assert_eq!(res.0.headers, None);
|
||||
assert_eq!(res.1, vec![0u8, 1u8, 2u8]);
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
async fn test_encode_payload_with_all_empty() {
|
||||
let empty: Vec<u8> = vec![];
|
||||
let message_as_bytes =
|
||||
encode_payload_with_headers("".to_string(), empty.clone(), None).unwrap();
|
||||
|
||||
// the expected message size
|
||||
let size = message_as_bytes.len();
|
||||
let expected_size = 38;
|
||||
assert_eq!(size, expected_size);
|
||||
|
||||
let expected_header_size = 30usize;
|
||||
assert_eq!(
|
||||
message_as_bytes[0..8],
|
||||
(expected_header_size as u64).to_be_bytes()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
message_as_bytes[8 + expected_header_size..8 + expected_header_size],
|
||||
empty
|
||||
);
|
||||
let res = parse_payload(&message_as_bytes).unwrap();
|
||||
|
||||
assert_eq!(res.0.mime_type, "");
|
||||
assert_eq!(res.0.headers, None);
|
||||
assert_eq!(res.1, empty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::storage::errors::ClientStorageError;
|
||||
use crate::topology::WasmTopologyError;
|
||||
use js_sys::Promise;
|
||||
use nym_client_core::config::GatewayEndpointConfig;
|
||||
use nym_client_core::error::ClientCoreError;
|
||||
use nym_crypto::asymmetric::identity::Ed25519RecoveryError;
|
||||
use nym_gateway_client::error::GatewayClientError;
|
||||
use nym_node_tester_utils::error::NetworkTestingError;
|
||||
use nym_sphinx::addressing::clients::RecipientFormattingError;
|
||||
use nym_sphinx::anonymous_replies::requests::InvalidAnonymousSenderTagRepresentation;
|
||||
use nym_topology::NymTopologyError;
|
||||
use nym_validator_client::ValidatorClientError;
|
||||
use thiserror::Error;
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_utils::simple_js_error;
|
||||
|
||||
// might as well start using well-defined error enum...
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WasmClientError {
|
||||
#[error(
|
||||
"A node test is already in progress. Wait for it to finish before starting another one."
|
||||
)]
|
||||
TestInProgress,
|
||||
|
||||
#[error("experienced an issue with internal client components: {source}")]
|
||||
BaseClientError {
|
||||
#[from]
|
||||
source: ClientCoreError,
|
||||
},
|
||||
|
||||
#[error("The provided gateway identity is invalid: {source}")]
|
||||
InvalidGatewayIdentity { source: Ed25519RecoveryError },
|
||||
|
||||
#[error("Gateway communication failure: {source}")]
|
||||
GatewayClientError {
|
||||
#[from]
|
||||
source: GatewayClientError,
|
||||
},
|
||||
|
||||
#[error("failed to query nym api: {source}")]
|
||||
NymApiError {
|
||||
#[from]
|
||||
source: ValidatorClientError,
|
||||
},
|
||||
|
||||
#[error("The provided wasm topology was invalid: {source}")]
|
||||
WasmTopologyError {
|
||||
#[from]
|
||||
source: WasmTopologyError,
|
||||
},
|
||||
|
||||
#[error("The provided nym topology was invalid: {source}")]
|
||||
TopologyError {
|
||||
#[from]
|
||||
source: NymTopologyError,
|
||||
},
|
||||
|
||||
#[error("failed to test the node: {source}")]
|
||||
NodeTestingFailure {
|
||||
#[from]
|
||||
source: NetworkTestingError,
|
||||
},
|
||||
|
||||
#[error("{raw} is not a valid url: {source}")]
|
||||
MalformedUrl {
|
||||
raw: String,
|
||||
source: url::ParseError,
|
||||
},
|
||||
|
||||
#[error("Network topology is currently unavailable")]
|
||||
UnavailableNetworkTopology,
|
||||
|
||||
#[error("Mixnode {mixnode_identity} is not present in the current network topology")]
|
||||
NonExistentMixnode { mixnode_identity: String },
|
||||
|
||||
#[error("Gateway {gateway_identity} is not present in the current network topology")]
|
||||
NonExistentGateway { gateway_identity: String },
|
||||
|
||||
#[error("{raw} is not a valid Nym network recipient: {source}")]
|
||||
MalformedRecipient {
|
||||
raw: String,
|
||||
source: RecipientFormattingError,
|
||||
},
|
||||
|
||||
#[error("{raw} is not a valid Nym AnonymousSenderTag: {source}")]
|
||||
MalformedSenderTag {
|
||||
raw: String,
|
||||
source: InvalidAnonymousSenderTagRepresentation,
|
||||
},
|
||||
|
||||
#[error(transparent)]
|
||||
StorageError {
|
||||
#[from]
|
||||
source: ClientStorageError,
|
||||
},
|
||||
|
||||
#[error("this client has already registered with a gateway: {gateway_config:?}")]
|
||||
AlreadyRegistered {
|
||||
gateway_config: GatewayEndpointConfig,
|
||||
},
|
||||
}
|
||||
|
||||
impl WasmClientError {
|
||||
pub fn into_rejected_promise(self) -> Promise {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WasmClientError> for JsValue {
|
||||
fn from(value: WasmClientError) -> Self {
|
||||
simple_js_error(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WasmClientError> for Promise {
|
||||
fn from(value: WasmClientError) -> Self {
|
||||
Promise::reject(&value.into())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_client_core::config::GatewayEndpointConfig;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub async fn get_gateway(api_server: String, preferred: Option<String>) -> GatewayEndpointConfig {
|
||||
let validator_client =
|
||||
nym_validator_client::client::NymApiClient::new(api_server.parse().unwrap());
|
||||
|
||||
let gateways = match validator_client.get_cached_gateways().await {
|
||||
Err(err) => panic!("failed to obtain list of all gateways - {err}"),
|
||||
Ok(gateways) => gateways,
|
||||
};
|
||||
|
||||
if let Some(preferred) = preferred {
|
||||
if let Some(details) = gateways
|
||||
.iter()
|
||||
.find(|g| g.gateway.identity_key == preferred)
|
||||
{
|
||||
return GatewayEndpointConfig {
|
||||
gateway_id: details.gateway.identity_key.clone(),
|
||||
gateway_owner: details.owner.to_string(),
|
||||
gateway_listener: format!(
|
||||
"ws://{}:{}",
|
||||
details.gateway.host, details.gateway.clients_port
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let details = gateways
|
||||
.first()
|
||||
.expect("current topology holds no gateways");
|
||||
|
||||
GatewayEndpointConfig {
|
||||
gateway_id: details.gateway.identity_key.clone(),
|
||||
gateway_owner: details.owner.to_string(),
|
||||
gateway_listener: format!(
|
||||
"ws://{}:{}",
|
||||
details.gateway.host, details.gateway.clients_port
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::WasmClientError;
|
||||
use crate::storage::ClientStorage;
|
||||
use crate::topology::WasmNymTopology;
|
||||
use js_sys::Promise;
|
||||
use nym_client_core::client::replies::reply_storage::browser_backend;
|
||||
use nym_client_core::config;
|
||||
use nym_client_core::init::helpers::current_gateways;
|
||||
use nym_client_core::init::{setup_gateway_from, GatewaySetup, InitialisationDetails};
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use nym_topology::{gateway, NymTopology};
|
||||
use nym_validator_client::client::IdentityKey;
|
||||
use nym_validator_client::NymApiClient;
|
||||
use rand::thread_rng;
|
||||
use url::Url;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use wasm_utils::PromisableResult;
|
||||
|
||||
// don't get too excited about the name, under the hood it's just a big fat placeholder
|
||||
// with no disk_persistence
|
||||
pub(crate) fn setup_reply_surb_storage_backend(
|
||||
config: config::ReplySurbs,
|
||||
) -> browser_backend::Backend {
|
||||
browser_backend::Backend::new(
|
||||
config.minimum_reply_surb_storage_threshold,
|
||||
config.maximum_reply_surb_storage_threshold,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_recipient(recipient: &str) -> Result<Recipient, WasmClientError> {
|
||||
Recipient::try_from_base58_string(recipient).map_err(|source| {
|
||||
WasmClientError::MalformedRecipient {
|
||||
raw: recipient.to_string(),
|
||||
source,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn parse_sender_tag(tag: &str) -> Result<AnonymousSenderTag, WasmClientError> {
|
||||
AnonymousSenderTag::try_from_base58_string(tag).map_err(|source| {
|
||||
WasmClientError::MalformedSenderTag {
|
||||
raw: tag.to_string(),
|
||||
source,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn current_network_topology_async(
|
||||
nym_api_url: String,
|
||||
) -> Result<WasmNymTopology, WasmClientError> {
|
||||
let url: Url = match nym_api_url.parse() {
|
||||
Ok(url) => url,
|
||||
Err(source) => {
|
||||
return Err(WasmClientError::MalformedUrl {
|
||||
raw: nym_api_url,
|
||||
source,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let api_client = NymApiClient::new(url);
|
||||
let mixnodes = api_client.get_cached_active_mixnodes().await?;
|
||||
let gateways = api_client.get_cached_gateways().await?;
|
||||
|
||||
Ok(NymTopology::from_detailed(mixnodes, gateways).into())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn current_network_topology(nym_api_url: String) -> Promise {
|
||||
future_to_promise(async move {
|
||||
current_network_topology_async(nym_api_url)
|
||||
.await
|
||||
.into_promise_result()
|
||||
})
|
||||
}
|
||||
|
||||
async fn setup_gateway(
|
||||
client_store: &ClientStorage,
|
||||
chosen_gateway: Option<IdentityKey>,
|
||||
gateways: &[gateway::Node],
|
||||
) -> Result<InitialisationDetails, WasmClientError> {
|
||||
let setup = if client_store.has_full_gateway_info().await? {
|
||||
GatewaySetup::MustLoad
|
||||
} else {
|
||||
GatewaySetup::new_fresh(chosen_gateway.clone(), None)
|
||||
};
|
||||
|
||||
setup_gateway_from(&setup, client_store, client_store, false, Some(gateways))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) async fn setup_gateway_from_api(
|
||||
client_store: &ClientStorage,
|
||||
chosen_gateway: Option<IdentityKey>,
|
||||
nym_apis: &[Url],
|
||||
) -> Result<InitialisationDetails, WasmClientError> {
|
||||
let mut rng = thread_rng();
|
||||
let gateways = current_gateways(&mut rng, nym_apis).await?;
|
||||
setup_gateway(client_store, chosen_gateway, &gateways).await
|
||||
}
|
||||
|
||||
pub(crate) async fn setup_from_topology(
|
||||
explicit_gateway: Option<IdentityKey>,
|
||||
topology: &NymTopology,
|
||||
client_store: &ClientStorage,
|
||||
) -> Result<InitialisationDetails, WasmClientError> {
|
||||
let gateways = topology.gateways();
|
||||
setup_gateway(client_store, explicit_gateway, gateways).await
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod client;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod encoded_payload_helper;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod error;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod gateway_selector;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod storage;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod tester;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod topology;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod validation;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod helpers;
|
||||
|
||||
mod constants;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn set_panic_hook() {
|
||||
// When the `console_error_panic_hook` feature is enabled, we can call the
|
||||
// `set_panic_hook` function at least once during initialization, and then
|
||||
// we will get better error messages if our code ever panics.
|
||||
//
|
||||
// For more details see
|
||||
// https://github.com/rustwasm/console_error_panic_hook#readme
|
||||
#[cfg(feature = "console_error_panic_hook")]
|
||||
console_error_panic_hook::set_once();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use thiserror::Error;
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_utils::simple_js_error;
|
||||
use wasm_utils::storage::error::StorageError;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ClientStorageError {
|
||||
#[error("failed to use the storage: {source}")]
|
||||
StorageError {
|
||||
#[from]
|
||||
source: StorageError,
|
||||
},
|
||||
|
||||
#[error("{typ} cryptographic key is not available in storage")]
|
||||
CryptoKeyNotInStorage { typ: String },
|
||||
|
||||
#[error("the prior gateway details are not available in the storage")]
|
||||
GatewayDetailsNotInStorage,
|
||||
}
|
||||
|
||||
impl From<ClientStorageError> for JsValue {
|
||||
fn from(value: ClientStorageError) -> Self {
|
||||
simple_js_error(value.to_string())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::config::Config;
|
||||
use crate::storage::errors::ClientStorageError;
|
||||
use js_sys::Promise;
|
||||
use nym_client_core::client::base_client::storage::gateway_details::PersistedGatewayDetails;
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
use nym_gateway_client::SharedKeys;
|
||||
use nym_sphinx::acknowledgements::AckKey;
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use wasm_utils::storage::{IdbVersionChangeEvent, WasmStorage};
|
||||
use wasm_utils::PromisableResult;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub(crate) mod errors;
|
||||
pub(crate) mod traits;
|
||||
|
||||
const STORAGE_NAME_PREFIX: &str = "wasm-client-storage";
|
||||
const STORAGE_VERSION: u32 = 1;
|
||||
|
||||
// v1 tables
|
||||
mod v1 {
|
||||
// stores
|
||||
pub const KEYS_STORE: &str = "keys";
|
||||
pub const CORE_STORE: &str = "core";
|
||||
|
||||
// keys
|
||||
pub const CONFIG: &str = "config";
|
||||
pub const GATEWAY_DETAILS: &str = "gateway_details";
|
||||
|
||||
pub const ED25519_IDENTITY_KEYPAIR: &str = "ed25519_identity_keypair";
|
||||
pub const X25519_ENCRYPTION_KEYPAIR: &str = "x25519_encryption_keypair";
|
||||
|
||||
// TODO: for those we could actually use the subtle crypto storage
|
||||
pub const AES128CTR_ACK_KEY: &str = "aes128ctr_ack_key";
|
||||
pub const AES128CTR_BLAKE3_HMAC_GATEWAY_KEYS: &str = "aes128ctr_blake3_hmac_gateway_keys";
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct ClientStorage {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) name: String,
|
||||
pub(crate) inner: Arc<WasmStorage>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl ClientStorage {
|
||||
fn db_name(client_id: &str) -> String {
|
||||
format!("{STORAGE_NAME_PREFIX}-{client_id}")
|
||||
}
|
||||
|
||||
pub(crate) async fn new_async(
|
||||
client_id: &str,
|
||||
passphrase: Option<String>,
|
||||
) -> Result<Self, ClientStorageError> {
|
||||
let name = Self::db_name(client_id);
|
||||
|
||||
// make sure the password is zeroized when no longer used, especially if we error out.
|
||||
// special care must be taken on JS side to ensure it's correctly used there.
|
||||
let passphrase = Zeroizing::new(passphrase);
|
||||
|
||||
let migrate_fn = Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
|
||||
// Even if the web-sys bindings expose the version as a f64, the IndexedDB API
|
||||
// works with an unsigned integer.
|
||||
// See <https://github.com/rustwasm/wasm-bindgen/issues/1149>
|
||||
let old_version = evt.old_version() as u32;
|
||||
|
||||
if old_version < 1 {
|
||||
// migrating to version 1
|
||||
let db = evt.db();
|
||||
|
||||
db.create_object_store(v1::KEYS_STORE)?;
|
||||
db.create_object_store(v1::CORE_STORE)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let inner = WasmStorage::new(
|
||||
&name,
|
||||
STORAGE_VERSION,
|
||||
migrate_fn,
|
||||
passphrase.as_ref().map(|p| p.as_bytes()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(ClientStorage {
|
||||
inner: Arc::new(inner),
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen(constructor)]
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(client_id: String, passphrase: String) -> Promise {
|
||||
future_to_promise(async move {
|
||||
Self::new_async(&client_id, Some(passphrase))
|
||||
.await
|
||||
.into_promise_result()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_unencrypted(client_id: String) -> Promise {
|
||||
future_to_promise(async move {
|
||||
Self::new_async(&client_id, None)
|
||||
.await
|
||||
.into_promise_result()
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: persist client's config
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn read_config(&self) -> Result<Option<Config>, ClientStorageError> {
|
||||
self.inner
|
||||
.read_value(v1::CORE_STORE, JsValue::from_str(v1::CONFIG))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) async fn may_read_gateway_details(
|
||||
&self,
|
||||
) -> Result<Option<PersistedGatewayDetails>, ClientStorageError> {
|
||||
self.inner
|
||||
.read_value(v1::CORE_STORE, JsValue::from_str(v1::GATEWAY_DETAILS))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) async fn must_read_gateway_details(
|
||||
&self,
|
||||
) -> Result<PersistedGatewayDetails, ClientStorageError> {
|
||||
self.may_read_gateway_details()
|
||||
.await?
|
||||
.ok_or(ClientStorageError::GatewayDetailsNotInStorage)
|
||||
}
|
||||
|
||||
async fn may_read_identity_keypair(
|
||||
&self,
|
||||
) -> Result<Option<identity::KeyPair>, ClientStorageError> {
|
||||
self.inner
|
||||
.read_value(
|
||||
v1::KEYS_STORE,
|
||||
JsValue::from_str(v1::ED25519_IDENTITY_KEYPAIR),
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn may_read_encryption_keypair(
|
||||
&self,
|
||||
) -> Result<Option<encryption::KeyPair>, ClientStorageError> {
|
||||
self.inner
|
||||
.read_value(
|
||||
v1::KEYS_STORE,
|
||||
JsValue::from_str(v1::X25519_ENCRYPTION_KEYPAIR),
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn may_read_ack_key(&self) -> Result<Option<AckKey>, ClientStorageError> {
|
||||
self.inner
|
||||
.read_value(v1::KEYS_STORE, JsValue::from_str(v1::AES128CTR_ACK_KEY))
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn may_read_gateway_shared_key(&self) -> Result<Option<SharedKeys>, ClientStorageError> {
|
||||
self.inner
|
||||
.read_value(
|
||||
v1::KEYS_STORE,
|
||||
JsValue::from_str(v1::AES128CTR_BLAKE3_HMAC_GATEWAY_KEYS),
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn must_read_identity_keypair(&self) -> Result<identity::KeyPair, ClientStorageError> {
|
||||
self.may_read_identity_keypair()
|
||||
.await?
|
||||
.ok_or(ClientStorageError::CryptoKeyNotInStorage {
|
||||
typ: v1::ED25519_IDENTITY_KEYPAIR.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn must_read_encryption_keypair(
|
||||
&self,
|
||||
) -> Result<encryption::KeyPair, ClientStorageError> {
|
||||
self.may_read_encryption_keypair()
|
||||
.await?
|
||||
.ok_or(ClientStorageError::CryptoKeyNotInStorage {
|
||||
typ: v1::X25519_ENCRYPTION_KEYPAIR.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn must_read_ack_key(&self) -> Result<AckKey, ClientStorageError> {
|
||||
self.may_read_ack_key()
|
||||
.await?
|
||||
.ok_or(ClientStorageError::CryptoKeyNotInStorage {
|
||||
typ: v1::AES128CTR_ACK_KEY.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn must_read_gateway_shared_key(&self) -> Result<SharedKeys, ClientStorageError> {
|
||||
self.may_read_gateway_shared_key()
|
||||
.await?
|
||||
.ok_or(ClientStorageError::CryptoKeyNotInStorage {
|
||||
typ: v1::AES128CTR_BLAKE3_HMAC_GATEWAY_KEYS.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn store_identity_keypair(
|
||||
&self,
|
||||
keypair: &identity::KeyPair,
|
||||
) -> Result<(), ClientStorageError> {
|
||||
self.inner
|
||||
.store_value(
|
||||
v1::KEYS_STORE,
|
||||
JsValue::from_str(v1::ED25519_IDENTITY_KEYPAIR),
|
||||
keypair,
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn store_encryption_keypair(
|
||||
&self,
|
||||
keypair: &encryption::KeyPair,
|
||||
) -> Result<(), ClientStorageError> {
|
||||
self.inner
|
||||
.store_value(
|
||||
v1::KEYS_STORE,
|
||||
JsValue::from_str(v1::X25519_ENCRYPTION_KEYPAIR),
|
||||
keypair,
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn store_ack_key(&self, key: &AckKey) -> Result<(), ClientStorageError> {
|
||||
self.inner
|
||||
.store_value(
|
||||
v1::KEYS_STORE,
|
||||
JsValue::from_str(v1::AES128CTR_ACK_KEY),
|
||||
key,
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn store_gateway_shared_key(&self, key: &SharedKeys) -> Result<(), ClientStorageError> {
|
||||
self.inner
|
||||
.store_value(
|
||||
v1::KEYS_STORE,
|
||||
JsValue::from_str(v1::AES128CTR_BLAKE3_HMAC_GATEWAY_KEYS),
|
||||
key,
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) async fn store_gateway_details(
|
||||
&self,
|
||||
gateway_endpoint: &PersistedGatewayDetails,
|
||||
) -> Result<(), ClientStorageError> {
|
||||
self.inner
|
||||
.store_value(
|
||||
v1::CORE_STORE,
|
||||
JsValue::from_str(v1::GATEWAY_DETAILS),
|
||||
gateway_endpoint,
|
||||
)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
// TODO: persist client's config
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn store_config(&self, config: &Config) -> Result<(), ClientStorageError> {
|
||||
self.inner
|
||||
.store_value(v1::CORE_STORE, JsValue::from_str(v1::CONFIG), config)
|
||||
.await
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub(crate) async fn has_full_gateway_info(&self) -> Result<bool, ClientStorageError> {
|
||||
let has_keys = self.may_read_gateway_shared_key().await?.is_some();
|
||||
let has_details = self.may_read_gateway_details().await?.is_some();
|
||||
|
||||
Ok(has_keys && has_details)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::storage::errors::ClientStorageError;
|
||||
use crate::storage::ClientStorage;
|
||||
use async_trait::async_trait;
|
||||
use nym_client_core::client::base_client::storage::gateway_details::{
|
||||
GatewayDetailsStore, PersistedGatewayDetails,
|
||||
};
|
||||
use nym_client_core::client::base_client::storage::MixnetClientStorage;
|
||||
use nym_client_core::client::key_manager::persistence::KeyStore;
|
||||
use nym_client_core::client::key_manager::KeyManager;
|
||||
use nym_client_core::client::replies::reply_storage::browser_backend;
|
||||
use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage;
|
||||
use wasm_utils::console_log;
|
||||
|
||||
// temporary until other variants are properly implemented (probably it should get changed into `ClientStorage`
|
||||
// implementing all traits and everything getting combined
|
||||
pub struct FullWasmClientStorage {
|
||||
pub(crate) keys_and_gateway_store: ClientStorage,
|
||||
pub(crate) reply_storage: browser_backend::Backend,
|
||||
pub(crate) credential_storage: EphemeralCredentialStorage,
|
||||
}
|
||||
|
||||
impl MixnetClientStorage for FullWasmClientStorage {
|
||||
type KeyStore = ClientStorage;
|
||||
type ReplyStore = browser_backend::Backend;
|
||||
type CredentialStore = EphemeralCredentialStorage;
|
||||
|
||||
type GatewayDetailsStore = ClientStorage;
|
||||
|
||||
fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) {
|
||||
(self.reply_storage, self.credential_storage)
|
||||
}
|
||||
|
||||
fn key_store(&self) -> &Self::KeyStore {
|
||||
&self.keys_and_gateway_store
|
||||
}
|
||||
|
||||
fn reply_store(&self) -> &Self::ReplyStore {
|
||||
&self.reply_storage
|
||||
}
|
||||
|
||||
fn credential_store(&self) -> &Self::CredentialStore {
|
||||
&self.credential_storage
|
||||
}
|
||||
|
||||
fn gateway_details_store(&self) -> &Self::GatewayDetailsStore {
|
||||
&self.keys_and_gateway_store
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl KeyStore for ClientStorage {
|
||||
type StorageError = ClientStorageError;
|
||||
|
||||
async fn load_keys(&self) -> Result<KeyManager, Self::StorageError> {
|
||||
console_log!("attempting to load cryptographic keys...");
|
||||
|
||||
// all keys implement `ZeroizeOnDrop`, so if we return an Error, whatever was already loaded will be cleared
|
||||
let identity_keypair = self.must_read_identity_keypair().await?;
|
||||
let encryption_keypair = self.must_read_encryption_keypair().await?;
|
||||
let ack_keypair = self.must_read_ack_key().await?;
|
||||
let gateway_shared_key = self.must_read_gateway_shared_key().await?;
|
||||
|
||||
Ok(KeyManager::from_keys(
|
||||
identity_keypair,
|
||||
encryption_keypair,
|
||||
gateway_shared_key,
|
||||
ack_keypair,
|
||||
))
|
||||
}
|
||||
|
||||
async fn store_keys(&self, keys: &KeyManager) -> Result<(), Self::StorageError> {
|
||||
console_log!("attempting to store cryptographic keys...");
|
||||
|
||||
self.store_identity_keypair(&keys.identity_keypair())
|
||||
.await?;
|
||||
self.store_encryption_keypair(&keys.encryption_keypair())
|
||||
.await?;
|
||||
self.store_ack_key(&keys.ack_key()).await?;
|
||||
self.store_gateway_shared_key(&keys.gateway_shared_key())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl GatewayDetailsStore for ClientStorage {
|
||||
type StorageError = ClientStorageError;
|
||||
|
||||
async fn load_gateway_details(&self) -> Result<PersistedGatewayDetails, Self::StorageError> {
|
||||
self.must_read_gateway_details().await
|
||||
}
|
||||
|
||||
async fn store_gateway_details(
|
||||
&self,
|
||||
details: &PersistedGatewayDetails,
|
||||
) -> Result<(), Self::StorageError> {
|
||||
self.store_gateway_details(details).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::tester::helpers::{NodeTestResult, WasmTestMessageExt};
|
||||
use futures::StreamExt;
|
||||
use nym_node_tester_utils::processor::Received;
|
||||
use nym_node_tester_utils::receiver::ReceivedReceiver;
|
||||
use nym_sphinx::chunking::fragment::FragmentIdentifier;
|
||||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::MutexGuard as AsyncMutexGuard;
|
||||
use wasm_utils::{console_error, console_log, console_warn};
|
||||
|
||||
pub(crate) struct EphemeralTestReceiver<'a> {
|
||||
sent_packets: u32,
|
||||
expected_acks: HashSet<FragmentIdentifier>,
|
||||
|
||||
received_valid_messages: HashSet<u32>,
|
||||
received_valid_acks: HashSet<FragmentIdentifier>,
|
||||
duplicate_packets: u32,
|
||||
duplicate_acks: u32,
|
||||
|
||||
timeout_duration: Duration,
|
||||
receiver_permit: AsyncMutexGuard<'a, ReceivedReceiver<WasmTestMessageExt>>,
|
||||
}
|
||||
|
||||
impl<'a> EphemeralTestReceiver<'a> {
|
||||
pub(crate) fn finish(self) -> NodeTestResult {
|
||||
NodeTestResult {
|
||||
sent_packets: self.sent_packets,
|
||||
received_packets: self.received_valid_messages.len() as u32,
|
||||
received_acks: self.received_valid_acks.len() as u32,
|
||||
duplicate_packets: self.duplicate_packets,
|
||||
duplicate_acks: self.duplicate_acks,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new(
|
||||
sent_packets: u32,
|
||||
expected_acks: HashSet<FragmentIdentifier>,
|
||||
receiver_permit: AsyncMutexGuard<'a, ReceivedReceiver<WasmTestMessageExt>>,
|
||||
timeout: Duration,
|
||||
) -> Self {
|
||||
EphemeralTestReceiver {
|
||||
sent_packets,
|
||||
expected_acks,
|
||||
received_valid_messages: Default::default(),
|
||||
received_valid_acks: Default::default(),
|
||||
duplicate_packets: 0,
|
||||
duplicate_acks: 0,
|
||||
timeout_duration: timeout,
|
||||
receiver_permit,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_next_received_packet(&mut self, packet: Option<Received<WasmTestMessageExt>>) -> bool {
|
||||
let Some(received_packet) = packet else {
|
||||
// can't do anything more...
|
||||
console_error!("packet receiver has stopped processing results!");
|
||||
return true
|
||||
};
|
||||
match received_packet {
|
||||
Received::Message(msg) => {
|
||||
if !self.received_valid_messages.insert(msg.msg_id) {
|
||||
self.duplicate_packets += 1;
|
||||
}
|
||||
}
|
||||
Received::Ack(frag_id) => {
|
||||
if self.expected_acks.contains(&frag_id) {
|
||||
if !self.received_valid_acks.insert(frag_id) {
|
||||
self.duplicate_acks += 1
|
||||
}
|
||||
} else {
|
||||
console_warn!("received an ack that was not part of the test! (id: {frag_id})")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.received_all() {
|
||||
console_log!("already received all the packets! finishing the test...");
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn received_all(&self) -> bool {
|
||||
self.received_valid_acks.len() == self.received_valid_messages.len()
|
||||
&& self.received_valid_acks.len() == self.sent_packets as usize
|
||||
}
|
||||
|
||||
pub(crate) async fn perform_test(mut self) -> NodeTestResult {
|
||||
let mut timeout_fut = wasm_timer::Delay::new(self.timeout_duration);
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = &mut timeout_fut => {
|
||||
console_warn!("reached test timeout before receiving all packets.");
|
||||
break
|
||||
}
|
||||
received_packet = self.receiver_permit.next() => {
|
||||
let is_done = self.on_next_received_packet(received_packet);
|
||||
if is_done {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// due to expansion of #[wasm_bindgen] macro on NodeTestResult
|
||||
#![allow(clippy::drop_non_drop)]
|
||||
|
||||
use crate::error::WasmClientError;
|
||||
use crate::tester::LockedGatewayClient;
|
||||
use js_sys::Promise;
|
||||
use nym_node_tester_utils::processor::Received;
|
||||
use nym_node_tester_utils::receiver::ReceivedReceiver;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{Mutex as AsyncMutex, MutexGuard as AsyncMutexGuard};
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use wasm_utils::{console_log, console_warn};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct ReceivedReceiverWrapper(Arc<AsyncMutex<ReceivedReceiver<WasmTestMessageExt>>>);
|
||||
|
||||
impl ReceivedReceiverWrapper {
|
||||
pub(super) fn new(inner: ReceivedReceiver<WasmTestMessageExt>) -> Self {
|
||||
ReceivedReceiverWrapper(Arc::new(AsyncMutex::new(inner)))
|
||||
}
|
||||
|
||||
pub(super) async fn clear_received_channel(&self) {
|
||||
let mut lost_msgs = 0;
|
||||
let mut lost_acks = 0;
|
||||
let mut permit = self.0.lock().await;
|
||||
while let Ok(Some(received)) = permit.try_next() {
|
||||
match received {
|
||||
Received::Message(_) => lost_msgs += 1,
|
||||
Received::Ack(_) => lost_acks += 1,
|
||||
}
|
||||
}
|
||||
if lost_msgs > 0 || lost_acks > 0 {
|
||||
console_warn!("while preparing for the test run, we cleared {lost_msgs} messages and {lost_acks} acks that were received in the meantime.")
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn lock(&self) -> AsyncMutexGuard<'_, ReceivedReceiver<WasmTestMessageExt>> {
|
||||
self.0.lock().await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Copy, Clone)]
|
||||
pub struct WasmTestMessageExt {
|
||||
pub test_id: u32,
|
||||
}
|
||||
|
||||
impl WasmTestMessageExt {
|
||||
pub fn new(test_id: u32) -> Self {
|
||||
WasmTestMessageExt { test_id }
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: maybe put it in the tester utils
|
||||
#[wasm_bindgen]
|
||||
pub struct NodeTestResult {
|
||||
pub sent_packets: u32,
|
||||
pub received_packets: u32,
|
||||
pub received_acks: u32,
|
||||
|
||||
pub duplicate_packets: u32,
|
||||
pub duplicate_acks: u32,
|
||||
}
|
||||
|
||||
impl Display for NodeTestResult {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "Test results: ")?;
|
||||
writeln!(f, "Total score: {:.2}%", self.score())?;
|
||||
writeln!(f, "Sent packets: {}", self.sent_packets)?;
|
||||
writeln!(f, "Received (valid) packets: {}", self.received_packets)?;
|
||||
writeln!(f, "Received (valid) acks: {}", self.received_acks)?;
|
||||
writeln!(f, "Received duplicate packets: {}", self.duplicate_packets)?;
|
||||
write!(f, "Received duplicate acks: {}", self.duplicate_acks)
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl NodeTestResult {
|
||||
pub fn log_details(&self) {
|
||||
console_log!("{}", self)
|
||||
}
|
||||
|
||||
pub fn score(&self) -> f32 {
|
||||
let expected = self.sent_packets * 2;
|
||||
let actual = (self.received_packets + self.received_acks)
|
||||
.saturating_sub(self.duplicate_packets + self.duplicate_acks);
|
||||
|
||||
actual as f32 / expected as f32 * 100.
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TestMarker {
|
||||
value: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl TestMarker {
|
||||
pub fn new(value: Arc<AtomicBool>) -> Self {
|
||||
Self { value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestMarker {
|
||||
// make sure to clear the test flag when the marker is dropped
|
||||
fn drop(&mut self) {
|
||||
self.value.store(false, Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait GatewayReconnection {
|
||||
fn disconnect_from_gateway(&self) -> Promise;
|
||||
|
||||
fn reconnect_to_gateway(&self) -> Promise;
|
||||
}
|
||||
|
||||
impl GatewayReconnection for LockedGatewayClient {
|
||||
fn disconnect_from_gateway(&self) -> Promise {
|
||||
let this = self.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
let mut guard = this.lock().await;
|
||||
guard
|
||||
.disconnect()
|
||||
.await
|
||||
.map_err(|err| JsValue::from(WasmClientError::from(err)))?;
|
||||
Ok(JsValue::undefined())
|
||||
})
|
||||
}
|
||||
|
||||
fn reconnect_to_gateway(&self) -> Promise {
|
||||
let this = self.clone();
|
||||
|
||||
future_to_promise(async move {
|
||||
let mut guard = this.lock().await;
|
||||
guard
|
||||
.try_reconnect()
|
||||
.await
|
||||
.map_err(|err| JsValue::from(WasmClientError::from(err)))?;
|
||||
Ok(JsValue::undefined())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::constants::NODE_TESTER_ID;
|
||||
use crate::error::WasmClientError;
|
||||
use crate::helpers::{current_network_topology_async, setup_from_topology};
|
||||
use crate::storage::ClientStorage;
|
||||
use crate::tester::ephemeral_receiver::EphemeralTestReceiver;
|
||||
use crate::tester::helpers::{
|
||||
GatewayReconnection, NodeTestResult, ReceivedReceiverWrapper, TestMarker, WasmTestMessageExt,
|
||||
};
|
||||
use crate::topology::WasmNymTopology;
|
||||
use futures::channel::mpsc;
|
||||
use js_sys::Promise;
|
||||
use nym_bandwidth_controller::wasm_mockups::{Client as FakeClient, DirectSigningNyxdClient};
|
||||
use nym_bandwidth_controller::BandwidthController;
|
||||
use nym_client_core::client::key_manager::ManagedKeys;
|
||||
use nym_client_core::init::InitialisationDetails;
|
||||
use nym_credential_storage::ephemeral_storage::EphemeralStorage;
|
||||
use nym_gateway_client::GatewayClient;
|
||||
use nym_node_tester_utils::receiver::SimpleMessageReceiver;
|
||||
use nym_node_tester_utils::{NodeTester, TestMessage};
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::addressing::nodes::NodeIdentity;
|
||||
use nym_sphinx::params::PacketSize;
|
||||
use nym_sphinx::preparer::PreparedFragment;
|
||||
use nym_task::TaskManager;
|
||||
use nym_topology::NymTopology;
|
||||
use nym_validator_client::client::IdentityKey;
|
||||
use rand::rngs::OsRng;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
|
||||
use std::sync::{Arc, Mutex as SyncMutex};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex as AsyncMutex;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::future_to_promise;
|
||||
use wasm_utils::{check_promise_result, console_log, PromisableResult};
|
||||
|
||||
mod ephemeral_receiver;
|
||||
pub(crate) mod helpers;
|
||||
|
||||
pub type NodeTestMessage = TestMessage<WasmTestMessageExt>;
|
||||
type LockedGatewayClient =
|
||||
Arc<AsyncMutex<GatewayClient<FakeClient<DirectSigningNyxdClient>, EphemeralStorage>>>;
|
||||
|
||||
pub(crate) const DEFAULT_TEST_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
pub(crate) const DEFAULT_TEST_PACKETS: u32 = 20;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct NymNodeTester {
|
||||
test_in_progress: Arc<AtomicBool>,
|
||||
|
||||
// we need to increment the nonce between tests to distinguish the packets
|
||||
// but we can't make the tester mutable because of wasm...
|
||||
// so we're using the atomics
|
||||
current_test_nonce: AtomicU32,
|
||||
|
||||
// blame all those mutexes on being unable to have an async method with internal mutability...
|
||||
tester: Arc<SyncMutex<NodeTester<OsRng>>>,
|
||||
gateway_client: LockedGatewayClient,
|
||||
|
||||
// we have to put it behind the lock due to wasm limitations and borrowing...
|
||||
// the mutex acquisition should be instant as there aren't going to be any threads attempting
|
||||
// to get simultaneous access
|
||||
processed_receiver: ReceivedReceiverWrapper,
|
||||
|
||||
// even though we don't use graceful shutdowns, other components rely on existence of this struct
|
||||
// and if it's dropped, everything will start going offline
|
||||
_task_manager: TaskManager,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct NymNodeTesterBuilder {
|
||||
gateway: Option<IdentityKey>,
|
||||
id: Option<String>,
|
||||
|
||||
base_topology: NymTopology,
|
||||
|
||||
// unimplemented
|
||||
bandwidth_controller:
|
||||
Option<BandwidthController<FakeClient<DirectSigningNyxdClient>, EphemeralStorage>>,
|
||||
}
|
||||
|
||||
fn address(keys: &ManagedKeys, gateway_identity: NodeIdentity) -> Recipient {
|
||||
Recipient::new(
|
||||
*keys.identity_public_key(),
|
||||
*keys.encryption_public_key(),
|
||||
gateway_identity,
|
||||
)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl NymNodeTesterBuilder {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
base_topology: WasmNymTopology,
|
||||
id: Option<String>,
|
||||
gateway: Option<IdentityKey>,
|
||||
) -> NymNodeTesterBuilder {
|
||||
NymNodeTesterBuilder {
|
||||
id,
|
||||
gateway,
|
||||
base_topology: base_topology.into(),
|
||||
bandwidth_controller: None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn _new_with_api(
|
||||
api_url: String,
|
||||
id: Option<String>,
|
||||
gateway: Option<IdentityKey>,
|
||||
) -> Result<Self, WasmClientError> {
|
||||
let topology = current_network_topology_async(api_url).await?;
|
||||
Ok(NymNodeTesterBuilder::new(topology, id, gateway))
|
||||
}
|
||||
|
||||
pub fn new_with_api(
|
||||
api_url: String,
|
||||
id: Option<String>,
|
||||
gateway: Option<IdentityKey>,
|
||||
) -> Promise {
|
||||
future_to_promise(async move {
|
||||
Self::_new_with_api(api_url, id, gateway)
|
||||
.await
|
||||
.into_promise_result()
|
||||
})
|
||||
}
|
||||
|
||||
async fn gateway_info(
|
||||
&self,
|
||||
client_store: &ClientStorage,
|
||||
) -> Result<InitialisationDetails, WasmClientError> {
|
||||
if let Ok(loaded) = InitialisationDetails::try_load(client_store, client_store).await {
|
||||
Ok(loaded)
|
||||
} else {
|
||||
setup_from_topology(self.gateway.clone(), &self.base_topology, client_store).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn _setup_client(mut self) -> Result<NymNodeTester, WasmClientError> {
|
||||
let task_manager = TaskManager::default();
|
||||
|
||||
let storage_id = if let Some(client_id) = &self.id {
|
||||
format!("{NODE_TESTER_ID}-{client_id}")
|
||||
} else {
|
||||
NODE_TESTER_ID.to_owned()
|
||||
};
|
||||
|
||||
let client_store = ClientStorage::new_async(&storage_id, None).await?;
|
||||
|
||||
let init_details = self.gateway_info(&client_store).await?;
|
||||
let gateway_endpoint = init_details.gateway_details;
|
||||
let gateway_identity = gateway_endpoint.try_get_gateway_identity_key()?;
|
||||
let managed_keys = init_details.managed_keys;
|
||||
|
||||
let (mixnet_message_sender, mixnet_message_receiver) = mpsc::unbounded();
|
||||
let (ack_sender, ack_receiver) = mpsc::unbounded();
|
||||
|
||||
let mut gateway_client = GatewayClient::new(
|
||||
gateway_endpoint.gateway_listener,
|
||||
managed_keys.identity_keypair(),
|
||||
gateway_identity,
|
||||
Some(managed_keys.must_get_gateway_shared_key()),
|
||||
mixnet_message_sender,
|
||||
ack_sender,
|
||||
Duration::from_secs(10),
|
||||
self.bandwidth_controller.take(),
|
||||
task_manager.subscribe(),
|
||||
);
|
||||
|
||||
gateway_client.set_disabled_credentials_mode(true);
|
||||
gateway_client.authenticate_and_start().await?;
|
||||
|
||||
// TODO: make those values configurable later
|
||||
let tester = NodeTester::new(
|
||||
OsRng,
|
||||
self.base_topology,
|
||||
Some(address(&managed_keys, gateway_identity)),
|
||||
PacketSize::default(),
|
||||
Duration::from_millis(5),
|
||||
Duration::from_millis(5),
|
||||
managed_keys.ack_key(),
|
||||
);
|
||||
|
||||
let (processed_sender, processed_receiver) = mpsc::unbounded();
|
||||
|
||||
let mut receiver = SimpleMessageReceiver::new_sphinx_receiver(
|
||||
managed_keys.encryption_keypair(),
|
||||
managed_keys.ack_key(),
|
||||
mixnet_message_receiver,
|
||||
ack_receiver,
|
||||
processed_sender,
|
||||
task_manager.subscribe(),
|
||||
);
|
||||
|
||||
nym_task::spawn(async move { receiver.run().await });
|
||||
|
||||
Ok(NymNodeTester {
|
||||
test_in_progress: Arc::new(AtomicBool::new(false)),
|
||||
current_test_nonce: Default::default(),
|
||||
tester: Arc::new(SyncMutex::new(tester)),
|
||||
gateway_client: Arc::new(AsyncMutex::new(gateway_client)),
|
||||
processed_receiver: ReceivedReceiverWrapper::new(processed_receiver),
|
||||
_task_manager: task_manager,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn setup_client(self) -> Promise {
|
||||
future_to_promise(async move { self._setup_client().await.into_promise_result() })
|
||||
}
|
||||
}
|
||||
|
||||
async fn test_mixnode(
|
||||
test_packets: Vec<PreparedFragment>,
|
||||
gateway_client: LockedGatewayClient,
|
||||
processed_receiver: ReceivedReceiverWrapper,
|
||||
_test_marker: TestMarker,
|
||||
timeout: Duration,
|
||||
) -> Result<NodeTestResult, WasmClientError> {
|
||||
let num_test_packets = test_packets.len() as u32;
|
||||
|
||||
let expected_ack_ids = test_packets
|
||||
.iter()
|
||||
.map(|p| p.fragment_identifier)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let mix_packets = test_packets.into_iter().map(|p| p.mix_packet).collect();
|
||||
|
||||
// start by clearing any messages that might have been received between tests
|
||||
processed_receiver.clear_received_channel().await;
|
||||
|
||||
// locking the gateway client so that we could get mutable access to data without having to declare
|
||||
// self mutable
|
||||
let mut gateway_permit = gateway_client.lock().await;
|
||||
gateway_permit.batch_send_mix_packets(mix_packets).await?;
|
||||
|
||||
let receiver_permit = processed_receiver.lock().await;
|
||||
let result =
|
||||
EphemeralTestReceiver::new(num_test_packets, expected_ack_ids, receiver_permit, timeout)
|
||||
.perform_test()
|
||||
.await;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl NymNodeTester {
|
||||
#[wasm_bindgen(constructor)]
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(
|
||||
topology: WasmNymTopology,
|
||||
id: Option<String>,
|
||||
gateway: Option<IdentityKey>,
|
||||
) -> Promise {
|
||||
console_log!("constructing node tester!");
|
||||
NymNodeTesterBuilder::new(topology, id, gateway).setup_client()
|
||||
}
|
||||
|
||||
async fn _new_with_api(
|
||||
api_url: String,
|
||||
id: Option<String>,
|
||||
gateway: Option<IdentityKey>,
|
||||
) -> Result<Self, WasmClientError> {
|
||||
NymNodeTesterBuilder::_new_with_api(api_url, id, gateway)
|
||||
.await?
|
||||
._setup_client()
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn new_with_api(
|
||||
api_url: String,
|
||||
id: Option<String>,
|
||||
gateway: Option<IdentityKey>,
|
||||
) -> Promise {
|
||||
future_to_promise(async move {
|
||||
Self::_new_with_api(api_url, id, gateway)
|
||||
.await
|
||||
.into_promise_result()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn disconnect_from_gateway(&self) -> Promise {
|
||||
self.gateway_client.disconnect_from_gateway()
|
||||
}
|
||||
|
||||
pub fn reconnect_to_gateway(&self) -> Promise {
|
||||
self.gateway_client.reconnect_to_gateway()
|
||||
}
|
||||
|
||||
fn prepare_test_packets(
|
||||
&self,
|
||||
mixnode_identity: String,
|
||||
test_nonce: u32,
|
||||
num_test_packets: u32,
|
||||
) -> Result<Vec<PreparedFragment>, WasmClientError> {
|
||||
let test_ext = WasmTestMessageExt::new(test_nonce);
|
||||
let mut tester_permit = self.tester.lock().expect("mutex got poisoned");
|
||||
tester_permit
|
||||
.existing_identity_mixnode_test_packets(
|
||||
mixnode_identity,
|
||||
test_ext,
|
||||
num_test_packets,
|
||||
None,
|
||||
)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn test_node(
|
||||
&self,
|
||||
mixnode_identity: String,
|
||||
timeout_millis: Option<u64>,
|
||||
num_test_packets: Option<u32>,
|
||||
) -> Promise {
|
||||
// establish test parameters
|
||||
let timeout = timeout_millis
|
||||
.map(Duration::from_millis)
|
||||
.unwrap_or(DEFAULT_TEST_TIMEOUT);
|
||||
let num_test_packets = num_test_packets.unwrap_or(DEFAULT_TEST_PACKETS);
|
||||
|
||||
// mark start of the test
|
||||
if self.test_in_progress.swap(true, Ordering::SeqCst) {
|
||||
return WasmClientError::TestInProgress.into_rejected_promise();
|
||||
}
|
||||
|
||||
// prepare test packets
|
||||
// (I simultaneously feel both disgusted and amazed by this workaround)
|
||||
let test_nonce = self.current_test_nonce.fetch_add(1, Ordering::Relaxed);
|
||||
let test_packets = check_promise_result!(self.prepare_test_packets(
|
||||
mixnode_identity,
|
||||
test_nonce,
|
||||
num_test_packets
|
||||
));
|
||||
|
||||
let processed_receiver_clone = self.processed_receiver.clone();
|
||||
let gateway_client_clone = Arc::clone(&self.gateway_client);
|
||||
let tester_marker = TestMarker::new(Arc::clone(&self.test_in_progress));
|
||||
|
||||
// start doing async things (send packets and watch for anything coming back)
|
||||
future_to_promise(async move {
|
||||
test_mixnode(
|
||||
test_packets,
|
||||
gateway_client_clone,
|
||||
processed_receiver_clone,
|
||||
tester_marker,
|
||||
timeout,
|
||||
)
|
||||
.await
|
||||
.into_promise_result()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_client_core::config::GatewayEndpointConfig;
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
use nym_topology::gateway::GatewayConversionError;
|
||||
use nym_topology::mix::{Layer, MixnodeConversionError};
|
||||
use nym_topology::{gateway, mix, MixLayer, NymTopology};
|
||||
use nym_validator_client::client::{IdentityKeyRef, MixId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use thiserror::Error;
|
||||
use wasm_bindgen::prelude::wasm_bindgen;
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_utils::{console_log, simple_js_error};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum WasmTopologyError {
|
||||
#[error("got invalid mix layer {value}. Expected 1, 2 or 3.")]
|
||||
InvalidMixLayer { value: u8 },
|
||||
|
||||
#[error(transparent)]
|
||||
GatewayConversion(#[from] GatewayConversionError),
|
||||
|
||||
#[error(transparent)]
|
||||
MixnodeConversion(#[from] MixnodeConversionError),
|
||||
|
||||
#[error("The provided mixnode map was malformed: {source}")]
|
||||
MalformedMixnodeMap { source: serde_wasm_bindgen::Error },
|
||||
|
||||
#[error("The provided gateway list was malformed: {source}")]
|
||||
MalformedGatewayList { source: serde_wasm_bindgen::Error },
|
||||
}
|
||||
|
||||
impl From<WasmTopologyError> for JsValue {
|
||||
fn from(value: WasmTopologyError) -> Self {
|
||||
simple_js_error(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Debug)]
|
||||
pub struct WasmNymTopology {
|
||||
inner: NymTopology,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmNymTopology {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
// expected: BTreeMap<MixLayer, Vec<WasmMixNode>>,
|
||||
// HashMap<MixLayer, Vec<WasmMixNode>> will also work because it has the same json representation
|
||||
mixnodes: JsValue,
|
||||
// expected: Vec<WasmGateway>
|
||||
gateways: JsValue,
|
||||
) -> Result<WasmNymTopology, WasmTopologyError> {
|
||||
let mixnodes: BTreeMap<MixLayer, Vec<WasmMixNode>> =
|
||||
serde_wasm_bindgen::from_value(mixnodes)
|
||||
.map_err(|source| WasmTopologyError::MalformedMixnodeMap { source })?;
|
||||
|
||||
let gateways: Vec<WasmGateway> = serde_wasm_bindgen::from_value(gateways)
|
||||
.map_err(|source| WasmTopologyError::MalformedGatewayList { source })?;
|
||||
|
||||
let mut converted_mixes = BTreeMap::new();
|
||||
|
||||
for (layer, nodes) in mixnodes {
|
||||
let layer_nodes = nodes
|
||||
.into_iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
converted_mixes.insert(layer, layer_nodes);
|
||||
}
|
||||
|
||||
let gateways = gateways
|
||||
.into_iter()
|
||||
.map(TryInto::try_into)
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
Ok(WasmNymTopology {
|
||||
inner: NymTopology::new(converted_mixes, gateways),
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn ensure_contains(&self, gateway_config: &GatewayEndpointConfig) -> bool {
|
||||
self.ensure_contains_gateway_id(&gateway_config.gateway_id)
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_contains_gateway_id(&self, gateway_id: IdentityKeyRef) -> bool {
|
||||
self.inner
|
||||
.gateways()
|
||||
.iter()
|
||||
.any(|g| g.identity_key.to_base58_string() == gateway_id)
|
||||
}
|
||||
|
||||
pub fn print(&self) {
|
||||
if !self.inner.mixes().is_empty() {
|
||||
console_log!("mixnodes:");
|
||||
for (layer, nodes) in self.inner.mixes() {
|
||||
console_log!("\tlayer {layer}:");
|
||||
for node in nodes {
|
||||
console_log!("\t\t{} - {}", node.mix_id, node.identity_key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console_log!("NO MIXNODES")
|
||||
}
|
||||
|
||||
if !self.inner.gateways().is_empty() {
|
||||
console_log!("gateways:");
|
||||
for gateway in self.inner.gateways() {
|
||||
console_log!("\t{}", gateway.identity_key)
|
||||
}
|
||||
} else {
|
||||
console_log!("NO GATEWAYS")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WasmNymTopology> for NymTopology {
|
||||
fn from(value: WasmNymTopology) -> Self {
|
||||
value.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NymTopology> for WasmNymTopology {
|
||||
fn from(value: NymTopology) -> Self {
|
||||
WasmNymTopology { inner: value }
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct WasmMixNode {
|
||||
pub mix_id: MixId,
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub owner: String,
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub host: String,
|
||||
pub mix_port: u16,
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub identity_key: String,
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub sphinx_key: String,
|
||||
pub layer: MixLayer,
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmMixNode {
|
||||
#[wasm_bindgen(constructor)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn new(
|
||||
mix_id: MixId,
|
||||
owner: String,
|
||||
host: String,
|
||||
mix_port: u16,
|
||||
identity_key: String,
|
||||
sphinx_key: String,
|
||||
layer: MixLayer,
|
||||
version: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
mix_id,
|
||||
owner,
|
||||
host,
|
||||
mix_port,
|
||||
identity_key,
|
||||
sphinx_key,
|
||||
layer,
|
||||
version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<WasmMixNode> for mix::Node {
|
||||
type Error = WasmTopologyError;
|
||||
|
||||
fn try_from(value: WasmMixNode) -> Result<Self, Self::Error> {
|
||||
let host = mix::Node::parse_host(&value.host)?;
|
||||
|
||||
// try to completely resolve the host in the mix situation to avoid doing it every
|
||||
// single time we want to construct a path
|
||||
let mix_host = mix::Node::extract_mix_host(&host, value.mix_port)?;
|
||||
|
||||
Ok(mix::Node {
|
||||
mix_id: value.mix_id,
|
||||
owner: value.owner,
|
||||
host,
|
||||
mix_host,
|
||||
identity_key: identity::PublicKey::from_base58_string(&value.identity_key)
|
||||
.map_err(MixnodeConversionError::from)?,
|
||||
sphinx_key: encryption::PublicKey::from_base58_string(&value.sphinx_key)
|
||||
.map_err(MixnodeConversionError::from)?,
|
||||
layer: Layer::try_from(value.layer)
|
||||
.map_err(|_| WasmTopologyError::InvalidMixLayer { value: value.layer })?,
|
||||
version: value.version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct WasmGateway {
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub owner: String,
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub host: String,
|
||||
pub mix_port: u16,
|
||||
pub clients_port: u16,
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub identity_key: String,
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub sphinx_key: String,
|
||||
#[wasm_bindgen(getter_with_clone)]
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl WasmGateway {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(
|
||||
owner: String,
|
||||
host: String,
|
||||
mix_port: u16,
|
||||
clients_port: u16,
|
||||
identity_key: String,
|
||||
sphinx_key: String,
|
||||
version: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
owner,
|
||||
host,
|
||||
mix_port,
|
||||
clients_port,
|
||||
identity_key,
|
||||
sphinx_key,
|
||||
version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<WasmGateway> for gateway::Node {
|
||||
type Error = WasmTopologyError;
|
||||
|
||||
fn try_from(value: WasmGateway) -> Result<Self, Self::Error> {
|
||||
let host = gateway::Node::parse_host(&value.host)?;
|
||||
|
||||
// try to completely resolve the host in the mix situation to avoid doing it every
|
||||
// single time we want to construct a path
|
||||
let mix_host = gateway::Node::extract_mix_host(&host, value.mix_port)?;
|
||||
|
||||
Ok(gateway::Node {
|
||||
owner: value.owner,
|
||||
host,
|
||||
mix_host,
|
||||
clients_port: value.clients_port,
|
||||
identity_key: identity::PublicKey::from_base58_string(&value.identity_key)
|
||||
.map_err(GatewayConversionError::from)?,
|
||||
sphinx_key: encryption::PublicKey::from_base58_string(&value.sphinx_key)
|
||||
.map_err(GatewayConversionError::from)?,
|
||||
version: value.version,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn validate_recipient(recipient: String) -> Result<(), JsError> {
|
||||
match Recipient::try_from_base58_string(recipient) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(JsError::new(format!("{}", e).as_str())),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::validate_recipient;
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_recipient_validation_ok() {
|
||||
let res = validate_recipient("DyQmXnst5NGGjzUZxRC5Bjs5bd7CBF3xMpsSmbRiizr2.GH6YTBP2NXU3AVqd8WjiTMVyeMjunXMEsp7gVCMEJqpD@336yuXAeGEgedRfqTJZsG2YV7P13QH1bHv1SjCZYarc9".to_string());
|
||||
assert!(res.is_ok())
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn test_recipient_validation_fails() {
|
||||
assert!(validate_recipient(" DyQmXnst5NGGjzUZxRC5Bjs5bd7CBF3xMpsSmbRiizr2.GH6YTBP2NXU3AVqd8WjiTMVyeMjunXMEsp7gVCMEJqpD@336yuXAeGEgedRfqTJZsG2YV7P13QH1bHv1SjCZYarc9".to_string()).is_err());
|
||||
assert!(validate_recipient(
|
||||
"DyQmXnst5NGGjzUZxRC5BjbRiizr2.GH6YTBP2NXU3AVqd8WD@336yuXAeGEgedRfqTJZQH1bHv1SjCZYarc9"
|
||||
.to_string()
|
||||
)
|
||||
.is_err());
|
||||
assert!(validate_recipient("🙀🙀🙀🙀".to_string()).is_err());
|
||||
assert!(validate_recipient("".to_string()).is_err());
|
||||
assert!(validate_recipient(" ".to_string()).is_err());
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@
|
||||
"dev": "run-p tauri:dev webpack:dev",
|
||||
"prebuild": "yarn --cwd .. build",
|
||||
"build": "run-s webpack:prod tauri:build",
|
||||
"build:wasm": "cd nym-client-wasm && wasm-pack build --scope nymproject",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"prestorybook": "yarn --cwd .. build",
|
||||
@@ -31,6 +32,7 @@
|
||||
"@nymproject/mui-theme": "^1.0.0",
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"@nymproject/types": "^1.0.0",
|
||||
"@nymproject/nym-client-wasm": "file:./nym-client-wasm/pkg",
|
||||
"@storybook/react": "^6.5.15",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
|
||||
@@ -41,7 +43,6 @@
|
||||
"lodash": "^4.17.21",
|
||||
"notistack": "^2.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nym-client-wasm": "1.1.1",
|
||||
"qrcode.react": "^1.0.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, Dialog, Divider } from '@mui/material';
|
||||
import { NymLogo } from '@nymproject/react/logo/NymLogo';
|
||||
import { ResultsCardDetail } from './ResultsCard';
|
||||
import { sleep } from 'src/utils/sleep';
|
||||
import { ResultsCardDetail } from './ResultsCard';
|
||||
|
||||
export const PrintResults = ({
|
||||
packetsSent,
|
||||
@@ -30,7 +30,7 @@ export const PrintResults = ({
|
||||
asyncPrint();
|
||||
window.addEventListener('afterprint', OnPrintRequestComplete);
|
||||
|
||||
() => window.removeEventListener('afterprint', OnPrintRequestComplete);
|
||||
return () => window.removeEventListener('afterprint', OnPrintRequestComplete);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { WasmGateway, WasmMixNode, WasmNymTopology } from 'nym-client-wasm';
|
||||
import { WasmGateway, WasmMixNode, WasmNymTopology } from '@nymproject/nym-client-wasm';
|
||||
|
||||
export const createDummyTopology = () => {
|
||||
const l1Mixnode = new WasmMixNode(
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
// limitations under the License.
|
||||
|
||||
/* eslint-disable no-restricted-globals */
|
||||
import { NymNodeTester, set_panic_hook, current_network_topology, NodeTestResult } from 'nym-client-wasm';
|
||||
import { NymNodeTester, set_panic_hook, current_network_topology, NodeTestResult } from '@nymproject/nym-client-wasm';
|
||||
import { Network } from 'src/types';
|
||||
import { NodeTestEvent } from './types';
|
||||
import { MAINNET_VALIDATOR_URL, QA_VALIDATOR_URL } from 'src/constants';
|
||||
import { NodeTestEvent } from './types';
|
||||
|
||||
console.log('Initializing worker');
|
||||
|
||||
|
||||
+1
-2
@@ -10,8 +10,7 @@
|
||||
"nym-connect/**",
|
||||
"explorer",
|
||||
"types",
|
||||
"clients/validator",
|
||||
"clients/webassembly/**"
|
||||
"clients/validator"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "lerna run clean",
|
||||
|
||||
@@ -1203,7 +1203,7 @@
|
||||
dependencies:
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.3", "@babel/runtime@^7.18.9", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
version "7.22.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec"
|
||||
integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==
|
||||
@@ -2390,6 +2390,20 @@
|
||||
prop-types "^15.8.1"
|
||||
react-is "^18.2.0"
|
||||
|
||||
"@mui/base@5.0.0-beta.5":
|
||||
version "5.0.0-beta.5"
|
||||
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.5.tgz#b566f3beb1eb2823139eabaf52014cf7be900015"
|
||||
integrity sha512-vy3TWLQYdGNecTaufR4wDNQFV2WEg6wRPi6BVbx6q1vP3K1mbxIn1+XOqOzfYBXjFHvMx0gZAo2TgWbaqfgvAA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.22.5"
|
||||
"@emotion/is-prop-valid" "^1.2.1"
|
||||
"@mui/types" "^7.2.4"
|
||||
"@mui/utils" "^5.13.6"
|
||||
"@popperjs/core" "^2.11.8"
|
||||
clsx "^1.2.1"
|
||||
prop-types "^15.8.1"
|
||||
react-is "^18.2.0"
|
||||
|
||||
"@mui/core-downloads-tracker@^5.13.4":
|
||||
version "5.13.4"
|
||||
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.13.4.tgz#7e4b491d8081b6d45ae51556d82cb16b31315a19"
|
||||
@@ -2417,16 +2431,16 @@
|
||||
react-is "^18.2.0"
|
||||
|
||||
"@mui/material@^5.0.1", "@mui/material@^5.2.2":
|
||||
version "5.13.5"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.13.5.tgz#c14f14824f3a37ae0c5ebddbc0034956bc6fec30"
|
||||
integrity sha512-eMay+Ue1OYXOFMQA5Aau7qbAa/kWHLAyi0McsbPTWssCbGehqkF6CIdPsfVGw6tlO+xPee1hUitphHJNL3xpOQ==
|
||||
version "5.13.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.13.6.tgz#caaba1e071e394c415208404ce6964e6c14c16d6"
|
||||
integrity sha512-/c2ZApeQm2sTYdQXjqEnldaBMBcUEiyu2VRS6bS39ZeNaAcCLBQbYocLR46R+f0S5dgpBzB0T4AsOABPOFYZ5Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
"@mui/base" "5.0.0-beta.4"
|
||||
"@babel/runtime" "^7.22.5"
|
||||
"@mui/base" "5.0.0-beta.5"
|
||||
"@mui/core-downloads-tracker" "^5.13.4"
|
||||
"@mui/system" "^5.13.5"
|
||||
"@mui/system" "^5.13.6"
|
||||
"@mui/types" "^7.2.4"
|
||||
"@mui/utils" "^5.13.1"
|
||||
"@mui/utils" "^5.13.6"
|
||||
"@types/react-transition-group" "^4.4.6"
|
||||
clsx "^1.2.1"
|
||||
csstype "^3.1.2"
|
||||
@@ -2476,16 +2490,16 @@
|
||||
jss-plugin-vendor-prefixer "^10.10.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/system@>= 5", "@mui/system@^5.0.1", "@mui/system@^5.13.5":
|
||||
version "5.13.5"
|
||||
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.13.5.tgz#9f67ea0c4f6974713f90b7b94c999fd3f40f8de3"
|
||||
integrity sha512-n0gzUxoZ2ZHZgnExkh2Htvo9uW2oakofgPRQrDoa/GQOWyRD0NH9MDszBwOb6AAoXZb+OV5TE7I4LeZ/dzgHYA==
|
||||
"@mui/system@>= 5", "@mui/system@^5.0.1", "@mui/system@^5.13.5", "@mui/system@^5.13.6":
|
||||
version "5.13.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.13.6.tgz#5bf4f84fad0c9ed771458f821e384f61abfa33ca"
|
||||
integrity sha512-G3Xr28uLqU3DyF6r2LQkHGw/ku4P0AHzlKVe7FGXOPl7X1u+hoe2xxj8Vdiq/69II/mh9OP21i38yBWgWb7WgQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
"@babel/runtime" "^7.22.5"
|
||||
"@mui/private-theming" "^5.13.1"
|
||||
"@mui/styled-engine" "^5.13.2"
|
||||
"@mui/types" "^7.2.4"
|
||||
"@mui/utils" "^5.13.1"
|
||||
"@mui/utils" "^5.13.6"
|
||||
clsx "^1.2.1"
|
||||
csstype "^3.1.2"
|
||||
prop-types "^15.8.1"
|
||||
@@ -2495,12 +2509,12 @@
|
||||
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.4.tgz#b6fade19323b754c5c6de679a38f068fd50b9328"
|
||||
integrity sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==
|
||||
|
||||
"@mui/utils@^5.10.3", "@mui/utils@^5.13.1", "@mui/utils@^5.7.0":
|
||||
version "5.13.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.13.1.tgz#86199e46014215f95da046a5ec803f4a39c96eee"
|
||||
integrity sha512-6lXdWwmlUbEU2jUI8blw38Kt+3ly7xkmV9ljzY4Q20WhsJMWiNry9CX8M+TaP/HbtuyR8XKsdMgQW7h7MM3n3A==
|
||||
"@mui/utils@^5.10.3", "@mui/utils@^5.13.1", "@mui/utils@^5.13.6", "@mui/utils@^5.7.0":
|
||||
version "5.13.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.13.6.tgz#aa29d75de59577585b9f23891b03592d40459ed7"
|
||||
integrity sha512-ggNlxl5NPSbp+kNcQLmSig6WVB0Id+4gOxhx644987v4fsji+CSXc+MFYLocFB/x4oHtzCUlSzbVHlJfP/fXoQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.21.0"
|
||||
"@babel/runtime" "^7.22.5"
|
||||
"@types/prop-types" "^15.7.5"
|
||||
"@types/react-is" "^18.2.0"
|
||||
prop-types "^15.8.1"
|
||||
@@ -2808,7 +2822,7 @@
|
||||
dependencies:
|
||||
nx "15.9.4"
|
||||
|
||||
"@nymproject/nym-client-wasm@file:clients/webassembly/pkg":
|
||||
"@nymproject/nym-client-wasm@file:./nym-wallet/nym-client-wasm/pkg":
|
||||
version "1.1.1"
|
||||
|
||||
"@nymproject/nym-validator-client@^0.18.0":
|
||||
@@ -3036,10 +3050,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570"
|
||||
integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==
|
||||
|
||||
"@remix-run/router@1.6.3":
|
||||
version "1.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.6.3.tgz#8205baf6e17ef93be35bf62c37d2d594e9be0dad"
|
||||
integrity sha512-EXJysQ7J3veRECd0kZFQwYYd5sJMcq2O/m60zu1W2l3oVQ9xtub8jTOtYRE0+M2iomyG/W3Ps7+vp2kna0C27Q==
|
||||
"@remix-run/router@1.7.0":
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.0.tgz#550a8d5760b78efc5d60f62b5829b0f74c1fde81"
|
||||
integrity sha512-Eu1V3kz3mV0wUpVTiFHuaT8UD1gj/0VnoFHQYX35xlslQUpe8CuYoKFn9d4WZFHm3yDywz6ALZuGdnUPKrNeAw==
|
||||
|
||||
"@rollup/plugin-commonjs@^24.0.1":
|
||||
version "24.1.0"
|
||||
@@ -5001,9 +5015,9 @@
|
||||
form-data "^3.0.0"
|
||||
|
||||
"@types/node@*", "@types/node@>=13.7.0":
|
||||
version "20.3.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.1.tgz#e8a83f1aa8b649377bb1fb5d7bac5cb90e784dfe"
|
||||
integrity sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg==
|
||||
version "20.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.2.tgz#fa6a90f2600e052a03c18b8cb3fd83dd4e599898"
|
||||
integrity sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==
|
||||
|
||||
"@types/node@^13.7.0":
|
||||
version "13.13.52"
|
||||
@@ -5011,9 +5025,9 @@
|
||||
integrity sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==
|
||||
|
||||
"@types/node@^14.0.10 || ^16.0.0", "@types/node@^14.14.20 || ^16.0.0", "@types/node@^16.7.13":
|
||||
version "16.18.36"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.36.tgz#0db5d7efc4760d36d0d1d22c85d1a53accd5dc27"
|
||||
integrity sha512-8egDX8dE50XyXWH6C6PRCNkTP106DuUrvdrednFouDSmCi7IOvrqr0frznfZaHifHH/3aq/7a7v9N4wdXMqhBQ==
|
||||
version "16.18.37"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.37.tgz#a1f8728e4dc30163deb41e9b7aba65d0c2d4eda1"
|
||||
integrity sha512-ql+4dw4PlPFBP495k8JzUX/oMNRI2Ei4PrMHgj8oT4VhGlYUzF4EYr0qk2fW+XBVGIrq8Zzk13m4cvyXZuv4pA==
|
||||
|
||||
"@types/normalize-package-data@^2.4.0":
|
||||
version "2.4.1"
|
||||
@@ -5113,9 +5127,9 @@
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^18.0.26":
|
||||
version "18.2.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.13.tgz#a98c09bde8b18f80021935b11d2d29ef5f4dcb2f"
|
||||
integrity sha512-vJ+zElvi/Zn9cVXB5slX2xL8PZodPCwPRDpittQdw43JR2AJ5k3vKdgJJyneV/cYgIbLQUwXa9JVDvUZXGba+Q==
|
||||
version "18.2.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.14.tgz#fa7a6fecf1ce35ca94e74874f70c56ce88f7a127"
|
||||
integrity sha512-A0zjq+QN/O0Kpe30hA1GidzyFjatVvrpIvWLxD+xv67Vt91TWWgco9IvrJBkeyHm1trGaFS/FSGqPlhyeZRm0g==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
@@ -5300,14 +5314,14 @@
|
||||
integrity sha512-3NoqvZC2W5gAC5DZbTpCeJ251vGQmgcWIHQJGq2J240HY6ErQ9aWKkwfoKJlHLx+A83WPNTZ9+3cd2ILxbvr1w==
|
||||
|
||||
"@typescript-eslint/eslint-plugin@^5.13.0", "@typescript-eslint/eslint-plugin@^5.7.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.0.tgz#2f4bea6a3718bed2ba52905358d0f45cd3620d31"
|
||||
integrity sha512-78B+anHLF1TI8Jn/cD0Q00TBYdMgjdOn980JfAVa9yw5sop8nyTfVOQAv6LWywkOGLclDBtv5z3oxN4w7jxyNg==
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.60.1.tgz#81382d6ecb92b8dda70e91f9035611cb2fecd1c3"
|
||||
integrity sha512-KSWsVvsJsLJv3c4e73y/Bzt7OpqMCADUO846bHcuWYSYM19bldbAeDv7dYyV0jwkbMfJ2XdlzwjhXtuD7OY6bw==
|
||||
dependencies:
|
||||
"@eslint-community/regexpp" "^4.4.0"
|
||||
"@typescript-eslint/scope-manager" "5.60.0"
|
||||
"@typescript-eslint/type-utils" "5.60.0"
|
||||
"@typescript-eslint/utils" "5.60.0"
|
||||
"@typescript-eslint/scope-manager" "5.60.1"
|
||||
"@typescript-eslint/type-utils" "5.60.1"
|
||||
"@typescript-eslint/utils" "5.60.1"
|
||||
debug "^4.3.4"
|
||||
grapheme-splitter "^1.0.4"
|
||||
ignore "^5.2.0"
|
||||
@@ -5316,78 +5330,78 @@
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/experimental-utils@^5.3.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.60.0.tgz#48ffa47238592397c3d857fe1403eed3b1d5e604"
|
||||
integrity sha512-ovid3u7CNBrr0Ct35LUPkNYH4e+z4Kc6dPfSG99oMmH9SfoEoefq09uSnJI4mUb/UM7a/peVM03G+MzLxrD16g==
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-5.60.1.tgz#d783bb63b9183541019a945eda6a9d96b096d985"
|
||||
integrity sha512-TXUdLxv2t8181nh5yLXl/Gr/zKj1ZofQ7m+ZdmG2+El0TYOHCvlZfc35D4nturemC3RUnf3KmLuFp3bVBjkG5w==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "5.60.0"
|
||||
"@typescript-eslint/utils" "5.60.1"
|
||||
|
||||
"@typescript-eslint/parser@^5.13.0", "@typescript-eslint/parser@^5.7.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.60.0.tgz#08f4daf5fc6548784513524f4f2f359cebb4068a"
|
||||
integrity sha512-jBONcBsDJ9UoTWrARkRRCgDz6wUggmH5RpQVlt7BimSwaTkTjwypGzKORXbR4/2Hqjk9hgwlon2rVQAjWNpkyQ==
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.60.1.tgz#0f2f58209c0862a73e3d5a56099abfdfa21d0fd3"
|
||||
integrity sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==
|
||||
dependencies:
|
||||
"@typescript-eslint/scope-manager" "5.60.0"
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
"@typescript-eslint/typescript-estree" "5.60.0"
|
||||
"@typescript-eslint/scope-manager" "5.60.1"
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
"@typescript-eslint/typescript-estree" "5.60.1"
|
||||
debug "^4.3.4"
|
||||
|
||||
"@typescript-eslint/scope-manager@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.60.0.tgz#ae511967b4bd84f1d5e179bb2c82857334941c1c"
|
||||
integrity sha512-hakuzcxPwXi2ihf9WQu1BbRj1e/Pd8ZZwVTG9kfbxAMZstKz8/9OoexIwnmLzShtsdap5U/CoQGRCWlSuPbYxQ==
|
||||
"@typescript-eslint/scope-manager@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz#35abdb47f500c68c08f2f2b4f22c7c79472854bb"
|
||||
integrity sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
"@typescript-eslint/visitor-keys" "5.60.0"
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
"@typescript-eslint/visitor-keys" "5.60.1"
|
||||
|
||||
"@typescript-eslint/type-utils@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.60.0.tgz#69b09087eb12d7513d5b07747e7d47f5533aa228"
|
||||
integrity sha512-X7NsRQddORMYRFH7FWo6sA9Y/zbJ8s1x1RIAtnlj6YprbToTiQnM6vxcMu7iYhdunmoC0rUWlca13D5DVHkK2g==
|
||||
"@typescript-eslint/type-utils@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.60.1.tgz#17770540e98d65ab4730c7aac618003f702893f4"
|
||||
integrity sha512-vN6UztYqIu05nu7JqwQGzQKUJctzs3/Hg7E2Yx8rz9J+4LgtIDFWjjl1gm3pycH0P3mHAcEUBd23LVgfrsTR8A==
|
||||
dependencies:
|
||||
"@typescript-eslint/typescript-estree" "5.60.0"
|
||||
"@typescript-eslint/utils" "5.60.0"
|
||||
"@typescript-eslint/typescript-estree" "5.60.1"
|
||||
"@typescript-eslint/utils" "5.60.1"
|
||||
debug "^4.3.4"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/types@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.60.0.tgz#3179962b28b4790de70e2344465ec97582ce2558"
|
||||
integrity sha512-ascOuoCpNZBccFVNJRSC6rPq4EmJ2NkuoKnd6LDNyAQmdDnziAtxbCGWCbefG1CNzmDvd05zO36AmB7H8RzKPA==
|
||||
"@typescript-eslint/types@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.60.1.tgz#a17473910f6b8d388ea83c9d7051af89c4eb7561"
|
||||
integrity sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==
|
||||
|
||||
"@typescript-eslint/typescript-estree@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.0.tgz#4ddf1a81d32a850de66642d9b3ad1e3254fb1600"
|
||||
integrity sha512-R43thAuwarC99SnvrBmh26tc7F6sPa2B3evkXp/8q954kYL6Ro56AwASYWtEEi+4j09GbiNAHqYwNNZuNlARGQ==
|
||||
"@typescript-eslint/typescript-estree@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz#8c71824b7165b64d5ebd7aa42968899525959834"
|
||||
integrity sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
"@typescript-eslint/visitor-keys" "5.60.0"
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
"@typescript-eslint/visitor-keys" "5.60.1"
|
||||
debug "^4.3.4"
|
||||
globby "^11.1.0"
|
||||
is-glob "^4.0.3"
|
||||
semver "^7.3.7"
|
||||
tsutils "^3.21.0"
|
||||
|
||||
"@typescript-eslint/utils@5.60.0", "@typescript-eslint/utils@^5.10.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.60.0.tgz#4667c5aece82f9d4f24a667602f0f300864b554c"
|
||||
integrity sha512-ba51uMqDtfLQ5+xHtwlO84vkdjrqNzOnqrnwbMHMRY8Tqeme8C2Q8Fc7LajfGR+e3/4LoYiWXUM6BpIIbHJ4hQ==
|
||||
"@typescript-eslint/utils@5.60.1", "@typescript-eslint/utils@^5.10.0":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.60.1.tgz#6861ebedbefba1ac85482d2bdef6f2ff1eb65b80"
|
||||
integrity sha512-tiJ7FFdFQOWssFa3gqb94Ilexyw0JVxj6vBzaSpfN/8IhoKkDuSAenUKvsSHw2A/TMpJb26izIszTXaqygkvpQ==
|
||||
dependencies:
|
||||
"@eslint-community/eslint-utils" "^4.2.0"
|
||||
"@types/json-schema" "^7.0.9"
|
||||
"@types/semver" "^7.3.12"
|
||||
"@typescript-eslint/scope-manager" "5.60.0"
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
"@typescript-eslint/typescript-estree" "5.60.0"
|
||||
"@typescript-eslint/scope-manager" "5.60.1"
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
"@typescript-eslint/typescript-estree" "5.60.1"
|
||||
eslint-scope "^5.1.1"
|
||||
semver "^7.3.7"
|
||||
|
||||
"@typescript-eslint/visitor-keys@5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.0.tgz#b48b29da3f5f31dd1656281727004589d2722a66"
|
||||
integrity sha512-wm9Uz71SbCyhUKgcaPRauBdTegUyY/ZWl8gLwD/i/ybJqscrrdVSFImpvUz16BLPChIeKBK5Fa9s6KDQjsjyWw==
|
||||
"@typescript-eslint/visitor-keys@5.60.1":
|
||||
version "5.60.1"
|
||||
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz#19a877358bf96318ec35d90bfe6bd1445cce9434"
|
||||
integrity sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==
|
||||
dependencies:
|
||||
"@typescript-eslint/types" "5.60.0"
|
||||
"@typescript-eslint/types" "5.60.1"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5":
|
||||
@@ -6045,9 +6059,9 @@ aria-query@5.1.3:
|
||||
deep-equal "^2.0.5"
|
||||
|
||||
aria-query@^5.0.0, aria-query@^5.1.3:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.2.1.tgz#bc285d9d654d1df121bcd0c134880d415ca67c15"
|
||||
integrity sha512-7uFg4b+lETFgdaJyETnILsXgnnzVnkHcgRbwbPwevm5x/LmUlt3MjczMRe1zg824iBgXZNRPTBftNYyRSKLp2g==
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
|
||||
integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==
|
||||
dependencies:
|
||||
dequal "^2.0.3"
|
||||
|
||||
@@ -6122,11 +6136,6 @@ array-union@^2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d"
|
||||
integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==
|
||||
|
||||
array-union@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/array-union/-/array-union-3.0.1.tgz#da52630d327f8b88cfbfb57728e2af5cd9b6b975"
|
||||
integrity sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==
|
||||
|
||||
array-uniq@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
|
||||
@@ -7086,9 +7095,9 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001503:
|
||||
version "1.0.30001507"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001507.tgz#fae53f6286e7564783eadea9b447819410a59534"
|
||||
integrity sha512-SFpUDoSLCaE5XYL2jfqe9ova/pbQHEmbheDf5r4diNwbAgR3qxM9NQtfsiSscjqoya5K7kFcHPUQ+VsUkIJR4A==
|
||||
version "1.0.30001508"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001508.tgz#4461bbc895c692a96da399639cc1e146e7302a33"
|
||||
integrity sha512-sdQZOJdmt3GJs1UMNpCCCyeuS2IEGLXnHyAo9yIO5JJDjbjoVRij4M1qep6P6gFpptD1PqIYgzM+gwJbOi92mw==
|
||||
|
||||
capture-exit@^2.0.0:
|
||||
version "2.0.0"
|
||||
@@ -7807,18 +7816,6 @@ copy-descriptor@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||
integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==
|
||||
|
||||
copy-webpack-plugin@^10.2.4:
|
||||
version "10.2.4"
|
||||
resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz#6c854be3fdaae22025da34b9112ccf81c63308fe"
|
||||
integrity sha512-xFVltahqlsRcyyJqQbDY6EYTtyQZF9rf+JPjwHObLdPFMEISqkFkr7mFoVOC6BfYS/dNThyoQKvziugm+OnwBg==
|
||||
dependencies:
|
||||
fast-glob "^3.2.7"
|
||||
glob-parent "^6.0.1"
|
||||
globby "^12.0.2"
|
||||
normalize-path "^3.0.0"
|
||||
schema-utils "^4.0.0"
|
||||
serialize-javascript "^6.0.0"
|
||||
|
||||
core-js-compat@^3.30.1, core-js-compat@^3.30.2, core-js-compat@^3.8.1:
|
||||
version "3.31.0"
|
||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.31.0.tgz#4030847c0766cc0e803dcdfb30055d7ef2064bf1"
|
||||
@@ -8913,9 +8910,9 @@ ejs@^3.1.7:
|
||||
jake "^10.8.5"
|
||||
|
||||
electron-to-chromium@^1.4.431:
|
||||
version "1.4.439"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.439.tgz#d0748e8128c18f92354d3412dfb2104c3fb90e8f"
|
||||
integrity sha512-BHpErPSNhb9FB25+OwQP6mCAf3ZXfGbmuvc4LzBNVJwpCcXQJm++LerimocYRG9FRxUVRKZqaB7d0+pImSTPSg==
|
||||
version "1.4.441"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.441.tgz#94dd9c1cbf081d83f032a4f1cd9f787e21fc24ce"
|
||||
integrity sha512-LlCgQ8zgYZPymf5H4aE9itwiIWH4YlCiv1HFLmmcBeFYi5E+3eaIFnjHzYtcFQbaKfAW+CqZ9pgxo33DZuoqPg==
|
||||
|
||||
elliptic@^6.5.3, elliptic@^6.5.4:
|
||||
version "6.5.4"
|
||||
@@ -9018,9 +9015,9 @@ env-paths@^2.2.0:
|
||||
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
|
||||
|
||||
envinfo@^7.7.3, envinfo@^7.7.4:
|
||||
version "7.9.0"
|
||||
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.9.0.tgz#47594a13081be0d9be6e513534e8c58dbb26c7a1"
|
||||
integrity sha512-RODB4txU+xImYDemN5DqaKC0CHk05XSVkOX4pq0hK26Qx+1LChkuOyUDlGEjYb3ACr0n9qBhFjg37hQuJvpkRQ==
|
||||
version "7.10.0"
|
||||
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.10.0.tgz#55146e3909cc5fe63c22da63fb15b05aeac35b13"
|
||||
integrity sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==
|
||||
|
||||
err-code@^2.0.2:
|
||||
version "2.0.3"
|
||||
@@ -9854,7 +9851,7 @@ fast-glob@^2.2.6:
|
||||
merge2 "^1.2.3"
|
||||
micromatch "^3.1.10"
|
||||
|
||||
fast-glob@^3.2.7, fast-glob@^3.2.9:
|
||||
fast-glob@^3.2.9:
|
||||
version "3.2.12"
|
||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
|
||||
integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==
|
||||
@@ -9981,9 +9978,9 @@ file-system-cache@^1.0.5:
|
||||
ramda "^0.28.0"
|
||||
|
||||
file-system-cache@^2.0.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-2.3.0.tgz#201feaf4c8cd97b9d0d608e96861bb6005f46fe6"
|
||||
integrity sha512-l4DMNdsIPsVnKrgEXbJwDJsA5mB8rGwHYERMgqQx/xAUtChPJMre1bXBzDEqqVbWv9AIbFezXMxeEkZDSrXUOQ==
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-2.4.0.tgz#18c842942fac3d27a698388002ef6405b8a3fc4a"
|
||||
integrity sha512-aqfrsrc43lOJdK6e4xZT/61y0s+oJgeMxbhIxjVIJ/H/lbBNIuQ+51DjQJKt36eQAj2j7wu0i0QQcYP/eGyudQ==
|
||||
dependencies:
|
||||
fs-extra "11.1.1"
|
||||
ramda "0.29.0"
|
||||
@@ -10552,7 +10549,7 @@ glob-parent@^3.1.0:
|
||||
is-glob "^3.1.0"
|
||||
path-dirname "^1.0.0"
|
||||
|
||||
glob-parent@^6.0.1, glob-parent@^6.0.2:
|
||||
glob-parent@^6.0.2:
|
||||
version "6.0.2"
|
||||
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
|
||||
integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
|
||||
@@ -10683,18 +10680,6 @@ globby@11.1.0, globby@^11.0.1, globby@^11.0.2, globby@^11.1.0:
|
||||
merge2 "^1.4.1"
|
||||
slash "^3.0.0"
|
||||
|
||||
globby@^12.0.2:
|
||||
version "12.2.0"
|
||||
resolved "https://registry.yarnpkg.com/globby/-/globby-12.2.0.tgz#2ab8046b4fba4ff6eede835b29f678f90e3d3c22"
|
||||
integrity sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==
|
||||
dependencies:
|
||||
array-union "^3.0.1"
|
||||
dir-glob "^3.0.1"
|
||||
fast-glob "^3.2.7"
|
||||
ignore "^5.1.9"
|
||||
merge2 "^1.4.1"
|
||||
slash "^4.0.0"
|
||||
|
||||
globby@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c"
|
||||
@@ -10974,11 +10959,6 @@ he@1.2.0, he@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
hello-wasm-pack@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/hello-wasm-pack/-/hello-wasm-pack-0.1.0.tgz#482a2e3371828056ac35f5b5fec76c0b99dcd530"
|
||||
integrity sha512-3hx0GDkDLf/a9ThCMV2qG4mwza8N/MCtm8aeFFc/cdBCL2zMJ1kW1wjNl7xPqD1lz8Yl5+uhnc/cpui4dLwz/w==
|
||||
|
||||
hex-rgb@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-4.3.0.tgz#af5e974e83bb2fefe44d55182b004ec818c07776"
|
||||
@@ -11051,9 +11031,9 @@ html-encoding-sniffer@^2.0.1:
|
||||
whatwg-encoding "^1.0.5"
|
||||
|
||||
html-entities@^2.1.0, html-entities@^2.3.2:
|
||||
version "2.3.6"
|
||||
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.6.tgz#966391d58e5737c77bca4025e31721b496ab7454"
|
||||
integrity sha512-9o0+dcpIw2/HxkNuYKxSJUF/MMRZQECK4GnF+oQOmJ83yCVHTWgCH5aOXxK5bozNRmM8wtgryjHD3uloPBDEGw==
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.4.0.tgz#edd0cee70402584c8c76cc2c0556db09d1f45061"
|
||||
integrity sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==
|
||||
|
||||
html-escaper@^2.0.0:
|
||||
version "2.0.2"
|
||||
@@ -11303,7 +11283,7 @@ ignore@^4.0.3, ignore@^4.0.6:
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
|
||||
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
|
||||
|
||||
ignore@^5.0.4, ignore@^5.1.9, ignore@^5.2.0:
|
||||
ignore@^5.0.4, ignore@^5.2.0:
|
||||
version "5.2.4"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
|
||||
integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==
|
||||
@@ -13724,9 +13704,9 @@ mem@^8.1.1:
|
||||
mimic-fn "^3.1.0"
|
||||
|
||||
memfs@^3.1.2, memfs@^3.2.2, memfs@^3.4.1, memfs@^3.4.3:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.5.3.tgz#d9b40fe4f8d5788c5f895bda804cd0d9eeee9f3b"
|
||||
integrity sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==
|
||||
version "3.6.0"
|
||||
resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6"
|
||||
integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ==
|
||||
dependencies:
|
||||
fs-monkey "^1.0.4"
|
||||
|
||||
@@ -14248,9 +14228,9 @@ minimatch@^8.0.2:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^9.0.0, minimatch@^9.0.1:
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.1.tgz#8a555f541cf976c622daf078bb28f29fb927c253"
|
||||
integrity sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==
|
||||
version "9.0.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.2.tgz#397e387fff22f6795844d00badc903a3d5de7057"
|
||||
integrity sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
@@ -16751,19 +16731,19 @@ react-resize-detector@^8.0.4:
|
||||
lodash "^4.17.21"
|
||||
|
||||
react-router-dom@6, react-router-dom@^6.7.0:
|
||||
version "6.13.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.13.0.tgz#6651f456bb2af42ef14f6880123b1f575539e81f"
|
||||
integrity sha512-6Nqoqd7fgwxxVGdbiMHTpDHCYPq62d7Wk1Of7B82vH7ZPwwsRaIa22zRZKPPg413R5REVNiyuQPKDG1bubcOFA==
|
||||
version "6.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.14.0.tgz#7ce6c3f73549e4d40216ba150253d3bf30812b33"
|
||||
integrity sha512-YEwlApKwzMMMbGbhh+Q7MsloTldcwMgHxUY/1g0uA62+B1hZo2jsybCWIDCL8zvIDB1FA0pBKY9chHbZHt+2dQ==
|
||||
dependencies:
|
||||
"@remix-run/router" "1.6.3"
|
||||
react-router "6.13.0"
|
||||
"@remix-run/router" "1.7.0"
|
||||
react-router "6.14.0"
|
||||
|
||||
react-router@6.13.0:
|
||||
version "6.13.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.13.0.tgz#7e4427a271dae0cafbdb88c56ccbd9b1434ee93f"
|
||||
integrity sha512-Si6KnfEnJw7gUQkNa70dlpI1bul46FuSxX5t5WwlUBxE25DAz2BjVkwaK8Y2s242bQrZPXCpmwLPtIO5pv4tXg==
|
||||
react-router@6.14.0:
|
||||
version "6.14.0"
|
||||
resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.14.0.tgz#1c3e8e922d934d43a253fd862c72c82167c0a7f1"
|
||||
integrity sha512-OD+vkrcGbvlwkspUFDgMzsu1RXwdjNh83YgG/28lBnDzgslhCgxIqoExLlxsfTpIygp7fc+Hd3esloNwzkm2xA==
|
||||
dependencies:
|
||||
"@remix-run/router" "1.6.3"
|
||||
"@remix-run/router" "1.7.0"
|
||||
|
||||
react-simple-maps@^2.3.0:
|
||||
version "2.3.0"
|
||||
@@ -17416,9 +17396,9 @@ rollup-plugin-web-worker-loader@^1.6.1:
|
||||
integrity sha512-4QywQSz1NXFHKdyiou16mH3ijpcfLtLGOrAqvAqu1Gx+P8+zj+3gwC2BSL/VW1d+LW4nIHC8F7d7OXhs9UdR2A==
|
||||
|
||||
rollup@^3.17.2, rollup@^3.2.1, rollup@^3.9.1:
|
||||
version "3.25.1"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.25.1.tgz#9fff79d22ff1a904b2b595a2fb9bc3793cb987d8"
|
||||
integrity sha512-tywOR+rwIt5m2ZAWSe5AIJcTat8vGlnPFAv15ycCrw33t6iFsXZ6mzHVFh2psSjxQPmI+xgzMZZizUAukBI4aQ==
|
||||
version "3.25.3"
|
||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.25.3.tgz#f9a8986f0f244bcfde2208da91ba46b8fd252551"
|
||||
integrity sha512-ZT279hx8gszBj9uy5FfhoG4bZx8c+0A1sbqtr7Q3KNWIizpTdDEPZbV2xcbvHsnFp4MavCQYZyzApJ+virB8Yw==
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
@@ -17903,11 +17883,6 @@ slash@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44"
|
||||
integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==
|
||||
|
||||
slash@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7"
|
||||
integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==
|
||||
|
||||
slice-ansi@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b"
|
||||
@@ -19071,9 +19046,9 @@ tslib@^1.8.1, tslib@^1.9.3:
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913"
|
||||
integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3"
|
||||
integrity sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==
|
||||
|
||||
tsutils@^3.21.0:
|
||||
version "3.21.0"
|
||||
@@ -19878,7 +19853,7 @@ webidl-conversions@^6.1.0:
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
|
||||
integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
|
||||
|
||||
webpack-cli@^4.8.0, webpack-cli@^4.9.2:
|
||||
webpack-cli@^4.8.0:
|
||||
version "4.10.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31"
|
||||
integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==
|
||||
@@ -19930,7 +19905,7 @@ webpack-dev-middleware@^5.3.1:
|
||||
range-parser "^1.2.1"
|
||||
schema-utils "^4.0.0"
|
||||
|
||||
webpack-dev-server@^4.5.0, webpack-dev-server@^4.7.4:
|
||||
webpack-dev-server@^4.5.0:
|
||||
version "4.15.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz#8944b29c12760b3a45bdaa70799b17cb91b03df7"
|
||||
integrity sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==
|
||||
@@ -20057,7 +20032,7 @@ webpack@4:
|
||||
watchpack "^1.7.4"
|
||||
webpack-sources "^1.4.1"
|
||||
|
||||
"webpack@>=4.43.0 <6.0.0", webpack@^5.70.0, webpack@^5.75.0, webpack@^5.9.0:
|
||||
"webpack@>=4.43.0 <6.0.0", webpack@^5.75.0, webpack@^5.9.0:
|
||||
version "5.88.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.0.tgz#a07aa2f8e7a64a8f1cec0c6c2e180e3cb34440c8"
|
||||
integrity sha512-O3jDhG5e44qIBSi/P6KpcCcH7HD+nYIHVBhdWFxcLOcIGN8zGo5nqF3BjyNCxIh4p1vFdNnreZv2h2KkoAw3lw==
|
||||
|
||||
Reference in New Issue
Block a user