Merge remote-tracking branch 'origin/release/v1.1.0' into develop
This commit is contained in:
+12
-2
@@ -16,11 +16,14 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
- wasm-client: uses updated wasm-compatible `client-core` so that it's now capable of packet retransmission, cover traffic and poisson delay (among other things!) ([#1673])
|
||||
- validator-api: add `interval_operating_cost` and `profit_margin_percent` to cmpute reward estimation endpoint
|
||||
- native-client/socks5-client/network-requester: improve handling error cases ([#1713])
|
||||
- vesting-contract: optional locked token pledge cap per account ([#1687]), defaults to 100_000 NYM
|
||||
- clients: add testing-only support for two more extended packet sizes (8kb and 16kb).
|
||||
|
||||
### Fixed
|
||||
|
||||
- validator-api, mixnode, gateway should now prefer values in config.toml over mainnet defaults ([#1645])
|
||||
- socks5-client: fix bug where in some cases packet reordering could trigger a connection being closed too early ([#1702])
|
||||
- validator-api should now correctly update historical uptimes for all mixnodes and gateways every 24h ([#1721])
|
||||
- socks5-client: fix bug where in some cases packet reordering could trigger a connection being closed too early ([#1702],[#1724])
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -28,7 +31,9 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
- socks5 client: graceful shutdown should fix error on disconnect in nym-connect ([#1591])
|
||||
- wasm-client: fixed build errors on MacOS and changed example JS code to use mainnet ([#1585])
|
||||
- gateway-client: will attempt to read now as many as 8 websocket messages at once, assuming they're already available on the socket ([#1669])
|
||||
- clients: bound the sphinx packet channel and reduce sending rate if gateway can't keep up ([#1703])
|
||||
- validator-api: changed error serialization on `inclusion_probability`, `stake-saturation` and `reward-estimation` endpoints to provide more accurate information ([#1681])
|
||||
- moved `Percent` struct to to `contracts-common`, change affects explorer-api
|
||||
- clients: bound the sphinx packet channel and reduce sending rate if gateway can't keep up ([#1703],[#1725])
|
||||
|
||||
[#1541]: https://github.com/nymtech/nym/pull/1541
|
||||
[#1558]: https://github.com/nymtech/nym/pull/1558
|
||||
@@ -42,9 +47,14 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
[#1669]: https://github.com/nymtech/nym/pull/1669
|
||||
[#1671]: https://github.com/nymtech/nym/pull/1671
|
||||
[#1673]: https://github.com/nymtech/nym/pull/1673
|
||||
[#1681]: https://github.com/nymtech/nym/pull/1681
|
||||
[#1687]: https://github.com/nymtech/nym/pull/1687
|
||||
[#1702]: https://github.com/nymtech/nym/pull/1702
|
||||
[#1703]: https://github.com/nymtech/nym/pull/1703
|
||||
[#1713]: https://github.com/nymtech/nym/pull/1713
|
||||
[#1721]: https://github.com/nymtech/nym/pull/1721
|
||||
[#1724]: https://github.com/nymtech/nym/pull/1724
|
||||
[#1725]: https://github.com/nymtech/nym/pull/1725
|
||||
|
||||
|
||||
## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2)
|
||||
|
||||
Generated
+27
@@ -737,7 +737,10 @@ name = "contracts-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1583,11 +1586,13 @@ version = "1.0.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap 3.2.8",
|
||||
"contracts-common",
|
||||
"dotenv",
|
||||
"humantime-serde",
|
||||
"isocountry",
|
||||
"itertools",
|
||||
"log",
|
||||
"logging",
|
||||
"maxminddb",
|
||||
"mixnet-contract-common",
|
||||
"network-defaults",
|
||||
@@ -2726,6 +2731,14 @@ dependencies = [
|
||||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "logging"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pretty_env_logger",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
version = "0.5.4"
|
||||
@@ -3068,6 +3081,7 @@ dependencies = [
|
||||
"clap_complete_fig",
|
||||
"dotenv",
|
||||
"log",
|
||||
"logging",
|
||||
"network-defaults",
|
||||
"nym-cli-commands",
|
||||
"pretty_env_logger",
|
||||
@@ -3123,6 +3137,7 @@ dependencies = [
|
||||
"gateway-client",
|
||||
"gateway-requests",
|
||||
"log",
|
||||
"logging",
|
||||
"network-defaults",
|
||||
"nymsphinx",
|
||||
"pemstore",
|
||||
@@ -3165,6 +3180,7 @@ dependencies = [
|
||||
"gateway-requests",
|
||||
"humantime-serde",
|
||||
"log",
|
||||
"logging",
|
||||
"mixnet-client",
|
||||
"mixnode-common",
|
||||
"network-defaults",
|
||||
@@ -3207,6 +3223,7 @@ dependencies = [
|
||||
"humantime-serde",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"logging",
|
||||
"mixnet-client",
|
||||
"mixnode-common",
|
||||
"nonexhaustive-delayqueue",
|
||||
@@ -3241,6 +3258,7 @@ dependencies = [
|
||||
"futures",
|
||||
"ipnetwork 0.20.0",
|
||||
"log",
|
||||
"logging",
|
||||
"network-defaults",
|
||||
"nymsphinx",
|
||||
"ordered-buffer",
|
||||
@@ -3266,6 +3284,7 @@ version = "1.0.2"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"log",
|
||||
"logging",
|
||||
"pretty_env_logger",
|
||||
"rocket",
|
||||
"serde",
|
||||
@@ -3292,6 +3311,7 @@ dependencies = [
|
||||
"gateway-client",
|
||||
"gateway-requests",
|
||||
"log",
|
||||
"logging",
|
||||
"network-defaults",
|
||||
"nymsphinx",
|
||||
"ordered-buffer",
|
||||
@@ -3351,6 +3371,7 @@ dependencies = [
|
||||
"coconut-interface",
|
||||
"config",
|
||||
"console-subscriber",
|
||||
"contracts-common",
|
||||
"cosmwasm-std",
|
||||
"credential-storage",
|
||||
"credentials",
|
||||
@@ -3365,6 +3386,7 @@ dependencies = [
|
||||
"humantime-serde",
|
||||
"inclusion-probability",
|
||||
"log",
|
||||
"logging",
|
||||
"mixnet-contract-common",
|
||||
"multisig-contract-common",
|
||||
"nymcoconut",
|
||||
@@ -6388,6 +6410,7 @@ dependencies = [
|
||||
"coconut-interface",
|
||||
"colored",
|
||||
"config",
|
||||
"contracts-common",
|
||||
"cosmrs",
|
||||
"cosmwasm-std",
|
||||
"cw3",
|
||||
@@ -6476,12 +6499,14 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
name = "vesting-contract"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"contracts-common",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"mixnet-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"vergen 5.1.17",
|
||||
"vesting-contract-common",
|
||||
]
|
||||
|
||||
@@ -6489,7 +6514,9 @@ dependencies = [
|
||||
name = "vesting-contract-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"contracts-common",
|
||||
"cosmwasm-std",
|
||||
"log",
|
||||
"mixnet-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
|
||||
@@ -41,6 +41,7 @@ members = [
|
||||
"common/execute",
|
||||
"common/inclusion-probability",
|
||||
"common/ledger",
|
||||
"common/logging",
|
||||
"common/mixnode-common",
|
||||
"common/network-defaults",
|
||||
"common/nonexhaustive-delayqueue",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use config::NymConfig;
|
||||
use nymsphinx::params::PacketSize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::marker::PhantomData;
|
||||
use std::path::PathBuf;
|
||||
@@ -247,8 +248,8 @@ impl<T: NymConfig> Config<T> {
|
||||
self.debug.disable_main_poisson_packet_distribution
|
||||
}
|
||||
|
||||
pub fn get_use_extended_packet_size(&self) -> bool {
|
||||
self.debug.use_extended_packet_size
|
||||
pub fn get_use_extended_packet_size(&self) -> Option<ExtendedPacketSize> {
|
||||
self.debug.use_extended_packet_size.clone()
|
||||
}
|
||||
|
||||
pub fn get_version(&self) -> &str {
|
||||
@@ -470,8 +471,16 @@ pub struct Debug {
|
||||
/// 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 sphinx packet use a NON-DEFAULT bigger size.
|
||||
pub use_extended_packet_size: Option<ExtendedPacketSize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ExtendedPacketSize {
|
||||
Extended8,
|
||||
Extended16,
|
||||
Extended32,
|
||||
}
|
||||
|
||||
impl Default for Debug {
|
||||
@@ -488,7 +497,17 @@ impl Default for Debug {
|
||||
topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT,
|
||||
disable_loop_cover_traffic_stream: false,
|
||||
disable_main_poisson_packet_distribution: false,
|
||||
use_extended_packet_size: false,
|
||||
use_extended_packet_size: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExtendedPacketSize> for PacketSize {
|
||||
fn from(size: ExtendedPacketSize) -> PacketSize {
|
||||
match size {
|
||||
ExtendedPacketSize::Extended8 => PacketSize::ExtendedPacket8,
|
||||
ExtendedPacketSize::Extended16 => PacketSize::ExtendedPacket16,
|
||||
ExtendedPacketSize::Extended32 => PacketSize::ExtendedPacket32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ completions = { path = "../../common/completions" }
|
||||
credential-storage = { path = "../../common/credential-storage" }
|
||||
credentials = { path = "../../common/credentials", optional = true }
|
||||
crypto = { path = "../../common/crypto" }
|
||||
logging = { path = "../../common/logging"}
|
||||
gateway-client = { path = "../../common/client-libs/gateway-client" }
|
||||
gateway-requests = { path = "../../gateway/gateway-requests" }
|
||||
network-defaults = { path = "../../common/network-defaults" }
|
||||
|
||||
@@ -30,7 +30,6 @@ use log::*;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::addressing::nodes::NodeIdentity;
|
||||
use nymsphinx::anonymous_replies::ReplySurb;
|
||||
use nymsphinx::params::PacketSize;
|
||||
use nymsphinx::receiver::ReconstructedMessage;
|
||||
use task::{wait_for_signal, ShutdownListener, ShutdownNotifier};
|
||||
|
||||
@@ -103,8 +102,9 @@ impl NymClient {
|
||||
topology_accessor,
|
||||
);
|
||||
|
||||
if self.config.get_base().get_use_extended_packet_size() {
|
||||
stream.set_custom_packet_size(PacketSize::ExtendedPacket)
|
||||
if let Some(size) = self.config.get_base().get_use_extended_packet_size() {
|
||||
log::debug!("Setting extended packet size: {:?}", size);
|
||||
stream.set_custom_packet_size(size.into());
|
||||
}
|
||||
|
||||
stream.start_with_shutdown(shutdown);
|
||||
@@ -132,8 +132,9 @@ impl NymClient {
|
||||
self.as_mix_recipient(),
|
||||
);
|
||||
|
||||
if self.config.get_base().get_use_extended_packet_size() {
|
||||
controller_config.set_custom_packet_size(PacketSize::ExtendedPacket)
|
||||
if let Some(size) = self.config.get_base().get_use_extended_packet_size() {
|
||||
log::debug!("Setting extended packet size: {:?}", size);
|
||||
controller_config.set_custom_packet_size(size.into());
|
||||
}
|
||||
|
||||
info!("Starting real traffic stream...");
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use clap::{crate_version, Parser};
|
||||
use error::ClientError;
|
||||
use logging::setup_logging;
|
||||
use network_defaults::setup_env;
|
||||
|
||||
pub mod client;
|
||||
@@ -36,25 +37,3 @@ fn banner() -> String {
|
||||
crate_version!()
|
||||
)
|
||||
}
|
||||
|
||||
fn setup_logging() {
|
||||
let mut log_builder = pretty_env_logger::formatted_timed_builder();
|
||||
if let Ok(s) = ::std::env::var("RUST_LOG") {
|
||||
log_builder.parse_filters(&s);
|
||||
} else {
|
||||
// default to 'Info'
|
||||
log_builder.filter(None, log::LevelFilter::Info);
|
||||
}
|
||||
|
||||
log_builder
|
||||
.filter_module("handlebars", log::LevelFilter::Warn)
|
||||
.filter_module("hyper", log::LevelFilter::Warn)
|
||||
.filter_module("mio", log::LevelFilter::Warn)
|
||||
.filter_module("reqwest", log::LevelFilter::Warn)
|
||||
.filter_module("sled", log::LevelFilter::Warn)
|
||||
.filter_module("tokio_reactor", log::LevelFilter::Warn)
|
||||
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
|
||||
.filter_module("tungstenite", log::LevelFilter::Warn)
|
||||
.filter_module("want", log::LevelFilter::Warn)
|
||||
.init();
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ completions = { path = "../../common/completions" }
|
||||
credential-storage = { path = "../../common/credential-storage" }
|
||||
credentials = { path = "../../common/credentials", optional = true }
|
||||
crypto = { path = "../../common/crypto" }
|
||||
logging = { path = "../../common/logging"}
|
||||
gateway-client = { path = "../../common/client-libs/gateway-client" }
|
||||
gateway-requests = { path = "../../gateway/gateway-requests" }
|
||||
network-defaults = { path = "../../common/network-defaults" }
|
||||
|
||||
@@ -36,7 +36,6 @@ use gateway_client::{
|
||||
use log::*;
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::addressing::nodes::NodeIdentity;
|
||||
use nymsphinx::params::PacketSize;
|
||||
use task::{wait_for_signal, ShutdownListener, ShutdownNotifier};
|
||||
|
||||
pub mod config;
|
||||
@@ -103,8 +102,9 @@ impl NymClient {
|
||||
topology_accessor,
|
||||
);
|
||||
|
||||
if self.config.get_base().get_use_extended_packet_size() {
|
||||
stream.set_custom_packet_size(PacketSize::ExtendedPacket)
|
||||
if let Some(size) = self.config.get_base().get_use_extended_packet_size() {
|
||||
log::debug!("Setting extended packet size: {:?}", size);
|
||||
stream.set_custom_packet_size(size.into());
|
||||
}
|
||||
|
||||
stream.start_with_shutdown(shutdown);
|
||||
@@ -132,8 +132,9 @@ impl NymClient {
|
||||
self.as_mix_recipient(),
|
||||
);
|
||||
|
||||
if self.config.get_base().get_use_extended_packet_size() {
|
||||
controller_config.set_custom_packet_size(PacketSize::ExtendedPacket)
|
||||
if let Some(size) = self.config.get_base().get_use_extended_packet_size() {
|
||||
log::debug!("Setting extended packet size: {:?}", size);
|
||||
controller_config.set_custom_packet_size(size.into());
|
||||
}
|
||||
|
||||
info!("Starting real traffic stream...");
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use clap::{crate_version, Parser};
|
||||
use error::Socks5ClientError;
|
||||
use logging::setup_logging;
|
||||
use network_defaults::setup_env;
|
||||
|
||||
pub mod client;
|
||||
@@ -36,25 +37,3 @@ fn banner() -> String {
|
||||
crate_version!()
|
||||
)
|
||||
}
|
||||
|
||||
fn setup_logging() {
|
||||
let mut log_builder = pretty_env_logger::formatted_timed_builder();
|
||||
if let Ok(s) = ::std::env::var("RUST_LOG") {
|
||||
log_builder.parse_filters(&s);
|
||||
} else {
|
||||
// default to 'Info'
|
||||
log_builder.filter(None, log::LevelFilter::Info);
|
||||
}
|
||||
|
||||
log_builder
|
||||
.filter_module("handlebars", log::LevelFilter::Warn)
|
||||
.filter_module("hyper", log::LevelFilter::Warn)
|
||||
.filter_module("mio", log::LevelFilter::Warn)
|
||||
.filter_module("reqwest", log::LevelFilter::Warn)
|
||||
.filter_module("sled", log::LevelFilter::Warn)
|
||||
.filter_module("tokio_reactor", log::LevelFilter::Warn)
|
||||
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
|
||||
.filter_module("tungstenite", log::LevelFilter::Warn)
|
||||
.filter_module("want", log::LevelFilter::Warn)
|
||||
.init();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// due to expansion of #[wasm_bindgen] macro on `Debug` Config struct
|
||||
#![allow(clippy::drop_non_drop)]
|
||||
|
||||
use client_core::config::{Debug as ConfigDebug, GatewayEndpoint};
|
||||
use client_core::config::{Debug as ConfigDebug, ExtendedPacketSize, GatewayEndpoint};
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
use wasm_bindgen::prelude::*;
|
||||
@@ -107,6 +107,11 @@ pub struct Debug {
|
||||
|
||||
impl From<Debug> for ConfigDebug {
|
||||
fn from(debug: Debug) -> Self {
|
||||
// For now we just always use the (older) 32kb extended size
|
||||
let use_extended_packet_size = debug
|
||||
.use_extended_packet_size
|
||||
.then(|| ExtendedPacketSize::Extended32);
|
||||
|
||||
ConfigDebug {
|
||||
average_packet_delay: Duration::from_millis(debug.average_packet_delay_ms),
|
||||
average_ack_delay: Duration::from_millis(debug.average_ack_delay_ms),
|
||||
@@ -126,7 +131,7 @@ impl From<Debug> for ConfigDebug {
|
||||
disable_loop_cover_traffic_stream: debug.disable_loop_cover_traffic_stream,
|
||||
disable_main_poisson_packet_distribution: debug
|
||||
.disable_main_poisson_packet_distribution,
|
||||
use_extended_packet_size: debug.use_extended_packet_size,
|
||||
use_extended_packet_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,7 +153,7 @@ impl From<ConfigDebug> for Debug {
|
||||
disable_loop_cover_traffic_stream: debug.disable_loop_cover_traffic_stream,
|
||||
disable_main_poisson_packet_distribution: debug
|
||||
.disable_main_poisson_packet_distribution,
|
||||
use_extended_packet_size: debug.use_extended_packet_size,
|
||||
use_extended_packet_size: debug.use_extended_packet_size.is_some(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ use gateway_client::{
|
||||
MixnetMessageSender,
|
||||
};
|
||||
use nymsphinx::addressing::clients::Recipient;
|
||||
use nymsphinx::params::PacketSize;
|
||||
use rand::rngs::OsRng;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use wasm_bindgen_futures::spawn_local;
|
||||
@@ -110,8 +109,8 @@ impl NymClient {
|
||||
topology_accessor,
|
||||
);
|
||||
|
||||
if self.config.debug.use_extended_packet_size {
|
||||
stream.set_custom_packet_size(PacketSize::ExtendedPacket)
|
||||
if let Some(size) = &self.config.debug.use_extended_packet_size {
|
||||
stream.set_custom_packet_size(size.clone().into());
|
||||
}
|
||||
|
||||
stream.start();
|
||||
@@ -135,8 +134,8 @@ impl NymClient {
|
||||
self.as_mix_recipient(),
|
||||
);
|
||||
|
||||
if self.config.debug.use_extended_packet_size {
|
||||
controller_config.set_custom_packet_size(PacketSize::ExtendedPacket)
|
||||
if let Some(size) = &self.config.debug.use_extended_packet_size {
|
||||
controller_config.set_custom_packet_size(size.clone().into());
|
||||
}
|
||||
|
||||
console_log!("Starting real traffic stream...");
|
||||
|
||||
@@ -62,9 +62,19 @@ impl PacketRouter {
|
||||
trace!("routing regular packet");
|
||||
received_messages.push(received_packet);
|
||||
} else if received_packet.len()
|
||||
== PacketSize::ExtendedPacket.plaintext_size() - ack_overhead
|
||||
== PacketSize::ExtendedPacket8.plaintext_size() - ack_overhead
|
||||
{
|
||||
trace!("routing extended packet");
|
||||
trace!("routing extended8 packet");
|
||||
received_messages.push(received_packet);
|
||||
} else if received_packet.len()
|
||||
== PacketSize::ExtendedPacket16.plaintext_size() - ack_overhead
|
||||
{
|
||||
trace!("routing extended16 packet");
|
||||
received_messages.push(received_packet);
|
||||
} else if received_packet.len()
|
||||
== PacketSize::ExtendedPacket32.plaintext_size() - ack_overhead
|
||||
{
|
||||
trace!("routing extended32 packet");
|
||||
received_messages.push(received_packet);
|
||||
} else {
|
||||
// this can happen if other clients are not padding their messages
|
||||
|
||||
@@ -23,6 +23,7 @@ pub struct Config {
|
||||
maximum_reconnection_backoff: Duration,
|
||||
initial_connection_timeout: Duration,
|
||||
maximum_connection_buffer_size: usize,
|
||||
use_legacy_version: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -31,12 +32,14 @@ impl Config {
|
||||
maximum_reconnection_backoff: Duration,
|
||||
initial_connection_timeout: Duration,
|
||||
maximum_connection_buffer_size: usize,
|
||||
use_legacy_version: bool,
|
||||
) -> Self {
|
||||
Config {
|
||||
initial_reconnection_backoff,
|
||||
maximum_reconnection_backoff,
|
||||
initial_connection_timeout,
|
||||
maximum_connection_buffer_size,
|
||||
use_legacy_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,7 +204,8 @@ impl SendWithoutResponse for Client {
|
||||
packet_mode: PacketMode,
|
||||
) -> io::Result<()> {
|
||||
trace!("Sending packet to {:?}", address);
|
||||
let framed_packet = FramedSphinxPacket::new(packet, packet_mode);
|
||||
let framed_packet =
|
||||
FramedSphinxPacket::new(packet, packet_mode, self.config.use_legacy_version);
|
||||
|
||||
if let Some(sender) = self.conn_new.get_mut(&address) {
|
||||
if let Err(err) = sender.channel.try_send(framed_packet) {
|
||||
@@ -259,6 +263,7 @@ mod tests {
|
||||
maximum_reconnection_backoff: Duration::from_millis(300_000),
|
||||
initial_connection_timeout: Duration::from_millis(1_500),
|
||||
maximum_connection_buffer_size: 128,
|
||||
use_legacy_version: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,14 @@ impl PacketForwarder {
|
||||
maximum_reconnection_backoff: Duration,
|
||||
initial_connection_timeout: Duration,
|
||||
maximum_connection_buffer_size: usize,
|
||||
use_legacy_version: bool,
|
||||
) -> (PacketForwarder, MixForwardingSender) {
|
||||
let client_config = Config::new(
|
||||
initial_reconnection_backoff,
|
||||
maximum_reconnection_backoff,
|
||||
initial_connection_timeout,
|
||||
maximum_connection_buffer_size,
|
||||
use_legacy_version,
|
||||
);
|
||||
|
||||
let (packet_sender, packet_receiver) = mpsc::unbounded();
|
||||
|
||||
@@ -13,6 +13,7 @@ colored = "2.0"
|
||||
cw3 = "0.13.1"
|
||||
mixnet-contract-common = { path= "../../cosmwasm-smart-contracts/mixnet-contract" }
|
||||
vesting-contract-common = { path= "../../cosmwasm-smart-contracts/vesting-contract" }
|
||||
contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common" }
|
||||
coconut-bandwidth-contract-common = { path= "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" }
|
||||
multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" }
|
||||
vesting-contract = { path = "../../../contracts/vesting" }
|
||||
|
||||
@@ -15,14 +15,12 @@ use cosmrs::rpc::query::Query;
|
||||
use cosmrs::rpc::Error as TendermintRpcError;
|
||||
use cosmrs::rpc::HttpClientUrl;
|
||||
use cosmrs::tx::Msg;
|
||||
use cosmwasm_std::Uint128;
|
||||
use execute::execute;
|
||||
use network_defaults::{ChainDetails, NymNetworkDetails};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryInto;
|
||||
use std::time::SystemTime;
|
||||
use vesting_contract_common::ExecuteMsg as VestingExecuteMsg;
|
||||
use vesting_contract_common::QueryMsg as VestingQueryMsg;
|
||||
|
||||
pub use crate::nymd::cosmwasm_client::client::CosmWasmClient;
|
||||
pub use crate::nymd::cosmwasm_client::signing_client::SigningCosmWasmClient;
|
||||
@@ -47,6 +45,7 @@ pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
|
||||
use mixnet_contract_common::MixId;
|
||||
pub use signing_client::Client as SigningNymdClient;
|
||||
pub use traits::{VestingQueryClient, VestingSigningClient};
|
||||
use vesting_contract_common::PledgeCap;
|
||||
|
||||
pub mod coin;
|
||||
pub mod cosmwasm_client;
|
||||
@@ -482,16 +481,6 @@ impl<C> NymdClient<C> {
|
||||
self.client.get_total_supply().await
|
||||
}
|
||||
|
||||
pub async fn vesting_get_locked_pledge_cap(&self) -> Result<Uint128, NymdError>
|
||||
where
|
||||
C: CosmWasmClient + Sync,
|
||||
{
|
||||
let request = VestingQueryMsg::GetLockedPledgeCap {};
|
||||
self.client
|
||||
.query_contract_smart(self.vesting_contract_address(), &request)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn simulate<I, M>(&self, messages: I) -> Result<SimulateResponse, NymdError>
|
||||
where
|
||||
C: SigningCosmWasmClient + Sync,
|
||||
@@ -737,12 +726,16 @@ impl<C> NymdClient<C> {
|
||||
#[execute("vesting")]
|
||||
fn _vesting_update_locked_pledge_cap(
|
||||
&self,
|
||||
amount: Uint128,
|
||||
address: String,
|
||||
cap: PledgeCap,
|
||||
fee: Option<Fee>,
|
||||
) -> (VestingExecuteMsg, Option<Fee>)
|
||||
where
|
||||
C: SigningCosmWasmClient + Sync,
|
||||
{
|
||||
(VestingExecuteMsg::UpdateLockedPledgeCap { amount }, fee)
|
||||
(
|
||||
VestingExecuteMsg::UpdateLockedPledgeCap { address, cap },
|
||||
fee,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ pub use crate::nymd::cosmwasm_client::client::CosmWasmClient;
|
||||
use crate::nymd::error::NymdError;
|
||||
use crate::nymd::NymdClient;
|
||||
use async_trait::async_trait;
|
||||
use contracts_common::ContractBuildInformation;
|
||||
use cosmwasm_std::{Coin as CosmWasmCoin, Timestamp};
|
||||
use mixnet_contract_common::MixId;
|
||||
use serde::Deserialize;
|
||||
use vesting_contract::vesting::Account;
|
||||
use vesting_contract_common::{
|
||||
messages::QueryMsg as VestingQueryMsg, AllDelegationsResponse, DelegationTimesResponse,
|
||||
@@ -16,6 +18,15 @@ use vesting_contract_common::{
|
||||
|
||||
#[async_trait]
|
||||
pub trait VestingQueryClient {
|
||||
async fn query_vesting_contract<T>(&self, query: VestingQueryMsg) -> Result<T, NymdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>;
|
||||
|
||||
async fn get_vesting_contract_version(&self) -> Result<ContractBuildInformation, NymdError> {
|
||||
self.query_vesting_contract(VestingQueryMsg::GetContractVersion {})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn locked_coins(
|
||||
&self,
|
||||
address: &str,
|
||||
@@ -107,6 +118,15 @@ pub trait VestingQueryClient {
|
||||
|
||||
#[async_trait]
|
||||
impl<C: CosmWasmClient + Sync + Send> VestingQueryClient for NymdClient<C> {
|
||||
async fn query_vesting_contract<T>(&self, query: VestingQueryMsg) -> Result<T, NymdError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
{
|
||||
self.client
|
||||
.query_contract_smart(self.vesting_contract_address(), &query)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn locked_coins(
|
||||
&self,
|
||||
vesting_account_address: &str,
|
||||
|
||||
@@ -9,6 +9,7 @@ use async_trait::async_trait;
|
||||
use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
|
||||
use mixnet_contract_common::{Gateway, MixId, MixNode};
|
||||
use vesting_contract_common::messages::{ExecuteMsg as VestingExecuteMsg, VestingSpecification};
|
||||
use vesting_contract_common::PledgeCap;
|
||||
|
||||
#[async_trait]
|
||||
pub trait VestingSigningClient {
|
||||
@@ -105,6 +106,7 @@ pub trait VestingSigningClient {
|
||||
staking_address: Option<String>,
|
||||
vesting_spec: Option<VestingSpecification>,
|
||||
amount: Coin,
|
||||
cap: Option<PledgeCap>,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NymdError>;
|
||||
}
|
||||
@@ -382,6 +384,7 @@ impl<C: SigningCosmWasmClient + Sync + Send> VestingSigningClient for NymdClient
|
||||
staking_address: Option<String>,
|
||||
vesting_spec: Option<VestingSpecification>,
|
||||
amount: Coin,
|
||||
cap: Option<PledgeCap>,
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NymdError> {
|
||||
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier)));
|
||||
@@ -389,6 +392,7 @@ impl<C: SigningCosmWasmClient + Sync + Send> VestingSigningClient for NymdClient
|
||||
owner_address: owner_address.to_string(),
|
||||
staking_address,
|
||||
vesting_spec,
|
||||
cap,
|
||||
};
|
||||
self.client
|
||||
.execute(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use thiserror::Error;
|
||||
use validator_api_requests::models::RequestError;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ValidatorAPIError {
|
||||
@@ -10,4 +11,7 @@ pub enum ValidatorAPIError {
|
||||
|
||||
#[error("Request failed with error message - {0}")]
|
||||
GenericRequestFailure(String),
|
||||
|
||||
#[error("The validator API has failed to resolve our request. It returned status code {status} and additional error message: {}", error.message())]
|
||||
ApiRequestFailure { status: u16, error: RequestError },
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::validator_api::error::ValidatorAPIError;
|
||||
use crate::validator_api::routes::{CORE_STATUS_COUNT, SINCE_ARG};
|
||||
use mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId};
|
||||
use reqwest::Response;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use validator_api_requests::coconut::{
|
||||
@@ -12,9 +13,10 @@ use validator_api_requests::coconut::{
|
||||
VerifyCredentialBody, VerifyCredentialResponse,
|
||||
};
|
||||
use validator_api_requests::models::{
|
||||
GatewayCoreStatusResponse, InclusionProbabilityResponse, MixNodeBondAnnotated,
|
||||
MixnodeCoreStatusResponse, MixnodeStatusResponse, RewardEstimationResponse,
|
||||
StakeSaturationResponse, UptimeResponse,
|
||||
GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse,
|
||||
InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeCoreStatusResponse,
|
||||
MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RequestError,
|
||||
RewardEstimationResponse, StakeSaturationResponse, UptimeResponse,
|
||||
};
|
||||
|
||||
pub mod error;
|
||||
@@ -47,6 +49,19 @@ impl Client {
|
||||
&self.url
|
||||
}
|
||||
|
||||
async fn send_get_request<K, V>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
params: Params<'_, K, V>,
|
||||
) -> Result<Response, ValidatorAPIError>
|
||||
where
|
||||
K: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
{
|
||||
let url = create_api_url(&self.url, path, params);
|
||||
Ok(self.reqwest_client.get(url).send().await?)
|
||||
}
|
||||
|
||||
async fn query_validator_api<T, K, V>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
@@ -57,8 +72,36 @@ impl Client {
|
||||
K: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
{
|
||||
let url = create_api_url(&self.url, path, params);
|
||||
Ok(self.reqwest_client.get(url).send().await?.json().await?)
|
||||
let res = self.send_get_request(path, params).await?;
|
||||
if res.status().is_success() {
|
||||
Ok(res.json().await?)
|
||||
} else {
|
||||
Err(ValidatorAPIError::GenericRequestFailure(res.text().await?))
|
||||
}
|
||||
}
|
||||
|
||||
// This works for endpoints returning Result<Json<T>, ErrorResponse>
|
||||
async fn query_validator_api_fallible<T, K, V>(
|
||||
&self,
|
||||
path: PathSegments<'_>,
|
||||
params: Params<'_, K, V>,
|
||||
) -> Result<T, ValidatorAPIError>
|
||||
where
|
||||
for<'a> T: Deserialize<'a>,
|
||||
K: AsRef<str>,
|
||||
V: AsRef<str>,
|
||||
{
|
||||
let res = self.send_get_request(path, params).await?;
|
||||
let status = res.status();
|
||||
if res.status().is_success() {
|
||||
Ok(res.json().await?)
|
||||
} else {
|
||||
let request_error: RequestError = res.json().await?;
|
||||
Err(ValidatorAPIError::ApiRequestFailure {
|
||||
status: status.as_u16(),
|
||||
error: request_error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn post_validator_api<B, T, K, V>(
|
||||
@@ -135,6 +178,74 @@ impl Client {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_mixnode_report(
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> Result<MixnodeStatusReportResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
routes::REPORT,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_gateway_report(
|
||||
&self,
|
||||
identity: IdentityKeyRef<'_>,
|
||||
) -> Result<GatewayStatusReportResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::GATEWAY,
|
||||
identity,
|
||||
routes::REPORT,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_mixnode_history(
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> Result<MixnodeUptimeHistoryResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::MIXNODE,
|
||||
&mix_id.to_string(),
|
||||
routes::HISTORY,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_gateway_history(
|
||||
&self,
|
||||
identity: IdentityKeyRef<'_>,
|
||||
) -> Result<GatewayUptimeHistoryResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS,
|
||||
routes::GATEWAY,
|
||||
identity,
|
||||
routes::HISTORY,
|
||||
],
|
||||
NO_PARAMS,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_rewarded_mixnodes_detailed(
|
||||
&self,
|
||||
) -> Result<Vec<MixNodeBondAnnotated>, ValidatorAPIError> {
|
||||
@@ -234,7 +345,7 @@ impl Client {
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> Result<RewardEstimationResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
self.query_validator_api_fallible(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
@@ -251,7 +362,7 @@ impl Client {
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> Result<StakeSaturationResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
self.query_validator_api_fallible(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
@@ -268,7 +379,7 @@ impl Client {
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> Result<InclusionProbabilityResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
self.query_validator_api_fallible(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
@@ -285,7 +396,7 @@ impl Client {
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> Result<UptimeResponse, ValidatorAPIError> {
|
||||
self.query_validator_api(
|
||||
self.query_validator_api_fallible(
|
||||
&[
|
||||
routes::API_VERSION,
|
||||
routes::STATUS_ROUTES,
|
||||
|
||||
@@ -10,7 +10,6 @@ pub const GATEWAYS: &str = "gateways";
|
||||
pub const DETAILED: &str = "detailed";
|
||||
pub const ACTIVE: &str = "active";
|
||||
pub const REWARDED: &str = "rewarded";
|
||||
|
||||
pub const COCONUT_ROUTES: &str = "coconut";
|
||||
pub const BANDWIDTH: &str = "bandwidth";
|
||||
|
||||
@@ -28,6 +27,8 @@ pub const CORE_STATUS_COUNT: &str = "core-status-count";
|
||||
pub const SINCE_ARG: &str = "since";
|
||||
|
||||
pub const STATUS: &str = "status";
|
||||
pub const REPORT: &str = "report";
|
||||
pub const HISTORY: &str = "history";
|
||||
pub const REWARD_ESTIMATION: &str = "reward-estimation";
|
||||
pub const AVG_UPTIME: &str = "avg_uptime";
|
||||
pub const STAKE_SATURATION: &str = "stake-saturation";
|
||||
|
||||
@@ -12,6 +12,7 @@ use validator_client::nymd::AccountId;
|
||||
use validator_client::nymd::VestingSigningClient;
|
||||
use validator_client::nymd::{CosmosCoin, Denom};
|
||||
use vesting_contract_common::messages::VestingSpecification;
|
||||
use vesting_contract_common::PledgeCap;
|
||||
|
||||
use crate::context::SigningClient;
|
||||
|
||||
@@ -34,6 +35,12 @@ pub struct Args {
|
||||
|
||||
#[clap(long)]
|
||||
pub staking_address: Option<String>,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
help = "Pledge cap as either absolute uNYM value or percentage, floats need to be in the 0.0 to 1.0 range and will be parsed as percentages, integers will be parsed as uNYM"
|
||||
)]
|
||||
pub pledge_cap: Option<PledgeCap>,
|
||||
}
|
||||
|
||||
pub async fn create(args: Args, client: SigningClient, network_details: &NymNetworkDetails) {
|
||||
@@ -55,6 +62,7 @@ pub async fn create(args: Args, client: SigningClient, network_details: &NymNetw
|
||||
args.staking_address,
|
||||
Some(vesting),
|
||||
coin.into(),
|
||||
args.pledge_cap,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -9,3 +9,8 @@ edition = "2021"
|
||||
[dependencies]
|
||||
cosmwasm-std = "1.0.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
thiserror = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1.0.0"
|
||||
|
||||
@@ -1,7 +1,132 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use cosmwasm_std::Decimal;
|
||||
use cosmwasm_std::Uint128;
|
||||
use schemars::JsonSchema;
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::ops::Mul;
|
||||
use std::str::FromStr;
|
||||
use thiserror::Error;
|
||||
|
||||
pub fn truncate_decimal(amount: Decimal) -> Uint128 {
|
||||
amount * Uint128::new(1)
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ContractsCommonError {
|
||||
#[error("Provided percent value ({0}) is greater than 100%")]
|
||||
InvalidPercent(Decimal),
|
||||
|
||||
#[error("{source}")]
|
||||
StdErr {
|
||||
#[from]
|
||||
source: cosmwasm_std::StdError,
|
||||
},
|
||||
}
|
||||
|
||||
/// Percent represents a value between 0 and 100%
|
||||
/// (i.e. between 0.0 and 1.0)
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal);
|
||||
|
||||
impl Percent {
|
||||
pub fn new(value: Decimal) -> Result<Self, ContractsCommonError> {
|
||||
if value > Decimal::one() {
|
||||
Err(ContractsCommonError::InvalidPercent(value))
|
||||
} else {
|
||||
Ok(Percent(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.0 == Decimal::zero()
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Self(Decimal::zero())
|
||||
}
|
||||
|
||||
pub fn hundred() -> Self {
|
||||
Self(Decimal::one())
|
||||
}
|
||||
|
||||
pub fn from_percentage_value(value: u64) -> Result<Self, ContractsCommonError> {
|
||||
Percent::new(Decimal::percent(value))
|
||||
}
|
||||
|
||||
pub fn value(&self) -> Decimal {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn round_to_integer(&self) -> u8 {
|
||||
let hundred = Decimal::from_ratio(100u32, 1u32);
|
||||
// we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range
|
||||
truncate_decimal(hundred * self.0).u128() as u8
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Percent {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
// Adding this as part of resolving release -> develop merge conflict.
|
||||
// At the time, unwraps were forbidden in develop, but not (yet) in release.
|
||||
// Clearly we should fix this.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0;
|
||||
write!(f, "{}%", adjusted)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Percent {
|
||||
type Err = ContractsCommonError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Percent::new(Decimal::from_str(s)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Decimal> for Percent {
|
||||
type Output = Decimal;
|
||||
|
||||
fn mul(self, rhs: Decimal) -> Self::Output {
|
||||
self.0 * rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Percent> for Decimal {
|
||||
type Output = Decimal;
|
||||
|
||||
fn mul(self, rhs: Percent) -> Self::Output {
|
||||
rhs * self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Uint128> for Percent {
|
||||
type Output = Uint128;
|
||||
|
||||
fn mul(self, rhs: Uint128) -> Self::Output {
|
||||
self.0 * rhs
|
||||
}
|
||||
}
|
||||
|
||||
// implement custom Deserialize because we want to validate Percent has the correct range
|
||||
fn de_decimal_percent<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let v = Decimal::deserialize(deserializer)?;
|
||||
if v > Decimal::one() {
|
||||
Err(D::Error::custom(
|
||||
"provided decimal percent is larger than 100%",
|
||||
))
|
||||
} else {
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: there's no reason this couldn't be used for proper binaries, but in that case
|
||||
// perhaps the struct should get renamed and moved to a "more" common crate
|
||||
@@ -31,3 +156,47 @@ pub struct ContractBuildInformation {
|
||||
/// Provides the rustc version that was used for the build, for example `1.52.0-nightly`.
|
||||
pub rustc_version: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn percent_serde() {
|
||||
let valid_value = Percent::from_percentage_value(80).unwrap();
|
||||
let serialized = serde_json::to_string(&valid_value).unwrap();
|
||||
|
||||
let deserialized: Percent = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(valid_value, deserialized);
|
||||
|
||||
let invalid_values = vec!["\"42\"", "\"1.1\"", "\"1.00000001\"", "\"foomp\"", "\"1a\""];
|
||||
for invalid_value in invalid_values {
|
||||
assert!(serde_json::from_str::<'_, Percent>(invalid_value).is_err())
|
||||
}
|
||||
assert_eq!(
|
||||
serde_json::from_str::<'_, Percent>("\"0.95\"").unwrap(),
|
||||
Percent::from_percentage_value(95).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_to_absolute_integer() {
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.0001\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 0);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.0099\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 0);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.0199\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 1);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.45123\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 45);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.999999999\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 99);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,6 @@ pub enum MixnetContractError {
|
||||
source: cosmwasm_std::StdError,
|
||||
},
|
||||
|
||||
#[error("Provided percent value is greater than 100%")]
|
||||
InvalidPercent,
|
||||
|
||||
#[error("Attempted to subtract decimals with overflow ({minuend}.sub({subtrahend}))")]
|
||||
OverflowDecimalSubtraction {
|
||||
minuend: Decimal,
|
||||
|
||||
@@ -73,6 +73,9 @@ pub struct Interval {
|
||||
// TODO add a better TS type generation
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
|
||||
#[serde(with = "string_rfc3339_offset_date_time")]
|
||||
// note: the `ts-rs failed to parse this attribute. It will be ignored.` warning emitted during
|
||||
// compilation is fine (I guess). `ts-rs` can't handle `with` serde attribute, but that's okay
|
||||
// since we explicitly specified this field should correspond to typescript's string
|
||||
current_epoch_start: OffsetDateTime,
|
||||
current_epoch_id: EpochId,
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "{ secs: number; nanos: number; }"))]
|
||||
|
||||
@@ -33,6 +33,7 @@ pub struct InitialRewardingParams {
|
||||
pub initial_reward_pool: Decimal,
|
||||
pub initial_staking_supply: Decimal,
|
||||
|
||||
pub staking_supply_scale_factor: Percent,
|
||||
pub sybil_resistance: Percent,
|
||||
pub active_set_work_factor: Decimal,
|
||||
pub interval_pool_emission: Percent,
|
||||
@@ -56,6 +57,7 @@ impl InitialRewardingParams {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool: self.initial_reward_pool,
|
||||
staking_supply: self.initial_staking_supply,
|
||||
staking_supply_scale_factor: self.staking_supply_scale_factor,
|
||||
epoch_reward_budget,
|
||||
stake_saturation_point,
|
||||
sybil_resistance: self.sybil_resistance,
|
||||
|
||||
@@ -27,6 +27,11 @@ pub struct IntervalRewardParams {
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
|
||||
pub staking_supply: Decimal,
|
||||
|
||||
/// Defines the percentage of stake needed to reach saturation for all of the nodes in the rewarded set.
|
||||
/// Also known as `beta`.
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string"))]
|
||||
pub staking_supply_scale_factor: Percent,
|
||||
|
||||
// computed values
|
||||
/// Current value of the computed reward budget per epoch, per node.
|
||||
/// It is expected to be constant throughout the interval.
|
||||
@@ -174,6 +179,10 @@ impl RewardingParams {
|
||||
self.interval.staking_supply = staking_supply;
|
||||
}
|
||||
|
||||
if let Some(staking_supply_scale_factor) = updates.staking_supply_scale_factor {
|
||||
self.interval.staking_supply_scale_factor = staking_supply_scale_factor
|
||||
}
|
||||
|
||||
if let Some(sybil_resistance_percent) = updates.sybil_resistance_percent {
|
||||
self.interval.sybil_resistance = sybil_resistance_percent;
|
||||
}
|
||||
@@ -245,6 +254,9 @@ pub struct IntervalRewardingParamsUpdate {
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string | null"))]
|
||||
pub staking_supply: Option<Decimal>,
|
||||
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string | null"))]
|
||||
pub staking_supply_scale_factor: Option<Percent>,
|
||||
|
||||
#[cfg_attr(feature = "generate-ts", ts(type = "string | null"))]
|
||||
pub sybil_resistance_percent: Option<Percent>,
|
||||
|
||||
@@ -262,6 +274,7 @@ impl IntervalRewardingParamsUpdate {
|
||||
// essentially at least a single field has to be a `Some`
|
||||
self.reward_pool.is_some()
|
||||
|| self.staking_supply.is_some()
|
||||
|| self.staking_supply_scale_factor.is_some()
|
||||
|| self.sybil_resistance_percent.is_some()
|
||||
|| self.active_set_work_factor.is_some()
|
||||
|| self.interval_pool_emission.is_some()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use contracts_common::truncate_decimal;
|
||||
use cosmwasm_std::{Coin, Decimal, Uint128};
|
||||
|
||||
/// Truncates all decimal points so that the reward would fit in a `Coin` and so that we would
|
||||
@@ -17,7 +18,3 @@ pub fn truncate_reward(reward: Decimal, denom: impl Into<String>) -> Coin {
|
||||
pub fn truncate_reward_amount(reward: Decimal) -> Uint128 {
|
||||
truncate_decimal(reward)
|
||||
}
|
||||
|
||||
pub fn truncate_decimal(amount: Decimal) -> Uint128 {
|
||||
amount * Uint128::new(1)
|
||||
}
|
||||
|
||||
@@ -41,7 +41,12 @@ impl Simulator {
|
||||
if self.interval.current_interval_id() + 1 == updated.current_interval_id() {
|
||||
let old = self.system_rewarding_params.interval;
|
||||
let reward_pool = old.reward_pool - self.pending_reward_pool_emission;
|
||||
let staking_supply = old.staking_supply + self.pending_reward_pool_emission;
|
||||
let staking_supply = old.staking_supply
|
||||
+ self
|
||||
.system_rewarding_params
|
||||
.interval
|
||||
.staking_supply_scale_factor
|
||||
* self.pending_reward_pool_emission;
|
||||
let epoch_reward_budget = reward_pool
|
||||
/ self.interval.epochs_in_interval().into_base_decimal()?
|
||||
* old.interval_pool_emission.value();
|
||||
@@ -55,6 +60,7 @@ impl Simulator {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool,
|
||||
staking_supply,
|
||||
staking_supply_scale_factor: old.staking_supply_scale_factor,
|
||||
epoch_reward_budget,
|
||||
stake_saturation_point,
|
||||
sybil_resistance: old.sybil_resistance,
|
||||
@@ -250,6 +256,7 @@ mod tests {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
|
||||
staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
epoch_reward_budget,
|
||||
stake_saturation_point,
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
@@ -613,6 +620,7 @@ mod tests {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(),
|
||||
staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(),
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
epoch_reward_budget,
|
||||
stake_saturation_point,
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::MixnetContractError;
|
||||
use crate::rewarding::helpers::truncate_decimal;
|
||||
use crate::{Layer, RewardedSetNodeStatus};
|
||||
use cosmwasm_std::{Addr, Uint128};
|
||||
use cosmwasm_std::{Coin, Decimal};
|
||||
use cosmwasm_std::Addr;
|
||||
use cosmwasm_std::Coin;
|
||||
use schemars::JsonSchema;
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
use std::ops::{Index, Mul};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Index;
|
||||
|
||||
// type aliases for better reasoning about available data
|
||||
pub type IdentityKey = String;
|
||||
@@ -24,87 +21,6 @@ pub type BlockHeight = u64;
|
||||
pub type EpochEventId = u32;
|
||||
pub type IntervalEventId = u32;
|
||||
|
||||
/// Percent represents a value between 0 and 100%
|
||||
/// (i.e. between 0.0 and 1.0)
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Serialize, Deserialize, JsonSchema,
|
||||
)]
|
||||
pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal);
|
||||
|
||||
impl Percent {
|
||||
pub fn new(value: Decimal) -> Result<Self, MixnetContractError> {
|
||||
if value > Decimal::one() {
|
||||
Err(MixnetContractError::InvalidPercent)
|
||||
} else {
|
||||
Ok(Percent(value))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_zero(&self) -> bool {
|
||||
self.0 == Decimal::zero()
|
||||
}
|
||||
|
||||
pub fn from_percentage_value(value: u64) -> Result<Self, MixnetContractError> {
|
||||
Percent::new(Decimal::percent(value))
|
||||
}
|
||||
|
||||
pub fn value(&self) -> Decimal {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn round_to_integer(&self) -> u8 {
|
||||
let hundred = Decimal::from_ratio(100u32, 1u32);
|
||||
// we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range
|
||||
truncate_decimal(hundred * self.0).u128() as u8
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Percent {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let adjusted = Decimal::from_ratio(100u32, 1u32) * self.0;
|
||||
write!(f, "{}%", adjusted)
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Decimal> for Percent {
|
||||
type Output = Decimal;
|
||||
|
||||
fn mul(self, rhs: Decimal) -> Self::Output {
|
||||
self.0 * rhs
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Percent> for Decimal {
|
||||
type Output = Decimal;
|
||||
|
||||
fn mul(self, rhs: Percent) -> Self::Output {
|
||||
rhs * self
|
||||
}
|
||||
}
|
||||
|
||||
impl Mul<Uint128> for Percent {
|
||||
type Output = Uint128;
|
||||
|
||||
fn mul(self, rhs: Uint128) -> Self::Output {
|
||||
self.0 * rhs
|
||||
}
|
||||
}
|
||||
|
||||
// implement custom Deserialize because we want to validate Percent has the correct range
|
||||
fn de_decimal_percent<'de, D>(deserializer: D) -> Result<Decimal, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let v = Decimal::deserialize(deserializer)?;
|
||||
if v > Decimal::one() {
|
||||
Err(D::Error::custom(
|
||||
"provided decimal percent is larger than 100%",
|
||||
))
|
||||
} else {
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)]
|
||||
pub struct LayerDistribution {
|
||||
pub layer1: u64,
|
||||
@@ -210,48 +126,3 @@ pub struct PagedRewardedSetResponse {
|
||||
pub nodes: Vec<(MixId, RewardedSetNodeStatus)>,
|
||||
pub start_next_after: Option<MixId>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn percent_serde() {
|
||||
let valid_value = Percent::from_percentage_value(80).unwrap();
|
||||
let serialized = serde_json::to_string(&valid_value).unwrap();
|
||||
|
||||
println!("{}", serialized);
|
||||
let deserialized: Percent = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(valid_value, deserialized);
|
||||
|
||||
let invalid_values = vec!["\"42\"", "\"1.1\"", "\"1.00000001\"", "\"foomp\"", "\"1a\""];
|
||||
for invalid_value in invalid_values {
|
||||
assert!(serde_json::from_str::<'_, Percent>(invalid_value).is_err())
|
||||
}
|
||||
assert_eq!(
|
||||
serde_json::from_str::<'_, Percent>("\"0.95\"").unwrap(),
|
||||
Percent::from_percentage_value(95).unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percent_to_absolute_integer() {
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.0001\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 0);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.0099\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 0);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.0199\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 1);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.45123\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 45);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"0.999999999\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 99);
|
||||
|
||||
let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap();
|
||||
assert_eq!(p.round_to_integer(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ edition = "2021"
|
||||
[dependencies]
|
||||
cosmwasm-std = "1.0.0"
|
||||
mixnet-contract-common = { path = "../mixnet-contract" }
|
||||
contracts-common = { path = "../contracts-common" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
log = "0.4"
|
||||
ts-rs = {version = "6.1.2", optional = true}
|
||||
|
||||
[features]
|
||||
|
||||
@@ -4,10 +4,13 @@
|
||||
#![warn(clippy::expect_used)]
|
||||
#![warn(clippy::unwrap_used)]
|
||||
|
||||
use contracts_common::Percent;
|
||||
use cosmwasm_std::{Addr, Coin, Timestamp, Uint128};
|
||||
use log::warn;
|
||||
use mixnet_contract_common::MixId;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub use messages::{ExecuteMsg, InitMsg, MigrateMsg, QueryMsg};
|
||||
|
||||
@@ -46,6 +49,46 @@ impl PledgeData {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub enum PledgeCap {
|
||||
Percent(Percent),
|
||||
Absolute(Uint128), // This has to be in unym
|
||||
}
|
||||
|
||||
impl FromStr for PledgeCap {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(cap: &str) -> Result<Self, Self::Err> {
|
||||
let cap = cap.replace('_', "").replace(',', ".");
|
||||
match Percent::from_str(&cap) {
|
||||
Ok(p) => {
|
||||
if p.is_zero() {
|
||||
warn!("Pledge cap set to 0%, are you sure this is right?")
|
||||
}
|
||||
Ok(PledgeCap::Percent(p))
|
||||
}
|
||||
Err(_) => {
|
||||
match cap.parse::<u128>() {
|
||||
Ok(i) => {
|
||||
if i < 100_000_000_000 {
|
||||
warn!("PledgeCap set to less then 100_000 NYM, are you sure this is right?");
|
||||
}
|
||||
Ok(PledgeCap::Absolute(Uint128::from(i)))
|
||||
}
|
||||
Err(_e) => Err(format!("Could not parse {} as Percent or Uint128", cap)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PledgeCap {
|
||||
fn default() -> Self {
|
||||
PledgeCap::Absolute(Uint128::from(100_000_000_000u128))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct OriginalVestingResponse {
|
||||
pub amount: Coin,
|
||||
@@ -102,3 +145,32 @@ pub struct AllDelegationsResponse {
|
||||
pub delegations: Vec<VestingDelegation>,
|
||||
pub start_next_after: Option<(u32, MixId, u64)>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use contracts_common::Percent;
|
||||
use cosmwasm_std::Uint128;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::PledgeCap;
|
||||
|
||||
#[test]
|
||||
fn test_pledge_cap_from_str() {
|
||||
assert_eq!(
|
||||
PledgeCap::from_str("0.1").unwrap(),
|
||||
PledgeCap::Percent(Percent::from_percentage_value(10).unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
PledgeCap::from_str("0,1").unwrap(),
|
||||
PledgeCap::Percent(Percent::from_percentage_value(10).unwrap())
|
||||
);
|
||||
assert_eq!(
|
||||
PledgeCap::from_str("100_000_000_000").unwrap(),
|
||||
PledgeCap::Absolute(Uint128::new(100_000_000_000))
|
||||
);
|
||||
assert_eq!(
|
||||
PledgeCap::from_str("100000000000").unwrap(),
|
||||
PledgeCap::Absolute(Uint128::new(100_000_000_000))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use cosmwasm_std::{Coin, Timestamp, Uint128};
|
||||
use cosmwasm_std::{Coin, Timestamp};
|
||||
use mixnet_contract_common::{
|
||||
mixnode::{MixNodeConfigUpdate, MixNodeCostParams},
|
||||
Gateway, MixId, MixNode,
|
||||
@@ -6,6 +6,8 @@ use mixnet_contract_common::{
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::PledgeCap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct InitMsg {
|
||||
@@ -83,6 +85,7 @@ pub enum ExecuteMsg {
|
||||
owner_address: String,
|
||||
staking_address: Option<String>,
|
||||
vesting_spec: Option<VestingSpecification>,
|
||||
cap: Option<PledgeCap>,
|
||||
},
|
||||
WithdrawVestedCoins {
|
||||
amount: Coin,
|
||||
@@ -120,7 +123,8 @@ pub enum ExecuteMsg {
|
||||
to_address: Option<String>,
|
||||
},
|
||||
UpdateLockedPledgeCap {
|
||||
amount: Uint128,
|
||||
address: String,
|
||||
cap: PledgeCap,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -156,6 +160,7 @@ impl ExecuteMsg {
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum QueryMsg {
|
||||
GetContractVersion {},
|
||||
LockedCoins {
|
||||
vesting_account_address: String,
|
||||
block_time: Option<Timestamp>,
|
||||
@@ -201,7 +206,6 @@ pub enum QueryMsg {
|
||||
GetCurrentVestingPeriod {
|
||||
address: String,
|
||||
},
|
||||
GetLockedPledgeCap {},
|
||||
GetDelegationTimes {
|
||||
address: String,
|
||||
mix_id: MixId,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "logging"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
log = "0.4.0"
|
||||
pretty_env_logger = "0.4.0"
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// I'd argue we should start transitioning from `log` to `tracing`
|
||||
pub fn setup_logging() {
|
||||
let mut log_builder = pretty_env_logger::formatted_timed_builder();
|
||||
if let Ok(s) = ::std::env::var("RUST_LOG") {
|
||||
log_builder.parse_filters(&s);
|
||||
} else {
|
||||
// default to 'Info'
|
||||
log_builder.filter(None, log::LevelFilter::Info);
|
||||
}
|
||||
|
||||
log_builder
|
||||
.filter_module("hyper", log::LevelFilter::Warn)
|
||||
.filter_module("tokio_reactor", log::LevelFilter::Warn)
|
||||
.filter_module("reqwest", log::LevelFilter::Warn)
|
||||
.filter_module("mio", log::LevelFilter::Warn)
|
||||
.filter_module("want", log::LevelFilter::Warn)
|
||||
.filter_module("tungstenite", log::LevelFilter::Warn)
|
||||
.filter_module("tokio_tungstenite", log::LevelFilter::Warn)
|
||||
.filter_module("handlebars", log::LevelFilter::Warn)
|
||||
.filter_module("sled", log::LevelFilter::Warn)
|
||||
.init();
|
||||
}
|
||||
@@ -116,7 +116,10 @@ impl SphinxPacketProcessor {
|
||||
trace!("received an ack packet!");
|
||||
Ok((None, data))
|
||||
}
|
||||
PacketSize::RegularPacket | PacketSize::ExtendedPacket => {
|
||||
PacketSize::RegularPacket
|
||||
| PacketSize::ExtendedPacket8
|
||||
| PacketSize::ExtendedPacket16
|
||||
| PacketSize::ExtendedPacket32 => {
|
||||
trace!("received a normal packet!");
|
||||
let (ack_data, message) = self.split_hop_data_into_ack_and_message(data)?;
|
||||
let (ack_first_hop, ack_packet) = SurbAck::try_recover_first_hop_packet(&ack_data)?;
|
||||
|
||||
@@ -25,8 +25,10 @@ impl Display for MixPacketFormattingError {
|
||||
InvalidPacketSize(actual) =>
|
||||
write!(
|
||||
f,
|
||||
"received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {} (EXTENDED))",
|
||||
actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(), PacketSize::ExtendedPacket.size()
|
||||
"received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))",
|
||||
actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(),
|
||||
PacketSize::ExtendedPacket8.size(), PacketSize::ExtendedPacket16.size(),
|
||||
PacketSize::ExtendedPacket32.size()
|
||||
),
|
||||
MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"),
|
||||
InvalidPacketMode => write!(f, "provided packet mode is invalid")
|
||||
|
||||
@@ -6,7 +6,6 @@ use bytes::{Buf, BufMut, BytesMut};
|
||||
use nymsphinx_params::packet_modes::InvalidPacketMode;
|
||||
use nymsphinx_params::packet_sizes::{InvalidPacketSize, PacketSize};
|
||||
use nymsphinx_types::SphinxPacket;
|
||||
use std::convert::TryFrom;
|
||||
use std::io;
|
||||
use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
@@ -75,7 +74,7 @@ impl Decoder for SphinxCodec {
|
||||
if src.is_empty() {
|
||||
// can't do anything if we have no bytes, but let's reserve enough for the most
|
||||
// conservative case, i.e. receiving an ack packet
|
||||
src.reserve(Header::SIZE + PacketSize::AckPacket.size());
|
||||
src.reserve(Header::LEGACY_SIZE + PacketSize::AckPacket.size());
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -87,7 +86,7 @@ impl Decoder for SphinxCodec {
|
||||
};
|
||||
|
||||
let sphinx_packet_size = header.packet_size.size();
|
||||
let frame_len = Header::SIZE + sphinx_packet_size;
|
||||
let frame_len = header.size() + sphinx_packet_size;
|
||||
|
||||
if src.len() < frame_len {
|
||||
// we don't have enough bytes to read the rest of frame
|
||||
@@ -96,7 +95,7 @@ impl Decoder for SphinxCodec {
|
||||
}
|
||||
|
||||
// advance buffer past the header - at this point we have enough bytes
|
||||
src.advance(Header::SIZE);
|
||||
src.advance(header.size());
|
||||
let sphinx_packet_bytes = src.split_to(sphinx_packet_size);
|
||||
let sphinx_packet = match SphinxPacket::from_bytes(&sphinx_packet_bytes) {
|
||||
Ok(sphinx_packet) => sphinx_packet,
|
||||
@@ -115,21 +114,27 @@ impl Decoder for SphinxCodec {
|
||||
// has appropriate capacity in anticipation of future calls to decode.
|
||||
// Failing to do so leads to inefficiency.
|
||||
|
||||
// if we have at least one more byte available, we can reserve enough bytes for
|
||||
// if we have enough bytes to decode the header of the next packet, we can reserve enough bytes for
|
||||
// the entire next frame, if not, we assume the next frame is an ack packet and
|
||||
// reserve for that.
|
||||
// we also assume the next packet coming from the same client will use exactly the same versioning
|
||||
// as the current packet
|
||||
let mut allocate_for_next_packet = header.size() + PacketSize::AckPacket.size();
|
||||
if !src.is_empty() {
|
||||
let next_packet_len = match PacketSize::try_from(src[0]) {
|
||||
Ok(next_packet_len) => next_packet_len,
|
||||
match Header::decode(src) {
|
||||
Ok(Some(next_header)) => {
|
||||
allocate_for_next_packet = next_header.size() + next_header.packet_size.size();
|
||||
}
|
||||
Ok(None) => {
|
||||
// we don't have enough information to know how much to reserve, fallback to the ack case
|
||||
}
|
||||
|
||||
// the next frame will be malformed but let's leave handling the error to the next
|
||||
// call to 'decode', as presumably, the current sphinx packet is still valid
|
||||
Err(_) => return Ok(Some(nymsphinx_packet)),
|
||||
};
|
||||
let next_frame_len = next_packet_len.size() + Header::SIZE;
|
||||
src.reserve(next_frame_len - 1);
|
||||
} else {
|
||||
src.reserve(Header::SIZE + PacketSize::AckPacket.size());
|
||||
}
|
||||
src.reserve(allocate_for_next_packet);
|
||||
|
||||
Ok(Some(nymsphinx_packet))
|
||||
}
|
||||
@@ -199,6 +204,8 @@ mod packet_encoding {
|
||||
#[cfg(test)]
|
||||
mod decode_will_allocate_enough_bytes_for_next_call {
|
||||
use super::*;
|
||||
use nymsphinx_params::packet_version::PacketVersion;
|
||||
use nymsphinx_params::PacketMode;
|
||||
|
||||
#[test]
|
||||
fn for_empty_bytes() {
|
||||
@@ -207,20 +214,23 @@ mod packet_encoding {
|
||||
assert!(SphinxCodec.decode(&mut empty_bytes).unwrap().is_none());
|
||||
assert_eq!(
|
||||
empty_bytes.capacity(),
|
||||
Header::SIZE + PacketSize::AckPacket.size()
|
||||
Header::LEGACY_SIZE + PacketSize::AckPacket.size()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_bytes_with_header() {
|
||||
fn for_bytes_with_legacy_header() {
|
||||
// if header gets decoded there should be enough bytes for the entire frame
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
for packet_size in packet_sizes {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::Legacy,
|
||||
packet_size,
|
||||
packet_mode: Default::default(),
|
||||
};
|
||||
@@ -228,12 +238,60 @@ mod packet_encoding {
|
||||
header.encode(&mut bytes);
|
||||
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_none());
|
||||
|
||||
assert_eq!(bytes.capacity(), Header::SIZE + packet_size.size())
|
||||
assert_eq!(bytes.capacity(), Header::LEGACY_SIZE + packet_size.size())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame() {
|
||||
fn for_bytes_with_versioned_header() {
|
||||
// if header gets decoded there should be enough bytes for the entire frame
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
for packet_size in packet_sizes {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::Versioned(123),
|
||||
packet_size,
|
||||
packet_mode: Default::default(),
|
||||
};
|
||||
let mut bytes = BytesMut::new();
|
||||
header.encode(&mut bytes);
|
||||
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_none());
|
||||
|
||||
assert_eq!(
|
||||
bytes.capacity(),
|
||||
Header::VERSIONED_SIZE + packet_size.size()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame_with_legacy_header() {
|
||||
// if full frame is used exactly, there should be enough space for header + ack packet
|
||||
let packet = FramedSphinxPacket {
|
||||
header: Header {
|
||||
packet_version: PacketVersion::Legacy,
|
||||
packet_size: Default::default(),
|
||||
packet_mode: Default::default(),
|
||||
},
|
||||
packet: make_valid_sphinx_packet(Default::default()),
|
||||
};
|
||||
|
||||
let mut bytes = BytesMut::new();
|
||||
SphinxCodec.encode(packet, &mut bytes).unwrap();
|
||||
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some());
|
||||
assert_eq!(
|
||||
bytes.capacity(),
|
||||
Header::LEGACY_SIZE + PacketSize::AckPacket.size()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame_with_versioned_header() {
|
||||
// if full frame is used exactly, there should be enough space for header + ack packet
|
||||
let packet = FramedSphinxPacket {
|
||||
header: Header::default(),
|
||||
@@ -245,17 +303,50 @@ mod packet_encoding {
|
||||
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some());
|
||||
assert_eq!(
|
||||
bytes.capacity(),
|
||||
Header::SIZE + PacketSize::AckPacket.size()
|
||||
Header::VERSIONED_SIZE + PacketSize::AckPacket.size()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame_with_extra_byte() {
|
||||
// if there was at least 1 byte left, there should be enough space for entire next frame
|
||||
fn for_full_frame_with_extra_bytes_with_legacy_header() {
|
||||
// if there was at least 2 byte left, there should be enough space for entire next frame
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
|
||||
for packet_size in packet_sizes {
|
||||
let first_packet = FramedSphinxPacket {
|
||||
header: Header {
|
||||
packet_version: PacketVersion::Legacy,
|
||||
packet_size: Default::default(),
|
||||
packet_mode: Default::default(),
|
||||
},
|
||||
packet: make_valid_sphinx_packet(Default::default()),
|
||||
};
|
||||
|
||||
let mut bytes = BytesMut::new();
|
||||
SphinxCodec.encode(first_packet, &mut bytes).unwrap();
|
||||
bytes.put_u8(packet_size as u8);
|
||||
bytes.put_u8(PacketMode::default() as u8);
|
||||
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some());
|
||||
|
||||
assert!(bytes.capacity() >= Header::LEGACY_SIZE + packet_size.size())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame_with_extra_bytes_with_versioned_header() {
|
||||
// if there was at least 3 byte left, there should be enough space for entire next frame
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
|
||||
for packet_size in packet_sizes {
|
||||
@@ -266,10 +357,12 @@ mod packet_encoding {
|
||||
|
||||
let mut bytes = BytesMut::new();
|
||||
SphinxCodec.encode(first_packet, &mut bytes).unwrap();
|
||||
bytes.put_u8(PacketVersion::new_versioned(123).as_u8().unwrap());
|
||||
bytes.put_u8(packet_size as u8);
|
||||
bytes.put_u8(PacketMode::default() as u8);
|
||||
assert!(SphinxCodec.decode(&mut bytes).unwrap().is_some());
|
||||
|
||||
assert!(bytes.capacity() >= Header::SIZE + packet_size.size())
|
||||
assert!(bytes.capacity() >= Header::VERSIONED_SIZE + packet_size.size())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use crate::codec::SphinxCodecError;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use nymsphinx_params::packet_sizes::PacketSize;
|
||||
use nymsphinx_params::packet_version::PacketVersion;
|
||||
use nymsphinx_params::PacketMode;
|
||||
use nymsphinx_types::SphinxPacket;
|
||||
use std::convert::TryFrom;
|
||||
@@ -17,12 +18,14 @@ pub struct FramedSphinxPacket {
|
||||
}
|
||||
|
||||
impl FramedSphinxPacket {
|
||||
pub fn new(packet: SphinxPacket, packet_mode: PacketMode) -> Self {
|
||||
pub fn new(packet: SphinxPacket, packet_mode: PacketMode, use_legacy_version: bool) -> Self {
|
||||
// If this fails somebody is using the library in a super incorrect way, because they
|
||||
// already managed to somehow create a sphinx packet
|
||||
let packet_size = PacketSize::get_type(packet.len()).unwrap();
|
||||
|
||||
FramedSphinxPacket {
|
||||
header: Header {
|
||||
packet_version: PacketVersion::new(use_legacy_version),
|
||||
packet_size,
|
||||
packet_mode,
|
||||
},
|
||||
@@ -48,6 +51,9 @@ impl FramedSphinxPacket {
|
||||
// but would that really be worth it?
|
||||
#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
|
||||
pub struct Header {
|
||||
/// Represents the wire format version used to construct this packet.
|
||||
pub(crate) packet_version: PacketVersion,
|
||||
|
||||
/// Represents type and consequently size of the included SphinxPacket.
|
||||
pub(crate) packet_size: PacketSize,
|
||||
|
||||
@@ -64,11 +70,25 @@ pub struct Header {
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub(crate) const SIZE: usize = 2;
|
||||
pub(crate) const LEGACY_SIZE: usize = 2;
|
||||
pub(crate) const VERSIONED_SIZE: usize = 3;
|
||||
|
||||
pub(crate) fn size(&self) -> usize {
|
||||
if self.packet_version.is_legacy() {
|
||||
Self::LEGACY_SIZE
|
||||
} else {
|
||||
Self::VERSIONED_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encode(&self, dst: &mut BytesMut) {
|
||||
// we reserve one byte for `packet_size` and the other for `mode`
|
||||
dst.reserve(Self::SIZE);
|
||||
dst.reserve(Self::LEGACY_SIZE);
|
||||
if let Some(version) = self.packet_version.as_u8() {
|
||||
dst.reserve(Self::VERSIONED_SIZE);
|
||||
dst.put_u8(version)
|
||||
}
|
||||
|
||||
dst.put_u8(self.packet_size as u8);
|
||||
dst.put_u8(self.packet_mode as u8);
|
||||
// reserve bytes for the actual packet
|
||||
@@ -76,16 +96,30 @@ impl Header {
|
||||
}
|
||||
|
||||
pub(crate) fn decode(src: &mut BytesMut) -> Result<Option<Self>, SphinxCodecError> {
|
||||
if src.len() < Self::SIZE {
|
||||
if src.len() < Self::LEGACY_SIZE {
|
||||
// can't do anything if we don't have enough bytes - but reserve enough for the next call
|
||||
src.reserve(Self::SIZE);
|
||||
src.reserve(Self::LEGACY_SIZE);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(Some(Header {
|
||||
packet_size: PacketSize::try_from(src[0])?,
|
||||
packet_mode: PacketMode::try_from(src[1])?,
|
||||
}))
|
||||
let packet_version = PacketVersion::from(src[0]);
|
||||
if packet_version.is_legacy() {
|
||||
Ok(Some(Header {
|
||||
packet_version,
|
||||
packet_size: PacketSize::try_from(src[0])?,
|
||||
packet_mode: PacketMode::try_from(src[1])?,
|
||||
}))
|
||||
} else if src.len() < Self::VERSIONED_SIZE {
|
||||
// we're missing that 1 byte to read the full header...
|
||||
src.reserve(Self::VERSIONED_SIZE);
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(Header {
|
||||
packet_version,
|
||||
packet_size: PacketSize::try_from(src[1])?,
|
||||
packet_mode: PacketMode::try_from(src[2])?,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +142,16 @@ mod header_encoding {
|
||||
// make sure this is still 'unknown' for if we make changes in the future
|
||||
assert!(PacketSize::try_from(unknown_packet_size).is_err());
|
||||
|
||||
let mut bytes = BytesMut::from([unknown_packet_size, PacketMode::default() as u8].as_ref());
|
||||
// unfortunately this will only work for the 'versioned' variant
|
||||
// due to the hack used to get legacy mode compatibility
|
||||
let mut bytes = BytesMut::from(
|
||||
[
|
||||
PacketVersion::new_versioned(123).as_u8().unwrap(),
|
||||
unknown_packet_size,
|
||||
PacketMode::default() as u8,
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
assert!(Header::decode(&mut bytes).is_err())
|
||||
}
|
||||
|
||||
@@ -127,23 +170,47 @@ mod header_encoding {
|
||||
let mut empty_bytes = BytesMut::new();
|
||||
let decode_attempt_1 = Header::decode(&mut empty_bytes).unwrap();
|
||||
assert!(decode_attempt_1.is_none());
|
||||
assert!(empty_bytes.capacity() > Header::SIZE);
|
||||
assert!(empty_bytes.capacity() > Header::LEGACY_SIZE);
|
||||
|
||||
let mut empty_bytes = BytesMut::with_capacity(1);
|
||||
let decode_attempt_2 = Header::decode(&mut empty_bytes).unwrap();
|
||||
assert!(decode_attempt_2.is_none());
|
||||
assert!(empty_bytes.capacity() > Header::SIZE);
|
||||
assert!(empty_bytes.capacity() > Header::LEGACY_SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet() {
|
||||
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_legacy_mode() {
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
for packet_size in packet_sizes {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::Legacy,
|
||||
packet_size,
|
||||
packet_mode: Default::default(),
|
||||
};
|
||||
let mut bytes = BytesMut::new();
|
||||
header.encode(&mut bytes);
|
||||
assert_eq!(bytes.capacity(), bytes.len() + packet_size.size())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_versioned_mode() {
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
for packet_size in packet_sizes {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::Versioned(123),
|
||||
packet_size,
|
||||
packet_mode: Default::default(),
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ pub use packet_sizes::PacketSize;
|
||||
|
||||
pub mod packet_modes;
|
||||
pub mod packet_sizes;
|
||||
pub mod packet_version;
|
||||
|
||||
// If somebody can provide an argument why it might be reasonable to have more than 255 mix hops,
|
||||
// I will change this to [`usize`]
|
||||
@@ -24,6 +25,17 @@ pub const DEFAULT_NUM_MIX_HOPS: u8 = 3;
|
||||
pub const FRAG_ID_LEN: usize = 5;
|
||||
pub type SerializedFragmentIdentifier = [u8; FRAG_ID_LEN];
|
||||
|
||||
// wait, wait, but why are we starting with version 7?
|
||||
// when packet header gets serialized, the following bytes (in that order) are put onto the wire:
|
||||
// - packet_version (starting with v1.1.0)
|
||||
// - packet_size indicator
|
||||
// - packet_mode
|
||||
// it also just so happens that the only valid values for packet_size indicator include values 1-6
|
||||
// therefore if we receive byte `7` (or larger than that) we'll know we received a versioned packet,
|
||||
// otherwise we should treat it as legacy
|
||||
/// Increment it whenever we perform any breaking change in the wire format!
|
||||
const CURRENT_PACKET_VERSION_NUMBER: u8 = 7;
|
||||
|
||||
// TODO: ask @AP about the choice of below algorithms
|
||||
|
||||
/// Hashing algorithm used during hkdf for ephemeral shared key generation per sphinx packet payload.
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::FRAG_ID_LEN;
|
||||
use nymsphinx_types::header::HEADER_SIZE;
|
||||
use nymsphinx_types::PAYLOAD_OVERHEAD_SIZE;
|
||||
use std::convert::TryFrom;
|
||||
use std::str::FromStr;
|
||||
|
||||
// it's up to the smart people to figure those values out : )
|
||||
const REGULAR_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 2 * 1024;
|
||||
@@ -15,11 +16,16 @@ const REGULAR_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 2 * 102
|
||||
const ACK_IV_SIZE: usize = 16;
|
||||
|
||||
const ACK_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + ACK_IV_SIZE + FRAG_ID_LEN;
|
||||
const EXTENDED_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 32 * 1024;
|
||||
const EXTENDED_PACKET_SIZE_8: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 8 * 1024;
|
||||
const EXTENDED_PACKET_SIZE_16: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 16 * 1024;
|
||||
const EXTENDED_PACKET_SIZE_32: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 32 * 1024;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidPacketSize;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InvalidExtendedPacketSize;
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PacketSize {
|
||||
@@ -30,7 +36,28 @@ pub enum PacketSize {
|
||||
AckPacket = 2,
|
||||
|
||||
// for example for streaming fast and furious in uncompressed 10bit 4K HDR quality
|
||||
ExtendedPacket = 3,
|
||||
ExtendedPacket32 = 3,
|
||||
|
||||
// for example for streaming fast and furious in heavily compressed lossy RealPlayer quality
|
||||
ExtendedPacket8 = 4,
|
||||
|
||||
// for example for streaming fast and furious in compressed XviD quality
|
||||
ExtendedPacket16 = 5,
|
||||
}
|
||||
|
||||
impl FromStr for PacketSize {
|
||||
type Err = InvalidPacketSize;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"regular" => Ok(Self::RegularPacket),
|
||||
"ack" => Ok(Self::AckPacket),
|
||||
"extended8" => Ok(Self::ExtendedPacket8),
|
||||
"extended16" => Ok(Self::ExtendedPacket16),
|
||||
"extended32" => Ok(Self::ExtendedPacket32),
|
||||
_ => Err(InvalidPacketSize),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for PacketSize {
|
||||
@@ -40,7 +67,9 @@ impl TryFrom<u8> for PacketSize {
|
||||
match value {
|
||||
_ if value == (PacketSize::RegularPacket as u8) => Ok(Self::RegularPacket),
|
||||
_ if value == (PacketSize::AckPacket as u8) => Ok(Self::AckPacket),
|
||||
_ if value == (PacketSize::ExtendedPacket as u8) => Ok(Self::ExtendedPacket),
|
||||
_ if value == (PacketSize::ExtendedPacket8 as u8) => Ok(Self::ExtendedPacket8),
|
||||
_ if value == (PacketSize::ExtendedPacket16 as u8) => Ok(Self::ExtendedPacket16),
|
||||
_ if value == (PacketSize::ExtendedPacket32 as u8) => Ok(Self::ExtendedPacket32),
|
||||
_ => Err(InvalidPacketSize),
|
||||
}
|
||||
}
|
||||
@@ -51,7 +80,9 @@ impl PacketSize {
|
||||
match self {
|
||||
PacketSize::RegularPacket => REGULAR_PACKET_SIZE,
|
||||
PacketSize::AckPacket => ACK_PACKET_SIZE,
|
||||
PacketSize::ExtendedPacket => EXTENDED_PACKET_SIZE,
|
||||
PacketSize::ExtendedPacket8 => EXTENDED_PACKET_SIZE_8,
|
||||
PacketSize::ExtendedPacket16 => EXTENDED_PACKET_SIZE_16,
|
||||
PacketSize::ExtendedPacket32 => EXTENDED_PACKET_SIZE_32,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,12 +99,33 @@ impl PacketSize {
|
||||
Ok(PacketSize::RegularPacket)
|
||||
} else if PacketSize::AckPacket.size() == size {
|
||||
Ok(PacketSize::AckPacket)
|
||||
} else if PacketSize::ExtendedPacket.size() == size {
|
||||
Ok(PacketSize::ExtendedPacket)
|
||||
} else if PacketSize::ExtendedPacket8.size() == size {
|
||||
Ok(PacketSize::ExtendedPacket8)
|
||||
} else if PacketSize::ExtendedPacket16.size() == size {
|
||||
Ok(PacketSize::ExtendedPacket16)
|
||||
} else if PacketSize::ExtendedPacket32.size() == size {
|
||||
Ok(PacketSize::ExtendedPacket32)
|
||||
} else {
|
||||
Err(InvalidPacketSize)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_extended_size(&self) -> bool {
|
||||
match self {
|
||||
PacketSize::RegularPacket | PacketSize::AckPacket => false,
|
||||
PacketSize::ExtendedPacket8
|
||||
| PacketSize::ExtendedPacket16
|
||||
| PacketSize::ExtendedPacket32 => true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_extended_size(self) -> Option<Self> {
|
||||
if self.is_extended_size() {
|
||||
Some(self)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PacketSize {
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{PacketSize, CURRENT_PACKET_VERSION_NUMBER};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum PacketVersion {
|
||||
// this will allow updated mixnodes to still understand packets from before the update
|
||||
Legacy,
|
||||
Versioned(u8),
|
||||
}
|
||||
|
||||
impl PacketVersion {
|
||||
pub fn new(use_legacy: bool) -> Self {
|
||||
if use_legacy {
|
||||
Self::new_legacy()
|
||||
} else {
|
||||
Self::new_versioned(CURRENT_PACKET_VERSION_NUMBER)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_legacy() -> Self {
|
||||
PacketVersion::Legacy
|
||||
}
|
||||
|
||||
pub fn new_versioned(version: u8) -> Self {
|
||||
PacketVersion::Versioned(version)
|
||||
}
|
||||
|
||||
pub fn is_legacy(&self) -> bool {
|
||||
matches!(self, PacketVersion::Legacy)
|
||||
}
|
||||
|
||||
pub fn as_u8(&self) -> Option<u8> {
|
||||
match self {
|
||||
PacketVersion::Legacy => None,
|
||||
PacketVersion::Versioned(version) => Some(*version),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for PacketVersion {
|
||||
fn from(v: u8) -> Self {
|
||||
match v {
|
||||
n if n == PacketSize::RegularPacket as u8 => PacketVersion::Legacy,
|
||||
n if n == PacketSize::AckPacket as u8 => PacketVersion::Legacy,
|
||||
n if n == PacketSize::ExtendedPacket8 as u8 => PacketVersion::Legacy,
|
||||
n if n == PacketSize::ExtendedPacket16 as u8 => PacketVersion::Legacy,
|
||||
n if n == PacketSize::ExtendedPacket32 as u8 => PacketVersion::Legacy,
|
||||
n => PacketVersion::Versioned(n),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PacketVersion {
|
||||
fn default() -> Self {
|
||||
PacketVersion::Versioned(CURRENT_PACKET_VERSION_NUMBER)
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ pub struct DelegationWithEverything {
|
||||
|
||||
// DEPRECATED, IF POSSIBLE TRY TO DISCONTINUE USE OF IT!
|
||||
pub pending_events: Vec<DelegationEvent>,
|
||||
pub mixnode_is_unbonding: Option<bool>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))]
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
- vesting-contract: added query for obtaining contract build information ([#1726])
|
||||
|
||||
[#1726]: https://github.com/nymtech/nym/pull/1726
|
||||
|
||||
|
||||
## [nym-contracts-v1.0.2](https://github.com/nymtech/nym/tree/nym-contracts-v1.0.2) (2022-09-13)
|
||||
|
||||
### Added
|
||||
|
||||
Generated
+6
@@ -221,7 +221,9 @@ name = "contracts-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1606,12 +1608,14 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
name = "vesting-contract"
|
||||
version = "1.1.0"
|
||||
dependencies = [
|
||||
"contracts-common",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"mixnet-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"vergen",
|
||||
"vesting-contract-common",
|
||||
]
|
||||
|
||||
@@ -1619,7 +1623,9 @@ dependencies = [
|
||||
name = "vesting-contract-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"contracts-common",
|
||||
"cosmwasm-std",
|
||||
"log",
|
||||
"mixnet-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
|
||||
@@ -497,6 +497,7 @@ mod tests {
|
||||
initial_rewarding_params: InitialRewardingParams {
|
||||
initial_reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(),
|
||||
initial_staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(),
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
sybil_resistance: Percent::from_percentage_value(23).unwrap(),
|
||||
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
|
||||
interval_pool_emission: Percent::from_percentage_value(1).unwrap(),
|
||||
@@ -535,6 +536,7 @@ mod tests {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(),
|
||||
staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(),
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
epoch_reward_budget: expected_epoch_reward_budget,
|
||||
stake_saturation_point: expected_stake_saturation_point,
|
||||
sybil_resistance: Percent::from_percentage_value(23).unwrap(),
|
||||
|
||||
@@ -1220,6 +1220,7 @@ mod tests {
|
||||
let update = IntervalRewardingParamsUpdate {
|
||||
reward_pool: Some(before.interval.reward_pool / two),
|
||||
staking_supply: Some(before.interval.staking_supply * four),
|
||||
staking_supply_scale_factor: None,
|
||||
sybil_resistance_percent: Some(Percent::from_percentage_value(42).unwrap()),
|
||||
active_set_work_factor: None,
|
||||
interval_pool_emission: None,
|
||||
|
||||
@@ -21,7 +21,8 @@ pub(crate) fn apply_reward_pool_changes(
|
||||
|
||||
let reward_pool = rewarding_params.interval.reward_pool - pending_pool_change.removed
|
||||
+ pending_pool_change.added;
|
||||
let staking_supply = rewarding_params.interval.staking_supply + pending_pool_change.removed;
|
||||
let staking_supply = rewarding_params.interval.staking_supply
|
||||
+ rewarding_params.interval.staking_supply_scale_factor * pending_pool_change.removed;
|
||||
let epoch_reward_budget = reward_pool / interval.epochs_in_interval().into_base_decimal()?
|
||||
* rewarding_params.interval.interval_pool_emission;
|
||||
let stake_saturation_point =
|
||||
|
||||
@@ -1718,6 +1718,7 @@ pub mod tests {
|
||||
let update = IntervalRewardingParamsUpdate {
|
||||
reward_pool: None,
|
||||
staking_supply: None,
|
||||
staking_supply_scale_factor: None,
|
||||
sybil_resistance_percent: None,
|
||||
active_set_work_factor: None,
|
||||
interval_pool_emission: None,
|
||||
@@ -1750,6 +1751,7 @@ pub mod tests {
|
||||
let empty_update = IntervalRewardingParamsUpdate {
|
||||
reward_pool: None,
|
||||
staking_supply: None,
|
||||
staking_supply_scale_factor: None,
|
||||
sybil_resistance_percent: None,
|
||||
active_set_work_factor: None,
|
||||
interval_pool_emission: None,
|
||||
@@ -1769,6 +1771,7 @@ pub mod tests {
|
||||
let update = IntervalRewardingParamsUpdate {
|
||||
reward_pool: None,
|
||||
staking_supply: None,
|
||||
staking_supply_scale_factor: None,
|
||||
sybil_resistance_percent: None,
|
||||
active_set_work_factor: None,
|
||||
interval_pool_emission: None,
|
||||
@@ -1803,6 +1806,7 @@ pub mod tests {
|
||||
let update = IntervalRewardingParamsUpdate {
|
||||
reward_pool: None,
|
||||
staking_supply: None,
|
||||
staking_supply_scale_factor: None,
|
||||
sybil_resistance_percent: None,
|
||||
active_set_work_factor: None,
|
||||
interval_pool_emission: None,
|
||||
@@ -1827,6 +1831,7 @@ pub mod tests {
|
||||
let update = IntervalRewardingParamsUpdate {
|
||||
reward_pool: None,
|
||||
staking_supply: None,
|
||||
staking_supply_scale_factor: None,
|
||||
sybil_resistance_percent: None,
|
||||
active_set_work_factor: None,
|
||||
interval_pool_emission: None,
|
||||
@@ -1867,6 +1872,7 @@ pub mod tests {
|
||||
let update = IntervalRewardingParamsUpdate {
|
||||
reward_pool: Some(old.interval.reward_pool / two),
|
||||
staking_supply: Some(old.interval.staking_supply * four),
|
||||
staking_supply_scale_factor: None,
|
||||
sybil_resistance_percent: None,
|
||||
active_set_work_factor: None,
|
||||
interval_pool_emission: None,
|
||||
|
||||
@@ -768,6 +768,7 @@ pub mod test_helpers {
|
||||
InitialRewardingParams {
|
||||
initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
|
||||
initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
|
||||
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
|
||||
|
||||
@@ -15,6 +15,7 @@ crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
|
||||
cosmwasm-std = { version = "1.0.0 "}
|
||||
@@ -22,4 +23,7 @@ cw-storage-plus = { version = "0.13.4", features = ["iterator"] }
|
||||
|
||||
schemars = "0.8"
|
||||
serde = { version = "1.0", default-features = false, features = ["derive"] }
|
||||
thiserror = { version = "1.0" }
|
||||
thiserror = { version = "1.0" }
|
||||
|
||||
[build-dependencies]
|
||||
vergen = { version = "5", default-features = false, features = ["build", "git", "rustc"] }
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use vergen::{vergen, Config};
|
||||
|
||||
fn main() {
|
||||
vergen(Config::default()).expect("failed to extract build metadata")
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
use crate::errors::ContractError;
|
||||
use crate::queued_migrations::migrate_to_v2_mixnet_contract;
|
||||
use crate::storage::{
|
||||
account_from_address, locked_pledge_cap, update_locked_pledge_cap, BlockTimestampSecs, ADMIN,
|
||||
DELEGATIONS, MIXNET_CONTRACT_ADDRESS, MIX_DENOM,
|
||||
account_from_address, BlockTimestampSecs, ADMIN, DELEGATIONS, MIXNET_CONTRACT_ADDRESS,
|
||||
MIX_DENOM,
|
||||
};
|
||||
use crate::traits::{
|
||||
DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, VestingAccount,
|
||||
};
|
||||
use crate::vesting::{populate_vesting_periods, Account};
|
||||
use contracts_common::ContractBuildInformation;
|
||||
use cosmwasm_std::{
|
||||
coin, entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, Order,
|
||||
QueryResponse, Response, StdResult, Timestamp, Uint128,
|
||||
@@ -25,8 +26,8 @@ use vesting_contract_common::messages::{
|
||||
ExecuteMsg, InitMsg, MigrateMsg, QueryMsg, VestingSpecification,
|
||||
};
|
||||
use vesting_contract_common::{
|
||||
AllDelegationsResponse, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeData,
|
||||
VestingDelegation,
|
||||
AllDelegationsResponse, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeCap,
|
||||
PledgeData, VestingDelegation,
|
||||
};
|
||||
|
||||
pub const INITIAL_LOCKED_PLEDGE_CAP: Uint128 = Uint128::new(100_000_000_000);
|
||||
@@ -59,8 +60,8 @@ pub fn execute(
|
||||
msg: ExecuteMsg,
|
||||
) -> Result<Response, ContractError> {
|
||||
match msg {
|
||||
ExecuteMsg::UpdateLockedPledgeCap { amount } => {
|
||||
try_update_locked_pledge_cap(amount, info, deps)
|
||||
ExecuteMsg::UpdateLockedPledgeCap { address, cap } => {
|
||||
try_update_locked_pledge_cap(address, cap, info, deps)
|
||||
}
|
||||
ExecuteMsg::TrackReward { amount, address } => {
|
||||
try_track_reward(deps, info, amount, &address)
|
||||
@@ -88,10 +89,12 @@ pub fn execute(
|
||||
owner_address,
|
||||
staking_address,
|
||||
vesting_spec,
|
||||
cap,
|
||||
} => try_create_periodic_vesting_account(
|
||||
&owner_address,
|
||||
staking_address,
|
||||
vesting_spec,
|
||||
cap,
|
||||
info,
|
||||
env,
|
||||
deps,
|
||||
@@ -144,14 +147,18 @@ pub fn execute(
|
||||
///
|
||||
/// Callable by ADMIN only, see [instantiate].
|
||||
pub fn try_update_locked_pledge_cap(
|
||||
amount: Uint128,
|
||||
address: String,
|
||||
cap: PledgeCap,
|
||||
info: MessageInfo,
|
||||
deps: DepsMut,
|
||||
) -> Result<Response, ContractError> {
|
||||
if info.sender != ADMIN.load(deps.storage)? {
|
||||
return Err(ContractError::NotAdmin(info.sender.as_str().to_string()));
|
||||
}
|
||||
update_locked_pledge_cap(amount, deps.storage)?;
|
||||
let mut account = account_from_address(&address, deps.storage, deps.api)?;
|
||||
|
||||
account.pledge_cap = Some(cap);
|
||||
// update_locked_pledge_cap(amount, deps.storage)?;
|
||||
Ok(Response::default())
|
||||
}
|
||||
|
||||
@@ -430,6 +437,7 @@ fn try_create_periodic_vesting_account(
|
||||
owner_address: &str,
|
||||
staking_address: Option<String>,
|
||||
vesting_spec: Option<VestingSpecification>,
|
||||
cap: Option<PledgeCap>,
|
||||
info: MessageInfo,
|
||||
env: Env,
|
||||
deps: DepsMut<'_>,
|
||||
@@ -437,6 +445,7 @@ fn try_create_periodic_vesting_account(
|
||||
if info.sender != ADMIN.load(deps.storage)? {
|
||||
return Err(ContractError::NotAdmin(info.sender.as_str().to_string()));
|
||||
}
|
||||
|
||||
let mix_denom = MIX_DENOM.load(deps.storage)?;
|
||||
|
||||
let account_exists = account_from_address(owner_address, deps.storage, deps.api).is_ok();
|
||||
@@ -452,6 +461,11 @@ fn try_create_periodic_vesting_account(
|
||||
|
||||
let owner_address = deps.api.addr_validate(owner_address)?;
|
||||
let staking_address = if let Some(staking_address) = staking_address {
|
||||
let staking_account_exists =
|
||||
account_from_address(&staking_address, deps.storage, deps.api).is_ok();
|
||||
if staking_account_exists {
|
||||
return Err(ContractError::StakingAccountAlreadyExists(staking_address));
|
||||
}
|
||||
Some(deps.api.addr_validate(&staking_address)?)
|
||||
} else {
|
||||
None
|
||||
@@ -472,6 +486,7 @@ fn try_create_periodic_vesting_account(
|
||||
coin.clone(),
|
||||
start_time,
|
||||
periods,
|
||||
cap,
|
||||
deps.storage,
|
||||
)?;
|
||||
|
||||
@@ -486,7 +501,7 @@ fn try_create_periodic_vesting_account(
|
||||
#[entry_point]
|
||||
pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
|
||||
let query_res = match msg {
|
||||
QueryMsg::GetLockedPledgeCap {} => to_binary(&get_locked_pledge_cap(deps)),
|
||||
QueryMsg::GetContractVersion {} => to_binary(&get_contract_version()),
|
||||
QueryMsg::LockedCoins {
|
||||
vesting_account_address,
|
||||
block_time,
|
||||
@@ -567,11 +582,6 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
|
||||
Ok(query_res?)
|
||||
}
|
||||
|
||||
/// Get locked_pledge_cap, the hard cap for staking/bonding with unvested tokens.
|
||||
pub fn get_locked_pledge_cap(deps: Deps<'_>) -> Uint128 {
|
||||
locked_pledge_cap(deps.storage)
|
||||
}
|
||||
|
||||
/// Get current vesting period for a given [crate::vesting::Account].
|
||||
pub fn try_get_current_vesting_period(
|
||||
address: &str,
|
||||
@@ -598,6 +608,21 @@ pub fn try_get_account(address: &str, deps: Deps<'_>) -> Result<Account, Contrac
|
||||
account_from_address(address, deps.storage, deps.api)
|
||||
}
|
||||
|
||||
/// Gets build information of this contract.
|
||||
pub fn get_contract_version() -> ContractBuildInformation {
|
||||
// as per docs
|
||||
// env! macro will expand to the value of the named environment variable at
|
||||
// compile time, yielding an expression of type `&'static str`
|
||||
ContractBuildInformation {
|
||||
build_timestamp: env!("VERGEN_BUILD_TIMESTAMP").to_string(),
|
||||
build_version: env!("VERGEN_BUILD_SEMVER").to_string(),
|
||||
commit_sha: env!("VERGEN_GIT_SHA").to_string(),
|
||||
commit_timestamp: env!("VERGEN_GIT_COMMIT_TIMESTAMP").to_string(),
|
||||
commit_branch: env!("VERGEN_GIT_BRANCH").to_string(),
|
||||
rustc_version: env!("VERGEN_RUSTC_SEMVER").to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets currently locked coins, see [crate::traits::VestingAccount::locked_coins]
|
||||
pub fn try_get_locked_coins(
|
||||
vesting_account_address: &str,
|
||||
|
||||
@@ -44,6 +44,8 @@ pub enum ContractError {
|
||||
InvalidAddress(String),
|
||||
#[error("VESTING ({}): Account already exists: {0}", line!())]
|
||||
AccountAlreadyExists(String),
|
||||
#[error("VESTING ({}): Staking account already exists: {0}", line!())]
|
||||
StakingAccountAlreadyExists(String),
|
||||
#[error("VESTING ({}): Too few coins sent for vesting account creation, sent {sent}, need at least {need}", line!())]
|
||||
MinVestingFunds { sent: u128, need: u128 },
|
||||
#[error("VESTING ({}): Maximum amount of locked coins has already been pledged: {current}, cap is {cap}", line!())]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::errors::ContractError;
|
||||
use crate::vesting::Account;
|
||||
use crate::{contract::INITIAL_LOCKED_PLEDGE_CAP, errors::ContractError};
|
||||
use cosmwasm_std::{Addr, Api, Storage, Uint128};
|
||||
use cw_storage_plus::{Item, Map};
|
||||
use mixnet_contract_common::{IdentityKey, MixId};
|
||||
@@ -20,21 +20,6 @@ pub const DELEGATIONS: Map<'_, (u32, MixId, BlockTimestampSecs), Uint128> = Map:
|
||||
pub const ADMIN: Item<'_, String> = Item::new("adm");
|
||||
pub const MIXNET_CONTRACT_ADDRESS: Item<'_, String> = Item::new("mix");
|
||||
pub const MIX_DENOM: Item<'_, String> = Item::new("den");
|
||||
pub const LOCKED_PLEDGE_CAP: Item<'_, Uint128> = Item::new("lck");
|
||||
|
||||
pub fn locked_pledge_cap(storage: &dyn Storage) -> Uint128 {
|
||||
LOCKED_PLEDGE_CAP
|
||||
.load(storage)
|
||||
.unwrap_or(INITIAL_LOCKED_PLEDGE_CAP)
|
||||
}
|
||||
|
||||
pub fn update_locked_pledge_cap(
|
||||
amount: Uint128,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<(), ContractError> {
|
||||
LOCKED_PLEDGE_CAP.save(storage, &amount)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn save_delegation(
|
||||
key: (u32, MixId, BlockTimestampSecs),
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
pub mod helpers {
|
||||
use crate::contract::instantiate;
|
||||
use crate::vesting::{populate_vesting_periods, Account};
|
||||
use contracts_common::Percent;
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info, MockApi, MockQuerier};
|
||||
use cosmwasm_std::{Addr, Coin, Empty, Env, MemoryStorage, OwnedDeps, Storage, Uint128};
|
||||
use std::str::FromStr;
|
||||
use vesting_contract_common::messages::{InitMsg, VestingSpecification};
|
||||
use vesting_contract_common::PledgeCap;
|
||||
|
||||
pub const TEST_COIN_DENOM: &str = "unym";
|
||||
|
||||
@@ -37,6 +40,7 @@ pub mod helpers {
|
||||
},
|
||||
start_time_ts,
|
||||
periods,
|
||||
None,
|
||||
storage,
|
||||
)
|
||||
.unwrap()
|
||||
@@ -56,6 +60,29 @@ pub mod helpers {
|
||||
},
|
||||
start_time,
|
||||
periods,
|
||||
Some(PledgeCap::from_str("0.1").unwrap()),
|
||||
storage,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn vesting_account_percent_fixture(storage: &mut dyn Storage, env: &Env) -> Account {
|
||||
let start_time = env.block.time;
|
||||
let periods =
|
||||
populate_vesting_periods(start_time.seconds(), VestingSpecification::default());
|
||||
|
||||
Account::new(
|
||||
Addr::unchecked("owner"),
|
||||
Some(Addr::unchecked("staking")),
|
||||
Coin {
|
||||
amount: Uint128::new(1_000_000_000_000),
|
||||
denom: TEST_COIN_DENOM.to_string(),
|
||||
},
|
||||
start_time,
|
||||
periods,
|
||||
Some(PledgeCap::Percent(
|
||||
Percent::from_percentage_value(10).unwrap(),
|
||||
)),
|
||||
storage,
|
||||
)
|
||||
.unwrap()
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::errors::ContractError;
|
||||
use crate::storage::locked_pledge_cap;
|
||||
use crate::storage::save_delegation;
|
||||
use crate::storage::MIXNET_CONTRACT_ADDRESS;
|
||||
use crate::traits::DelegatingAccount;
|
||||
@@ -38,8 +37,9 @@ impl DelegatingAccount for Account {
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Response, ContractError> {
|
||||
let current_balance = self.load_balance(storage)?;
|
||||
let total_pledged_after = self.total_pledged_locked(storage, env)? + coin.amount;
|
||||
let locked_pledge_cap = locked_pledge_cap(storage);
|
||||
let total_pledged_locked = self.total_pledged_locked(storage, env)?;
|
||||
let total_pledged_after = total_pledged_locked + coin.amount;
|
||||
let locked_pledge_cap = self.absolute_pledge_cap()?;
|
||||
|
||||
if locked_pledge_cap < total_pledged_after {
|
||||
return Err(ContractError::LockedPledgeCapReached {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use super::PledgeData;
|
||||
use crate::errors::ContractError;
|
||||
use crate::storage::locked_pledge_cap;
|
||||
use crate::storage::MIXNET_CONTRACT_ADDRESS;
|
||||
use crate::traits::GatewayBondingAccount;
|
||||
use crate::traits::VestingAccount;
|
||||
@@ -22,8 +21,9 @@ impl GatewayBondingAccount for Account {
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Response, ContractError> {
|
||||
let current_balance = self.load_balance(storage)?;
|
||||
let total_pledged_after = self.total_pledged_locked(storage, env)? + pledge.amount;
|
||||
let locked_pledge_cap = locked_pledge_cap(storage);
|
||||
let total_pledged_locked = self.total_pledged_locked(storage, env)?;
|
||||
let total_pledged_after = total_pledged_locked + pledge.amount;
|
||||
let locked_pledge_cap = self.absolute_pledge_cap()?;
|
||||
|
||||
if locked_pledge_cap < total_pledged_after {
|
||||
return Err(ContractError::LockedPledgeCapReached {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
use super::Account;
|
||||
use crate::errors::ContractError;
|
||||
use crate::storage::locked_pledge_cap;
|
||||
use crate::storage::MIXNET_CONTRACT_ADDRESS;
|
||||
use crate::traits::MixnodeBondingAccount;
|
||||
use crate::traits::VestingAccount;
|
||||
@@ -39,8 +38,9 @@ impl MixnodeBondingAccount for Account {
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Response, ContractError> {
|
||||
let current_balance = self.load_balance(storage)?;
|
||||
let total_pledged_after = self.total_pledged_locked(storage, env)? + pledge.amount;
|
||||
let locked_pledge_cap = locked_pledge_cap(storage);
|
||||
let total_pledged_locked = self.total_pledged_locked(storage, env)?;
|
||||
let total_pledged_after = total_pledged_locked + pledge.amount;
|
||||
let locked_pledge_cap = self.absolute_pledge_cap()?;
|
||||
|
||||
if locked_pledge_cap < total_pledged_after {
|
||||
return Err(ContractError::LockedPledgeCapReached {
|
||||
|
||||
@@ -5,12 +5,13 @@ use crate::storage::{
|
||||
remove_delegation, remove_gateway_pledge, save_account, save_balance, save_bond_pledge,
|
||||
save_gateway_pledge, save_withdrawn, BlockTimestampSecs, DELEGATIONS, KEY,
|
||||
};
|
||||
use crate::traits::VestingAccount;
|
||||
use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128};
|
||||
use cw_storage_plus::Bound;
|
||||
use mixnet_contract_common::MixId;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use vesting_contract_common::{Period, PledgeData};
|
||||
use vesting_contract_common::{Period, PledgeCap, PledgeData};
|
||||
|
||||
mod delegating_account;
|
||||
mod gateway_bonding_account;
|
||||
@@ -31,6 +32,8 @@ pub struct Account {
|
||||
pub periods: Vec<VestingPeriod>,
|
||||
pub coin: Coin,
|
||||
storage_key: u32,
|
||||
#[serde(default)]
|
||||
pub pledge_cap: Option<PledgeCap>,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
@@ -40,6 +43,7 @@ impl Account {
|
||||
coin: Coin,
|
||||
start_time: Timestamp,
|
||||
periods: Vec<VestingPeriod>,
|
||||
pledge_cap: Option<PledgeCap>,
|
||||
storage: &mut dyn Storage,
|
||||
) -> Result<Self, ContractError> {
|
||||
let storage_key = generate_storage_key(storage)?;
|
||||
@@ -51,12 +55,24 @@ impl Account {
|
||||
periods,
|
||||
coin,
|
||||
storage_key,
|
||||
pledge_cap,
|
||||
};
|
||||
save_account(&account, storage)?;
|
||||
account.save_balance(amount, storage)?;
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
pub fn pledge_cap(&self) -> PledgeCap {
|
||||
self.pledge_cap.clone().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn absolute_pledge_cap(&self) -> Result<Uint128, ContractError> {
|
||||
match self.pledge_cap() {
|
||||
PledgeCap::Absolute(cap) => Ok(cap),
|
||||
PledgeCap::Percent(p) => Ok(p * self.get_original_vesting()?.amount.amount),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn coin(&self) -> Coin {
|
||||
self.coin.clone()
|
||||
}
|
||||
|
||||
@@ -155,15 +155,8 @@ impl VestingAccount for Account {
|
||||
let block_time = block_time.unwrap_or(env.block.time);
|
||||
let delegated_free = self.get_delegated_free(Some(block_time), env, storage)?;
|
||||
|
||||
let period = self.get_current_vesting_period(block_time)?;
|
||||
let start_time = match period {
|
||||
Period::Before => 0,
|
||||
Period::After => u64::MAX,
|
||||
Period::In(idx) => self.periods[idx].start_time,
|
||||
};
|
||||
|
||||
let delegations_before_start_time =
|
||||
self.total_delegations_at_timestamp(storage, start_time)?;
|
||||
self.total_delegations_at_timestamp(storage, block_time.seconds())?;
|
||||
|
||||
let amount = delegations_before_start_time - delegated_free.amount;
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ pub fn populate_vesting_periods(
|
||||
mod tests {
|
||||
use crate::contract::*;
|
||||
use crate::storage::*;
|
||||
use crate::support::tests::helpers::vesting_account_percent_fixture;
|
||||
use crate::support::tests::helpers::{
|
||||
init_contract, vesting_account_mid_fixture, vesting_account_new_fixture, TEST_COIN_DENOM,
|
||||
};
|
||||
@@ -51,6 +52,7 @@ mod tests {
|
||||
use mixnet_contract_common::{Gateway, MixNode, Percent};
|
||||
use vesting_contract_common::messages::{ExecuteMsg, VestingSpecification};
|
||||
use vesting_contract_common::Period;
|
||||
use vesting_contract_common::PledgeCap;
|
||||
|
||||
#[test]
|
||||
fn test_account_creation() {
|
||||
@@ -61,6 +63,7 @@ mod tests {
|
||||
owner_address: "owner".to_string(),
|
||||
staking_address: Some("staking".to_string()),
|
||||
vesting_spec: None,
|
||||
cap: Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))),
|
||||
};
|
||||
// Try creating an account when not admin
|
||||
let response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone());
|
||||
@@ -429,12 +432,38 @@ mod tests {
|
||||
assert_eq!(locked_coins.amount, Uint128::new(660_000_000_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_percent_cap() {
|
||||
let mut deps = init_contract();
|
||||
let env = mock_env();
|
||||
|
||||
let account = vesting_account_percent_fixture(&mut deps.storage, &env);
|
||||
|
||||
assert_eq!(
|
||||
account.absolute_pledge_cap().unwrap(),
|
||||
Uint128::new(100_000_000_000)
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delegations() {
|
||||
let mut deps = init_contract();
|
||||
let env = mock_env();
|
||||
|
||||
let account = vesting_account_new_fixture(&mut deps.storage, &env);
|
||||
// let account = vesting_account_new_fixture(&mut deps.storage, &env);
|
||||
|
||||
let msg = ExecuteMsg::CreateAccount {
|
||||
owner_address: "owner".to_string(),
|
||||
staking_address: Some("staking".to_string()),
|
||||
vesting_spec: None,
|
||||
cap: Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))),
|
||||
};
|
||||
let info = mock_info("admin", &coins(1_000_000_000_000, TEST_COIN_DENOM));
|
||||
|
||||
let _response = execute(deps.as_mut(), env.clone(), info.clone(), msg.clone());
|
||||
let account = load_account(&Addr::unchecked("owner"), &deps.storage)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// Try delegating too much
|
||||
let err = account.try_delegate_to_mixnode(
|
||||
@@ -474,7 +503,7 @@ mod tests {
|
||||
let balance = account.load_balance(&deps.storage).unwrap();
|
||||
assert_eq!(balance, Uint128::new(910000000000));
|
||||
|
||||
// Try delegating too much again
|
||||
// Try delegating too much againcalca
|
||||
let err = account.try_delegate_to_mixnode(
|
||||
1,
|
||||
Coin {
|
||||
@@ -489,6 +518,10 @@ mod tests {
|
||||
let total_delegations = account.total_delegations_for_mix(1, &deps.storage).unwrap();
|
||||
assert_eq!(Uint128::new(90_000_000_000), total_delegations);
|
||||
|
||||
let account = load_account(&Addr::unchecked("owner"), &deps.storage)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
// Current period -> block_time: None
|
||||
let delegated_free = account
|
||||
.get_delegated_free(None, &env, &deps.storage)
|
||||
@@ -853,6 +886,7 @@ mod tests {
|
||||
},
|
||||
Timestamp::from_seconds(account_creation_timestamp),
|
||||
periods,
|
||||
Some(PledgeCap::Absolute(Uint128::from(100_000_000_000u128))),
|
||||
deps.as_mut().storage,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -27,6 +27,8 @@ maxminddb = "0.23.0"
|
||||
dotenv = "0.15.0"
|
||||
|
||||
mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
network-defaults = { path = "../common/network-defaults" }
|
||||
logging = { path = "../common/logging"}
|
||||
task = { path = "../common/task" }
|
||||
validator-client = { path = "../common/client-libs/validator-client", features=["nymd-client"] }
|
||||
|
||||
@@ -58,7 +58,10 @@ impl GeoLocateTask {
|
||||
continue;
|
||||
}
|
||||
|
||||
match geo_ip.query(&cache_item.mix_node().host) {
|
||||
match geo_ip.query(
|
||||
&cache_item.mix_node().host,
|
||||
Some(cache_item.mix_node().mix_port),
|
||||
) {
|
||||
Ok(opt) => match opt {
|
||||
Some(location) => {
|
||||
let location = Location::new(location);
|
||||
|
||||
@@ -4,9 +4,14 @@
|
||||
use isocountry::CountryCode;
|
||||
use log::warn;
|
||||
use maxminddb::{geoip2::Country, MaxMindDBError, Reader};
|
||||
use std::{net::IpAddr, str::FromStr, sync::Arc};
|
||||
use std::{
|
||||
net::{IpAddr, ToSocketAddrs},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
const DEFAULT_DATABASE_PATH: &str = "./geo_ip/GeoLite2-Country.mmdb";
|
||||
const FAKE_PORT: u16 = 1234;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GeoIpError {
|
||||
@@ -52,10 +57,27 @@ impl GeoIp {
|
||||
GeoIp { db: reader }
|
||||
}
|
||||
|
||||
pub fn query(&self, address: &str) -> Result<Option<Location>, GeoIpError> {
|
||||
let ip: IpAddr = FromStr::from_str(address).map_err(|e| {
|
||||
error!("Fail to create IpAddr from {}: {}", &address, e);
|
||||
GeoIpError::NoValidIP
|
||||
pub fn query(&self, address: &str, port: Option<u16>) -> Result<Option<Location>, GeoIpError> {
|
||||
let ip: IpAddr = FromStr::from_str(address).or_else(|_| {
|
||||
debug!(
|
||||
"Fail to create IpAddr from {}. Trying using internal lookup...",
|
||||
&address
|
||||
);
|
||||
let p = port.unwrap_or(FAKE_PORT);
|
||||
let socket_addr = (address, p)
|
||||
.to_socket_addrs()
|
||||
.map_err(|e| {
|
||||
error!("Fail to resolve IP address from {}:{}: {}", &address, p, e);
|
||||
GeoIpError::NoValidIP
|
||||
})?
|
||||
.next()
|
||||
.ok_or_else(|| {
|
||||
error!("Fail to resolve IP address from {}:{}", &address, p);
|
||||
GeoIpError::NoValidIP
|
||||
})?;
|
||||
let ip = socket_addr.ip();
|
||||
debug!("Internal lookup succeed, resolved ip: {}", ip);
|
||||
Ok(ip)
|
||||
})?;
|
||||
let result = self
|
||||
.db
|
||||
|
||||
@@ -39,7 +39,7 @@ fn find_location(request: &Request<'_>) -> Result<Location, (Status, LocationErr
|
||||
let location = geo_ip
|
||||
.0
|
||||
.clone()
|
||||
.query(&ip)
|
||||
.query(&ip, None)
|
||||
.map_err(|e| match e {
|
||||
GeoIpError::NoValidIP => (Status::Forbidden, LocationError::NoIP),
|
||||
GeoIpError::InternalError => {
|
||||
|
||||
@@ -6,6 +6,7 @@ extern crate rocket_okapi;
|
||||
use clap::Parser;
|
||||
use dotenv::dotenv;
|
||||
use log::info;
|
||||
use logging::setup_logging;
|
||||
use network_defaults::setup_env;
|
||||
use task::ShutdownNotifier;
|
||||
|
||||
@@ -115,18 +116,3 @@ async fn wait_for_signal() {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_logging() {
|
||||
let mut log_builder = pretty_env_logger::formatted_timed_builder();
|
||||
if let Ok(s) = ::std::env::var("RUST_LOG") {
|
||||
log_builder.parse_filters(&s);
|
||||
} else {
|
||||
// default to 'Info'
|
||||
log_builder.filter(None, log::LevelFilter::Info);
|
||||
}
|
||||
|
||||
log_builder
|
||||
.filter_module("tokio_reactor", log::LevelFilter::Warn)
|
||||
.filter_module("reqwest", log::LevelFilter::Warn)
|
||||
.init();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use crate::client::ThreadsafeValidatorClient;
|
||||
use crate::helpers::best_effort_small_dec_to_f64;
|
||||
use crate::mix_node::models::EconomicDynamicsStats;
|
||||
use mixnet_contract_common::rewarding::helpers::truncate_decimal;
|
||||
use contracts_common::truncate_decimal;
|
||||
use mixnet_contract_common::MixId;
|
||||
|
||||
pub(crate) async fn retrieve_mixnode_econ_stats(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use crate::cache::Cache;
|
||||
use crate::mix_nodes::location::Location;
|
||||
use contracts_common::Percent;
|
||||
use mixnet_contract_common::Delegation;
|
||||
use mixnet_contract_common::{Addr, Coin, Layer, MixId, MixNode};
|
||||
use serde::Deserialize;
|
||||
@@ -24,6 +25,7 @@ pub(crate) enum MixnodeStatus {
|
||||
pub(crate) struct PrettyDetailedMixNodeBond {
|
||||
// I leave this to @MS to refactor this type as a lot of things here are redundant thanks to
|
||||
// the existence of `MixNodeDetails`
|
||||
pub mix_id: MixId,
|
||||
pub location: Option<Location>,
|
||||
pub status: MixnodeStatus,
|
||||
pub pledge_amount: Coin,
|
||||
@@ -35,6 +37,8 @@ pub(crate) struct PrettyDetailedMixNodeBond {
|
||||
pub avg_uptime: u8,
|
||||
pub estimated_operator_apy: f64,
|
||||
pub estimated_delegators_apy: f64,
|
||||
pub operating_cost: Coin,
|
||||
pub profit_margin_percent: Percent,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)]
|
||||
|
||||
@@ -126,19 +126,6 @@ impl ThreadsafeMixNodesCache {
|
||||
self.mixnodes.read().await.get_mixnode(mix_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_mixnode_by_identity(
|
||||
&self,
|
||||
pubkey: &str,
|
||||
) -> Option<MixNodeBondAnnotated> {
|
||||
let all_nodes = self.get_mixnodes().await?;
|
||||
for (_, node) in all_nodes {
|
||||
if node.mix_node().identity_key == pubkey {
|
||||
return Some(node);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn get_mixnodes(&self) -> Option<HashMap<MixId, MixNodeBondAnnotated>> {
|
||||
self.mixnodes.read().await.get_mixnodes()
|
||||
}
|
||||
@@ -154,6 +141,7 @@ impl ThreadsafeMixNodesCache {
|
||||
let rewarding_info = &node.mixnode_details.rewarding_details;
|
||||
|
||||
PrettyDetailedMixNodeBond {
|
||||
mix_id,
|
||||
location: location.and_then(|l| l.location.clone()),
|
||||
status: mixnodes_guard.determine_node_status(mix_id),
|
||||
pledge_amount: truncate_reward(rewarding_info.operator, denom),
|
||||
@@ -165,6 +153,8 @@ impl ThreadsafeMixNodesCache {
|
||||
stake_saturation: best_effort_small_dec_to_f64(node.stake_saturation) as f32,
|
||||
estimated_operator_apy: best_effort_small_dec_to_f64(node.estimated_operator_apy),
|
||||
estimated_delegators_apy: best_effort_small_dec_to_f64(node.estimated_delegators_apy),
|
||||
operating_cost: rewarding_info.cost_params.interval_operating_cost.clone(),
|
||||
profit_margin_percent: rewarding_info.cost_params.profit_margin_percent,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use mixnet_contract_common::MixNode;
|
||||
use mixnet_contract_common::{MixId, MixNode};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{Route, State};
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
@@ -20,41 +20,39 @@ pub fn ping_make_default_routes(settings: &OpenApiSettings) -> (Vec<Route>, Open
|
||||
openapi_get_routes_spec![settings: index]
|
||||
}
|
||||
|
||||
// TODO: I'm not deprecating this one explicitly since we don't have
|
||||
// a decision on whether nodes should be accessed (as in using URL) by id or identity key
|
||||
#[openapi(tag = "ping")]
|
||||
#[get("/<pubkey>")]
|
||||
#[get("/<mix_id>")]
|
||||
pub(crate) async fn index(
|
||||
pubkey: &str,
|
||||
mix_id: MixId,
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Option<Json<PingResponse>> {
|
||||
match state.inner.ping.clone().get(pubkey).await {
|
||||
match state.inner.ping.clone().get(mix_id).await {
|
||||
Some(cache_value) => {
|
||||
trace!("Returning cached value for {}", pubkey);
|
||||
trace!("Returning cached value for {}", mix_id);
|
||||
Some(Json(PingResponse {
|
||||
pending: cache_value.pending,
|
||||
ports: cache_value.ports,
|
||||
}))
|
||||
}
|
||||
None => {
|
||||
trace!("No cache value for {}", pubkey);
|
||||
trace!("No cache value for {}", mix_id);
|
||||
|
||||
match state.inner.get_mix_node_by_pubkey(pubkey).await {
|
||||
match state.inner.get_mix_node(mix_id).await {
|
||||
Some(node) => {
|
||||
// set status to pending, so that any HTTP requests are pending
|
||||
state.inner.ping.set_pending(pubkey).await;
|
||||
state.inner.ping.set_pending(mix_id).await;
|
||||
|
||||
// do the check
|
||||
let ports = Some(port_check(node.mix_node()).await);
|
||||
trace!("Tested mix node {}: {:?}", pubkey, ports);
|
||||
trace!("Tested mix node {}: {:?}", mix_id, ports);
|
||||
let response = PingResponse {
|
||||
ports,
|
||||
pending: false,
|
||||
};
|
||||
|
||||
// cache for 1 min
|
||||
trace!("Caching value for {}", pubkey);
|
||||
state.inner.ping.set(pubkey, response.clone()).await;
|
||||
trace!("Caching value for {}", mix_id);
|
||||
state.inner.ping.set(mix_id, response.clone()).await;
|
||||
|
||||
// return response
|
||||
Some(Json(response))
|
||||
|
||||
@@ -2,11 +2,12 @@ use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use mixnet_contract_common::MixId;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub(crate) type PingCache = HashMap<String, PingCacheItem>;
|
||||
pub(crate) type PingCache = HashMap<MixId, PingCacheItem>;
|
||||
|
||||
const PING_TTL: Duration = Duration::from_secs(60 * 5); // 5 mins, before port check will be re-tried (only while pending)
|
||||
const CACHE_TTL: Duration = Duration::from_secs(60 * 60); // 1 hour, to cache result from port check
|
||||
@@ -23,11 +24,11 @@ impl ThreadsafePingCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get(&self, identity_key: &str) -> Option<PingResponse> {
|
||||
pub(crate) async fn get(&self, mix_id: MixId) -> Option<PingResponse> {
|
||||
self.inner
|
||||
.read()
|
||||
.await
|
||||
.get(identity_key)
|
||||
.get(&mix_id)
|
||||
.filter(|cache_item| cache_item.valid_until > SystemTime::now())
|
||||
.map(|cache_item| {
|
||||
if cache_item.pending {
|
||||
@@ -43,9 +44,9 @@ impl ThreadsafePingCache {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn set_pending(&self, identity_key: &str) {
|
||||
pub(crate) async fn set_pending(&self, mix_id: MixId) {
|
||||
self.inner.write().await.insert(
|
||||
identity_key.to_string(),
|
||||
mix_id,
|
||||
PingCacheItem {
|
||||
pending: true,
|
||||
valid_until: SystemTime::now() + PING_TTL,
|
||||
@@ -54,9 +55,9 @@ impl ThreadsafePingCache {
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) async fn set(&self, identity_key: &str, item: PingResponse) {
|
||||
pub(crate) async fn set(&self, mix_id: MixId, item: PingResponse) {
|
||||
self.inner.write().await.insert(
|
||||
identity_key.to_string(),
|
||||
mix_id,
|
||||
PingCacheItem {
|
||||
pending: false,
|
||||
valid_until: SystemTime::now() + CACHE_TTL,
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::path::Path;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use log::info;
|
||||
use mixnet_contract_common::{IdentityKeyRef, MixId};
|
||||
use mixnet_contract_common::MixId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::client::ThreadsafeValidatorClient;
|
||||
@@ -41,13 +41,6 @@ impl ExplorerApiState {
|
||||
pub(crate) async fn get_mix_node(&self, mix_id: MixId) -> Option<MixNodeBondAnnotated> {
|
||||
self.mixnodes.get_mixnode(mix_id).await
|
||||
}
|
||||
|
||||
pub(crate) async fn get_mix_node_by_pubkey(
|
||||
&self,
|
||||
pubkey: IdentityKeyRef<'_>,
|
||||
) -> Option<MixNodeBondAnnotated> {
|
||||
self.mixnodes.get_mixnode_by_identity(pubkey).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
"react-router-dom": "6",
|
||||
"react-simple-maps": "^2.3.0",
|
||||
"react-tooltip": "^4.2.21",
|
||||
"use-clipboard-copy": "^0.2.0"
|
||||
"use-clipboard-copy": "^0.2.0",
|
||||
"big.js": "^6.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
|
||||
@@ -14,6 +14,7 @@ export const VALIDATORS_API = `${VALIDATOR_BASE_URL}/validators`;
|
||||
export const BLOCK_API = `${VALIDATOR_API_BASE_URL}/block`;
|
||||
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
|
||||
export const UPTIME_STORY_API = `${VALIDATOR_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
|
||||
export const UPTIME_STORY_API_GATEWAY = `${VALIDATOR_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this.
|
||||
|
||||
// errors
|
||||
export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us.";
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
BLOCK_API,
|
||||
COUNTRY_DATA_API,
|
||||
GATEWAYS_API,
|
||||
UPTIME_STORY_API_GATEWAY,
|
||||
MIXNODE_API,
|
||||
MIXNODE_PING,
|
||||
MIXNODES_API,
|
||||
@@ -15,6 +16,8 @@ import {
|
||||
DelegationsResponse,
|
||||
UniqDelegationsResponse,
|
||||
GatewayResponse,
|
||||
GatewayReportResponse,
|
||||
UptimeStoryResponse,
|
||||
MixNodeDescriptionResponse,
|
||||
MixNodeResponse,
|
||||
MixNodeResponseItem,
|
||||
@@ -23,7 +26,6 @@ import {
|
||||
StatsResponse,
|
||||
StatusResponse,
|
||||
SummaryOverviewResponse,
|
||||
UptimeStoryResponse,
|
||||
ValidatorsResponse,
|
||||
} from '../typeDefs/explorer-api';
|
||||
|
||||
@@ -92,6 +94,12 @@ export class Api {
|
||||
return res.json();
|
||||
};
|
||||
|
||||
static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json();
|
||||
|
||||
static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json();
|
||||
|
||||
static fetchValidators = async (): Promise<ValidatorsResponse> => {
|
||||
const res = await fetch(VALIDATORS_API);
|
||||
const json = await res.json();
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
|
||||
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
|
||||
import { Box } from '@mui/system';
|
||||
import { cellStyles } from './Universal-DataGrid';
|
||||
import { currencyToString } from '../utils/currency';
|
||||
import { GatewayEnrichedRowType } from './Gateways';
|
||||
import { MixnodeRowType } from './MixNodes';
|
||||
|
||||
export type ColumnsType = {
|
||||
@@ -43,42 +46,60 @@ function formatCellValues(val: string | number, field: string) {
|
||||
export const DetailTable: React.FC<{
|
||||
tableName: string;
|
||||
columnsData: ColumnsType[];
|
||||
rows: MixnodeRowType[];
|
||||
}> = ({ tableName, columnsData, rows }: UniversalTableProps) => (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columnsData?.map(({ field, title, flex }) => (
|
||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
|
||||
{title}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((eachRow) => (
|
||||
<TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
{columnsData?.map((_, index) => (
|
||||
<TableCell
|
||||
key={_.title}
|
||||
component="th"
|
||||
scope="row"
|
||||
variant="body"
|
||||
sx={{
|
||||
...cellStyles,
|
||||
padding: 2,
|
||||
width: 200,
|
||||
fontSize: 14,
|
||||
}}
|
||||
data-testid={`${_.title.replace(/ /g, '-')}-value`}
|
||||
>
|
||||
{formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)}
|
||||
rows: MixnodeRowType[] | GatewayEnrichedRowType[];
|
||||
}> = ({ tableName, columnsData, rows }: UniversalTableProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
|
||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{tooltipInfo && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Tooltip
|
||||
title={tooltipInfo}
|
||||
id={field}
|
||||
placement="top-start"
|
||||
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
||||
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
|
||||
maxWidth={230}
|
||||
arrow
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{title}
|
||||
</Box>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((eachRow) => (
|
||||
<TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
{columnsData?.map((data, index) => (
|
||||
<TableCell
|
||||
key={data.title}
|
||||
component="th"
|
||||
scope="row"
|
||||
variant="body"
|
||||
sx={{
|
||||
...cellStyles,
|
||||
padding: 2,
|
||||
width: 200,
|
||||
fontSize: 14,
|
||||
}}
|
||||
data-testid={`${data.title.replace(/ /g, '-')}-value`}
|
||||
>
|
||||
{formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,48 @@
|
||||
import { GatewayResponse } from '../typeDefs/explorer-api';
|
||||
import { GatewayResponse, GatewayResponseItem, GatewayReportResponse } from '../typeDefs/explorer-api';
|
||||
|
||||
export type GatewayRowType = {
|
||||
id: string;
|
||||
owner: string;
|
||||
identity_key: string;
|
||||
identityKey: string;
|
||||
bond: number;
|
||||
host: string;
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type GatewayEnrichedRowType = GatewayRowType & {
|
||||
routingScore: string;
|
||||
avgUptime: string;
|
||||
clientsPort: number;
|
||||
mixPort: number;
|
||||
};
|
||||
|
||||
export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] {
|
||||
return !arrayOfGateways
|
||||
? []
|
||||
: arrayOfGateways.map((gw) => ({
|
||||
id: gw.owner,
|
||||
owner: gw.owner,
|
||||
identity_key: gw.gateway.identity_key || '',
|
||||
identityKey: gw.gateway.identity_key || '',
|
||||
location: gw?.gateway?.location || '',
|
||||
bond: gw.pledge_amount.amount || 0,
|
||||
host: gw.gateway.host || '',
|
||||
}));
|
||||
}
|
||||
|
||||
export function gatewayEnrichedToGridRow(
|
||||
gateway: GatewayResponseItem,
|
||||
report: GatewayReportResponse,
|
||||
): GatewayEnrichedRowType {
|
||||
return {
|
||||
id: gateway.owner,
|
||||
owner: gateway.owner,
|
||||
identityKey: gateway.gateway.identity_key || '',
|
||||
location: gateway?.gateway?.location || '',
|
||||
bond: gateway.pledge_amount.amount || 0,
|
||||
host: gateway.gateway.host || '',
|
||||
clientsPort: gateway.gateway.clients_port || 0,
|
||||
mixPort: gateway.gateway.mix_port || 0,
|
||||
routingScore: `${report.most_recent}%`,
|
||||
avgUptime: `${report.last_day || report.last_hour}%`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,6 +39,14 @@ export const EconomicsInfoColumns: ColumnsType[] = [
|
||||
tooltipInfo:
|
||||
'Percentage of the delegates rewards that the operator takes as fee before rewards are distributed to the delegates.',
|
||||
},
|
||||
{
|
||||
field: 'operatingCost',
|
||||
title: 'Operating Cost',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo:
|
||||
'Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators.',
|
||||
},
|
||||
{
|
||||
field: 'avgUptime',
|
||||
title: 'Routing Score',
|
||||
|
||||
@@ -26,6 +26,9 @@ const row: EconomicsInfoRowWithIndex = {
|
||||
profitMargin: {
|
||||
value: '10 %',
|
||||
},
|
||||
operatingCost: {
|
||||
value: '11121 NYM',
|
||||
},
|
||||
stakeSaturation: {
|
||||
value: '80 %',
|
||||
progressBarValue: 80,
|
||||
@@ -64,6 +67,9 @@ const emptyRow: EconomicsInfoRowWithIndex = {
|
||||
profitMargin: {
|
||||
value: '-',
|
||||
},
|
||||
operatingCost: {
|
||||
value: '-',
|
||||
},
|
||||
stakeSaturation: {
|
||||
value: '-',
|
||||
progressBarValue: 0,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { currencyToString } from '../../../utils/currency';
|
||||
import { currencyToString, unymToNym } from '../../../utils/currency';
|
||||
import { useMixnodeContext } from '../../../context/mixnode';
|
||||
import { ApiState, MixNodeEconomicDynamicsStatsResponse } from '../../../typeDefs/explorer-api';
|
||||
import { EconomicsInfoRowWithIndex } from './types';
|
||||
import { toPercentIntegerString } from '../../../utils';
|
||||
|
||||
const selectionChance = (economicDynamicsStats: ApiState<MixNodeEconomicDynamicsStatsResponse> | undefined) => {
|
||||
const inclusionProbability = economicDynamicsStats?.data?.active_set_inclusion_probability;
|
||||
@@ -29,9 +30,13 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
|
||||
const estimatedOperatorRewards =
|
||||
currencyToString((economicDynamicsStats?.data?.estimated_operator_reward || '').toString()) || '-';
|
||||
const stakeSaturation = economicDynamicsStats?.data?.stake_saturation || '-';
|
||||
const profitMargin = mixNode?.data?.mix_node.profit_margin_percent || '-';
|
||||
const profitMargin = mixNode?.data?.profit_margin_percent
|
||||
? toPercentIntegerString(mixNode?.data?.profit_margin_percent)
|
||||
: '-';
|
||||
const avgUptime = economicDynamicsStats?.data?.current_interval_uptime;
|
||||
|
||||
const opCost = mixNode?.data?.operating_cost;
|
||||
|
||||
return {
|
||||
id: 1,
|
||||
estimatedTotalReward: {
|
||||
@@ -50,6 +55,9 @@ export const EconomicsInfoRows = (): EconomicsInfoRowWithIndex => {
|
||||
profitMargin: {
|
||||
value: profitMargin ? `${profitMargin} %` : '-',
|
||||
},
|
||||
operatingCost: {
|
||||
value: opCost ? `${unymToNym(opCost.amount, 6)} NYM` : '-',
|
||||
},
|
||||
avgUptime: {
|
||||
value: avgUptime ? `${avgUptime} %` : '-',
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface EconomicsInfoRow {
|
||||
stakeSaturation: EconomicsRowsType;
|
||||
profitMargin: EconomicsRowsType;
|
||||
avgUptime: EconomicsRowsType;
|
||||
operatingCost: EconomicsRowsType;
|
||||
}
|
||||
|
||||
export type EconomicsInfoRowWithIndex = EconomicsInfoRow & { id: number };
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api';
|
||||
import { toPercentIntegerString } from '../../utils';
|
||||
import { unymToNym } from '../../utils/currency';
|
||||
|
||||
export type MixnodeRowType = {
|
||||
mix_id: number;
|
||||
id: string;
|
||||
status: MixnodeStatus;
|
||||
owner: string;
|
||||
@@ -15,6 +18,7 @@ export type MixnodeRowType = {
|
||||
profit_percentage: string;
|
||||
avg_uptime: string;
|
||||
stake_saturation: number;
|
||||
operating_cost: string;
|
||||
};
|
||||
|
||||
export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] {
|
||||
@@ -26,9 +30,11 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
|
||||
const delegations = Number(item.total_delegation.amount) || 0;
|
||||
const totalBond = pledge + delegations;
|
||||
const selfPercentage = ((pledge * 100) / totalBond).toFixed(2);
|
||||
const profitPercentage = item.mix_node.profit_margin_percent || 0;
|
||||
const profitPercentage = toPercentIntegerString(item.profit_margin_percent) || 0;
|
||||
const stakeSaturation = typeof item.stake_saturation === 'number' ? item.stake_saturation * 100 : 0;
|
||||
|
||||
return {
|
||||
mix_id: item.mix_id,
|
||||
id: item.owner,
|
||||
status: item.status,
|
||||
owner: item.owner,
|
||||
@@ -42,5 +48,6 @@ export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem):
|
||||
profit_percentage: `${profitPercentage}%`,
|
||||
avg_uptime: `${item.avg_uptime}%` || '-',
|
||||
stake_saturation: stakeSaturation,
|
||||
operating_cost: `${unymToNym(item.operating_cost.amount, 6)} NYM`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import { Tooltip as MUITooltip, TooltipComponentsPropsOverrides, TooltipProps } from '@mui/material';
|
||||
|
||||
type ValueType<T> = T[keyof T];
|
||||
|
||||
type Props = {
|
||||
text: string;
|
||||
id: string;
|
||||
placement?: ValueType<Pick<TooltipProps, 'placement'>>;
|
||||
tooltipSx?: TooltipComponentsPropsOverrides;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const Tooltip = ({ text, id, placement, tooltipSx, children }: Props) => (
|
||||
<MUITooltip
|
||||
title={text}
|
||||
id={id}
|
||||
placement={placement || 'top-start'}
|
||||
componentsProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
maxWidth: 200,
|
||||
background: (t) => t.palette.nym.networkExplorer.tooltip.background,
|
||||
color: (t) => t.palette.nym.networkExplorer.tooltip.color,
|
||||
'& .MuiTooltip-arrow': {
|
||||
color: (t) => t.palette.nym.networkExplorer.tooltip.background,
|
||||
},
|
||||
},
|
||||
...tooltipSx,
|
||||
},
|
||||
}}
|
||||
arrow
|
||||
>
|
||||
{children as ReactElement<any, any>}
|
||||
</MUITooltip>
|
||||
);
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from 'react';
|
||||
import { ApiState, GatewayReportResponse, UptimeStoryResponse } from '../typeDefs/explorer-api';
|
||||
import { Api } from '../api';
|
||||
import { useApiState } from './hooks';
|
||||
|
||||
/**
|
||||
* This context provides the state for a single gateway by identity key.
|
||||
*/
|
||||
|
||||
interface GatewayState {
|
||||
uptimeReport?: ApiState<GatewayReportResponse>;
|
||||
uptimeStory?: ApiState<UptimeStoryResponse>;
|
||||
}
|
||||
|
||||
export const GatewayContext = React.createContext<GatewayState>({});
|
||||
|
||||
export const useGatewayContext = (): React.ContextType<typeof GatewayContext> =>
|
||||
React.useContext<GatewayState>(GatewayContext);
|
||||
|
||||
/**
|
||||
* Provides a state context for a gateway by identity
|
||||
* @param gatewayIdentityKey The identity key of the gateway
|
||||
*/
|
||||
export const GatewayContextProvider = ({
|
||||
gatewayIdentityKey,
|
||||
children,
|
||||
}: {
|
||||
gatewayIdentityKey: string;
|
||||
children: JSX.Element;
|
||||
}) => {
|
||||
const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = useApiState<GatewayReportResponse>(
|
||||
gatewayIdentityKey,
|
||||
Api.fetchGatewayReportById,
|
||||
'Failed to fetch gateway uptime report by id',
|
||||
);
|
||||
|
||||
const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState<UptimeStoryResponse>(
|
||||
gatewayIdentityKey,
|
||||
Api.fetchGatewayUptimeStoryById,
|
||||
'Failed to fetch gateway uptime history',
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
// when the identity key changes, remove all previous data
|
||||
clearUptimeReportById();
|
||||
clearUptimeHistory();
|
||||
Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]);
|
||||
}, [gatewayIdentityKey]);
|
||||
|
||||
const state = React.useMemo<GatewayState>(
|
||||
() => ({
|
||||
uptimeReport,
|
||||
uptimeStory,
|
||||
}),
|
||||
[uptimeReport, uptimeStory],
|
||||
);
|
||||
|
||||
return <GatewayContext.Provider value={state}>{children}</GatewayContext.Provider>;
|
||||
};
|
||||
@@ -1,30 +1,47 @@
|
||||
import * as React from 'react';
|
||||
import { ApiState } from '../typeDefs/explorer-api';
|
||||
|
||||
type WrappedApiFn<T> = () => Promise<ApiState<T>>;
|
||||
|
||||
/**
|
||||
* Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously
|
||||
* @param id The id to fetch
|
||||
* @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter)
|
||||
* @param errorMessage A static error message, to use when no dynamic error message is returned
|
||||
*/
|
||||
export const useApiState = <T>(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
fn: Function,
|
||||
id: string,
|
||||
fn: (argId: string) => Promise<T>,
|
||||
errorMessage: string,
|
||||
): [ApiState<T>, WrappedApiFn<T>] => {
|
||||
): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] => {
|
||||
// stores the state
|
||||
const [value, setValue] = React.useState<ApiState<T>>();
|
||||
const wrappedFn = React.useCallback(async () => {
|
||||
|
||||
// clear the value
|
||||
const clearValueFn = () => setValue(undefined);
|
||||
|
||||
// this provides a method to trigger the delegate to fetch data
|
||||
const wrappedFetchFn = React.useCallback(async () => {
|
||||
setValue({ isLoading: true });
|
||||
try {
|
||||
// keep previous state and set to loading
|
||||
setValue((prevState) => ({ ...prevState, isLoading: true }));
|
||||
const data = await fn();
|
||||
setValue({
|
||||
|
||||
// delegate to user function to get data and set if successful
|
||||
const data = await fn(id);
|
||||
const newValue: ApiState<T> = {
|
||||
isLoading: false,
|
||||
data,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
setValue({
|
||||
// return the caught error or create a new error with the static error message
|
||||
const newValue: ApiState<T> = {
|
||||
error: error instanceof Error ? error : new Error(errorMessage),
|
||||
isLoading: false,
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
}
|
||||
}, [setValue, fn]);
|
||||
return [value || { isLoading: true }, wrappedFn];
|
||||
}, [setValue, fn, id, errorMessage]);
|
||||
return [value, wrappedFetchFn, clearValueFn];
|
||||
};
|
||||
|
||||
@@ -96,19 +96,22 @@ export const MainContextProvider: React.FC = ({ children }) => {
|
||||
const filterMixnodes = async (filters: { [key in EnumFilterKey]: number[] }, status?: MixnodeStatus) => {
|
||||
setMixnodes((d) => ({ ...d, isLoading: true }));
|
||||
const mxns = status ? await Api.fetchMixnodesActiveSetByStatus(status) : await Api.fetchMixnodes();
|
||||
|
||||
const filtered = mxns?.filter(
|
||||
(m) =>
|
||||
m.mix_node.profit_margin_percent >= filters.profitMargin[0] &&
|
||||
m.mix_node.profit_margin_percent <= filters.profitMargin[1] &&
|
||||
+m.profit_margin_percent >= filters.profitMargin[0] / 100 &&
|
||||
+m.profit_margin_percent <= filters.profitMargin[1] / 100 &&
|
||||
m.stake_saturation >= filters.stakeSaturation[0] &&
|
||||
m.stake_saturation <= filters.stakeSaturation[1] &&
|
||||
m.avg_uptime >= filters.routingScore[0] &&
|
||||
m.avg_uptime <= filters.routingScore[1],
|
||||
);
|
||||
|
||||
setMixnodes({ data: filtered, isLoading: false });
|
||||
};
|
||||
|
||||
const fetchGateways = async () => {
|
||||
setGateways((d) => ({ ...d, isLoading: true }));
|
||||
try {
|
||||
const data = await Api.fetchGateways();
|
||||
setGateways({ data, isLoading: false });
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
UptimeStoryResponse,
|
||||
} from '../typeDefs/explorer-api';
|
||||
import { Api } from '../api';
|
||||
import { useApiState } from './hooks';
|
||||
import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../components/MixNodes';
|
||||
|
||||
/**
|
||||
@@ -153,47 +154,3 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
|
||||
|
||||
return <MixnodeContext.Provider value={state}>{children}</MixnodeContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously
|
||||
* @param id The id to fetch
|
||||
* @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter)
|
||||
* @param errorMessage A static error message, to use when no dynamic error message is returned
|
||||
*/
|
||||
function useApiState<T>(
|
||||
id: string,
|
||||
fn: (argId: string) => Promise<T>,
|
||||
errorMessage: string,
|
||||
): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] {
|
||||
// stores the state
|
||||
const [value, setValue] = React.useState<ApiState<T> | undefined>();
|
||||
|
||||
// clear the value
|
||||
const clearValueFn = () => setValue(undefined);
|
||||
|
||||
// this provides a method to trigger the delegate to fetch data
|
||||
const wrappedFetchFn = React.useCallback(async () => {
|
||||
try {
|
||||
// keep previous state and set to loading
|
||||
setValue((prevState) => ({ ...prevState, isLoading: true }));
|
||||
|
||||
// delegate to user function to get data and set if successful
|
||||
const data = await fn(id);
|
||||
const newValue: ApiState<T> = {
|
||||
isLoading: false,
|
||||
data,
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
// return the caught error or create a new error with the static error message
|
||||
const newValue: ApiState<T> = {
|
||||
error: error instanceof Error ? error : new Error(errorMessage),
|
||||
isLoading: false,
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
}
|
||||
}, [setValue, fn]);
|
||||
return [value || { isLoading: true }, wrappedFetchFn, clearValueFn];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import * as React from 'react';
|
||||
import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { GatewayResponseItem } from '../../typeDefs/explorer-api';
|
||||
import { ColumnsType, DetailTable } from '../../components/DetailTable';
|
||||
import { gatewayEnrichedToGridRow, GatewayEnrichedRowType } from '../../components/Gateways';
|
||||
import { ComponentError } from '../../components/ComponentError';
|
||||
import { ContentCard } from '../../components/ContentCard';
|
||||
import { TwoColSmallTable } from '../../components/TwoColSmallTable';
|
||||
import { UptimeChart } from '../../components/UptimeChart';
|
||||
import { GatewayContextProvider, useGatewayContext } from '../../context/gateway';
|
||||
import { useMainContext } from '../../context/main';
|
||||
import { Title } from '../../components/Title';
|
||||
|
||||
const columns: ColumnsType[] = [
|
||||
{
|
||||
field: 'identityKey',
|
||||
title: 'Identity Key',
|
||||
headerAlign: 'left',
|
||||
width: 230,
|
||||
},
|
||||
{
|
||||
field: 'bond',
|
||||
title: 'Bond',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
},
|
||||
{
|
||||
field: 'routingScore',
|
||||
title: 'Routing Score',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo:
|
||||
'Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.',
|
||||
},
|
||||
{
|
||||
field: 'avgUptime',
|
||||
title: 'Avg. Score',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo: 'Is the average routing score in the last 24 hours',
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
title: 'IP',
|
||||
headerAlign: 'left',
|
||||
width: 99,
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
title: 'Location',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: 'owner',
|
||||
title: 'Owner',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Shows gateway details
|
||||
*/
|
||||
const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: GatewayResponseItem | undefined }) => {
|
||||
const [enrichGateway, setEnrichGateway] = React.useState<GatewayEnrichedRowType>();
|
||||
const [status, setStatus] = React.useState<number[] | undefined>();
|
||||
const { uptimeReport, uptimeStory } = useGatewayContext();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (uptimeReport?.data && selectedGateway) {
|
||||
setEnrichGateway(gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data));
|
||||
}
|
||||
}, [uptimeReport, selectedGateway]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (enrichGateway) {
|
||||
setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]);
|
||||
}
|
||||
}, [enrichGateway]);
|
||||
|
||||
return (
|
||||
<Box component="main">
|
||||
<Title text="Gateway Detail" />
|
||||
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<DetailTable
|
||||
columnsData={columns}
|
||||
tableName="Gateway detail table"
|
||||
rows={enrichGateway ? [enrichGateway] : []}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2} mt={0}>
|
||||
<Grid item xs={12} md={4}>
|
||||
{status && (
|
||||
<ContentCard title="Gateway Status">
|
||||
<TwoColSmallTable
|
||||
loading={false}
|
||||
keys={['Mix port', 'Client WS API Port']}
|
||||
values={status.map((each) => each)}
|
||||
icons={status.map((elem) => !!elem)}
|
||||
/>
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12} md={8}>
|
||||
{uptimeStory && (
|
||||
<ContentCard title="Routing Score">
|
||||
{uptimeStory.error && <ComponentError text="There was a problem retrieving routing score." />}
|
||||
<UptimeChart loading={uptimeStory.isLoading} xLabel="date" uptimeStory={uptimeStory} />
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Guard component to handle loading and not found states
|
||||
*/
|
||||
const PageGatewayDetailGuard: React.FC = () => {
|
||||
const [selectedGateway, setSelectedGateway] = React.useState<GatewayResponseItem | undefined>();
|
||||
const { gateways } = useMainContext();
|
||||
const { id } = useParams<{ id: string | undefined }>();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (gateways?.data) {
|
||||
setSelectedGateway(gateways.data.find((gateway) => gateway.gateway.identity_key === id));
|
||||
}
|
||||
}, [gateways, id]);
|
||||
|
||||
if (gateways?.isLoading) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
if (gateways?.error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(gateways?.error);
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Oh no! Could not load mixnode <code>{id || ''}</code>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
// loaded, but not found
|
||||
if (gateways && !gateways.isLoading && !gateways.data) {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
<AlertTitle>Gateway not found</AlertTitle>
|
||||
Sorry, we could not find a mixnode with id <code>{id || ''}</code>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper component that adds the mixnode content based on the `id` in the address URL
|
||||
*/
|
||||
export const PageGatewayDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string | undefined }>();
|
||||
|
||||
if (!id) {
|
||||
return <Alert severity="error">Oh no! No mixnode identity key specified</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<GatewayContextProvider gatewayIdentityKey={id}>
|
||||
<PageGatewayDetailGuard />
|
||||
</GatewayContextProvider>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Button, Card, Grid, Typography } from '@mui/material';
|
||||
import { Link as RRDLink } from 'react-router-dom';
|
||||
import { Box, Button, Card, Grid, Typography, Link as MuiLink } from '@mui/material';
|
||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { SelectChangeEvent } from '@mui/material/Select';
|
||||
import { useMainContext } from '../../context/main';
|
||||
@@ -10,6 +11,7 @@ import { CustomColumnHeading } from '../../components/CustomColumnHeading';
|
||||
import { Title } from '../../components/Title';
|
||||
import { cellStyles, UniversalDataGrid } from '../../components/Universal-DataGrid';
|
||||
import { currencyToString } from '../../utils/currency';
|
||||
import { Tooltip } from '../../components/Tooltip';
|
||||
|
||||
export const PageGateways: React.FC = () => {
|
||||
const { gateways } = useMainContext();
|
||||
@@ -43,29 +45,20 @@ export const PageGateways: React.FC = () => {
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'owner',
|
||||
headerName: 'Owner',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||
width: 380,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles} data-testid="owner">
|
||||
{params.value}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'identity_key',
|
||||
field: 'identityKey',
|
||||
headerName: 'Identity Key',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 380,
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles} data-testid="identity-key">
|
||||
<MuiLink
|
||||
sx={{ ...cellStyles }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/gateway/${params.row.identityKey}`}
|
||||
>
|
||||
{params.value}
|
||||
</Typography>
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -84,7 +77,7 @@ export const PageGateways: React.FC = () => {
|
||||
{
|
||||
field: 'host',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
|
||||
width: 110,
|
||||
width: 180,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
@@ -96,7 +89,7 @@ export const PageGateways: React.FC = () => {
|
||||
{
|
||||
field: 'location',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
|
||||
width: 150,
|
||||
width: 180,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
@@ -105,10 +98,33 @@ export const PageGateways: React.FC = () => {
|
||||
sx={{ ...cellStyles, justifyContent: 'flex-start' }}
|
||||
data-testid="location-button"
|
||||
>
|
||||
{params.value}
|
||||
<Tooltip text={params.value} id="gateway-location-text">
|
||||
<Box
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{params.value}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'owner',
|
||||
headerName: 'Owner',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
|
||||
width: 380,
|
||||
headerAlign: 'left',
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<Typography sx={cellStyles} data-testid="owner">
|
||||
{params.value}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const handlePageSize = (event: SelectChangeEvent<string>) => {
|
||||
@@ -133,7 +149,12 @@ export const PageGateways: React.FC = () => {
|
||||
pageSize={pageSize}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
<UniversalDataGrid rows={gatewayToGridRow(filteredGateways)} columns={columns} pageSize={pageSize} />
|
||||
<UniversalDataGrid
|
||||
pagination
|
||||
rows={gatewayToGridRow(filteredGateways)}
|
||||
columns={columns}
|
||||
pageSize={pageSize}
|
||||
/>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
|
||||
import { Button, Card, Grid, Link as MuiLink } from '@mui/material';
|
||||
import { Button, Card, Grid, Link as MuiLink, Box } from '@mui/material';
|
||||
import { Link as RRDLink, useParams, useNavigate } from 'react-router-dom';
|
||||
import { SelectChangeEvent } from '@mui/material/Select';
|
||||
import { SxProps } from '@mui/system';
|
||||
@@ -18,6 +18,7 @@ import { currencyToString } from '../../utils/currency';
|
||||
import { splice } from '../../utils';
|
||||
import { getMixNodeStatusColor } from '../../components/MixNodes/Status';
|
||||
import { MixNodeStatusDropdown } from '../../components/MixNodes/StatusDropdown';
|
||||
import { Tooltip } from '../../components/Tooltip';
|
||||
|
||||
const getCellFontStyle = (theme: Theme, row: MixnodeRowType, textColor?: string) => {
|
||||
const color = textColor || getMixNodeStatusColor(theme, row.status);
|
||||
@@ -81,6 +82,24 @@ export const PageMixnodes: React.FC = () => {
|
||||
};
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'mix_id',
|
||||
headerName: 'Mix ID',
|
||||
renderHeader: () => <CustomColumnHeading headingTitle="Mix ID" />,
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 100,
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={getCellStyles(theme, params.row)}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnode/${params.value}`}
|
||||
data-testid="mix-id"
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'identity_key',
|
||||
headerName: 'Identity Key',
|
||||
@@ -98,7 +117,7 @@ export const PageMixnodes: React.FC = () => {
|
||||
<MuiLink
|
||||
sx={getCellStyles(theme, params.row)}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnode/${params.value}`}
|
||||
to={`/network-components/mixnode/${params.row.mix_id}`}
|
||||
data-testid="identity-link"
|
||||
>
|
||||
{splice(7, 29, params.value)}
|
||||
@@ -118,7 +137,7 @@ export const PageMixnodes: React.FC = () => {
|
||||
<MuiLink
|
||||
sx={getCellStyles(theme, params.row)}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnode/${params.row.identity_key}`}
|
||||
to={`/network-components/mixnode/${params.row.mix_id}`}
|
||||
>
|
||||
{currencyToString(params.value)}
|
||||
</MuiLink>
|
||||
@@ -143,7 +162,7 @@ export const PageMixnodes: React.FC = () => {
|
||||
...getCellStyles(theme, params.row, params.value > 100 ? 'theme.palette.warning.main' : undefined),
|
||||
}}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnode/${params.row.identity_key}`}
|
||||
to={`/network-components/mixnode/${params.row.mix_id}`}
|
||||
>
|
||||
{`${params.value.toFixed(2)} %`}
|
||||
</MuiLink>
|
||||
@@ -161,7 +180,7 @@ export const PageMixnodes: React.FC = () => {
|
||||
<MuiLink
|
||||
sx={getCellStyles(theme, params.row)}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnode/${params.row.identity_key}`}
|
||||
to={`/network-components/mixnode/${params.row.mix_id}`}
|
||||
>
|
||||
{currencyToString(params.value)}
|
||||
</MuiLink>
|
||||
@@ -183,7 +202,29 @@ export const PageMixnodes: React.FC = () => {
|
||||
<MuiLink
|
||||
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnode/${params.row.identity_key}`}
|
||||
to={`/network-components/mixnode/${params.row.mix_id}`}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
),
|
||||
},
|
||||
{
|
||||
field: 'operating_cost',
|
||||
headerName: 'Operating cost',
|
||||
renderHeader: () => (
|
||||
<CustomColumnHeading
|
||||
headingTitle="Operating cost"
|
||||
tooltipInfo="Monthly operational cost of running this node. This cost is set by the operator and it influences how the rewards are split between the operator and delegators."
|
||||
/>
|
||||
),
|
||||
headerClassName: 'MuiDataGrid-header-override',
|
||||
width: 170,
|
||||
headerAlign: 'left',
|
||||
renderCell: (params: GridRenderCellParams) => (
|
||||
<MuiLink
|
||||
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnode/${params.row.mix_id}`}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
@@ -205,7 +246,7 @@ export const PageMixnodes: React.FC = () => {
|
||||
<MuiLink
|
||||
sx={{ ...getCellStyles(theme, params.row), textAlign: 'left' }}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnode/${params.row.identity_key}`}
|
||||
to={`/network-components/mixnode/${params.row.mix_id}`}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
@@ -244,7 +285,17 @@ export const PageMixnodes: React.FC = () => {
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
>
|
||||
{params.value}
|
||||
<Tooltip text={params.value} id="mixnode-location-text">
|
||||
<Box
|
||||
sx={{
|
||||
overflow: 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
textOverflow: 'ellipsis',
|
||||
}}
|
||||
>
|
||||
{params.value}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
@@ -259,7 +310,7 @@ export const PageMixnodes: React.FC = () => {
|
||||
<MuiLink
|
||||
sx={getCellStyles(theme, params.row)}
|
||||
component={RRDLink}
|
||||
to={`/network-components/mixnode/${params.row.identity_key}`}
|
||||
to={`/network-components/mixnode/${params.row.mix_id}`}
|
||||
>
|
||||
{params.value}
|
||||
</MuiLink>
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
import { Routes as ReactRouterRoutes, Route, useNavigate } from 'react-router-dom';
|
||||
import { BIG_DIPPER } from '../api/constants';
|
||||
import { PageGateways } from '../pages/Gateways';
|
||||
import { PageGatewayDetail } from '../pages/GatewayDetail';
|
||||
import { PageMixnodeDetail } from '../pages/MixnodeDetail';
|
||||
import { PageMixnodes } from '../pages/Mixnodes';
|
||||
|
||||
@@ -18,6 +19,7 @@ export const NetworkComponentsRoutes: React.FC = () => (
|
||||
<Route path="mixnodes" element={<PageMixnodes />} />
|
||||
<Route path="mixnode/:id" element={<PageMixnodeDetail />} />
|
||||
<Route path="gateways" element={<PageGateways />} />
|
||||
<Route path="gateway/:id" element={<PageGatewayDetail />} />
|
||||
<Route path="validators" element={<ValidatorRoute />} />
|
||||
<Route path="gateways/:id" element={<h1> Specific Gateways ID</h1>} />
|
||||
</ReactRouterRoutes>
|
||||
|
||||
@@ -30,7 +30,6 @@ export interface MixNode {
|
||||
sphinx_key: string;
|
||||
identity_key: string;
|
||||
version: string;
|
||||
profit_margin_percent: number;
|
||||
location: string;
|
||||
}
|
||||
|
||||
@@ -70,6 +69,7 @@ export const toMixnodeStatus = (status?: MixnodeStatusWithAll): MixnodeStatus |
|
||||
};
|
||||
|
||||
export interface MixNodeResponseItem {
|
||||
mix_id: number;
|
||||
pledge_amount: Amount;
|
||||
total_delegation: Amount;
|
||||
owner: string;
|
||||
@@ -85,6 +85,8 @@ export interface MixNodeResponseItem {
|
||||
mix_node: MixNode;
|
||||
avg_uptime: number;
|
||||
stake_saturation: number;
|
||||
operating_cost: Amount;
|
||||
profit_margin_percent: string;
|
||||
}
|
||||
|
||||
export type MixNodeResponse = MixNodeResponseItem[];
|
||||
@@ -126,12 +128,9 @@ export type GatewayResponse = GatewayResponseItem[];
|
||||
export interface GatewayReportResponse {
|
||||
identity: string;
|
||||
owner: string;
|
||||
most_recent_ipv4: boolean;
|
||||
most_recent_ipv6: boolean;
|
||||
last_hour_ipv4: number;
|
||||
last_hour_ipv6: number;
|
||||
last_day_ipv4: number;
|
||||
last_day_ipv6: number;
|
||||
most_recent: number;
|
||||
last_hour: number;
|
||||
last_day: number;
|
||||
}
|
||||
|
||||
export type GatewayHistoryResponse = StatsResponse;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { printableCoin } from '@nymproject/nym-validator-client';
|
||||
import Big from 'big.js';
|
||||
|
||||
const DENOM = process.env.CURRENCY_DENOM || 'unym';
|
||||
const DENOM_STAKING = process.env.CURRENCY_STAKING_DENOM || 'unyx';
|
||||
@@ -14,3 +15,38 @@ export const stakingCurrencyToString = (amount: string, denom: string = DENOM_ST
|
||||
amount,
|
||||
denom,
|
||||
});
|
||||
|
||||
/**
|
||||
* Converts a decimal number to a pretty representation
|
||||
* with fixed decimal places.
|
||||
*
|
||||
* @param val - a decimal number of string form
|
||||
* @param dp - number of decimal places (4 by default ie. 0.0000)
|
||||
* @returns A prettyfied decimal number
|
||||
*/
|
||||
export const toDisplay = (val: string | number | Big, dp = 4) => {
|
||||
let displayValue;
|
||||
try {
|
||||
displayValue = Big(val).toFixed(dp);
|
||||
} catch (e: any) {
|
||||
console.warn(`${displayValue} not a valid decimal number: ${e}`);
|
||||
}
|
||||
return displayValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a decimal number of μNYM (micro NYM) to NYM.
|
||||
*
|
||||
* @param unym - a decimal number of μNYM
|
||||
* @param dp - number of decimal places (4 by default ie. 0.0000)
|
||||
* @returns The corresponding decimal number in NYM
|
||||
*/
|
||||
export const unymToNym = (unym: string | number | Big, dp = 4) => {
|
||||
let nym;
|
||||
try {
|
||||
nym = Big(unym).div(1_000_000).toFixed(dp);
|
||||
} catch (e: any) {
|
||||
console.warn(`${unym} not a valid decimal number: ${e}`);
|
||||
}
|
||||
return nym;
|
||||
};
|
||||
|
||||
@@ -45,3 +45,11 @@ export const splice = (start: number, deleteCount: number, address?: string): st
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a stringified percentage float (0.0-1.0) to a stringified integer (0-100).
|
||||
*
|
||||
* @param value - the percentage to convert
|
||||
* @returns A stringified integer
|
||||
*/
|
||||
export const toPercentIntegerString = (value: string) => Math.round(Number(value) * 100).toString();
|
||||
|
||||
@@ -56,6 +56,7 @@ credentials = { path = "../common/credentials" }
|
||||
config = { path = "../common/config" }
|
||||
crypto = { path = "../common/crypto" }
|
||||
completions = { path = "../common/completions" }
|
||||
logging = { path = "../common/logging"}
|
||||
gateway-requests = { path = "gateway-requests" }
|
||||
mixnet-client = { path = "../common/client-libs/mixnet-client" }
|
||||
mixnode-common = { path = "../common/mixnode-common" }
|
||||
|
||||
@@ -82,8 +82,10 @@ impl fmt::Display for GatewayRequestsError {
|
||||
RequestOfInvalidSize(actual) =>
|
||||
write!(
|
||||
f,
|
||||
"received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {} (EXTENDED))",
|
||||
actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(), PacketSize::ExtendedPacket.size()
|
||||
"received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))",
|
||||
actual, PacketSize::AckPacket.size(), PacketSize::RegularPacket.size(),
|
||||
PacketSize::ExtendedPacket8.size(), PacketSize::ExtendedPacket16.size(),
|
||||
PacketSize::ExtendedPacket32.size()
|
||||
),
|
||||
MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"),
|
||||
MalformedEncryption => write!(f, "the received encrypted data was malformed"),
|
||||
|
||||
@@ -289,6 +289,10 @@ impl Config {
|
||||
self.debug.maximum_connection_buffer_size
|
||||
}
|
||||
|
||||
pub fn get_use_legacy_sphinx_framing(&self) -> bool {
|
||||
self.debug.use_legacy_framed_packet_version
|
||||
}
|
||||
|
||||
pub fn get_message_retrieval_limit(&self) -> i64 {
|
||||
self.debug.message_retrieval_limit
|
||||
}
|
||||
@@ -456,6 +460,12 @@ struct Debug {
|
||||
|
||||
/// Number of messages from offline client that can be pulled at once from the storage.
|
||||
message_retrieval_limit: i64,
|
||||
|
||||
/// Specifies whether the mixnode should be using the legacy framing for the sphinx packets.
|
||||
// it's set to true by default. The reason for that decision is to preserve compatibility with the
|
||||
// existing nodes whilst everyone else is upgrading and getting the code for handling the new field.
|
||||
// It shall be disabled in the subsequent releases.
|
||||
use_legacy_framed_packet_version: bool,
|
||||
}
|
||||
|
||||
impl Default for Debug {
|
||||
@@ -468,6 +478,8 @@ impl Default for Debug {
|
||||
maximum_connection_buffer_size: DEFAULT_MAXIMUM_CONNECTION_BUFFER_SIZE,
|
||||
stored_messages_filename_length: DEFAULT_STORED_MESSAGE_FILENAME_LENGTH,
|
||||
message_retrieval_limit: DEFAULT_MESSAGE_RETRIEVAL_LIMIT,
|
||||
// TODO: remember to change it in one of future releases!!
|
||||
use_legacy_framed_packet_version: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user