Compare commits

..

12 Commits

Author SHA1 Message Date
Gala 0a9978b81e fix build a rever console utility addition 2022-10-27 12:37:18 +02:00
Gala 7cdfdf8437 requested changes 2022-10-27 11:05:19 +02:00
Gala fca32424b4 fix build 2022-10-24 12:01:37 +02:00
Gala 7ad9c15c9e PR requested changes 2022-10-24 11:51:46 +02:00
Gala 54716c7c09 fix build 2022-10-21 10:55:45 +02:00
Gala 2a0b1a3734 adding correct toolpit text and cleaning 2022-10-20 17:53:17 +02:00
Gala 7f35b3660b remove gateway name and desc 2022-10-20 17:02:17 +02:00
Gala 8470288079 fixing gateways pagination 2022-10-20 15:33:31 +02:00
Gala d5a65841af adding link style 2022-10-20 15:27:55 +02:00
Gala 76144928a0 adding loading state for gateways 2022-10-20 15:19:27 +02:00
Gala d6ffdb86ce adding uptime chart 2022-10-20 15:11:38 +02:00
Gala 74f9205be1 create gateway details page 2022-10-20 15:04:36 +02:00
110 changed files with 3785 additions and 2672 deletions
-18
View File
@@ -3,21 +3,3 @@
RUST_LOG=info
RUST_BACKTRACE=1
#########################################
# geoipupdate (needed for explorer-api) #
#########################################
# MaxMind account ID (change it to a valid account ID)
GEOIPUPDATE_ACCOUNT_ID=xxx
# MaxMind license key (change it to a valid license key)
GEOIPUPDATE_LICENSE_KEY=xxx
# List of space-separated database edition IDs. Edition IDs may
# consist of letters, digits, and dashes. For example, GeoIP2-City
# would download the GeoIP2 City database (GeoIP2-City).
GEOIPUPDATE_EDITION_IDS=GeoLite2-Country
# The number of hours between geoipupdate runs. If this is not set
# or is set to 0, geoipupdate will run once and exit.
GEOIPUPDATE_FREQUENCY=72
# The path to the directory where geoipupdate will download the
# database.
GEOIP_DB_DIRECTORY=./explorer-api/geo_ip
-4
View File
@@ -19,7 +19,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
### 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])
### Changed
@@ -27,7 +26,6 @@ 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])
[#1541]: https://github.com/nymtech/nym/pull/1541
[#1558]: https://github.com/nymtech/nym/pull/1558
@@ -41,8 +39,6 @@ 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
[#1702]: https://github.com/nymtech/nym/pull/1702
[#1703]: https://github.com/nymtech/nym/pull/1703
## [nym-binaries-1.0.2](https://github.com/nymtech/nym/tree/nym-binaries-1.0.2)
Generated
-1
View File
@@ -1583,7 +1583,6 @@ version = "1.0.1"
dependencies = [
"chrono",
"clap 3.2.8",
"dotenv",
"humantime-serde",
"isocountry",
"itertools",
+4 -16
View File
@@ -2,12 +2,12 @@ test: clippy-all cargo-test wasm fmt
test-all: test cargo-test-expensive
no-clippy: build cargo-test wasm fmt
happy: fmt clippy-happy test
clippy-all: clippy-main clippy-coconut clippy-all-contracts clippy-all-wallet clippy-all-connect clippy-all-wasm-client
clippy-all: clippy-main clippy-coconut clippy-all-contracts clippy-all-wallet clippy-all-connect
clippy-happy: clippy-happy-main clippy-happy-contracts clippy-happy-wallet clippy-happy-connect
cargo-test: test-main test-contracts test-wallet test-connect test-coconut test-wasm-client
cargo-test: test-main test-contracts test-wallet test-connect test-coconut
cargo-test-expensive: test-main-expensive test-contracts-expensive test-wallet-expensive test-connect-expensive test-coconut-expensive
build: build-contracts build-wallet build-main build-connect build-wasm-client
fmt: fmt-main fmt-contracts fmt-wallet fmt-connect fmt-wasm-client
build: build-contracts build-wallet build-main build-connect
fmt: fmt-main fmt-contracts fmt-wallet fmt-connect
clippy-happy-main:
cargo clippy
@@ -40,9 +40,6 @@ clippy-all-wallet:
clippy-all-connect:
cargo clippy --workspace --manifest-path nym-connect/Cargo.toml --all-features -- -D warnings
clippy-all-wasm-client:
cargo clippy --workspace --manifest-path clients/webassembly/Cargo.toml --all-features --target wasm32-unknown-unknown -- -D warnings
test-main:
cargo test --workspace
@@ -71,9 +68,6 @@ test-wallet:
test-wallet-expensive:
cargo test --manifest-path nym-wallet/Cargo.toml --all-features -- --ignored
test-wasm-client:
cargo test --workspace --manifest-path clients/webassembly/Cargo.toml --all-features
test-connect:
cargo test --manifest-path nym-connect/Cargo.toml --all-features
@@ -92,9 +86,6 @@ build-wallet:
build-connect:
cargo build --manifest-path nym-connect/Cargo.toml --workspace
build-wasm-client:
cargo build --manifest-path clients/webassembly/Cargo.toml --workspace --target wasm32-unknown-unknown
build-nym-cli:
cargo build --release --manifest-path tools/nym-cli/Cargo.toml
@@ -110,9 +101,6 @@ fmt-wallet:
fmt-connect:
cargo fmt --manifest-path nym-connect/Cargo.toml --all
fmt-wasm-client:
cargo fmt --manifest-path clients/webassembly/Cargo.toml --all
wasm:
RUSTFLAGS='-C link-arg=-s' cargo build --manifest-path contracts/Cargo.toml --release --target wasm32-unknown-unknown
@@ -16,7 +16,6 @@ use rand::{rngs::OsRng, CryptoRng, Rng};
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::error::TrySendError;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time;
@@ -172,18 +171,11 @@ impl LoopCoverTrafficStream<OsRng> {
)
.expect("Somehow failed to generate a loop cover message with a valid topology");
if let Err(err) = self.mix_tx.try_send(vec![cover_message]) {
match err {
TrySendError::Full(_) => {
// This isn't a problem, if the channel is full means we're already sending the
// max amount of messages downstream can handle.
log::debug!("Failed to send cover message - channel full");
}
TrySendError::Closed(_) => {
log::warn!("Failed to send cover message - channel closed");
}
}
}
// if this one fails, there's no retrying because it means that either:
// - we run out of memory
// - the receiver channel is closed
// in either case there's no recovery and we can only panic
self.mix_tx.unbounded_send(vec![cover_message]).unwrap();
// TODO: I'm not entirely sure whether this is really required, because I'm not 100%
// sure how `yield_now()` works - whether it just notifies the scheduler or whether it
+15 -17
View File
@@ -2,15 +2,15 @@
// SPDX-License-Identifier: Apache-2.0
use crate::spawn_future;
use futures::channel::mpsc;
use futures::StreamExt;
use gateway_client::GatewayClient;
use log::*;
use nymsphinx::forwarding::packet::MixPacket;
pub type BatchMixMessageSender = tokio::sync::mpsc::Sender<Vec<MixPacket>>;
pub type BatchMixMessageReceiver = tokio::sync::mpsc::Receiver<Vec<MixPacket>>;
pub type BatchMixMessageSender = mpsc::UnboundedSender<Vec<MixPacket>>;
pub type BatchMixMessageReceiver = mpsc::UnboundedReceiver<Vec<MixPacket>>;
// We remind ourselves that 32 x 32kb = 1024kb, a reasonable size for a network buffer.
pub const MIX_MESSAGE_RECEIVER_BUFFER_SIZE: usize = 32;
const MAX_FAILURE_COUNT: usize = 100;
pub struct MixTrafficController {
@@ -25,17 +25,15 @@ pub struct MixTrafficController {
}
impl MixTrafficController {
pub fn new(gateway_client: GatewayClient) -> (MixTrafficController, BatchMixMessageSender) {
let (sphinx_message_sender, sphinx_message_receiver) =
tokio::sync::mpsc::channel(MIX_MESSAGE_RECEIVER_BUFFER_SIZE);
(
MixTrafficController {
gateway_client,
mix_rx: sphinx_message_receiver,
consecutive_gateway_failure_count: 0,
},
sphinx_message_sender,
)
pub fn new(
mix_rx: BatchMixMessageReceiver,
gateway_client: GatewayClient,
) -> MixTrafficController {
MixTrafficController {
gateway_client,
mix_rx,
consecutive_gateway_failure_count: 0,
}
}
async fn on_messages(&mut self, mut mix_packets: Vec<MixPacket>) {
@@ -74,7 +72,7 @@ impl MixTrafficController {
while !shutdown.is_shutdown() {
tokio::select! {
mix_packets = self.mix_rx.recv() => match mix_packets {
mix_packets = self.mix_rx.next() => match mix_packets {
Some(mix_packets) => {
self.on_messages(mix_packets).await;
},
@@ -98,7 +96,7 @@ impl MixTrafficController {
spawn_future(async move {
debug!("Started MixTrafficController without graceful shutdown support");
while let Some(mix_packets) = self.mix_rx.recv().await {
while let Some(mix_packets) = self.mix_rx.next().await {
self.on_messages(mix_packets).await;
}
})
@@ -27,23 +27,6 @@ use tokio::time;
#[cfg(target_arch = "wasm32")]
use wasm_timer;
// The minimum time between increasing the average delay between packets. If we hit the ceiling in
// the available buffer space we want to take somewhat swift action, but we still need to give a
// short time to give the channel a chance reduce pressure.
const INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS: u64 = 1;
// The minimum time between decreasing the average delay between packets. We don't want to change
// to quickly to keep things somewhat stable. Also there are buffers downstreams meaning we need to
// wait a little to see the effect before we decrease further.
const DECREASE_DELAY_MIN_CHANGE_INTERVAL_SECS: u64 = 30;
// If we enough time passes without any sign of backpressure in the channel, we can consider
// lowering the average delay. The goal is to keep somewhat stable, rather than maxing out
// bandwidth at all times.
const ACCEPTABLE_TIME_WITHOUT_BACKPRESSURE_SECS: u64 = 30;
// The maximum multiplier we apply to the base average Poisson delay.
const MAX_DELAY_MULTIPLIER: u32 = 6;
// The minium multiplier we apply to the base average Poisson delay.
const MIN_DELAY_MULTIPLIER: u32 = 1;
/// Configurable parameters of the `OutQueueControl`
pub(crate) struct Config {
/// Average delay an acknowledgement packet is going to get delay at a single mixnode.
@@ -85,101 +68,6 @@ impl Config {
}
}
struct SendingDelayController {
/// Multiply the average sending delay.
/// This is normally set to unity, but if we detect backpressure we increase this
/// multiplier. We use discrete steps.
current_multiplier: u32,
/// Maximum delay multiplier
upper_bound: u32,
/// Minimum delay multiplier
lower_bound: u32,
/// To make sure we don't change the multiplier to fast, we limit a change to some duration
#[cfg(not(target_arch = "wasm32"))]
time_when_changed: time::Instant,
#[cfg(target_arch = "wasm32")]
time_when_changed: wasm_timer::Instant,
/// If we have a long enough time without any backpressure detected we try reducing the sending
/// delay multiplier
#[cfg(not(target_arch = "wasm32"))]
time_when_backpressure_detected: time::Instant,
#[cfg(target_arch = "wasm32")]
time_when_backpressure_detected: wasm_timer::Instant,
}
#[cfg(not(target_arch = "wasm32"))]
fn get_time_now() -> time::Instant {
time::Instant::now()
}
#[cfg(target_arch = "wasm32")]
fn get_time_now() -> wasm_timer::Instant {
wasm_timer::Instant::now()
}
impl SendingDelayController {
fn new(lower_bound: u32, upper_bound: u32) -> Self {
assert!(lower_bound <= upper_bound);
let now = get_time_now();
SendingDelayController {
current_multiplier: MIN_DELAY_MULTIPLIER,
upper_bound,
lower_bound,
time_when_changed: now,
time_when_backpressure_detected: now,
}
}
fn current_multiplier(&self) -> u32 {
self.current_multiplier
}
fn increase_delay_multiplier(&mut self) {
self.current_multiplier =
(self.current_multiplier + 1).clamp(self.lower_bound, self.upper_bound);
self.time_when_changed = get_time_now();
log::debug!(
"Increasing sending delay multiplier to: {}",
self.current_multiplier
);
}
fn decrease_delay_multiplier(&mut self) {
self.current_multiplier =
(self.current_multiplier - 1).clamp(self.lower_bound, self.upper_bound);
self.time_when_changed = get_time_now();
log::debug!(
"Decreasing sending delay multiplier to: {}",
self.current_multiplier
);
}
fn record_backpressure_detected(&mut self) {
self.time_when_backpressure_detected = get_time_now();
}
fn not_increased_delay_recently(&self) -> bool {
get_time_now()
> self.time_when_changed + Duration::from_secs(INCREASE_DELAY_MIN_CHANGE_INTERVAL_SECS)
}
fn is_sending_reliable(&self) -> bool {
let now = get_time_now();
let delay_change_interval = Duration::from_secs(DECREASE_DELAY_MIN_CHANGE_INTERVAL_SECS);
let acceptable_time_without_backpressure =
Duration::from_secs(ACCEPTABLE_TIME_WITHOUT_BACKPRESSURE_SECS);
now > self.time_when_backpressure_detected + acceptable_time_without_backpressure
&& now > self.time_when_changed + delay_change_interval
}
}
pub(crate) struct OutQueueControl<R>
where
R: CryptoRng + Rng,
@@ -201,10 +89,6 @@ where
#[cfg(target_arch = "wasm32")]
next_delay: Option<Pin<Box<wasm_timer::Delay>>>,
// To make sure we don't overload the mix_tx channel, we limit the rate we are pushing
// messages.
sending_rate_controller: SendingDelayController,
/// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them
/// out to the network without any further delays.
mix_tx: BatchMixMessageSender,
@@ -272,10 +156,6 @@ where
ack_key,
sent_notifier,
next_delay: None,
sending_rate_controller: SendingDelayController::new(
MIN_DELAY_MULTIPLIER,
MAX_DELAY_MULTIPLIER,
),
mix_tx,
real_receiver,
our_full_destination,
@@ -332,8 +212,15 @@ where
}
};
if let Err(err) = self.mix_tx.send(vec![next_message]).await {
log::error!("Failed to send - channel closed: {}", err);
// if this one fails, there's no retrying because it means that either:
// - we run out of memory
// - the receiver channel is closed
// in either case there's no recovery and we can only panic
if let Err(err) = self.mix_tx.unbounded_send(vec![next_message]) {
log::warn!(
"Failed to send {} packets (possible process shutdown?)",
err.into_inner().len()
);
}
// JS: Not entirely sure why or how it fixes stuff, but without the yield call,
@@ -347,44 +234,7 @@ where
tokio::task::yield_now().await;
}
fn current_average_message_sending_delay(&self) -> Duration {
self.config.average_message_sending_delay
* self.sending_rate_controller.current_multiplier()
}
fn adjust_current_average_message_sending_delay(&mut self) {
let used_slots = self.mix_tx.max_capacity() - self.mix_tx.capacity();
log::trace!(
"used_slots: {used_slots}, current_multiplier: {}",
self.sending_rate_controller.current_multiplier()
);
// Even just a single used slot is enough to signal backpressure
if used_slots > 0 {
log::trace!("Backpressure detected");
self.sending_rate_controller.record_backpressure_detected();
}
// If the buffer is running out, slow down the sending rate
if self.mix_tx.capacity() == 0
&& self.sending_rate_controller.not_increased_delay_recently()
{
self.sending_rate_controller.increase_delay_multiplier();
}
// Very carefully step up the sending rate in case it seems like we can solidly handle the
// current rate.
if self.sending_rate_controller.is_sending_reliable() {
self.sending_rate_controller.decrease_delay_multiplier();
}
}
fn poll_poisson(&mut self, cx: &mut Context<'_>) -> Poll<Option<StreamMessage>> {
// The average delay could change depending on if backpressure in the downstream channel
// (mix_tx) was detected.
self.adjust_current_average_message_sending_delay();
let avg_delay = self.current_average_message_sending_delay();
if let Some(ref mut next_delay) = &mut self.next_delay {
// it is not yet time to return a message
if next_delay.as_mut().poll(cx).is_pending() {
@@ -393,6 +243,7 @@ where
// we know it's time to send a message, so let's prepare delay for the next one
// Get the `now` by looking at the current `delay` deadline
let avg_delay = self.config.average_message_sending_delay;
let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_delay);
// The next interval value is `next_poisson_delay` after the one that just
+17 -12
View File
@@ -6,7 +6,9 @@ use client_core::client::inbound_messages::{
InputMessage, InputMessageReceiver, InputMessageSender,
};
use client_core::client::key_manager::KeyManager;
use client_core::client::mix_traffic::{BatchMixMessageSender, MixTrafficController};
use client_core::client::mix_traffic::{
BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController,
};
use client_core::client::real_messages_control;
use client_core::client::real_messages_control::RealMessagesController;
use client_core::client::received_buffer::{
@@ -261,13 +263,13 @@ impl NymClient {
// over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for
// requests?
fn start_mix_traffic_controller(
&mut self,
mix_rx: BatchMixMessageReceiver,
gateway_client: GatewayClient,
shutdown: ShutdownListener,
) -> BatchMixMessageSender {
) {
info!("Starting mix traffic controller...");
let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client);
mix_traffic_controller.start_with_shutdown(shutdown);
mix_tx
MixTrafficController::new(mix_rx, gateway_client).start_with_shutdown(shutdown);
}
fn start_websocket_listener(
@@ -354,6 +356,11 @@ impl NymClient {
// rather than creating them here, so say for example the buffer controller would create the request channels
// and would allow anyone to clone the sender channel
// sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet
// they are used by cover traffic stream and real traffic stream
// sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic
let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded();
// unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway
// unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer
let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded();
@@ -390,13 +397,11 @@ impl NymClient {
.start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe())
.await;
// The sphinx_message_sender is the transmitter for any component generating sphinx packets
// that are to be sent to the mixnet. They are used by cover traffic stream and real
// traffic stream.
// The MixTrafficController then sends the actual traffic
let sphinx_message_sender =
Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe());
self.start_mix_traffic_controller(
sphinx_message_receiver,
gateway_client,
shutdown.subscribe(),
);
self.start_real_traffic_controller(
shared_topology_accessor.clone(),
reply_key_storage,
+17 -12
View File
@@ -13,7 +13,9 @@ use client_core::client::inbound_messages::{
InputMessage, InputMessageReceiver, InputMessageSender,
};
use client_core::client::key_manager::KeyManager;
use client_core::client::mix_traffic::{BatchMixMessageSender, MixTrafficController};
use client_core::client::mix_traffic::{
BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController,
};
use client_core::client::real_messages_control::RealMessagesController;
use client_core::client::received_buffer::{
ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController,
@@ -261,13 +263,13 @@ impl NymClient {
// over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for
// requests?
fn start_mix_traffic_controller(
&mut self,
mix_rx: BatchMixMessageReceiver,
gateway_client: GatewayClient,
shutdown: ShutdownListener,
) -> BatchMixMessageSender {
) {
info!("Starting mix traffic controller...");
let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client);
mix_traffic_controller.start_with_shutdown(shutdown);
mix_tx
MixTrafficController::new(mix_rx, gateway_client).start_with_shutdown(shutdown);
}
fn start_socks5_listener(
@@ -343,6 +345,11 @@ impl NymClient {
// rather than creating them here, so say for example the buffer controller would create the request channels
// and would allow anyone to clone the sender channel
// sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet
// they are used by cover traffic stream and real traffic stream
// sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic
let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded();
// unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway
// unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer
let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded();
@@ -379,13 +386,11 @@ impl NymClient {
.start_gateway_client(mixnet_messages_sender, ack_sender, shutdown.subscribe())
.await;
// The sphinx_message_sender is the transmitter for any component generating sphinx packets
// that are to be sent to the mixnet. They are used by cover traffic stream and real
// traffic stream.
// The MixTrafficController then sends the actual traffic
let sphinx_message_sender =
Self::start_mix_traffic_controller(gateway_client, shutdown.subscribe());
self.start_mix_traffic_controller(
sphinx_message_receiver,
gateway_client,
shutdown.subscribe(),
);
self.start_real_traffic_controller(
shared_topology_accessor.clone(),
reply_key_storage,
+3 -5
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client-wasm"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jedrzej Stuczynski <andrew@nymtech.net>"]
version = "1.0.1"
version = "1.0.0"
edition = "2021"
keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"]
license = "Apache-2.0"
@@ -55,8 +55,6 @@ wee_alloc = { version = "0.4", optional = true }
wasm-bindgen-test = "0.3"
[package.metadata.wasm-pack.profile.release]
wasm-opt = true
wasm-opt = false
[profile.release]
lto = true
opt-level = 'z'
@@ -2,11 +2,6 @@ const CopyWebpackPlugin = require('copy-webpack-plugin');
const path = require('path');
module.exports = {
performance: {
hints: false,
maxEntrypointSize: 512000,
maxAssetSize: 512000
},
entry: {
bootstrap: './bootstrap.js',
worker: './worker.js',
@@ -27,7 +22,6 @@ module.exports = {
},
],
}),
],
experiments: { syncWebAssembly: true },
};
-1
View File
@@ -73,7 +73,6 @@ async function main() {
// const preferredGateway = 'CgQrYP8etksSBf4nALNqp93SHPpgFwEUyTsjBNNLj5WM';
const gatewayEndpoint = await get_gateway(validator, preferredGateway);
gatewayEndpoint.gateway_listener = "wss://gateway1.nymtech.net:443"; // this is needed if we want it to work on the web. However this gateway is a v1 gateway, we will need to change for v2 once we get there
// only really useful if you want to adjust some settings like traffic rate
// (if not needed you can just pass a null)
File diff suppressed because it is too large Load Diff
+13 -11
View File
@@ -6,7 +6,7 @@ use client_core::client::{
cover_traffic_stream::LoopCoverTrafficStream,
inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender},
key_manager::KeyManager,
mix_traffic::{BatchMixMessageSender, MixTrafficController},
mix_traffic::{BatchMixMessageReceiver, BatchMixMessageSender, MixTrafficController},
real_messages_control::{self, RealMessagesController},
received_buffer::{
ReceivedBufferMessage, ReceivedBufferRequestReceiver, ReceivedBufferRequestSender,
@@ -253,11 +253,13 @@ impl NymClient {
// TODO: if we want to send control messages to gateway_client, this CAN'T take the ownership
// over it. Perhaps GatewayClient needs to be thread-shareable or have some channel for
// requests?
fn start_mix_traffic_controller(gateway_client: GatewayClient) -> BatchMixMessageSender {
fn start_mix_traffic_controller(
&mut self,
mix_rx: BatchMixMessageReceiver,
gateway_client: GatewayClient,
) {
console_log!("Starting mix traffic controller...");
let (mix_traffic_controller, mix_tx) = MixTrafficController::new(gateway_client);
mix_traffic_controller.start();
mix_tx
MixTrafficController::new(mix_rx, gateway_client).start();
}
// TODO: this procedure is extremely overcomplicated, because it's based off native client's behaviour
@@ -305,6 +307,11 @@ impl NymClient {
// rather than creating them here, so say for example the buffer controller would create the request channels
// and would allow anyone to clone the sender channel
// sphinx_message_sender is the transmitter for any component generating sphinx packets that are to be sent to the mixnet
// they are used by cover traffic stream and real traffic stream
// sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic
let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded();
// unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway
// unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer
let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded();
@@ -332,12 +339,7 @@ impl NymClient {
.start_gateway_client(mixnet_messages_sender, ack_sender)
.await;
// The sphinx_message_sender is the transmitter for any component generating sphinx packets
// that are to be sent to the mixnet. They are used by cover traffic stream and real
// traffic stream.
// The MixTrafficController then sends the actual traffic
let sphinx_message_sender = Self::start_mix_traffic_controller(gateway_client);
self.start_mix_traffic_controller(sphinx_message_receiver, gateway_client);
self.start_real_traffic_controller(
shared_topology_accessor.clone(),
ack_receiver,
-2
View File
@@ -1,2 +0,0 @@
allow-unwrap-in-tests = true
allow-expect-in-tests = true
@@ -9,7 +9,7 @@ use crate::utils::{pretty_cosmwasm_coin, show_error_passthrough};
use comfy_table::Table;
use cosmwasm_std::Addr;
use mixnet_contract_common::{Delegation, PendingEpochEvent, PendingEpochEventKind};
use mixnet_contract_common::{Delegation, PendingEpochEvent, PendingEpochEventData};
#[derive(Debug, Parser)]
pub struct Args {}
@@ -90,8 +90,8 @@ async fn print_delegation_events(
]);
for event in events {
match event.event.kind {
PendingEpochEventKind::Delegate {
match event.event {
PendingEpochEventData::Delegate {
owner,
mix_id,
amount,
@@ -107,7 +107,7 @@ async fn print_delegation_events(
]);
}
}
PendingEpochEventKind::Undelegate {
PendingEpochEventData::Undelegate {
owner,
mix_id,
proxy,
@@ -11,8 +11,6 @@ use cosmwasm_std::Event;
/// * `event`: event to search through.
/// * `key`: key associated with the particular attribute
pub fn must_find_attribute(event: &Event, key: &str) -> String {
// due to how the function is supposed to work, the unwrap is fine in this instance
#[allow(clippy::unwrap_used)]
may_find_attribute(event, key).unwrap()
}
@@ -1,9 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
pub mod events;
pub mod types;
@@ -1,9 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{Decimal, Uint128};
pub const TOKEN_SUPPLY: Uint128 = Uint128::new(1_000_000_000_000_000);
use cosmwasm_std::Decimal;
// I'm still not 100% sure how to feel about existence of this file
// This is equivalent of representing our display coin with 6 decimal places.
@@ -4,10 +4,8 @@
// due to code generated by JsonSchema
#![allow(clippy::field_reassign_with_default)]
use crate::constants::TOKEN_SUPPLY;
use crate::helpers::IntoBaseDecimal;
use crate::{Addr, MixId};
use cosmwasm_std::{Coin, Decimal, StdResult};
use cosmwasm_std::{Coin, Decimal};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -36,11 +34,13 @@ pub struct Delegation {
pub owner: Addr,
/// Id of the MixNode that this delegation was performed against.
#[serde(alias = "node_id")]
pub mix_id: MixId,
// Note to UI/UX devs: there's absolutely no point in displaying this value to the users,
// it would serve them no purpose. It's only used for calculating rewards
/// Value of the "unit delegation" associated with the mixnode at the time of delegation.
#[serde(alias = "crr")]
pub cumulative_reward_ratio: Decimal,
/// Original delegation amount. Note that it is never mutated as delegation accumulates rewards.
@@ -62,11 +62,6 @@ impl Delegation {
height: u64,
proxy: Option<Addr>,
) -> Self {
assert!(
amount.amount <= TOKEN_SUPPLY,
"delegation cannot be larger than the token supply"
);
Delegation {
owner,
mix_id,
@@ -94,8 +89,10 @@ impl Delegation {
(mix_id, owner_proxy_subkey)
}
pub fn dec_amount(&self) -> StdResult<Decimal> {
self.amount.amount.into_base_decimal()
pub fn dec_amount(&self) -> Decimal {
// the unwrap here is fine as we're guaranteed our base coin amount is going to fit in a Decimal
// with 0 decimal places
Decimal::from_atomics(self.amount.amount, 0).unwrap()
}
pub fn proxy_storage_key(&self) -> OwnerProxySubKey {
@@ -4,7 +4,7 @@
use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
use crate::reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate};
use crate::rewarding::RewardDistribution;
use crate::{BlockHeight, ContractStateParams, IdentityKeyRef, Interval, Layer, MixId};
use crate::{ContractStateParams, IdentityKeyRef, Interval, Layer, MixId};
pub use contracts_common::events::*;
use cosmwasm_std::{Addr, Coin, Decimal, Event};
@@ -96,7 +96,7 @@ pub const PROXY_KEY: &str = "proxy";
// delegation/undelegation
pub const DELEGATOR_KEY: &str = "delegator";
pub const DELEGATION_TARGET_KEY: &str = "delegation_target";
pub const UNIT_REWARD_KEY: &str = "unit_reward";
pub const DELEGATION_HEIGHT_KEY: &str = "delegation_latest_block_height";
// bonding/unbonding
pub const MIX_ID_KEY: &str = "mix_id";
@@ -120,45 +120,52 @@ pub const UPDATED_MIXNODE_COST_PARAMS_KEY: &str = "updated_mixnode_cost_params";
// rewarding
pub const INTERVAL_KEY: &str = "interval_details";
pub const TOTAL_MIXNODE_REWARD_KEY: &str = "total_node_reward";
pub const TOTAL_PLEDGE_KEY: &str = "pledge";
pub const TOTAL_DELEGATIONS_KEY: &str = "delegated";
pub const OPERATOR_REWARD_KEY: &str = "operator_reward";
pub const DELEGATES_REWARD_KEY: &str = "delegates_reward";
pub const APPROXIMATE_TIME_LEFT_SECS_KEY: &str = "approximate_time_left_secs";
pub const INTERVAL_REWARDING_PARAMS_UPDATE_KEY: &str = "interval_rewarding_params_update";
pub const UPDATED_INTERVAL_REWARDING_PARAMS_KEY: &str = "updated_interval_rewarding_params";
pub const PRIOR_DELEGATES_KEY: &str = "prior_delegates";
pub const PRIOR_UNIT_REWARD_KEY: &str = "prior_unit_reward";
pub const PRIOR_UNIT_REWARD: &str = "prior_unit_reward";
pub const DISTRIBUTED_DELEGATION_REWARDS_KEY: &str = "distributed_delegation_rewards";
pub const FURTHER_DELEGATIONS_TO_REWARD_KEY: &str = "further_delegations";
pub const NO_REWARD_REASON_KEY: &str = "no_reward_reason";
pub const BOND_NOT_FOUND_VALUE: &str = "bond_not_found";
pub const BOND_TOO_FRESH_VALUE: &str = "bond_too_fresh";
pub const ZERO_PERFORMANCE_VALUE: &str = "zero_performance";
// rewarded set update
pub const ACTIVE_SET_SIZE_KEY: &str = "active_set_size";
pub const REWARDED_SET_SIZE_KEY: &str = "rewarded_set_size";
pub const NODES_IN_REWARDED_SET_KEY: &str = "nodes_in_rewarded_set";
pub const CURRENT_INTERVAL_ID_KEY: &str = "current_interval";
pub const NEW_CURRENT_INTERVAL_KEY: &str = "new_current_interval";
pub const NEW_CURRENT_EPOCH_KEY: &str = "new_current_epoch";
pub const BLOCK_HEIGHT_KEY: &str = "block_height";
pub const RECONCILIATION_ERROR_EVENT: &str = "reconciliation_error";
// interval
pub const EVENTS_EXECUTED_KEY: &str = "number_of_events_executed";
pub const EVENT_CREATION_HEIGHT_KEY: &str = "created_at";
pub const REWARDED_SET_NODES_KEY: &str = "rewarded_set_nodes";
pub const NEW_EPOCHS_DURATION_SECS_KEY: &str = "new_epoch_durations_secs";
pub const NEW_EPOCHS_IN_INTERVAL: &str = "new_epochs_in_interval";
pub fn new_delegation_event(
created_at: BlockHeight,
delegator: &Addr,
proxy: &Option<Addr>,
amount: &Coin,
mix_id: MixId,
unit_reward: Decimal,
) -> Event {
Event::new(MixnetEventType::Delegation)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(DELEGATOR_KEY, delegator)
.add_optional_attribute(PROXY_KEY, proxy.as_ref())
.add_attribute(AMOUNT_KEY, amount.to_string())
.add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string())
.add_attribute(UNIT_REWARD_KEY, unit_reward.to_string())
}
pub fn new_delegation_on_unbonded_node_event(
@@ -211,9 +218,8 @@ pub fn new_withdraw_delegator_reward_event(
.add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string())
}
pub fn new_active_set_update_event(created_at: BlockHeight, new_size: u32) -> Event {
pub fn new_active_set_update_event(new_size: u32) -> Event {
Event::new(MixnetEventType::ActiveSetUpdate)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(ACTIVE_SET_SIZE_KEY, new_size.to_string())
}
@@ -230,13 +236,10 @@ pub fn new_pending_active_set_update_event(
}
pub fn new_rewarding_params_update_event(
created_at: BlockHeight,
update: IntervalRewardingParamsUpdate,
updated: IntervalRewardParams,
) -> Event {
Event::new(MixnetEventType::IntervalRewardingParamsUpdate)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(
INTERVAL_REWARDING_PARAMS_UPDATE_KEY,
update.to_inline_json(),
@@ -262,14 +265,8 @@ pub fn new_pending_rewarding_params_update_event(
)
}
pub fn new_undelegation_event(
created_at: BlockHeight,
delegator: &Addr,
proxy: &Option<Addr>,
mix_id: MixId,
) -> Event {
pub fn new_undelegation_event(delegator: &Addr, proxy: &Option<Addr>, mix_id: MixId) -> Event {
Event::new(MixnetEventType::Undelegation)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(DELEGATOR_KEY, delegator)
.add_optional_attribute(PROXY_KEY, proxy.as_ref())
.add_attribute(MIX_ID_KEY, mix_id.to_string())
@@ -330,10 +327,8 @@ pub fn new_mixnode_bonding_event(
.add_attribute(AMOUNT_KEY, amount.to_string())
}
pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Event {
Event::new(MixnetEventType::MixnodeUnbonding)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(MIX_ID_KEY, mix_id.to_string())
pub fn new_mixnode_unbonding_event(mix_id: MixId) -> Event {
Event::new(MixnetEventType::MixnodeUnbonding).add_attribute(MIX_ID_KEY, mix_id.to_string())
}
pub fn new_pending_mixnode_unbonding_event(
@@ -375,13 +370,8 @@ pub fn new_mixnode_pending_cost_params_update_event(
.add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json())
}
pub fn new_mixnode_cost_params_update_event(
created_at: BlockHeight,
mix_id: MixId,
new_costs: &MixNodeCostParams,
) -> Event {
pub fn new_mixnode_cost_params_update_event(mix_id: MixId, new_costs: &MixNodeCostParams) -> Event {
Event::new(MixnetEventType::MixnodeCostParamsUpdate)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(MIX_ID_KEY, mix_id.to_string())
.add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json())
}
@@ -471,7 +461,7 @@ pub fn new_mix_rewarding_event(
interval.current_epoch_absolute_id().to_string(),
)
.add_attribute(PRIOR_DELEGATES_KEY, prior_delegates.to_string())
.add_attribute(PRIOR_UNIT_REWARD_KEY, prior_unit_reward.to_string())
.add_attribute(PRIOR_UNIT_REWARD, prior_unit_reward.to_string())
.add_attribute(MIX_ID_KEY, mix_id.to_string())
.add_attribute(
OPERATOR_REWARD_KEY,
@@ -507,13 +497,11 @@ pub fn new_reconcile_pending_events() -> Event {
}
pub fn new_interval_config_update_event(
created_at: BlockHeight,
epochs_in_interval: u32,
epoch_duration_secs: u64,
updated_rewarding_params: IntervalRewardParams,
) -> Event {
Event::new(MixnetEventType::IntervalConfigUpdate)
.add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string())
.add_attribute(
NEW_EPOCHS_DURATION_SECS_KEY,
epoch_duration_secs.to_string(),
@@ -1,33 +1,13 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{Decimal, StdError, StdResult, Uint128};
use cosmwasm_std::Decimal;
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128));
pub fn compare_decimals(a: Decimal, b: Decimal) {
let epsilon = Decimal::from_ratio(1u128, 100_000_000u128);
if a > b {
assert!(a - b < epsilon, "{} != {}", a, b)
} else {
assert!(b - a < epsilon, "{} != {}", a, b)
}
}
pub fn into_base_decimal(val: impl Into<Uint128>) -> StdResult<Decimal> {
val.into_base_decimal()
}
pub trait IntoBaseDecimal {
fn into_base_decimal(self) -> StdResult<Decimal>;
}
impl<T> IntoBaseDecimal for T
where
T: Into<Uint128>,
{
fn into_base_decimal(self) -> StdResult<Decimal> {
let atomics = self.into();
Decimal::from_atomics(atomics, 0).map_err(|_| StdError::GenericErr {
msg: format!("Decimal range exceeded for {atomics} with 0 decimal places."),
})
}
}
@@ -142,17 +142,14 @@ impl JsonSchema for Interval {
impl Interval {
/// Initialize epoch in the contract with default values.
pub fn init_interval(epochs_in_interval: u32, epoch_length: Duration, env: &Env) -> Self {
// if this fails it means the value provided from the chain itself (via cosmwasm) is invalid,
// so we really have to panic here as anything beyond that point would be invalid anyway
#[allow(clippy::expect_used)]
let current_epoch_start =
OffsetDateTime::from_unix_timestamp(env.block.time.seconds() as i64)
.expect("The timestamp provided via env.block.time is invalid");
Interval {
id: 0,
epochs_in_interval,
current_epoch_start,
// I really don't see a way for this to fail, unless the blockchain is lying to us
current_epoch_start: OffsetDateTime::from_unix_timestamp(
env.block.time.seconds() as i64
)
.expect("Invalid timestamp from env.block.time"),
current_epoch_id: 0,
epoch_length,
total_elapsed_epochs: 0,
@@ -1,9 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
mod constants;
pub mod delegation;
pub mod error;
@@ -37,8 +34,7 @@ pub use mixnode::{
};
pub use msg::*;
pub use pending_events::{
PendingEpochEvent, PendingEpochEventData, PendingEpochEventKind, PendingIntervalEvent,
PendingIntervalEventData, PendingIntervalEventKind,
PendingEpochEvent, PendingEpochEventData, PendingIntervalEvent, PendingIntervalEventData,
};
pub use reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate, RewardingParams};
pub use types::*;
@@ -4,14 +4,13 @@
// due to code generated by JsonSchema
#![allow(clippy::field_reassign_with_default)]
use crate::constants::{TOKEN_SUPPLY, UNIT_DELEGATION_BASE};
use crate::constants::UNIT_DELEGATION_BASE;
use crate::error::MixnetContractError;
use crate::helpers::IntoBaseDecimal;
use crate::reward_params::{NodeRewardParams, RewardingParams};
use crate::rewarding::helpers::truncate_reward;
use crate::rewarding::RewardDistribution;
use crate::{Delegation, EpochId, IdentityKey, MixId, Percent, SphinxKey};
use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128};
use cosmwasm_std::{Addr, Coin, Decimal, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
@@ -65,7 +64,7 @@ impl MixNodeDetails {
self.rewarding_details.pending_operator_reward(pledge)
}
pub fn pending_detailed_operator_reward(&self) -> StdResult<Decimal> {
pub fn pending_detailed_operator_reward(&self) -> Decimal {
let pledge = self.original_pledge();
self.rewarding_details
.pending_detailed_operator_reward(pledge)
@@ -79,27 +78,34 @@ impl MixNodeDetails {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct MixNodeRewarding {
/// Information provided by the operator that influence the cost function.
#[serde(alias = "cp")]
pub cost_params: MixNodeCostParams,
/// Total pledge and compounded reward earned by the node operator.
#[serde(alias = "op")]
pub operator: Decimal,
/// Total delegation and compounded reward earned by all node delegators.
#[serde(alias = "dg")]
pub delegates: Decimal,
/// Cumulative reward earned by the "unit delegation" since the block 0.
#[serde(alias = "tur")]
pub total_unit_reward: Decimal,
/// Value of the theoretical "unit delegation" that has delegated to this mixnode at block 0.
#[serde(alias = "ud")]
pub unit_delegation: Decimal,
/// Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt
/// to reward it multiple times in the same epoch.
#[serde(alias = "le")]
pub last_rewarded_epoch: EpochId,
// technically we don't need that field to determine reward magnitude or anything
// but it saves on extra queries to determine if we're removing the final delegation
// (so that we could zero the field correctly)
#[serde(alias = "uqd")]
pub unique_delegations: u32,
}
@@ -108,21 +114,16 @@ impl MixNodeRewarding {
cost_params: MixNodeCostParams,
initial_pledge: &Coin,
current_epoch: EpochId,
) -> Result<Self, MixnetContractError> {
assert!(
initial_pledge.amount <= TOKEN_SUPPLY,
"pledge cannot be larger than the token supply"
);
Ok(MixNodeRewarding {
) -> Self {
MixNodeRewarding {
cost_params,
operator: initial_pledge.amount.into_base_decimal()?,
operator: Decimal::from_atomics(initial_pledge.amount, 0).unwrap(),
delegates: Decimal::zero(),
total_unit_reward: Decimal::zero(),
unit_delegation: UNIT_DELEGATION_BASE,
last_rewarded_epoch: current_epoch,
unique_delegations: 0,
})
}
}
/// Determines whether this node is still bonded. This is performed via a simple check,
@@ -141,30 +142,27 @@ impl MixNodeRewarding {
}
}
pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> StdResult<Decimal> {
let initial_dec = original_pledge.amount.into_base_decimal()?;
pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> Decimal {
let initial_dec = Decimal::from_atomics(original_pledge.amount, 0).unwrap();
if initial_dec > self.operator {
panic!(
"seems slashing has occurred while it has not been implemented nor accounted for!"
)
}
Ok(self.operator - initial_dec)
self.operator - initial_dec
}
pub fn operator_pledge_with_reward(&self, denom: impl Into<String>) -> Coin {
truncate_reward(self.operator, denom)
}
pub fn pending_delegator_reward(&self, delegation: &Delegation) -> StdResult<Coin> {
let delegator_reward = self.determine_delegation_reward(delegation)?;
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
pub fn pending_delegator_reward(&self, delegation: &Delegation) -> Coin {
let delegator_reward = self.determine_delegation_reward(delegation);
truncate_reward(delegator_reward, &delegation.amount.denom)
}
pub fn withdraw_operator_reward(
&mut self,
original_pledge: &Coin,
) -> Result<Coin, MixnetContractError> {
let initial_dec = original_pledge.amount.into_base_decimal()?;
pub fn withdraw_operator_reward(&mut self, original_pledge: &Coin) -> Coin {
let initial_dec = Decimal::from_atomics(original_pledge.amount, 0).unwrap();
if initial_dec > self.operator {
panic!(
"seems slashing has occurred while it has not been implemented nor accounted for!"
@@ -173,14 +171,14 @@ impl MixNodeRewarding {
let diff = self.operator - initial_dec;
self.operator = initial_dec;
Ok(truncate_reward(diff, &original_pledge.denom))
truncate_reward(diff, &original_pledge.denom)
}
pub fn withdraw_delegator_reward(
&mut self,
delegation: &mut Delegation,
) -> Result<Coin, MixnetContractError> {
let reward = self.determine_delegation_reward(delegation)?;
let reward = self.determine_delegation_reward(delegation);
self.decrease_delegates_decimal(reward)?;
delegation.cumulative_reward_ratio = self.full_reward_ratio();
@@ -310,27 +308,23 @@ impl MixNodeRewarding {
self.distribute_rewards(reward_distribution, absolute_epoch_id)
}
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> StdResult<Decimal> {
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal {
let starting_ratio = delegation.cumulative_reward_ratio;
let ending_ratio = self.full_reward_ratio();
let adjust = starting_ratio + self.unit_delegation;
Ok((ending_ratio - starting_ratio) * delegation.dec_amount()? / adjust)
(ending_ratio - starting_ratio) * delegation.dec_amount() / adjust
}
// this updates `unique_delegations` field
pub fn add_base_delegation(&mut self, amount: Uint128) -> Result<(), MixnetContractError> {
self.increase_delegates_uint128(amount)?;
pub fn add_base_delegation(&mut self, amount: Uint128) {
self.increase_delegates_uint128(amount);
self.unique_delegations += 1;
Ok(())
}
pub fn increase_delegates_uint128(
&mut self,
amount: Uint128,
) -> Result<(), MixnetContractError> {
self.delegates += amount.into_base_decimal()?;
Ok(())
pub fn increase_delegates_uint128(&mut self, amount: Uint128) {
// the unwrap here is fine as the value is guaranteed to fit under provided constraints
self.delegates += Decimal::from_atomics(amount, 0).unwrap()
}
// this updates `unique_delegations` field
@@ -348,7 +342,7 @@ impl MixNodeRewarding {
&mut self,
amount: Uint128,
) -> Result<(), MixnetContractError> {
let amount_dec = amount.into_base_decimal()?;
let amount_dec = Decimal::from_atomics(amount, 0).unwrap();
self.decrease_delegates_decimal(amount_dec)
}
@@ -381,8 +375,8 @@ impl MixNodeRewarding {
}
pub fn undelegate(&mut self, delegation: &Delegation) -> Result<Coin, MixnetContractError> {
let reward = self.determine_delegation_reward(delegation)?;
let full_amount = reward + delegation.dec_amount()?;
let reward = self.determine_delegation_reward(delegation);
let full_amount = reward + delegation.dec_amount();
self.remove_delegation_decimal(full_amount)?;
Ok(truncate_reward(full_amount, &delegation.amount.denom))
}
@@ -434,6 +428,7 @@ impl MixNodeRewarding {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct MixNodeBond {
/// Unique id assigned to the bonded mixnode.
#[serde(alias = "id")]
pub mix_id: MixId,
/// Address of the owner of this mixnode.
@@ -2,8 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
use crate::delegation::OwnerProxySubKey;
use crate::error::MixnetContractError;
use crate::helpers::IntoBaseDecimal;
use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
use crate::reward_params::{
IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams,
@@ -42,17 +40,14 @@ pub struct InitialRewardingParams {
}
impl InitialRewardingParams {
pub fn into_rewarding_params(
self,
epochs_in_interval: u32,
) -> Result<RewardingParams, MixnetContractError> {
pub fn into_rewarding_params(self, epochs_in_interval: u32) -> RewardingParams {
let epoch_reward_budget = self.initial_reward_pool
/ epochs_in_interval.into_base_decimal()?
/ Decimal::from_atomics(epochs_in_interval, 0).unwrap()
* self.interval_pool_emission;
let stake_saturation_point =
self.initial_staking_supply / self.rewarded_set_size.into_base_decimal()?;
self.initial_staking_supply / Decimal::from_atomics(self.rewarded_set_size, 0).unwrap();
Ok(RewardingParams {
RewardingParams {
interval: IntervalRewardParams {
reward_pool: self.initial_reward_pool,
staking_supply: self.initial_staking_supply,
@@ -64,7 +59,7 @@ impl InitialRewardingParams {
},
rewarded_set_size: self.rewarded_set_size,
active_set_size: self.active_set_size,
})
}
}
}
@@ -3,7 +3,7 @@
use crate::mixnode::MixNodeCostParams;
use crate::reward_params::IntervalRewardingParamsUpdate;
use crate::{BlockHeight, EpochEventId, IntervalEventId, MixId};
use crate::{EpochEventId, IntervalEventId, MixId};
use cosmwasm_std::{Addr, Coin};
use serde::{Deserialize, Serialize};
@@ -14,13 +14,7 @@ pub struct PendingEpochEvent {
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct PendingEpochEventData {
pub created_at: BlockHeight,
pub kind: PendingEpochEventKind,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum PendingEpochEventKind {
pub enum PendingEpochEventData {
// can't just pass the `Delegation` struct here as it's impossible to determine
// `cumulative_reward_ratio` ahead of time
Delegate {
@@ -42,15 +36,6 @@ pub enum PendingEpochEventKind {
},
}
impl PendingEpochEventKind {
pub fn attach_source_height(self, created_at: BlockHeight) -> PendingEpochEventData {
PendingEpochEventData {
created_at,
kind: self,
}
}
}
impl From<(EpochEventId, PendingEpochEventData)> for PendingEpochEvent {
fn from(data: (EpochEventId, PendingEpochEventData)) -> Self {
PendingEpochEvent {
@@ -67,13 +52,7 @@ pub struct PendingIntervalEvent {
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct PendingIntervalEventData {
pub created_at: BlockHeight,
pub kind: PendingIntervalEventKind,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum PendingIntervalEventKind {
pub enum PendingIntervalEventData {
ChangeMixCostParams {
mix_id: MixId,
new_costs: MixNodeCostParams,
@@ -88,15 +67,6 @@ pub enum PendingIntervalEventKind {
},
}
impl PendingIntervalEventKind {
pub fn attach_source_height(self, created_at: BlockHeight) -> PendingIntervalEventData {
PendingIntervalEventData {
created_at,
kind: self,
}
}
}
impl From<(IntervalEventId, PendingIntervalEventData)> for PendingIntervalEvent {
fn from(data: (IntervalEventId, PendingIntervalEventData)) -> Self {
PendingIntervalEvent {
@@ -1,7 +1,6 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::helpers::IntoBaseDecimal;
use crate::{error::MixnetContractError, Percent};
use cosmwasm_std::Decimal;
use schemars::JsonSchema;
@@ -106,33 +105,24 @@ impl RewardingParams {
pub fn dec_rewarded_set_size(&self) -> Decimal {
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
// with 0 decimal places
#[allow(clippy::unwrap_used)]
self.rewarded_set_size.into_base_decimal().unwrap()
Decimal::from_atomics(self.rewarded_set_size, 0).unwrap()
}
pub fn dec_active_set_size(&self) -> Decimal {
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
// with 0 decimal places
#[allow(clippy::unwrap_used)]
self.active_set_size.into_base_decimal().unwrap()
Decimal::from_atomics(self.active_set_size, 0).unwrap()
}
fn dec_standby_set_size(&self) -> Decimal {
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
// with 0 decimal places
#[allow(clippy::unwrap_used)]
(self.rewarded_set_size - self.active_set_size)
.into_base_decimal()
.unwrap()
Decimal::from_atomics(self.rewarded_set_size - self.active_set_size, 0).unwrap()
}
pub fn apply_epochs_in_interval_change(&mut self, new_epochs_in_interval: u32) {
// the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal
// with 0 decimal places
#[allow(clippy::unwrap_used)]
let new_epochs_in_interval = new_epochs_in_interval.into_base_decimal().unwrap();
self.interval.epoch_reward_budget = self.interval.reward_pool / new_epochs_in_interval
self.interval.epoch_reward_budget = self.interval.reward_pool
/ Decimal::from_atomics(new_epochs_in_interval, 0).unwrap()
* self.interval.interval_pool_emission;
}
@@ -201,13 +191,13 @@ impl RewardingParams {
if recompute_epoch_budget {
self.interval.epoch_reward_budget = self.interval.reward_pool
/ epochs_in_interval.into_base_decimal()?
/ Decimal::from_atomics(epochs_in_interval, 0).unwrap()
* self.interval.interval_pool_emission;
}
if recompute_saturation_point {
self.interval.stake_saturation_point =
self.interval.staking_supply / self.rewarded_set_size.into_base_decimal()?
self.interval.stake_saturation_point = self.interval.staking_supply
/ Decimal::from_atomics(self.rewarded_set_size, 0).unwrap();
}
Ok(())
@@ -29,7 +29,7 @@ pub struct RewardEstimate {
pub operating_cost: Decimal,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq)]
pub struct RewardDistribution {
pub operator: Decimal,
pub delegates: Decimal,
@@ -0,0 +1,457 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::MixnetContractError;
use crate::mixnode::{MixNodeCostParams, MixNodeRewarding};
use crate::reward_params::{IntervalRewardParams, NodeRewardParams, RewardingParams};
use crate::rewarding::helpers::truncate_reward;
use crate::rewarding::RewardDistribution;
use crate::{Delegation, Interval, Percent};
use cosmwasm_std::{Addr, Coin, Decimal};
pub struct Simulator {
pub node_rewarding_details: MixNodeRewarding,
pub node_delegations: Vec<Delegation>,
pub system_rewarding_params: RewardingParams,
pub interval: Interval,
}
impl Simulator {
pub fn new(
profit_margin_percent: Percent,
interval_operating_cost: Coin,
system_rewarding_params: RewardingParams,
initial_pledge: Coin,
interval: Interval,
) -> Self {
let cost_params = MixNodeCostParams {
profit_margin_percent,
interval_operating_cost,
};
Simulator {
node_rewarding_details: MixNodeRewarding::initialise_new(
cost_params,
&initial_pledge,
Default::default(),
),
node_delegations: vec![],
system_rewarding_params,
interval,
}
}
pub fn delegate(&mut self, amount: Coin) {
let cumulative_reward_ratio = self.node_rewarding_details.total_unit_reward;
// let record = self.node_rewarding_details.increment_period();
// self.node_historical_records.insert(period, record);
self.node_rewarding_details
.add_base_delegation(amount.amount);
// we don't care about the owner/node details here
self.node_delegations.push(Delegation::new(
Addr::unchecked("bob"),
42,
cumulative_reward_ratio,
amount,
123,
None,
))
}
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal {
self.node_rewarding_details
.determine_delegation_reward(delegation)
}
// since this is a simulator only, not something to be used in the production code, the unwraps are fine
// if user inputs are invalid
pub fn undelegate(
&mut self,
delegation_index: usize,
) -> Result<(Coin, Coin), MixnetContractError> {
let delegation = self.node_delegations.remove(delegation_index);
let reward = self.determine_delegation_reward(&delegation);
self.node_rewarding_details
.decrease_delegates_decimal(delegation.dec_amount() + reward)?;
self.node_rewarding_details.unique_delegations -= 1;
let reward_denom = &delegation.amount.denom;
let truncated_reward = truncate_reward(reward, reward_denom);
// if this was last delegation, move all leftover decimal tokens to the operator
// (this is literally in the order of a millionth of a micronym)
if self.node_delegations.is_empty() {
assert_eq!(self.node_rewarding_details.unique_delegations, 0);
self.node_rewarding_details.operator += self.node_rewarding_details.delegates;
self.node_rewarding_details.delegates = Decimal::zero();
}
Ok((delegation.amount, truncated_reward))
}
pub fn determine_total_delegation_reward(&self) -> Decimal {
let mut total = Decimal::zero();
for delegation in &self.node_delegations {
total += self.determine_delegation_reward(delegation)
}
total
}
pub fn simulate_epoch(&mut self, node_params: NodeRewardParams) -> RewardDistribution {
let reward_distribution = self.node_rewarding_details.calculate_epoch_reward(
&self.system_rewarding_params,
node_params,
self.interval.epochs_in_interval(),
);
self.node_rewarding_details.distribute_rewards(
reward_distribution,
self.interval.current_epoch_absolute_id(),
);
self.interval = self.interval.advance_epoch();
reward_distribution
}
// assume node state doesn't change in the interval (kinda unrealistic)
pub fn simulate_interval(&mut self, node_params: NodeRewardParams) {
assert_eq!(self.interval.current_epoch_id(), 0);
let id = self.interval.current_interval_id();
let mut distributed = Decimal::zero();
for _ in 0..self.interval.epochs_in_interval() {
let distr = self.simulate_epoch(node_params);
distributed += distr.operator;
distributed += distr.delegates;
}
assert_eq!(id + 1, self.interval.current_interval_id());
// update reward pool and all of that
let old = self.system_rewarding_params.interval;
let reward_pool = old.reward_pool - distributed;
let staking_supply = old.staking_supply + distributed;
let epoch_reward_budget = reward_pool
/ Decimal::from_atomics(self.interval.epochs_in_interval(), 0).unwrap()
* old.interval_pool_emission.value();
let stake_saturation_point = staking_supply
/ Decimal::from_atomics(self.system_rewarding_params.rewarded_set_size, 0).unwrap();
let updated_params = RewardingParams {
interval: IntervalRewardParams {
reward_pool,
staking_supply,
epoch_reward_budget,
stake_saturation_point,
sybil_resistance: old.sybil_resistance,
active_set_work_factor: old.active_set_work_factor,
interval_pool_emission: old.interval_pool_emission,
},
rewarded_set_size: self.system_rewarding_params.rewarded_set_size,
active_set_size: self.system_rewarding_params.active_set_size,
};
self.system_rewarding_params = updated_params;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helpers::compare_decimals;
use crate::reward_params::IntervalRewardParams;
use crate::rewarding::helpers::truncate_reward_amount;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::{coin, Uint128};
use std::time::Duration;
fn base_simulator(initial_pledge: u128) -> Simulator {
let profit_margin = Percent::from_percentage_value(10).unwrap();
let interval_operating_cost = Coin::new(40_000_000, "unym");
let epochs_in_interval = 720u32;
let rewarded_set_size = 240;
let active_set_size = 100;
let interval_pool_emission = Percent::from_percentage_value(2).unwrap();
let reward_pool = 250_000_000_000_000u128;
let staking_supply = 100_000_000_000_000u128;
let epoch_reward_budget =
interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval);
let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size);
let rewarding_params = RewardingParams {
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
epoch_reward_budget,
stake_saturation_point,
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
active_set_work_factor: Decimal::percent(1000), // value '10'
interval_pool_emission,
},
rewarded_set_size,
active_set_size,
};
let interval = Interval::init_interval(
epochs_in_interval,
Duration::from_secs(60 * 60),
&mock_env(),
);
let initial_pledge = Coin::new(initial_pledge, "unym");
Simulator::new(
profit_margin,
interval_operating_cost,
rewarding_params,
initial_pledge,
interval,
)
}
// essentially our delegations + estimated rewards HAVE TO equal to what we actually determined
fn check_rewarding_invariant(simulator: &Simulator) {
let delegation_sum: Decimal = simulator
.node_delegations
.iter()
.map(|d| d.dec_amount())
.sum();
let reward_sum = simulator.determine_total_delegation_reward();
compare_decimals(
delegation_sum + reward_sum,
simulator.node_rewarding_details.delegates,
)
}
#[test]
fn simulator_returns_expected_values_for_base_case() {
let mut simulator = base_simulator(10000_000000);
let epoch_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards = simulator.simulate_epoch(epoch_params);
assert_eq!(rewards.delegates, Decimal::zero());
compare_decimals(rewards.operator, "1128452.5416104363".parse().unwrap());
}
#[test]
fn single_delegation_at_genesis() {
let mut simulator = base_simulator(10000_000000);
simulator.delegate(Coin::new(18000_000000, "unym"));
let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards = simulator.simulate_epoch(node_params);
compare_decimals(rewards.delegates, "1795950.2602660495".parse().unwrap());
compare_decimals(rewards.operator, "1363716.856243172".parse().unwrap());
compare_decimals(
rewards.delegates,
simulator.determine_total_delegation_reward(),
);
assert_eq!(
simulator.node_rewarding_details.operator,
rewards.operator + Decimal::from_atomics(10000_000000u128, 0).unwrap()
);
assert_eq!(
simulator.node_rewarding_details.delegates,
rewards.delegates + Decimal::from_atomics(18000_000000u128, 0).unwrap()
);
}
#[test]
fn delegation_and_undelegation() {
let mut simulator = base_simulator(10000_000000);
let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards1 = simulator.simulate_epoch(node_params);
let expected_operator1 = "1128452.5416104363".parse().unwrap();
assert_eq!(rewards1.delegates, Decimal::zero());
compare_decimals(rewards1.operator, expected_operator1);
simulator.delegate(Coin::new(18000_000000, "unym"));
let rewards2 = simulator.simulate_epoch(node_params);
let expected_operator2 = "1363843.413584609".parse().unwrap();
let expected_delegator_reward1 = "1795952.25874404".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward1);
compare_decimals(rewards2.operator, expected_operator2);
let rewards3 = simulator.simulate_epoch(node_params);
let expected_operator3 = "1364017.7824440491".parse().unwrap();
let expected_delegator_reward2 = "1796135.9269468693".parse().unwrap();
compare_decimals(rewards3.delegates, expected_delegator_reward2);
compare_decimals(rewards3.operator, expected_operator3);
let (delegation, reward) = simulator.undelegate(0).unwrap();
assert_eq!(delegation.amount.u128(), 18000_000000);
assert_eq!(
reward.amount,
(expected_delegator_reward1 + expected_delegator_reward2) * Uint128::new(1)
);
let base_op = Decimal::from_atomics(10000_000000u128, 0).unwrap();
compare_decimals(
simulator.node_rewarding_details.operator,
base_op + expected_operator1 + expected_operator2 + expected_operator3,
);
assert_eq!(Decimal::zero(), simulator.node_rewarding_details.delegates);
}
#[test]
fn withdrawing_operator_reward() {
// essentially all delegators' rewards (and the operator itself) are still correctly computed
let original_pledge = coin(10000_000000, "unym");
let mut simulator = base_simulator(original_pledge.amount.u128());
let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
// delegating at different times still work)
simulator.delegate(Coin::new(18000_000000, "unym"));
simulator.delegate(Coin::new(4000_000000, "unym"));
// "normal", sanity check rewarding
let rewards1 = simulator.simulate_epoch(node_params);
let expected_operator1 = "1411087.1007647323".parse().unwrap();
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
compare_decimals(rewards1.delegates, expected_delegator_reward1);
compare_decimals(rewards1.operator, expected_operator1);
check_rewarding_invariant(&simulator);
let reward = simulator
.node_rewarding_details
.withdraw_operator_reward(&original_pledge);
assert_eq!(reward.amount, truncate_reward_amount(expected_operator1));
assert_eq!(
simulator.node_rewarding_details.operator,
Decimal::from_atomics(original_pledge.amount, 0).unwrap()
);
let rewards2 = simulator.simulate_epoch(node_params);
let expected_operator2 = "1411113.0004067947".parse().unwrap();
let expected_delegator_reward2 = "2200183.3879084454".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward2);
compare_decimals(rewards2.operator, expected_operator2);
check_rewarding_invariant(&simulator);
}
#[test]
fn withdrawing_delegator_reward() {
// essentially all delegators' rewards (and the operator itself) are still correctly computed
let mut simulator = base_simulator(10000_000000);
let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
// delegating at different times still work)
simulator.delegate(Coin::new(18000_000000, "unym"));
simulator.delegate(Coin::new(4000_000000, "unym"));
// "normal", sanity check rewarding
let rewards1 = simulator.simulate_epoch(node_params);
let expected_operator1 = "1411087.1007647323".parse().unwrap();
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
compare_decimals(rewards1.delegates, expected_delegator_reward1);
compare_decimals(rewards1.operator, expected_operator1);
check_rewarding_invariant(&simulator);
// reference to our `18000_000000` delegation
let delegation1 = &mut simulator.node_delegations[0];
let reward = simulator
.node_rewarding_details
.withdraw_delegator_reward(delegation1)
.unwrap();
let expected_del1_reward = "1799968.1174089068".parse().unwrap();
assert_eq!(reward.amount, truncate_reward_amount(expected_del1_reward));
// new reward after withdrawal
let rewards2 = simulator.simulate_epoch(node_params);
let expected_operator2 = "1411250.1907492676".parse().unwrap();
let expected_delegator_reward2 = "2200004.051009689".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward2);
compare_decimals(rewards2.operator, expected_operator2);
check_rewarding_invariant(&simulator);
// check final values
let reward_del1 = simulator
.node_rewarding_details
.withdraw_delegator_reward(&mut simulator.node_delegations[0])
.unwrap();
let expected_del1_reward = "1799970.5883041779".parse().unwrap();
assert_eq!(
reward_del1.amount,
truncate_reward_amount(expected_del1_reward)
);
let reward_del2 = simulator
.node_rewarding_details
.withdraw_delegator_reward(&mut simulator.node_delegations[1])
.unwrap();
let first: Decimal = "399992.91497975704".parse().unwrap();
let second: Decimal = "400033.4627055114".parse().unwrap();
let expected_del2_reward = first + second;
assert_eq!(
reward_del2.amount,
truncate_reward_amount(expected_del2_reward)
);
}
#[test]
fn simulating_multiple_epochs() {
let mut simulator = base_simulator(10000_000000);
let mut is_active = true;
let mut performance = Percent::from_percentage_value(100).unwrap();
for epoch in 0..720 {
if epoch == 0 {
simulator.delegate(Coin::new(18000_000000, "unym"))
}
if epoch == 42 {
simulator.delegate(Coin::new(2000_000000, "unym"))
}
if epoch == 89 {
is_active = false;
}
if epoch == 123 {
simulator.delegate(Coin::new(6666_000000, "unym"))
}
if epoch == 167 {
performance = Percent::from_percentage_value(90).unwrap();
}
if epoch == 245 {
simulator.delegate(Coin::new(2050_000000, "unym"))
}
if epoch == 264 {
let (delegation, _reward) = simulator.undelegate(1).unwrap();
// sanity check to make sure we undelegated what we wanted to undelegate : )
assert_eq!(delegation.amount.u128(), 2000_000000);
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
}
if epoch == 345 {
is_active = true;
}
if epoch == 358 {
performance = Percent::from_percentage_value(100).unwrap();
}
if epoch == 458 {
let (delegation, _reward) = simulator.undelegate(0).unwrap();
// sanity check to make sure we undelegated what we wanted to undelegate : )
assert_eq!(delegation.amount.u128(), 18000_000000);
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
}
if epoch == 545 {
simulator.delegate(Coin::new(5000_000000, "unym"))
}
// this has to always hold
check_rewarding_invariant(&simulator);
let node_params = NodeRewardParams::new(performance, is_active);
simulator.simulate_epoch(node_params);
}
// after everyone undelegates, there should be nothing left in the delegates pool
let delegations = simulator.node_delegations.len();
for _ in 0..delegations {
simulator.undelegate(0).unwrap();
}
assert_eq!(Decimal::zero(), simulator.node_rewarding_details.delegates);
}
}
@@ -1,865 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::MixnetContractError;
use crate::helpers::IntoBaseDecimal;
use crate::reward_params::NodeRewardParams;
use crate::rewarding::simulator::simulated_node::SimulatedNode;
use crate::rewarding::RewardDistribution;
use crate::{
Delegation, Interval, IntervalRewardParams, MixId, MixNodeCostParams, RewardingParams,
};
use cosmwasm_std::{Coin, Decimal};
use std::collections::BTreeMap;
pub mod simulated_node;
pub struct Simulator {
pub nodes: BTreeMap<MixId, SimulatedNode>,
pub system_rewarding_params: RewardingParams,
pub interval: Interval,
next_mix_id: MixId,
pending_reward_pool_emission: Decimal,
}
impl Simulator {
pub fn new(system_rewarding_params: RewardingParams, interval: Interval) -> Self {
Simulator {
nodes: Default::default(),
system_rewarding_params,
interval,
next_mix_id: 0,
pending_reward_pool_emission: Default::default(),
}
}
fn advance_epoch(&mut self) -> Result<(), MixnetContractError> {
let updated = self.interval.advance_epoch();
// we rolled over an interval
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 epoch_reward_budget = reward_pool
/ self.interval.epochs_in_interval().into_base_decimal()?
* old.interval_pool_emission.value();
let stake_saturation_point = staking_supply
/ self
.system_rewarding_params
.rewarded_set_size
.into_base_decimal()?;
let updated_params = RewardingParams {
interval: IntervalRewardParams {
reward_pool,
staking_supply,
epoch_reward_budget,
stake_saturation_point,
sybil_resistance: old.sybil_resistance,
active_set_work_factor: old.active_set_work_factor,
interval_pool_emission: old.interval_pool_emission,
},
rewarded_set_size: self.system_rewarding_params.rewarded_set_size,
active_set_size: self.system_rewarding_params.active_set_size,
};
self.system_rewarding_params = updated_params;
self.pending_reward_pool_emission = Decimal::zero();
}
self.interval = updated;
Ok(())
}
pub fn bond(
&mut self,
pledge: Coin,
cost_params: MixNodeCostParams,
) -> Result<MixId, MixnetContractError> {
let mix_id = self.next_mix_id;
self.nodes.insert(
mix_id,
SimulatedNode::new(
mix_id,
cost_params,
&pledge,
self.interval.current_epoch_absolute_id(),
)?,
);
self.next_mix_id += 1;
Ok(mix_id)
}
pub fn delegate<S: Into<String>>(
&mut self,
delegator: S,
delegation: Coin,
mix_id: MixId,
) -> Result<(), MixnetContractError> {
let node = self
.nodes
.get_mut(&mix_id)
.ok_or(MixnetContractError::MixNodeBondNotFound { mix_id })?;
node.delegate(delegator, delegation)
}
// since this is a simulator only, not something to be used in the production code, the unwraps are fine
// if user inputs are invalid
pub fn undelegate<S: Into<String>>(
&mut self,
delegator: S,
mix_id: MixId,
) -> Result<(Coin, Coin), MixnetContractError> {
let node = self
.nodes
.get_mut(&mix_id)
.ok_or(MixnetContractError::MixNodeBondNotFound { mix_id })?;
node.undelegate(delegator)
}
pub fn simulate_epoch_single_node(
&mut self,
params: NodeRewardParams,
) -> Result<RewardDistribution, MixnetContractError> {
assert_eq!(self.nodes.len(), 1);
if let Some(&id) = self.nodes.keys().next() {
let mut params_map = BTreeMap::new();
params_map.insert(id, params);
Ok(self
.simulate_epoch(&params_map)?
.remove(&id)
.unwrap_or_default())
} else {
Ok(RewardDistribution::default())
}
}
pub fn simulate_epoch(
&mut self,
node_params: &BTreeMap<MixId, NodeRewardParams>,
) -> Result<BTreeMap<MixId, RewardDistribution>, MixnetContractError> {
let mut params_keys = node_params.keys().copied().collect::<Vec<_>>();
params_keys.sort_unstable();
let mut node_keys = self.nodes.keys().copied().collect::<Vec<_>>();
node_keys.sort_unstable();
if params_keys != node_keys {
panic!("invalid node rewarding params provided");
}
let mut dist = BTreeMap::new();
for (mix_id, node) in self.nodes.iter_mut() {
let reward_distribution = node.rewarding_details.calculate_epoch_reward(
&self.system_rewarding_params,
node_params[mix_id],
self.interval.epochs_in_interval(),
);
node.rewarding_details.distribute_rewards(
reward_distribution,
self.interval.current_epoch_absolute_id(),
);
self.pending_reward_pool_emission += reward_distribution.operator;
self.pending_reward_pool_emission += reward_distribution.delegates;
dist.insert(*mix_id, reward_distribution);
}
self.advance_epoch()?;
Ok(dist)
}
pub fn determine_delegation_reward(
&self,
delegation: &Delegation,
) -> Result<Decimal, MixnetContractError> {
Ok(self.nodes[&delegation.mix_id]
.rewarding_details
.determine_delegation_reward(delegation)?)
}
pub fn determine_total_delegation_reward(&self) -> Result<Decimal, MixnetContractError> {
let mut total = Decimal::zero();
for node in self.nodes.values() {
for delegation in node.delegations.values() {
total += node
.rewarding_details
.determine_delegation_reward(delegation)?
}
}
Ok(total)
}
// assume node state doesn't change in the interval (kinda unrealistic)
pub fn simulate_full_interval(
&mut self,
node_params: &BTreeMap<MixId, NodeRewardParams>,
) -> Result<(), MixnetContractError> {
for _ in 0..self.interval.epochs_in_interval() {
self.simulate_epoch(node_params)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::helpers::compare_decimals;
use crate::Percent;
use cosmwasm_std::testing::mock_env;
use std::time::Duration;
#[cfg(test)]
mod single_node_case {
use super::*;
use crate::helpers::compare_decimals;
use crate::reward_params::IntervalRewardParams;
use crate::rewarding::helpers::truncate_reward_amount;
use crate::Percent;
use cosmwasm_std::coin;
use cosmwasm_std::testing::mock_env;
use std::time::Duration;
// explicitly marking this as part of #[allow(clippy::unwrap_used)] until
// https://github.com/rust-lang/rust-clippy/pull/9686
// is merged into a release
#[allow(clippy::unwrap_used)]
fn base_simulator(initial_pledge: u128) -> Simulator {
let profit_margin = Percent::from_percentage_value(10).unwrap();
let interval_operating_cost = Coin::new(40_000_000, "unym");
let epochs_in_interval = 720u32;
let rewarded_set_size = 240;
let active_set_size = 100;
let interval_pool_emission = Percent::from_percentage_value(2).unwrap();
let reward_pool = 250_000_000_000_000u128;
let staking_supply = 100_000_000_000_000u128;
let epoch_reward_budget =
interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval);
let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size);
let rewarding_params = RewardingParams {
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
epoch_reward_budget,
stake_saturation_point,
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
active_set_work_factor: Decimal::percent(1000), // value '10'
interval_pool_emission,
},
rewarded_set_size,
active_set_size,
};
let interval = Interval::init_interval(
epochs_in_interval,
Duration::from_secs(60 * 60),
&mock_env(),
);
let initial_pledge = Coin::new(initial_pledge, "unym");
let mut simulator = Simulator::new(rewarding_params, interval);
let cost_params = MixNodeCostParams {
profit_margin_percent: profit_margin,
interval_operating_cost,
};
simulator.bond(initial_pledge, cost_params).unwrap();
simulator
}
// essentially our delegations + estimated rewards HAVE TO equal to what we actually determined
//
// explicitly marking this as part of #[allow(clippy::unwrap_used)] until
// https://github.com/rust-lang/rust-clippy/pull/9686
// is merged into a release
#[allow(clippy::unwrap_used)]
fn check_rewarding_invariant(simulator: &Simulator) {
for node in simulator.nodes.values() {
let delegation_sum: Decimal = node
.delegations
.values()
.map(|d| d.dec_amount().unwrap())
.sum();
let reward_sum: Decimal = node
.delegations
.values()
.map(|d| {
node.rewarding_details
.determine_delegation_reward(d)
.unwrap()
})
.sum();
// let reward_sum = simulator.determine_total_delegation_reward();
compare_decimals(
delegation_sum + reward_sum,
node.rewarding_details.delegates,
None,
)
}
}
#[test]
fn simulator_returns_expected_values_for_base_case() {
let mut simulator = base_simulator(10000_000000);
let epoch_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards = simulator.simulate_epoch_single_node(epoch_params).unwrap();
assert_eq!(rewards.delegates, Decimal::zero());
compare_decimals(
rewards.operator,
"1128452.5416104363".parse().unwrap(),
None,
);
}
#[test]
fn single_delegation_at_genesis() {
let mut simulator = base_simulator(10000_000000);
simulator
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
.unwrap();
let node_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards = simulator.simulate_epoch_single_node(node_params).unwrap();
compare_decimals(
rewards.delegates,
"1795950.2602660495".parse().unwrap(),
None,
);
compare_decimals(rewards.operator, "1363716.856243172".parse().unwrap(), None);
compare_decimals(
rewards.delegates,
simulator.determine_total_delegation_reward().unwrap(),
None,
);
let node = &simulator.nodes[&0];
assert_eq!(
node.rewarding_details.operator,
rewards.operator + Decimal::from_atomics(10000_000000u128, 0).unwrap()
);
assert_eq!(
node.rewarding_details.delegates,
rewards.delegates + Decimal::from_atomics(18000_000000u128, 0).unwrap()
);
}
#[test]
fn delegation_and_undelegation() {
let mut simulator = base_simulator(10000_000000);
let node_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator1 = "1128452.5416104363".parse().unwrap();
assert_eq!(rewards1.delegates, Decimal::zero());
compare_decimals(rewards1.operator, expected_operator1, None);
simulator
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
.unwrap();
let rewards2 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator2 = "1363843.413584609".parse().unwrap();
let expected_delegator_reward1 = "1795952.25874404".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward1, None);
compare_decimals(rewards2.operator, expected_operator2, None);
let rewards3 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator3 = "1364017.7824440491".parse().unwrap();
let expected_delegator_reward2 = "1796135.9269468693".parse().unwrap();
compare_decimals(rewards3.delegates, expected_delegator_reward2, None);
compare_decimals(rewards3.operator, expected_operator3, None);
let (delegation, reward) = simulator.undelegate("alice", 0).unwrap();
assert_eq!(delegation.amount.u128(), 18000_000000);
assert_eq!(
reward.amount,
truncate_reward_amount(expected_delegator_reward1 + expected_delegator_reward2)
);
let base_op = Decimal::from_atomics(10000_000000u128, 0).unwrap();
let node = &simulator.nodes[&0];
compare_decimals(
node.rewarding_details.operator,
base_op + expected_operator1 + expected_operator2 + expected_operator3,
None,
);
assert_eq!(Decimal::zero(), node.rewarding_details.delegates);
}
#[test]
fn withdrawing_operator_reward() {
// essentially all delegators' rewards (and the operator itself) are still correctly computed
let original_pledge = coin(10000_000000, "unym");
let mut simulator = base_simulator(original_pledge.amount.u128());
let node_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
// delegating at different times still work)
simulator
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
.unwrap();
simulator
.delegate("bob", Coin::new(4000_000000, "unym"), 0)
.unwrap();
// "normal", sanity check rewarding
let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator1 = "1411087.1007647323".parse().unwrap();
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
compare_decimals(rewards1.delegates, expected_delegator_reward1, None);
compare_decimals(rewards1.operator, expected_operator1, None);
check_rewarding_invariant(&simulator);
let node = simulator.nodes.get_mut(&0).unwrap();
let reward = node
.rewarding_details
.withdraw_operator_reward(&original_pledge)
.unwrap();
assert_eq!(reward.amount, truncate_reward_amount(expected_operator1));
assert_eq!(
node.rewarding_details.operator,
Decimal::from_atomics(original_pledge.amount, 0).unwrap()
);
let rewards2 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator2 = "1411113.0004067947".parse().unwrap();
let expected_delegator_reward2 = "2200183.3879084454".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward2, None);
compare_decimals(rewards2.operator, expected_operator2, None);
check_rewarding_invariant(&simulator);
}
#[test]
fn withdrawing_delegator_reward() {
// essentially all delegators' rewards (and the operator itself) are still correctly computed
let mut simulator = base_simulator(10000_000000);
let node_params =
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
// delegating at different times still work)
simulator
.delegate("alice", Coin::new(18000_000000, "unym"), 0)
.unwrap();
simulator
.delegate("bob", Coin::new(4000_000000, "unym"), 0)
.unwrap();
// "normal", sanity check rewarding
let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator1 = "1411087.1007647323".parse().unwrap();
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
compare_decimals(rewards1.delegates, expected_delegator_reward1, None);
compare_decimals(rewards1.operator, expected_operator1, None);
check_rewarding_invariant(&simulator);
// reference to our `18000_000000` delegation
let node = simulator.nodes.get_mut(&0).unwrap();
let delegation1 = node.delegations.get_mut("alice").unwrap();
let reward = node
.rewarding_details
.withdraw_delegator_reward(delegation1)
.unwrap();
let expected_del1_reward = "1799968.1174089068".parse().unwrap();
assert_eq!(reward.amount, truncate_reward_amount(expected_del1_reward));
// new reward after withdrawal
let rewards2 = simulator.simulate_epoch_single_node(node_params).unwrap();
let expected_operator2 = "1411250.1907492676".parse().unwrap();
let expected_delegator_reward2 = "2200004.051009689".parse().unwrap();
compare_decimals(rewards2.delegates, expected_delegator_reward2, None);
compare_decimals(rewards2.operator, expected_operator2, None);
check_rewarding_invariant(&simulator);
// check final values
let node = simulator.nodes.get_mut(&0).unwrap();
let delegation1 = node.delegations.get_mut("alice").unwrap();
let reward_del1 = node
.rewarding_details
.withdraw_delegator_reward(delegation1)
.unwrap();
let expected_del1_reward = "1799970.5883041779".parse().unwrap();
assert_eq!(
reward_del1.amount,
truncate_reward_amount(expected_del1_reward)
);
let delegation2 = node.delegations.get_mut("bob").unwrap();
let reward_del2 = node
.rewarding_details
.withdraw_delegator_reward(delegation2)
.unwrap();
let first: Decimal = "399992.91497975704".parse().unwrap();
let second: Decimal = "400033.4627055114".parse().unwrap();
let expected_del2_reward = first + second;
assert_eq!(
reward_del2.amount,
truncate_reward_amount(expected_del2_reward)
);
}
#[test]
fn simulating_multiple_epochs() {
let mut simulator = base_simulator(10000_000000);
let mut is_active = true;
let mut performance = Percent::from_percentage_value(100).unwrap();
for epoch in 0..720 {
if epoch == 0 {
simulator
.delegate("a", Coin::new(18000_000000, "unym"), 0)
.unwrap()
}
if epoch == 42 {
simulator
.delegate("b", Coin::new(2000_000000, "unym"), 0)
.unwrap()
}
if epoch == 89 {
is_active = false;
}
if epoch == 123 {
simulator
.delegate("c", Coin::new(6666_000000, "unym"), 0)
.unwrap()
}
if epoch == 167 {
performance = Percent::from_percentage_value(90).unwrap();
}
if epoch == 245 {
simulator
.delegate("d", Coin::new(2050_000000, "unym"), 0)
.unwrap()
}
if epoch == 264 {
let (delegation, _reward) = simulator.undelegate("b", 0).unwrap();
// sanity check to make sure we undelegated what we wanted to undelegate : )
assert_eq!(delegation.amount.u128(), 2000_000000);
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
}
if epoch == 345 {
is_active = true;
}
if epoch == 358 {
performance = Percent::from_percentage_value(100).unwrap();
}
if epoch == 458 {
let (delegation, _reward) = simulator.undelegate("a", 0).unwrap();
// sanity check to make sure we undelegated what we wanted to undelegate : )
assert_eq!(delegation.amount.u128(), 18000_000000);
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
}
if epoch == 545 {
simulator
.delegate("e", Coin::new(5000_000000, "unym"), 0)
.unwrap()
}
// this has to always hold
check_rewarding_invariant(&simulator);
let node_params = NodeRewardParams::new(performance, is_active);
simulator.simulate_epoch_single_node(node_params).unwrap();
}
// after everyone undelegates, there should be nothing left in the delegates pool
simulator.undelegate("c", 0).unwrap();
simulator.undelegate("d", 0).unwrap();
simulator.undelegate("e", 0).unwrap();
let node = &simulator.nodes[&0];
assert_eq!(Decimal::zero(), node.rewarding_details.delegates);
}
}
#[test]
fn multiple_nodes_against_known_values() {
#![allow(clippy::inconsistent_digit_grouping)]
// TODO: this test can be further improved by checking values after EVERY interval
// rather than just checking the final results
let epochs_in_interval = 1u32;
let rewarded_set_size = 10;
let active_set_size = 6;
let interval_pool_emission = Percent::from_percentage_value(2).unwrap();
let reward_pool = 250_000_000_000_000u128;
let staking_supply = 100_000_000_000_000u128;
let epoch_reward_budget =
interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval);
let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size);
let rewarding_params = RewardingParams {
interval: IntervalRewardParams {
reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(),
staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(),
epoch_reward_budget,
stake_saturation_point,
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
active_set_work_factor: Decimal::percent(1000), // value '10'
interval_pool_emission,
},
rewarded_set_size,
active_set_size,
};
let interval = Interval::init_interval(
epochs_in_interval,
Duration::from_secs(60 * 60),
&mock_env(),
);
let mut simulator = Simulator::new(rewarding_params, interval);
let n0 = simulator
.bond(
Coin::new(11_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n0)
.unwrap();
let n1 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(11_000_000_000000, "unym"), n1)
.unwrap();
let n2 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n2)
.unwrap();
let n3 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
interval_operating_cost: Coin::new(500_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n3)
.unwrap();
let n4 = simulator
.bond(
Coin::new(1000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_999_000_000000, "unym"), n4)
.unwrap();
let n5 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n5)
.unwrap();
let n6 = simulator
.bond(
Coin::new(11_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n6)
.unwrap();
let n7 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n7)
.unwrap();
let n8 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
interval_operating_cost: Coin::new(500_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n8)
.unwrap();
let n9 = simulator
.bond(
Coin::new(1_000_000_000000, "unym"),
MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
interval_operating_cost: Coin::new(40_000_000, "unym"),
},
)
.unwrap();
simulator
.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n9)
.unwrap();
let uptime_1 = Percent::from_percentage_value(100).unwrap();
let uptime_09 = Percent::from_percentage_value(90).unwrap();
let uptime_0 = Percent::from_percentage_value(0).unwrap();
let node_params = [
(n0, NodeRewardParams::new(uptime_1, true)),
(n1, NodeRewardParams::new(uptime_1, true)),
(n2, NodeRewardParams::new(uptime_1, true)),
(n3, NodeRewardParams::new(uptime_09, true)),
(n4, NodeRewardParams::new(uptime_09, true)),
(n5, NodeRewardParams::new(uptime_0, true)),
(n6, NodeRewardParams::new(uptime_1, false)),
(n7, NodeRewardParams::new(uptime_1, false)),
(n8, NodeRewardParams::new(uptime_09, false)),
(n9, NodeRewardParams::new(uptime_0, false)),
]
.into_iter()
.collect::<BTreeMap<_, _>>();
for _ in 0..23 {
simulator.simulate_full_interval(&node_params).unwrap();
}
// we allow the delta to be within 0.1unym,
// because the expected values, especially for the reward pool,
// do not provide us with any higher precision
let epsilon = Some(Decimal::from_ratio(1u32, 10u32));
let expected_reward_pool = "184876811322111.7".parse().unwrap();
let expected_staking_supply = "165123188677888.3".parse().unwrap();
compare_decimals(
expected_reward_pool,
simulator.system_rewarding_params.interval.reward_pool,
epsilon,
);
compare_decimals(
expected_staking_supply,
simulator.system_rewarding_params.interval.staking_supply,
epsilon,
);
let expected_n0_pledge = "24307061704726.808".parse().unwrap();
let expected_n0_delegated = "2031528592775.6752".parse().unwrap();
let node = &simulator.nodes[&0].rewarding_details;
compare_decimals(node.operator, expected_n0_pledge, epsilon);
compare_decimals(node.delegates, expected_n0_delegated, epsilon);
let expected_n1_pledge = "3544171010629.92".parse().unwrap();
let expected_n1_delegated = "20854154351479.96".parse().unwrap();
let node = &simulator.nodes[&1].rewarding_details;
compare_decimals(node.operator, expected_n1_pledge, epsilon);
compare_decimals(node.delegates, expected_n1_delegated, epsilon);
let expected_n2_pledge = "3781120900089.8865".parse().unwrap();
let expected_n2_delegated = "18634530734287.53".parse().unwrap();
let node = &simulator.nodes[&2].rewarding_details;
compare_decimals(node.operator, expected_n2_pledge, epsilon);
compare_decimals(node.delegates, expected_n2_delegated, epsilon);
let expected_n3_pledge = "2313562111772.3165".parse().unwrap();
let expected_n3_delegated = "16090515100131.858".parse().unwrap();
let node = &simulator.nodes[&3].rewarding_details;
compare_decimals(node.operator, expected_n3_pledge, epsilon);
compare_decimals(node.delegates, expected_n3_delegated, epsilon);
let expected_n4_pledge = "1419679306492.7962".parse().unwrap();
let expected_n4_delegated = "16802494863659.93".parse().unwrap();
let node = &simulator.nodes[&4].rewarding_details;
compare_decimals(node.operator, expected_n4_pledge, epsilon);
compare_decimals(node.delegates, expected_n4_delegated, epsilon);
let expected_n5_pledge = "1000000000000".parse().unwrap();
let expected_n5_delegated = "7000000000000".parse().unwrap();
let node = &simulator.nodes[&5].rewarding_details;
compare_decimals(node.operator, expected_n5_pledge, epsilon);
compare_decimals(node.delegates, expected_n5_delegated, epsilon);
let expected_n6_pledge = "14114996375922.574".parse().unwrap();
let expected_n6_delegated = "1249173915284.053".parse().unwrap();
let node = &simulator.nodes[&6].rewarding_details;
compare_decimals(node.operator, expected_n6_pledge, epsilon);
compare_decimals(node.delegates, expected_n6_delegated, epsilon);
let expected_n7_pledge = "1225564192694.3525".parse().unwrap();
let expected_n7_delegated = "9931461332688.53".parse().unwrap();
let node = &simulator.nodes[&7].rewarding_details;
compare_decimals(node.operator, expected_n7_pledge, epsilon);
compare_decimals(node.delegates, expected_n7_delegated, epsilon);
let expected_n8_pledge = "1112319106593.8608".parse().unwrap();
let expected_n8_delegated = "7710855078658.264".parse().unwrap();
let node = &simulator.nodes[&8].rewarding_details;
compare_decimals(node.operator, expected_n8_pledge, epsilon);
compare_decimals(node.delegates, expected_n8_delegated, epsilon);
let expected_n9_pledge = "1000000000000".parse().unwrap();
let expected_n9_delegated = "7000000000000".parse().unwrap();
let node = &simulator.nodes[&9].rewarding_details;
compare_decimals(node.operator, expected_n9_pledge, epsilon);
compare_decimals(node.delegates, expected_n9_delegated, epsilon);
}
}
@@ -1,81 +0,0 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{Delegation, EpochId, MixId, MixNodeCostParams, MixNodeRewarding};
use cosmwasm_std::{Addr, Coin};
use std::collections::HashMap;
use crate::error::MixnetContractError;
use crate::rewarding::helpers::truncate_reward;
pub struct SimulatedNode {
pub mix_id: MixId,
pub rewarding_details: MixNodeRewarding,
pub delegations: HashMap<String, Delegation>,
}
impl SimulatedNode {
pub fn new(
mix_id: MixId,
cost_params: MixNodeCostParams,
initial_pledge: &Coin,
current_epoch: EpochId,
) -> Result<Self, MixnetContractError> {
Ok(SimulatedNode {
mix_id,
rewarding_details: MixNodeRewarding::initialise_new(
cost_params,
initial_pledge,
current_epoch,
)?,
delegations: HashMap::new(),
})
}
pub fn delegate<S: Into<String>>(
&mut self,
delegator: S,
delegation: Coin,
) -> Result<(), MixnetContractError> {
self.rewarding_details
.add_base_delegation(delegation.amount)?;
let delegator = delegator.into();
let delegation = Delegation::new(
Addr::unchecked(&delegator),
self.mix_id,
self.rewarding_details.total_unit_reward,
delegation,
42,
None,
);
self.delegations.insert(delegator, delegation);
Ok(())
}
pub fn undelegate<S: Into<String>>(
&mut self,
delegator: S,
) -> Result<(Coin, Coin), MixnetContractError> {
let delegator = delegator.into();
let delegation = self.delegations.remove(&delegator).ok_or(
MixnetContractError::NoMixnodeDelegationFound {
mix_id: MixId::MAX,
address: delegator,
proxy: None,
},
)?;
let reward = self
.rewarding_details
.determine_delegation_reward(&delegation)?;
self.rewarding_details
.remove_delegation_decimal(delegation.dec_amount()? + reward)?;
let reward_denom = &delegation.amount.denom;
let truncated_reward = truncate_reward(reward, reward_denom);
Ok((delegation.amount, truncated_reward))
}
}
@@ -20,7 +20,6 @@ pub type SphinxKeyRef<'a> = &'a str;
pub type EpochId = u32;
pub type IntervalId = u32;
pub type MixId = u32;
pub type BlockHeight = u64;
pub type EpochEventId = u32;
pub type IntervalEventId = u32;
@@ -61,7 +60,7 @@ impl Percent {
impl Display for Percent {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let adjusted = Decimal::from_ratio(100u32, 1u32) * self.0;
let adjusted = Decimal::from_atomics(100u32, 0).unwrap() * self.0;
write!(f, "{}%", adjusted)
}
}
@@ -119,10 +118,6 @@ impl LayerDistribution {
(Layer::Two, self.layer2),
(Layer::Three, self.layer3),
];
// we explicitly put 3 elements into the iterator, so the iterator is DEFINITELY
// not empty and thus the unwrap cannot fail
#[allow(clippy::unwrap_used)]
layers.iter().min_by_key(|x| x.1).unwrap().0
}
@@ -1,9 +1,5 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
use cosmwasm_std::{Addr, Coin, Timestamp, Uint128};
use mixnet_contract_common::MixId;
use schemars::JsonSchema;
-1
View File
@@ -1 +0,0 @@
This project was partially funded through the NGI0 PET Fund, a fund established by NL.net with financial support from the European Commission's NGI programme, under the aegis of DG Communications Networks, Content and Technology under grant agreement No 825310.
+10 -23
View File
@@ -13,13 +13,6 @@ pub struct OrderedMessageBuffer {
messages: HashMap<u64, OrderedMessage>,
}
/// Data returned from `OrderedMessageBuffer` on a successful read of gapless ordered data.
#[derive(Debug, PartialEq, Eq)]
pub struct ReadContiguousData {
pub data: Vec<u8>,
pub last_index: u64,
}
impl OrderedMessageBuffer {
pub fn new() -> OrderedMessageBuffer {
OrderedMessageBuffer {
@@ -49,7 +42,7 @@ impl OrderedMessageBuffer {
/// a read will return the bytes of messages 0, 1, 2. Subsequent reads will
/// return `None` until message 3 comes in, at which point 3, 4, and any
/// further contiguous messages which have arrived will be returned.
pub fn read(&mut self) -> Option<ReadContiguousData> {
pub fn read(&mut self) -> Option<Vec<u8>> {
if !self.messages.contains_key(&self.next_index) {
return None;
}
@@ -73,10 +66,7 @@ impl OrderedMessageBuffer {
.collect();
trace!("Returning {} bytes from ordered message buffer", data.len());
Some(ReadContiguousData {
data,
last_index: index,
})
Some(data)
}
}
@@ -112,11 +102,11 @@ mod test_chunking_and_reassembling {
};
buffer.write(first_message);
let first_read = buffer.read().unwrap().data;
let first_read = buffer.read().unwrap();
assert_eq!(vec![1, 2, 3, 4], first_read);
buffer.write(second_message);
let second_read = buffer.read().unwrap().data;
let second_read = buffer.read().unwrap();
assert_eq!(vec![5, 6, 7, 8], second_read);
assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty
@@ -138,7 +128,7 @@ mod test_chunking_and_reassembling {
buffer.write(first_message);
buffer.write(second_message);
let second_read = buffer.read();
assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], second_read.unwrap().data);
assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], second_read.unwrap());
assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty
}
@@ -157,8 +147,8 @@ mod test_chunking_and_reassembling {
buffer.write(second_message);
buffer.write(first_message);
let read = buffer.read().unwrap().data;
assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], read);
let read = buffer.read();
assert_eq!(vec![1, 2, 3, 4, 5, 6, 7, 8], read.unwrap());
assert_eq!(None, buffer.read()); // second read on fully ordered result set is empty
}
}
@@ -192,7 +182,7 @@ mod test_chunking_and_reassembling {
#[test]
fn everything_up_to_the_indexing_gap_is_returned() {
let mut buffer = setup();
let ordered_bytes = buffer.read().unwrap().data;
let ordered_bytes = buffer.read().unwrap();
assert_eq!([0, 0, 0, 0, 1, 1, 1, 1].to_vec(), ordered_bytes);
// we shouldn't get any more from a second attempt if nothing is added
@@ -218,7 +208,7 @@ mod test_chunking_and_reassembling {
};
buffer.write(two_message);
let more_ordered_bytes = buffer.read().unwrap().data;
let more_ordered_bytes = buffer.read().unwrap();
assert_eq!([2, 2, 2, 2, 3, 3, 3, 3].to_vec(), more_ordered_bytes);
// let's add another message
@@ -237,10 +227,7 @@ mod test_chunking_and_reassembling {
};
buffer.write(four_message);
assert_eq!(
[4, 4, 4, 4, 5, 5, 5, 5].to_vec(),
buffer.read().unwrap().data
);
assert_eq!([4, 4, 4, 4, 5, 5, 5, 5].to_vec(), buffer.read().unwrap());
// at this point we should again get back nothing if we try a read
assert_eq!(None, buffer.read());
+1 -1
View File
@@ -2,7 +2,7 @@ mod buffer;
mod message;
mod sender;
pub use buffer::{OrderedMessageBuffer, ReadContiguousData};
pub use buffer::OrderedMessageBuffer;
pub use message::MessageError;
pub use message::OrderedMessage;
pub use sender::OrderedMessageSender;
@@ -4,7 +4,7 @@
use futures::channel::mpsc;
use futures::StreamExt;
use log::*;
use ordered_buffer::{OrderedMessage, OrderedMessageBuffer, ReadContiguousData};
use ordered_buffer::{OrderedMessage, OrderedMessageBuffer};
use socks5_requests::ConnectionId;
use std::collections::{HashMap, HashSet};
use task::ShutdownListener;
@@ -38,13 +38,12 @@ pub enum ControllerCommand {
struct ActiveConnection {
is_closed: bool,
closed_at_index: Option<u64>,
connection_sender: Option<ConnectionSender>,
ordered_buffer: OrderedMessageBuffer,
}
impl ActiveConnection {
fn write_to_buf(&mut self, payload: Vec<u8>, is_closed: bool) {
fn write_to_buf(&mut self, payload: Vec<u8>) {
let ordered_message = match OrderedMessage::try_from_bytes(payload) {
Ok(msg) => msg,
Err(err) => {
@@ -52,13 +51,10 @@ impl ActiveConnection {
return;
}
};
if is_closed {
self.closed_at_index = Some(ordered_message.index);
}
self.ordered_buffer.write(ordered_message);
}
fn read_from_buf(&mut self) -> Option<ReadContiguousData> {
fn read_from_buf(&mut self) -> Option<Vec<u8>> {
self.ordered_buffer.read()
}
}
@@ -103,7 +99,6 @@ impl Controller {
is_closed: false,
connection_sender: Some(connection_sender),
ordered_buffer: OrderedMessageBuffer::new(),
closed_at_index: None,
};
if let Some(_active_conn) = self.active_connections.insert(conn_id, active_connection) {
error!("Received a duplicate 'Connect'!")
@@ -132,23 +127,21 @@ impl Controller {
fn send_to_connection(&mut self, conn_id: ConnectionId, payload: Vec<u8>, is_closed: bool) {
if let Some(active_connection) = self.active_connections.get_mut(&conn_id) {
if !payload.is_empty() {
active_connection.write_to_buf(payload, is_closed);
active_connection.write_to_buf(payload);
} else if !is_closed {
error!("Tried to write an empty message to a not-closing connection. Please let us know if you see this message");
}
// if messages get unordered, make sure we don't lose information about
// remote socket getting closed!
active_connection.is_closed |= is_closed;
if let Some(payload) = active_connection.read_from_buf() {
if let Some(closed_at_index) = active_connection.closed_at_index {
if payload.last_index > closed_at_index {
active_connection.is_closed = true;
}
}
if let Err(err) = active_connection
.connection_sender
.as_mut()
.unwrap()
.unbounded_send(ConnectionMessage {
payload: payload.data,
payload,
socket_closed: active_connection.is_closed,
})
{
+14 -21
View File
@@ -5,11 +5,11 @@ use crate::currency::{DecCoin, RegisteredCoins};
use crate::error::TypesError;
use crate::mixnode::MixNodeCostParams;
use mixnet_contract_common::{
BlockHeight, EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, MixId,
EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, MixId,
PendingEpochEvent as MixnetContractPendingEpochEvent,
PendingEpochEventKind as MixnetContractPendingEpochEventKind,
PendingEpochEventData as MixnetContractPendingEpochEventData,
PendingIntervalEvent as MixnetContractPendingIntervalEvent,
PendingIntervalEventKind as MixnetContractPendingIntervalEventKind,
PendingIntervalEventData as MixnetContractPendingIntervalEventData,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -22,7 +22,6 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
pub struct PendingEpochEvent {
pub id: EpochEventId,
pub created_at: BlockHeight,
pub event: PendingEpochEventData,
}
@@ -33,8 +32,7 @@ impl PendingEpochEvent {
) -> Result<Self, TypesError> {
Ok(PendingEpochEvent {
id: pending_event.id,
created_at: pending_event.event.created_at,
event: PendingEpochEventData::try_from_mixnet_contract(pending_event.event.kind, reg)?,
event: PendingEpochEventData::try_from_mixnet_contract(pending_event.event, reg)?,
})
}
}
@@ -67,11 +65,11 @@ pub enum PendingEpochEventData {
impl PendingEpochEventData {
pub fn try_from_mixnet_contract(
pending_event: MixnetContractPendingEpochEventKind,
pending_event: MixnetContractPendingEpochEventData,
reg: &RegisteredCoins,
) -> Result<Self, TypesError> {
match pending_event {
MixnetContractPendingEpochEventKind::Delegate {
MixnetContractPendingEpochEventData::Delegate {
owner,
mix_id,
amount,
@@ -82,7 +80,7 @@ impl PendingEpochEventData {
amount: reg.attempt_convert_to_display_dec_coin(amount.into())?,
proxy: proxy.map(|p| p.into_string()),
}),
MixnetContractPendingEpochEventKind::Undelegate {
MixnetContractPendingEpochEventData::Undelegate {
owner,
mix_id,
proxy,
@@ -91,10 +89,10 @@ impl PendingEpochEventData {
mix_id,
proxy: proxy.map(|p| p.into_string()),
}),
MixnetContractPendingEpochEventKind::UnbondMixnode { mix_id } => {
MixnetContractPendingEpochEventData::UnbondMixnode { mix_id } => {
Ok(PendingEpochEventData::UnbondMixnode { mix_id })
}
MixnetContractPendingEpochEventKind::UpdateActiveSetSize { new_size } => {
MixnetContractPendingEpochEventData::UpdateActiveSetSize { new_size } => {
Ok(PendingEpochEventData::UpdateActiveSetSize { new_size })
}
}
@@ -109,7 +107,6 @@ impl PendingEpochEventData {
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
pub struct PendingIntervalEvent {
pub id: IntervalEventId,
pub created_at: BlockHeight,
pub event: PendingIntervalEventData,
}
@@ -120,11 +117,7 @@ impl PendingIntervalEvent {
) -> Result<Self, TypesError> {
Ok(PendingIntervalEvent {
id: pending_event.id,
created_at: pending_event.event.created_at,
event: PendingIntervalEventData::try_from_mixnet_contract(
pending_event.event.kind,
reg,
)?,
event: PendingIntervalEventData::try_from_mixnet_contract(pending_event.event, reg)?,
})
}
}
@@ -152,11 +145,11 @@ pub enum PendingIntervalEventData {
impl PendingIntervalEventData {
pub fn try_from_mixnet_contract(
pending_event: MixnetContractPendingIntervalEventKind,
pending_event: MixnetContractPendingIntervalEventData,
reg: &RegisteredCoins,
) -> Result<Self, TypesError> {
match pending_event {
MixnetContractPendingIntervalEventKind::ChangeMixCostParams { mix_id, new_costs } => {
MixnetContractPendingIntervalEventData::ChangeMixCostParams { mix_id, new_costs } => {
Ok(PendingIntervalEventData::ChangeMixCostParams {
mix_id,
new_costs: MixNodeCostParams::from_mixnet_contract_mixnode_cost_params(
@@ -164,10 +157,10 @@ impl PendingIntervalEventData {
)?,
})
}
MixnetContractPendingIntervalEventKind::UpdateRewardingParams { update } => {
MixnetContractPendingIntervalEventData::UpdateRewardingParams { update } => {
Ok(PendingIntervalEventData::UpdateRewardingParams { update })
}
MixnetContractPendingIntervalEventKind::UpdateIntervalConfig {
MixnetContractPendingIntervalEventData::UpdateIntervalConfig {
epochs_in_interval,
epoch_duration_secs,
} => Ok(PendingIntervalEventData::UpdateIntervalConfig {
-2
View File
@@ -1,2 +0,0 @@
allow-unwrap-in-tests = true
allow-expect-in-tests = true
+9 -13
View File
@@ -63,7 +63,7 @@ pub fn instantiate(
Interval::init_interval(msg.epochs_in_interval, msg.epoch_duration, &env);
let reward_params = msg
.initial_rewarding_params
.into_rewarding_params(msg.epochs_in_interval)?;
.into_rewarding_params(msg.epochs_in_interval);
interval_storage::initialise_storage(deps.storage, starting_interval)?;
mixnet_params_storage::initialise_storage(deps.storage, state)?;
@@ -169,19 +169,17 @@ pub fn execute(
owner_signature,
),
ExecuteMsg::UnbondMixnode {} => {
crate::mixnodes::transactions::try_remove_mixnode(deps, env, info)
crate::mixnodes::transactions::try_remove_mixnode(deps, info)
}
ExecuteMsg::UnbondMixnodeOnBehalf { owner } => {
crate::mixnodes::transactions::try_remove_mixnode_on_behalf(deps, env, info, owner)
crate::mixnodes::transactions::try_remove_mixnode_on_behalf(deps, info, owner)
}
ExecuteMsg::UpdateMixnodeCostParams { new_costs } => {
crate::mixnodes::transactions::try_update_mixnode_cost_params(
deps, env, info, new_costs,
)
crate::mixnodes::transactions::try_update_mixnode_cost_params(deps, info, new_costs)
}
ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { new_costs, owner } => {
crate::mixnodes::transactions::try_update_mixnode_cost_params_on_behalf(
deps, env, info, new_costs, owner,
deps, info, new_costs, owner,
)
}
ExecuteMsg::UpdateMixnodeConfig { new_config } => {
@@ -225,21 +223,19 @@ pub fn execute(
// delegation-related:
ExecuteMsg::DelegateToMixnode { mix_id } => {
crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_id)
crate::delegations::transactions::try_delegate_to_mixnode(deps, info, mix_id)
}
ExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, delegate } => {
crate::delegations::transactions::try_delegate_to_mixnode_on_behalf(
deps, env, info, mix_id, delegate,
deps, info, mix_id, delegate,
)
}
ExecuteMsg::UndelegateFromMixnode { mix_id } => {
crate::delegations::transactions::try_remove_delegation_from_mixnode(
deps, env, info, mix_id,
)
crate::delegations::transactions::try_remove_delegation_from_mixnode(deps, info, mix_id)
}
ExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, delegate } => {
crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf(
deps, env, info, mix_id, delegate,
deps, info, mix_id, delegate,
)
}
@@ -453,7 +453,6 @@ mod tests {
#[test]
fn all_retrieved_delegations_are_from_the_specified_delegator() {
let mut test = TestSetup::new();
let env = test.env();
// it means we have, for example, delegation from "delegator1" towards mix1, mix2, ...., from "delegator2" towards mix1, mix2, ...., etc
add_dummy_mixes_with_delegations(&mut test, 50, 100);
@@ -463,7 +462,6 @@ mod tests {
for mix_id in 1..=25 {
try_delegate_to_mixnode_on_behalf(
test.deps_mut(),
env.clone(),
mock_info(vesting_contract.as_ref(), &[coin(100_000, TEST_COIN_DENOM)]),
mix_id,
with_proxy.into(),
@@ -6,37 +6,34 @@ use crate::interval::storage as interval_storage;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::mixnodes::storage as mixnodes_storage;
use crate::support::helpers::validate_delegation_stake;
use cosmwasm_std::{Addr, Coin, DepsMut, Env, MessageInfo, Response};
use cosmwasm_std::{Addr, Coin, DepsMut, MessageInfo, Response};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::events::{
new_pending_delegation_event, new_pending_undelegation_event,
};
use mixnet_contract_common::pending_events::PendingEpochEventKind;
use mixnet_contract_common::pending_events::PendingEpochEventData;
use mixnet_contract_common::{Delegation, MixId};
pub(crate) fn try_delegate_to_mixnode(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
mix_id: MixId,
) -> Result<Response, MixnetContractError> {
_try_delegate_to_mixnode(deps, env, mix_id, info.sender, info.funds, None)
_try_delegate_to_mixnode(deps, mix_id, info.sender, info.funds, None)
}
pub(crate) fn try_delegate_to_mixnode_on_behalf(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
mix_id: MixId,
delegate: String,
) -> Result<Response, MixnetContractError> {
let delegate = deps.api.addr_validate(&delegate)?;
_try_delegate_to_mixnode(deps, env, mix_id, delegate, info.funds, Some(info.sender))
_try_delegate_to_mixnode(deps, mix_id, delegate, info.funds, Some(info.sender))
}
pub(crate) fn _try_delegate_to_mixnode(
deps: DepsMut<'_>,
env: Env,
mix_id: MixId,
delegate: Addr,
amount: Vec<Coin>,
@@ -62,40 +59,37 @@ pub(crate) fn _try_delegate_to_mixnode(
// push the event onto the queue and wait for it to be picked up at the end of the epoch
let cosmos_event = new_pending_delegation_event(&delegate, &proxy, &delegation, mix_id);
let epoch_event = PendingEpochEventKind::Delegate {
let epoch_event = PendingEpochEventData::Delegate {
owner: delegate,
mix_id,
amount: delegation,
proxy,
};
interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?;
interval_storage::push_new_epoch_event(deps.storage, &epoch_event)?;
Ok(Response::new().add_event(cosmos_event))
}
pub(crate) fn try_remove_delegation_from_mixnode(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
mix_id: MixId,
) -> Result<Response, MixnetContractError> {
_try_remove_delegation_from_mixnode(deps, env, mix_id, info.sender, None)
_try_remove_delegation_from_mixnode(deps, mix_id, info.sender, None)
}
pub(crate) fn try_remove_delegation_from_mixnode_on_behalf(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
mix_id: MixId,
delegate: String,
) -> Result<Response, MixnetContractError> {
let delegate = deps.api.addr_validate(&delegate)?;
_try_remove_delegation_from_mixnode(deps, env, mix_id, delegate, Some(info.sender))
_try_remove_delegation_from_mixnode(deps, mix_id, delegate, Some(info.sender))
}
pub(crate) fn _try_remove_delegation_from_mixnode(
deps: DepsMut<'_>,
env: Env,
mix_id: MixId,
delegate: Addr,
proxy: Option<Addr>,
@@ -117,12 +111,12 @@ pub(crate) fn _try_remove_delegation_from_mixnode(
// push the event onto the queue and wait for it to be picked up at the end of the epoch
let cosmos_event = new_pending_undelegation_event(&delegate, &proxy, mix_id);
let epoch_event = PendingEpochEventKind::Undelegate {
let epoch_event = PendingEpochEventData::Undelegate {
owner: delegate,
mix_id,
proxy,
};
interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?;
interval_storage::push_new_epoch_event(deps.storage, &epoch_event)?;
Ok(Response::new().add_event(cosmos_event))
}
@@ -144,11 +138,10 @@ mod tests {
#[test]
fn can_only_be_done_towards_an_existing_mixnode() {
let mut test = TestSetup::new();
let env = test.env();
let owner = "delegator";
let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]);
let res = try_delegate_to_mixnode(test.deps_mut(), env, sender, 42);
let res = try_delegate_to_mixnode(test.deps_mut(), sender, 42);
assert_eq!(
res,
Err(MixnetContractError::MixNodeBondNotFound { mix_id: 42 })
@@ -158,19 +151,17 @@ mod tests {
#[test]
fn must_contain_non_zero_amount_of_coins() {
let mut test = TestSetup::new();
let env = test.env();
let owner = "delegator";
let mix_id = test.add_dummy_mixnode("mix-owner", None);
let sender1 = mock_info(owner, &[coin(0, TEST_COIN_DENOM)]);
let sender2 = mock_info(owner, &[]);
let sender3 = mock_info(owner, &[coin(1000, "some-weird-coin")]);
let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id);
let res = try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id);
assert_eq!(res, Err(MixnetContractError::EmptyDelegation));
let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender2, mix_id);
let res = try_delegate_to_mixnode(test.deps_mut(), sender2, mix_id);
assert_eq!(res, Err(MixnetContractError::EmptyDelegation));
let res = try_delegate_to_mixnode(test.deps_mut(), env, sender3, mix_id);
let res = try_delegate_to_mixnode(test.deps_mut(), sender3, mix_id);
assert_eq!(
res,
Err(MixnetContractError::WrongDenom {
@@ -183,8 +174,6 @@ mod tests {
#[test]
fn if_applicable_must_contain_at_least_the_minimum_pledge() {
let mut test = TestSetup::new();
let env = test.env();
let owner = "delegator";
let mix_id = test.add_dummy_mixnode("mix-owner", None);
let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]);
@@ -199,7 +188,7 @@ mod tests {
.save(test.deps_mut().storage, &contract_state)
.unwrap();
let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id);
let res = try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id);
assert_eq!(
res,
Err(MixnetContractError::InsufficientDelegation {
@@ -208,14 +197,13 @@ mod tests {
})
);
let res = try_delegate_to_mixnode(test.deps_mut(), env, sender2, mix_id);
let res = try_delegate_to_mixnode(test.deps_mut(), sender2, mix_id);
assert!(res.is_ok())
}
#[test]
fn can_only_be_done_towards_fully_bonded_mixnode() {
let mut test = TestSetup::new();
let env = test.env();
let owner = "delegator";
let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]);
@@ -238,33 +226,17 @@ mod tests {
)
.unwrap();
try_remove_mixnode(test.deps_mut(), mock_info("mix-owner-unbonded", &[])).unwrap();
try_remove_mixnode(
test.deps_mut(),
env.clone(),
mock_info("mix-owner-unbonded", &[]),
)
.unwrap();
try_remove_mixnode(
test.deps_mut(),
env.clone(),
mock_info("mix-owner-unbonded-leftover", &[]),
)
.unwrap();
test.execute_all_pending_events();
try_remove_mixnode(
test.deps_mut(),
env.clone(),
mock_info("mix-owner-unbonding", &[]),
)
.unwrap();
try_remove_mixnode(test.deps_mut(), mock_info("mix-owner-unbonding", &[])).unwrap();
let res = try_delegate_to_mixnode(
test.deps_mut(),
env.clone(),
sender.clone(),
mix_id_unbonding,
);
let res = try_delegate_to_mixnode(test.deps_mut(), sender.clone(), mix_id_unbonding);
assert_eq!(
res,
Err(MixnetContractError::MixnodeIsUnbonding {
@@ -272,12 +244,7 @@ mod tests {
})
);
let res = try_delegate_to_mixnode(
test.deps_mut(),
env.clone(),
sender.clone(),
mix_id_unbonded,
);
let res = try_delegate_to_mixnode(test.deps_mut(), sender.clone(), mix_id_unbonded);
assert_eq!(
res,
Err(MixnetContractError::MixNodeBondNotFound {
@@ -285,8 +252,7 @@ mod tests {
})
);
let res =
try_delegate_to_mixnode(test.deps_mut(), env, sender, mix_id_unbonded_leftover);
let res = try_delegate_to_mixnode(test.deps_mut(), sender, mix_id_unbonded_leftover);
assert_eq!(
res,
Err(MixnetContractError::MixNodeBondNotFound {
@@ -298,26 +264,22 @@ mod tests {
#[test]
fn can_still_be_done_if_prior_delegation_exists() {
let mut test = TestSetup::new();
let env = test.env();
let owner = "delegator";
let mix_id = test.add_dummy_mixnode("mix-owner", None);
let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]);
let sender2 = mock_info(owner, &[coin(50_000_000, TEST_COIN_DENOM)]);
let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id);
let res = try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id);
assert!(res.is_ok());
// still OK
let res = try_delegate_to_mixnode(test.deps_mut(), env, sender2, mix_id);
let res = try_delegate_to_mixnode(test.deps_mut(), sender2, mix_id);
assert!(res.is_ok())
}
#[test]
fn correctly_pushes_appropriate_epoch_event() {
let mut test = TestSetup::new();
let env = test.env();
let owner = "delegator";
let mix_id = test.add_dummy_mixnode("mix-owner", None);
@@ -327,15 +289,15 @@ mod tests {
let sender1 = mock_info(owner, &[amount1.clone()]);
let sender2 = mock_info(test.vesting_contract().as_str(), &[amount2.clone()]);
try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap();
try_delegate_to_mixnode_on_behalf(test.deps_mut(), env, sender2, mix_id, owner.into())
try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id).unwrap();
try_delegate_to_mixnode_on_behalf(test.deps_mut(), sender2, mix_id, owner.into())
.unwrap();
let events = test.pending_epoch_events();
assert_eq!(
events[0].kind,
PendingEpochEventKind::Delegate {
events[0],
PendingEpochEventData::Delegate {
owner: Addr::unchecked(owner),
mix_id,
amount: amount1,
@@ -344,8 +306,8 @@ mod tests {
);
assert_eq!(
events[1].kind,
PendingEpochEventKind::Delegate {
events[1],
PendingEpochEventData::Delegate {
owner: Addr::unchecked(owner),
mix_id,
amount: amount2,
@@ -367,12 +329,11 @@ mod tests {
#[test]
fn cannot_be_performed_if_delegation_never_existed() {
let mut test = TestSetup::new();
let env = test.env();
let owner = "delegator";
let sender = mock_info(owner, &[]);
let mix_id = test.add_dummy_mixnode("mix-owner", None);
let res = try_remove_delegation_from_mixnode(test.deps_mut(), env, sender, mix_id);
let res = try_remove_delegation_from_mixnode(test.deps_mut(), sender, mix_id);
assert_eq!(
res,
Err(MixnetContractError::NoMixnodeDelegationFound {
@@ -386,16 +347,14 @@ mod tests {
#[test]
fn cannot_be_performed_if_the_delegation_is_still_pending() {
let mut test = TestSetup::new();
let env = test.env();
let owner = "delegator";
let mix_id = test.add_dummy_mixnode("mix-owner", None);
let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]);
let sender2 = mock_info(owner, &[]);
try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap();
try_delegate_to_mixnode(test.deps_mut(), sender1, mix_id).unwrap();
let res = try_remove_delegation_from_mixnode(test.deps_mut(), env, sender2, mix_id);
let res = try_remove_delegation_from_mixnode(test.deps_mut(), sender2, mix_id);
assert_eq!(
res,
Err(MixnetContractError::NoMixnodeDelegationFound {
@@ -409,8 +368,6 @@ mod tests {
#[test]
fn as_long_as_delegation_exists_can_always_be_performed() {
let mut test = TestSetup::new();
let env = test.env();
let owner = "delegator";
let sender = mock_info(owner, &[]);
@@ -425,30 +382,19 @@ mod tests {
try_remove_mixnode(
test.deps_mut(),
env.clone(),
mock_info("mix-owner-unbonded-leftover", &[]),
)
.unwrap();
test.execute_all_pending_events();
try_remove_mixnode(
test.deps_mut(),
env.clone(),
mock_info("mix-owner-unbonding", &[]),
)
.unwrap();
try_remove_mixnode(test.deps_mut(), mock_info("mix-owner-unbonding", &[])).unwrap();
let res = try_remove_delegation_from_mixnode(
test.deps_mut(),
env.clone(),
sender.clone(),
normal_mix_id,
);
let res =
try_remove_delegation_from_mixnode(test.deps_mut(), sender.clone(), normal_mix_id);
assert!(res.is_ok());
let res = try_remove_delegation_from_mixnode(
test.deps_mut(),
env.clone(),
sender.clone(),
mix_id_unbonding,
);
@@ -456,7 +402,6 @@ mod tests {
let res = try_remove_delegation_from_mixnode(
test.deps_mut(),
env,
sender,
mix_id_unbonded_leftover,
);
@@ -274,7 +274,7 @@ pub mod tests {
assert_eq!(Err(MixnetContractError::AlreadyOwnsMixnode), result);
// but after he unbonds it, it's all fine again
pending_events::unbond_mixnode(deps.as_mut(), &env, 123, mix_id).unwrap();
pending_events::unbond_mixnode(deps.as_mut(), &env, mix_id).unwrap();
let result = try_add_gateway(deps.as_mut(), env, info, gateway, sig);
assert!(result.is_ok());
+1 -5
View File
@@ -6,12 +6,11 @@ use crate::rewards::storage as rewards_storage;
use cosmwasm_std::{Response, Storage};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::events::new_interval_config_update_event;
use mixnet_contract_common::{BlockHeight, Interval};
use mixnet_contract_common::Interval;
use std::time::Duration;
pub(crate) fn change_interval_config(
store: &mut dyn Storage,
request_creation: BlockHeight,
mut current_interval: Interval,
epochs_in_interval: u32,
epoch_duration_secs: u64,
@@ -26,7 +25,6 @@ pub(crate) fn change_interval_config(
storage::save_interval(store, &current_interval)?;
Ok(Response::new().add_event(new_interval_config_update_event(
request_creation,
epochs_in_interval,
epoch_duration_secs,
rewarding_params.interval,
@@ -52,7 +50,6 @@ mod tests {
// if we half the number of epochs, the reward budget should get doubled
change_interval_config(
&mut deps.storage,
123,
initial_interval,
initial_interval.epochs_in_interval() / 2,
initial_interval.epoch_length_secs(),
@@ -75,7 +72,6 @@ mod tests {
// and similarly when we double number of epochs, the reward budget should get halved
change_interval_config(
&mut deps.storage,
123,
initial_interval,
initial_interval.epochs_in_interval() * 2,
initial_interval.epoch_length_secs(),
+40 -85
View File
@@ -17,12 +17,9 @@ use mixnet_contract_common::events::{
new_rewarding_params_update_event, new_undelegation_event,
};
use mixnet_contract_common::mixnode::MixNodeCostParams;
use mixnet_contract_common::pending_events::{
PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData,
PendingIntervalEventKind,
};
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate;
use mixnet_contract_common::{BlockHeight, Delegation, MixId};
use mixnet_contract_common::{Delegation, MixId};
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
pub(crate) trait ContractExecutableEvent {
@@ -35,7 +32,6 @@ pub(crate) trait ContractExecutableEvent {
pub(crate) fn delegate(
deps: DepsMut<'_>,
env: &Env,
created_at: BlockHeight,
owner: Addr,
mix_id: MixId,
amount: Coin,
@@ -107,16 +103,9 @@ pub(crate) fn delegate(
};
// add the amount we're intending to delegate (whether it's fresh or we're adding to the existing one)
mix_rewarding.add_base_delegation(stored_delegation_amount.amount)?;
mix_rewarding.add_base_delegation(stored_delegation_amount.amount);
let cosmos_event = new_delegation_event(
created_at,
&owner,
&proxy,
&new_delegation_amount,
mix_id,
mix_rewarding.total_unit_reward,
);
let cosmos_event = new_delegation_event(&owner, &proxy, &new_delegation_amount, mix_id);
let delegation = Delegation::new(
owner,
@@ -141,7 +130,6 @@ pub(crate) fn delegate(
pub(crate) fn undelegate(
deps: DepsMut<'_>,
created_at: BlockHeight,
owner: Addr,
mix_id: MixId,
proxy: Option<Addr>,
@@ -165,7 +153,7 @@ pub(crate) fn undelegate(
let return_tokens = send_to_proxy_or_owner(&proxy, &owner, vec![tokens_to_return.clone()]);
let mut response = Response::new()
.add_message(return_tokens)
.add_event(new_undelegation_event(created_at, &owner, &proxy, mix_id));
.add_event(new_undelegation_event(&owner, &proxy, mix_id));
if let Some(proxy) = &proxy {
// we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract
@@ -189,7 +177,6 @@ pub(crate) fn undelegate(
pub(crate) fn unbond_mixnode(
deps: DepsMut<'_>,
env: &Env,
created_at: BlockHeight,
mix_id: MixId,
) -> Result<Response, MixnetContractError> {
// if we're here it means user executed `_try_remove_mixnode` and as a result node was set to be
@@ -220,7 +207,7 @@ pub(crate) fn unbond_mixnode(
let mut response = Response::new()
.add_message(return_tokens)
.add_event(new_mixnode_unbonding_event(created_at, mix_id));
.add_event(new_mixnode_unbonding_event(mix_id));
if let Some(proxy) = &proxy {
// we can only attempt to send the message to the vesting contract if the proxy IS the vesting contract
@@ -242,7 +229,6 @@ pub(crate) fn unbond_mixnode(
pub(crate) fn update_active_set_size(
deps: DepsMut<'_>,
created_at: BlockHeight,
active_set_size: u32,
) -> Result<Response, MixnetContractError> {
// We don't have to check for authorization as this event can only be pushed
@@ -255,30 +241,28 @@ pub(crate) fn update_active_set_size(
rewarding_params.try_change_active_set_size(active_set_size)?;
rewards_storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?;
Ok(Response::new().add_event(new_active_set_update_event(created_at, active_set_size)))
Ok(Response::new().add_event(new_active_set_update_event(active_set_size)))
}
impl ContractExecutableEvent for PendingEpochEventData {
fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result<Response, MixnetContractError> {
// note that the basic validation on all those events was already performed before
// they were pushed onto the queue
match self.kind {
PendingEpochEventKind::Delegate {
match self {
PendingEpochEventData::Delegate {
owner,
mix_id,
amount,
proxy,
} => delegate(deps, env, self.created_at, owner, mix_id, amount, proxy),
PendingEpochEventKind::Undelegate {
} => delegate(deps, env, owner, mix_id, amount, proxy),
PendingEpochEventData::Undelegate {
owner,
mix_id,
proxy,
} => undelegate(deps, self.created_at, owner, mix_id, proxy),
PendingEpochEventKind::UnbondMixnode { mix_id } => {
unbond_mixnode(deps, env, self.created_at, mix_id)
}
PendingEpochEventKind::UpdateActiveSetSize { new_size } => {
update_active_set_size(deps, self.created_at, new_size)
} => undelegate(deps, owner, mix_id, proxy),
PendingEpochEventData::UnbondMixnode { mix_id } => unbond_mixnode(deps, env, mix_id),
PendingEpochEventData::UpdateActiveSetSize { new_size } => {
update_active_set_size(deps, new_size)
}
}
}
@@ -286,7 +270,6 @@ impl ContractExecutableEvent for PendingEpochEventData {
pub(crate) fn change_mix_cost_params(
deps: DepsMut<'_>,
created_at: BlockHeight,
mix_id: MixId,
new_costs: MixNodeCostParams,
) -> Result<Response, MixnetContractError> {
@@ -302,7 +285,7 @@ pub(crate) fn change_mix_cost_params(
_ => return Ok(Response::default()),
};
let cosmos_event = new_mixnode_cost_params_update_event(created_at, mix_id, &new_costs);
let cosmos_event = new_mixnode_cost_params_update_event(mix_id, &new_costs);
// TODO: can we just change cost_params without breaking rewarding calculation?
// (I'm almost certain we can, but well, it has to be tested)
@@ -314,7 +297,6 @@ pub(crate) fn change_mix_cost_params(
pub(crate) fn update_rewarding_params(
deps: DepsMut<'_>,
created_at: BlockHeight,
updated_params: IntervalRewardingParamsUpdate,
) -> Result<Response, MixnetContractError> {
// We don't have to check for authorization as this event can only be pushed
@@ -329,7 +311,6 @@ pub(crate) fn update_rewarding_params(
rewards_storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?;
Ok(Response::new().add_event(new_rewarding_params_update_event(
created_at,
updated_params,
rewarding_params.interval,
)))
@@ -337,7 +318,6 @@ pub(crate) fn update_rewarding_params(
pub(crate) fn update_interval_config(
deps: DepsMut,
created_at: BlockHeight,
epochs_in_interval: u32,
epoch_duration_secs: u64,
) -> Result<Response, MixnetContractError> {
@@ -346,10 +326,8 @@ pub(crate) fn update_interval_config(
// Furthermore, we don't need to check whether the interval is finished as the
// queue is only emptied upon the interval finishing.
let interval = storage::current_interval(deps.storage)?;
change_interval_config(
deps.storage,
created_at,
interval,
epochs_in_interval,
epoch_duration_secs,
@@ -360,23 +338,18 @@ impl ContractExecutableEvent for PendingIntervalEventData {
fn execute(self, deps: DepsMut<'_>, _env: &Env) -> Result<Response, MixnetContractError> {
// note that the basic validation on all those events was already performed before
// they were pushed onto the queue
match self.kind {
PendingIntervalEventKind::ChangeMixCostParams {
match self {
PendingIntervalEventData::ChangeMixCostParams {
mix_id: mix,
new_costs,
} => change_mix_cost_params(deps, self.created_at, mix, new_costs),
PendingIntervalEventKind::UpdateRewardingParams { update } => {
update_rewarding_params(deps, self.created_at, update)
} => change_mix_cost_params(deps, mix, new_costs),
PendingIntervalEventData::UpdateRewardingParams { update } => {
update_rewarding_params(deps, update)
}
PendingIntervalEventKind::UpdateIntervalConfig {
PendingIntervalEventData::UpdateIntervalConfig {
epochs_in_interval,
epoch_duration_secs,
} => update_interval_config(
deps,
self.created_at,
epochs_in_interval,
epoch_duration_secs,
),
} => update_interval_config(deps, epochs_in_interval, epoch_duration_secs),
}
}
}
@@ -417,12 +390,11 @@ mod tests {
test.add_immediate_delegation(owner1, delegation, mix_id);
let env = test.env();
unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap();
unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap();
let res_increase = delegate(
test.deps_mut(),
&env,
123,
Addr::unchecked(owner1),
mix_id,
delegation_coin.clone(),
@@ -448,7 +420,6 @@ mod tests {
let res_fresh = delegate(
test.deps_mut(),
&env,
123,
Addr::unchecked(owner2),
mix_id,
delegation_coin.clone(),
@@ -482,12 +453,11 @@ mod tests {
test.add_immediate_delegation(owner1, delegation, mix_id);
let env = test.env();
try_remove_mixnode(test.deps_mut(), env.clone(), mock_info("mix-owner", &[])).unwrap();
try_remove_mixnode(test.deps_mut(), mock_info("mix-owner", &[])).unwrap();
let res_increase = delegate(
test.deps_mut(),
&env,
123,
Addr::unchecked(owner1),
mix_id,
delegation_coin.clone(),
@@ -513,7 +483,6 @@ mod tests {
let res_fresh = delegate(
test.deps_mut(),
&env,
123,
Addr::unchecked(owner2),
mix_id,
delegation_coin.clone(),
@@ -549,7 +518,6 @@ mod tests {
let res = delegate(
test.deps_mut(),
&env,
123,
Addr::unchecked(owner),
mix_id,
delegation_coin_new,
@@ -612,7 +580,6 @@ mod tests {
let res = delegate(
test.deps_mut(),
&env,
123,
Addr::unchecked(owner),
mix_id,
delegation_coin_new,
@@ -678,7 +645,6 @@ mod tests {
let res = delegate(
test.deps_mut(),
&env,
123,
Addr::unchecked(owner),
mix_id,
delegation_coin.clone(),
@@ -713,7 +679,7 @@ mod tests {
let owner2 = "delegator2";
let env = test.env();
unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap();
unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap();
let vesting_contract = test.vesting_contract();
let dummy_proxy = Addr::unchecked("not-vesting-contract");
@@ -722,7 +688,6 @@ mod tests {
let res_vesting = delegate(
test.deps_mut(),
&env,
123,
Addr::unchecked(owner1),
mix_id,
delegation_coin.clone(),
@@ -769,7 +734,6 @@ mod tests {
let res_other_proxy = delegate(
test.deps_mut(),
&env,
123,
Addr::unchecked(owner1),
mix_id,
delegation_coin.clone(),
@@ -814,7 +778,7 @@ mod tests {
let owner = Addr::unchecked("delegator");
let res = undelegate(test.deps_mut(), 123, owner, mix_id, None).unwrap();
let res = undelegate(test.deps_mut(), owner, mix_id, None).unwrap();
assert!(get_bank_send_msg(&res).is_none());
}
@@ -829,7 +793,7 @@ mod tests {
// this should never happen in actual code, but if we manually messed something up,
// lets make sure this throws an error
rewards_storage::MIXNODE_REWARDING.remove(test.deps_mut().storage, mix_id);
let res = undelegate(test.deps_mut(), 123, owner, mix_id, None);
let res = undelegate(test.deps_mut(), owner, mix_id, None);
assert!(matches!(
res,
Err(MixnetContractError::InconsistentState { .. })
@@ -863,8 +827,7 @@ mod tests {
let expected_return = delegation + truncated_reward.u128();
let res =
undelegate(test.deps_mut(), 123, Addr::unchecked(owner), mix_id, None).unwrap();
let res = undelegate(test.deps_mut(), Addr::unchecked(owner), mix_id, None).unwrap();
let (receiver, sent_amount) = get_bank_send_msg(&res).unwrap();
assert_eq!(receiver, owner);
assert_eq!(sent_amount[0].amount.u128(), expected_return);
@@ -912,7 +875,6 @@ mod tests {
// for a fresh delegation, nothing was added to the storage either
let res_vesting = undelegate(
test.deps_mut(),
123,
Addr::unchecked(owner1),
mix_id,
Some(vesting_contract.clone()),
@@ -958,7 +920,6 @@ mod tests {
let res_other_proxy = undelegate(
test.deps_mut(),
123,
Addr::unchecked(owner2),
mix_id,
Some(dummy_proxy.clone()),
@@ -1003,7 +964,7 @@ mod tests {
let mut test = TestSetup::new();
let env = test.env();
let res = unbond_mixnode(test.deps_mut(), &env, 123, 1);
let res = unbond_mixnode(test.deps_mut(), &env, 1);
assert!(matches!(
res,
Err(MixnetContractError::InconsistentState { .. })
@@ -1033,7 +994,7 @@ mod tests {
let expected_return = pledge + truncated_reward;
let env = test.env();
let res = unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap();
let res = unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap();
let (receiver, sent_amount) = get_bank_send_msg(&res).unwrap();
assert_eq!(receiver, owner);
assert_eq!(sent_amount[0].amount, expected_return);
@@ -1081,7 +1042,7 @@ mod tests {
test.add_dummy_mixnode_with_proxy(owner2, Some(pledge), dummy_proxy.clone());
let env = test.env();
let res_vesting = unbond_mixnode(test.deps_mut(), &env, 123, mix_id_vesting).unwrap();
let res_vesting = unbond_mixnode(test.deps_mut(), &env, mix_id_vesting).unwrap();
assert!(mixnodes_storage::mixnode_bonds()
.may_load(test.deps().storage, mix_id_vesting)
@@ -1116,7 +1077,7 @@ mod tests {
assert!(found_track);
let res_other_proxy =
unbond_mixnode(test.deps_mut(), &env, 123, mix_id_other_proxy).unwrap();
unbond_mixnode(test.deps_mut(), &env, mix_id_other_proxy).unwrap();
assert!(mixnodes_storage::mixnode_bonds()
.may_load(test.deps().storage, mix_id_other_proxy)
.unwrap()
@@ -1142,7 +1103,7 @@ mod tests {
.load(test.deps().storage)
.unwrap();
update_active_set_size(test.deps_mut(), 123, 50).unwrap();
update_active_set_size(test.deps_mut(), 50).unwrap();
let updated = rewards_storage::REWARDING_PARAMS
.load(test.deps().storage)
.unwrap();
@@ -1163,14 +1124,14 @@ mod tests {
let mix_id = test.add_dummy_mixnode("mix-owner", None);
let env = test.env();
unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap();
unbond_mixnode(test.deps_mut(), &env, mix_id).unwrap();
let new_params = MixNodeCostParams {
profit_margin_percent: Percent::from_percentage_value(42).unwrap(),
interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM),
};
let res = change_mix_cost_params(test.deps_mut(), 123, mix_id, new_params);
let res = change_mix_cost_params(test.deps_mut(), mix_id, new_params);
assert_eq!(res, Ok(Response::default()));
}
@@ -1186,16 +1147,11 @@ mod tests {
interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM),
};
let res = change_mix_cost_params(test.deps_mut(), 123, mix_id, new_params.clone());
let res = change_mix_cost_params(test.deps_mut(), mix_id, new_params.clone());
assert_eq!(
res,
Ok(
Response::new().add_event(new_mixnode_cost_params_update_event(
123,
mix_id,
&new_params
))
)
Ok(Response::new()
.add_event(new_mixnode_cost_params_update_event(mix_id, &new_params)))
);
let after = test.mix_rewarding(mix_id).cost_params;
@@ -1226,7 +1182,7 @@ mod tests {
rewarded_set_size: None,
};
let res = update_rewarding_params(test.deps_mut(), 123, update);
let res = update_rewarding_params(test.deps_mut(), update);
assert!(res.is_ok());
let after = rewards_storage::REWARDING_PARAMS
.load(test.deps().storage)
@@ -1274,7 +1230,6 @@ mod tests {
// and change epoch length
update_interval_config(
test.deps_mut(),
123,
interval_before.epochs_in_interval() / 2,
1234,
)
+6 -8
View File
@@ -238,7 +238,7 @@ mod tests {
mod pending_epoch_events {
use super::*;
use cosmwasm_std::Addr;
use mixnet_contract_common::pending_events::PendingEpochEventKind;
use mixnet_contract_common::pending_events::PendingEpochEventData;
use rand_chacha::rand_core::RngCore;
fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) {
@@ -248,13 +248,12 @@ mod tests {
}
fn push_dummy_epoch_action(test: &mut TestSetup) {
let dummy_action = PendingEpochEventKind::Undelegate {
let dummy_action = PendingEpochEventData::Undelegate {
owner: Addr::unchecked("foomp"),
mix_id: test.rng.next_u32(),
proxy: None,
};
let env = test.env();
storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap();
storage::push_new_epoch_event(test.deps_mut().storage, &dummy_action).unwrap();
}
#[test]
@@ -380,7 +379,7 @@ mod tests {
mod pending_interval_events {
use super::*;
use crate::support::tests::fixtures;
use mixnet_contract_common::pending_events::PendingIntervalEventKind;
use mixnet_contract_common::pending_events::PendingIntervalEventData;
use rand_chacha::rand_core::RngCore;
fn push_n_dummy_interval_actions(test: &mut TestSetup, n: usize) {
@@ -390,12 +389,11 @@ mod tests {
}
fn push_dummy_interval_action(test: &mut TestSetup) {
let dummy_action = PendingIntervalEventKind::ChangeMixCostParams {
let dummy_action = PendingIntervalEventData::ChangeMixCostParams {
mix_id: test.rng.next_u32(),
new_costs: fixtures::mix_node_cost_params_fixture(),
};
let env = test.env();
storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap();
storage::push_new_interval_event(test.deps_mut().storage, &dummy_action).unwrap();
}
#[test]
+7 -13
View File
@@ -6,13 +6,11 @@ use crate::constants::{
LAST_EPOCH_EVENT_ID_KEY, LAST_INTERVAL_EVENT_ID_KEY, PENDING_EPOCH_EVENTS_NAMESPACE,
PENDING_INTERVAL_EVENTS_NAMESPACE, REWARDED_SET_KEY,
};
use cosmwasm_std::{Env, Order, StdResult, Storage};
use cosmwasm_std::{Order, StdResult, Storage};
use cw_storage_plus::{Item, Map};
use mixnet_contract_common::pending_events::{
PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData,
};
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
use mixnet_contract_common::{
EpochEventId, Interval, IntervalEventId, MixId, PendingIntervalEventKind, RewardedSetNodeStatus,
EpochEventId, Interval, IntervalEventId, MixId, RewardedSetNodeStatus,
};
use std::collections::HashMap;
@@ -66,22 +64,18 @@ pub(crate) fn next_interval_event_id_counter(
pub(crate) fn push_new_epoch_event(
storage: &mut dyn Storage,
env: &Env,
event: PendingEpochEventKind,
event: &PendingEpochEventData,
) -> StdResult<()> {
let event_id = next_epoch_event_id_counter(storage)?;
let event_data = event.attach_source_height(env.block.height);
PENDING_EPOCH_EVENTS.save(storage, event_id, &event_data)
PENDING_EPOCH_EVENTS.save(storage, event_id, event)
}
pub(crate) fn push_new_interval_event(
storage: &mut dyn Storage,
env: &Env,
event: PendingIntervalEventKind,
event: &PendingIntervalEventData,
) -> StdResult<()> {
let event_id = next_interval_event_id_counter(storage)?;
let event_data = event.attach_source_height(env.block.height);
PENDING_INTERVAL_EVENTS.save(storage, event_id, &event_data)
PENDING_INTERVAL_EVENTS.save(storage, event_id, event)
}
pub(crate) fn update_rewarded_set(
+44 -69
View File
@@ -15,7 +15,7 @@ use mixnet_contract_common::events::{
new_pending_interval_config_update_event, new_pending_interval_events_execution_event,
new_reconcile_pending_events,
};
use mixnet_contract_common::pending_events::PendingIntervalEventKind;
use mixnet_contract_common::pending_events::PendingIntervalEventData;
use mixnet_contract_common::MixId;
use std::collections::BTreeSet;
@@ -270,18 +270,17 @@ pub(crate) fn try_update_interval_config(
if force_immediately || interval.is_current_interval_over(&env) {
change_interval_config(
deps.storage,
env.block.height,
interval,
epochs_in_interval,
epoch_duration_secs,
)
} else {
// push the interval event
let interval_event = PendingIntervalEventKind::UpdateIntervalConfig {
let interval_event = PendingIntervalEventData::UpdateIntervalConfig {
epochs_in_interval,
epoch_duration_secs,
};
push_new_interval_event(deps.storage, &env, interval_event)?;
push_new_interval_event(deps.storage, &interval_event)?;
let time_left = interval.secs_until_current_interval_end(&env);
Ok(
Response::new().add_event(new_pending_interval_config_update_event(
@@ -299,33 +298,31 @@ mod tests {
use crate::support::tests::fixtures;
use crate::support::tests::test_helpers::TestSetup;
use cosmwasm_std::Addr;
use mixnet_contract_common::pending_events::PendingEpochEventKind;
use mixnet_contract_common::pending_events::PendingEpochEventData;
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) {
// if you attempt to undelegate non-existent delegation,
// it will return an empty response, but will not fail
let env = test.env();
for i in 0..n {
let dummy_action = PendingEpochEventKind::Undelegate {
let dummy_action = PendingEpochEventData::Undelegate {
owner: Addr::unchecked("foomp"),
mix_id: i as MixId,
proxy: None,
};
storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap();
storage::push_new_epoch_event(test.deps_mut().storage, &dummy_action).unwrap();
}
}
fn push_n_dummy_interval_actions(test: &mut TestSetup, n: usize) {
// if you attempt to update cost parameters of an unbonded mixnode,
// it will return an empty response, but will not fail
let env = test.env();
for i in 0..n {
let dummy_action = PendingIntervalEventKind::ChangeMixCostParams {
let dummy_action = PendingIntervalEventData::ChangeMixCostParams {
mix_id: i as MixId,
new_costs: fixtures::mix_node_cost_params_fixture(),
};
storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap();
storage::push_new_interval_event(test.deps_mut().storage, &dummy_action).unwrap();
}
}
@@ -338,7 +335,7 @@ mod tests {
new_active_set_update_event, new_delegation_on_unbonded_node_event,
new_undelegation_event,
};
use mixnet_contract_common::pending_events::PendingEpochEventKind;
use mixnet_contract_common::pending_events::PendingEpochEventData;
#[test]
fn without_limit_executes_all_actions() {
@@ -401,15 +398,14 @@ mod tests {
);
push_n_dummy_epoch_actions(&mut test, 10);
let action_with_event = PendingEpochEventKind::UpdateActiveSetSize { new_size: 50 };
storage::push_new_epoch_event(test.deps_mut().storage, &env, action_with_event)
.unwrap();
let action_with_event = PendingEpochEventData::UpdateActiveSetSize { new_size: 50 };
storage::push_new_epoch_event(test.deps_mut().storage, &action_with_event).unwrap();
push_n_dummy_epoch_actions(&mut test, 10);
let (res, executed) =
perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap();
assert_eq!(
res,
Response::new().add_event(new_active_set_update_event(env.block.height, 50))
Response::new().add_event(new_active_set_update_event(50))
);
assert_eq!(executed, 21);
assert_eq!(
@@ -437,13 +433,13 @@ mod tests {
// delegate to node that doesn't exist,
// we expect to receive BankMsg with tokens being returned,
// and event regarding delegation
let non_existent_delegation = PendingEpochEventKind::Delegate {
let non_existent_delegation = PendingEpochEventData::Delegate {
owner: Addr::unchecked("foomp"),
mix_id: 123,
amount: coin(123, TEST_COIN_DENOM),
proxy: None,
};
storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation)
storage::push_new_epoch_event(test.deps_mut().storage, &non_existent_delegation)
.unwrap();
expected_events.push(new_delegation_on_unbonded_node_event(
&Addr::unchecked("foomp"),
@@ -457,13 +453,13 @@ mod tests {
// delegation to node that doesn't exist with vesting contract
// we expect the same as above PLUS TrackUndelegation message
let non_existent_delegation = PendingEpochEventKind::Delegate {
let non_existent_delegation = PendingEpochEventData::Delegate {
owner: Addr::unchecked("foomp2"),
mix_id: 123,
amount: coin(123, TEST_COIN_DENOM),
proxy: Some(vesting_contract.clone()),
};
storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation)
storage::push_new_epoch_event(test.deps_mut().storage, &non_existent_delegation)
.unwrap();
expected_events.push(new_delegation_on_unbonded_node_event(
&Addr::unchecked("foomp2"),
@@ -483,24 +479,18 @@ mod tests {
expected_messages.push(SubMsg::new(track_undelegate_message));
// updating active set should only emit events and no cosmos messages
let action_with_event = PendingEpochEventKind::UpdateActiveSetSize { new_size: 50 };
storage::push_new_epoch_event(test.deps_mut().storage, &env, action_with_event)
.unwrap();
expected_events.push(new_active_set_update_event(env.block.height, 50));
let action_with_event = PendingEpochEventData::UpdateActiveSetSize { new_size: 50 };
storage::push_new_epoch_event(test.deps_mut().storage, &action_with_event).unwrap();
expected_events.push(new_active_set_update_event(50));
// undelegation just returns tokens and emits event
let legit_undelegate = PendingEpochEventKind::Undelegate {
let legit_undelegate = PendingEpochEventData::Undelegate {
owner: delegator.clone(),
mix_id: legit_mix,
proxy: None,
};
storage::push_new_epoch_event(test.deps_mut().storage, &env, legit_undelegate).unwrap();
expected_events.push(new_undelegation_event(
env.block.height,
&delegator,
&None,
legit_mix,
));
storage::push_new_epoch_event(test.deps_mut().storage, &legit_undelegate).unwrap();
expected_events.push(new_undelegation_event(&delegator, &None, legit_mix));
expected_messages.push(SubMsg::new(BankMsg::Send {
to_address: delegator.into_string(),
amount: coins(amount, TEST_COIN_DENOM),
@@ -650,20 +640,16 @@ mod tests {
rewarded_set_size: Some(500),
..Default::default()
};
let action_with_event = PendingIntervalEventKind::UpdateRewardingParams { update };
storage::push_new_interval_event(test.deps_mut().storage, &env, action_with_event)
.unwrap();
let action_with_event = PendingIntervalEventData::UpdateRewardingParams { update };
storage::push_new_interval_event(test.deps_mut().storage, &action_with_event).unwrap();
push_n_dummy_interval_actions(&mut test, 10);
let (res, executed) =
perform_pending_interval_actions(test.deps_mut(), &env, None).unwrap();
let updated_params = test.rewarding_params().interval;
assert_eq!(
res,
Response::new().add_event(new_rewarding_params_update_event(
env.block.height,
update,
updated_params
))
Response::new()
.add_event(new_rewarding_params_update_event(update, updated_params))
);
assert_eq!(executed, 21);
assert_eq!(
@@ -687,45 +673,38 @@ mod tests {
profit_margin_percent: Percent::from_percentage_value(12).unwrap(),
interval_operating_cost: coin(123_000, TEST_COIN_DENOM),
};
let cost_change = PendingIntervalEventKind::ChangeMixCostParams {
let cost_change = PendingIntervalEventData::ChangeMixCostParams {
mix_id: legit_mix,
new_costs: new_costs.clone(),
};
storage::push_new_interval_event(test.deps_mut().storage, &env, cost_change).unwrap();
expected_events.push(new_mixnode_cost_params_update_event(
env.block.height,
legit_mix,
&new_costs,
));
storage::push_new_interval_event(test.deps_mut().storage, &cost_change).unwrap();
expected_events.push(new_mixnode_cost_params_update_event(legit_mix, &new_costs));
let update = IntervalRewardingParamsUpdate {
rewarded_set_size: Some(500),
..Default::default()
};
let change_params = PendingIntervalEventKind::UpdateRewardingParams { update };
storage::push_new_interval_event(test.deps_mut().storage, &env, change_params).unwrap();
let change_params = PendingIntervalEventData::UpdateRewardingParams { update };
storage::push_new_interval_event(test.deps_mut().storage, &change_params).unwrap();
let interval = test.current_interval();
let mut expected_updated = test.rewarding_params();
expected_updated
.try_apply_updates(update, interval.epochs_in_interval())
.unwrap();
expected_events.push(new_rewarding_params_update_event(
env.block.height,
update,
expected_updated.interval,
));
let change_interval = PendingIntervalEventKind::UpdateIntervalConfig {
let change_interval = PendingIntervalEventData::UpdateIntervalConfig {
epochs_in_interval: 123,
epoch_duration_secs: 1000,
};
let mut expected_updated2 = expected_updated;
expected_updated2.apply_epochs_in_interval_change(123);
storage::push_new_interval_event(test.deps_mut().storage, &env, change_interval)
.unwrap();
storage::push_new_interval_event(test.deps_mut().storage, &change_interval).unwrap();
expected_events.push(new_interval_config_update_event(
env.block.height,
123,
1000,
expected_updated2.interval,
@@ -806,7 +785,7 @@ mod tests {
use mixnet_contract_common::events::{
new_delegation_on_unbonded_node_event, new_rewarding_params_update_event,
};
use mixnet_contract_common::pending_events::PendingEpochEventKind;
use mixnet_contract_common::pending_events::PendingEpochEventData;
use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate;
#[test]
@@ -988,19 +967,18 @@ mod tests {
#[test]
fn catches_all_emitted_cosmos_events_and_messages() {
let mut test = TestSetup::new();
let env = test.env();
let mut expected_events = vec![new_reconcile_pending_events()];
let mut expected_messages: Vec<SubMsg<Empty>> = Vec::new();
// epoch event
let non_existent_delegation = PendingEpochEventKind::Delegate {
let non_existent_delegation = PendingEpochEventData::Delegate {
owner: Addr::unchecked("foomp"),
mix_id: 123,
amount: coin(123, TEST_COIN_DENOM),
proxy: None,
};
storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation)
storage::push_new_epoch_event(test.deps_mut().storage, &non_existent_delegation)
.unwrap();
expected_events.push(new_delegation_on_unbonded_node_event(
&Addr::unchecked("foomp"),
@@ -1018,15 +996,14 @@ mod tests {
rewarded_set_size: Some(500),
..Default::default()
};
let change_params = PendingIntervalEventKind::UpdateRewardingParams { update };
storage::push_new_interval_event(test.deps_mut().storage, &env, change_params).unwrap();
let change_params = PendingIntervalEventData::UpdateRewardingParams { update };
storage::push_new_interval_event(test.deps_mut().storage, &change_params).unwrap();
let interval = test.current_interval();
let mut expected_updated = test.rewarding_params();
expected_updated
.try_apply_updates(update, interval.epochs_in_interval())
.unwrap();
expected_events.push(new_rewarding_params_update_event(
env.block.height,
update,
expected_updated.interval,
));
@@ -1261,19 +1238,18 @@ mod tests {
#[test]
fn if_executes_any_events_it_propagates_responses() {
let mut test = TestSetup::new();
let env = test.env();
let current_active_set = test.rewarding_params().active_set_size;
let mut expected_events = Vec::new();
let mut expected_messages: Vec<SubMsg<Empty>> = Vec::new();
let non_existent_delegation = PendingEpochEventKind::Delegate {
let non_existent_delegation = PendingEpochEventData::Delegate {
owner: Addr::unchecked("foomp"),
mix_id: 123,
amount: coin(123, TEST_COIN_DENOM),
proxy: None,
};
storage::push_new_epoch_event(test.deps_mut().storage, &env, non_existent_delegation)
storage::push_new_epoch_event(test.deps_mut().storage, &non_existent_delegation)
.unwrap();
expected_events.push(new_delegation_on_unbonded_node_event(
&Addr::unchecked("foomp"),
@@ -1291,15 +1267,14 @@ mod tests {
rewarded_set_size: Some(500),
..Default::default()
};
let change_params = PendingIntervalEventKind::UpdateRewardingParams { update };
storage::push_new_interval_event(test.deps_mut().storage, &env, change_params).unwrap();
let change_params = PendingIntervalEventData::UpdateRewardingParams { update };
storage::push_new_interval_event(test.deps_mut().storage, &change_params).unwrap();
let interval = test.current_interval();
let mut expected_updated = test.rewarding_params();
expected_updated
.try_apply_updates(update, interval.epochs_in_interval())
.unwrap();
expected_events.push(new_rewarding_params_update_event(
env.block.height,
update,
expected_updated.interval,
));
@@ -1534,8 +1509,8 @@ mod tests {
// make sure it's actually saved to pending events
let events = test.pending_interval_events();
assert!(matches!(events[0].kind,
PendingIntervalEventKind::UpdateIntervalConfig { epochs_in_interval, epoch_duration_secs } if epochs_in_interval == 100 && epoch_duration_secs == 1000
assert!(matches!(events[0],
PendingIntervalEventData::UpdateIntervalConfig { epochs_in_interval, epoch_duration_secs } if epochs_in_interval == 100 && epoch_duration_secs == 1000
));
test.execute_all_pending_events();
-3
View File
@@ -1,9 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
mod constants;
pub mod contract;
mod delegations;
+1 -1
View File
@@ -78,7 +78,7 @@ pub(crate) fn save_new_mixnode(
let mix_id = next_mixnode_id_counter(storage)?;
let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id();
let mixnode_rewarding = MixNodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?;
let mixnode_rewarding = MixNodeRewarding::initialise_new(cost_params, &pledge, current_epoch);
let mixnode_bond = MixNodeBond::new(
mix_id,
owner,
+2 -2
View File
@@ -1174,7 +1174,7 @@ pub(crate) mod tests {
.save(deps.as_mut().storage, id, &rewarding_details)
.unwrap();
pending_events::unbond_mixnode(deps.as_mut(), &mock_env(), 123, id).unwrap();
pending_events::unbond_mixnode(deps.as_mut(), &mock_env(), id).unwrap();
let res = query_owned_mixnode(deps.as_ref(), address.clone()).unwrap();
assert!(res.mixnode_details.is_none());
assert_eq!(address, res.address);
@@ -1263,7 +1263,7 @@ pub(crate) mod tests {
// add and unbond the mixnode
let mix_id =
test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, sender, good_mixnode_pledge());
pending_events::unbond_mixnode(deps.as_mut(), &mock_env(), 123, mix_id).unwrap();
pending_events::unbond_mixnode(deps.as_mut(), &mock_env(), mix_id).unwrap();
let res = query_unbonded_mixnode(deps.as_ref(), mix_id).unwrap();
assert_eq!(res.unbonded_info.unwrap().owner, sender);
+24 -50
View File
@@ -17,7 +17,7 @@ use mixnet_contract_common::events::{
new_mixnode_pending_cost_params_update_event, new_pending_mixnode_unbonding_event,
};
use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams};
use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind};
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
use mixnet_contract_common::MixNode;
pub fn try_add_mixnode(
@@ -119,26 +119,23 @@ fn _try_add_mixnode(
pub fn try_remove_mixnode_on_behalf(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
owner: String,
) -> Result<Response, MixnetContractError> {
let proxy = info.sender;
let owner = deps.api.addr_validate(&owner)?;
_try_remove_mixnode(deps, env, owner, Some(proxy))
_try_remove_mixnode(deps, owner, Some(proxy))
}
pub fn try_remove_mixnode(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
) -> Result<Response, MixnetContractError> {
_try_remove_mixnode(deps, env, info.sender, None)
_try_remove_mixnode(deps, info.sender, None)
}
pub(crate) fn _try_remove_mixnode(
deps: DepsMut<'_>,
env: Env,
owner: Addr,
proxy: Option<Addr>,
) -> Result<Response, MixnetContractError> {
@@ -164,10 +161,10 @@ pub(crate) fn _try_remove_mixnode(
)?;
// push the event to execute it at the end of the epoch
let epoch_event = PendingEpochEventKind::UnbondMixnode {
let epoch_event = PendingEpochEventData::UnbondMixnode {
mix_id: existing_bond.mix_id,
};
interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?;
interval_storage::push_new_epoch_event(deps.storage, &epoch_event)?;
Ok(
Response::new().add_event(new_pending_mixnode_unbonding_event(
@@ -232,29 +229,26 @@ pub(crate) fn _try_update_mixnode_config(
pub(crate) fn try_update_mixnode_cost_params(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
new_costs: MixNodeCostParams,
) -> Result<Response, MixnetContractError> {
let owner = info.sender;
_try_update_mixnode_cost_params(deps, env, new_costs, owner, None)
_try_update_mixnode_cost_params(deps, new_costs, owner, None)
}
pub(crate) fn try_update_mixnode_cost_params_on_behalf(
deps: DepsMut,
env: Env,
info: MessageInfo,
new_costs: MixNodeCostParams,
owner: String,
) -> Result<Response, MixnetContractError> {
let owner = deps.api.addr_validate(&owner)?;
let proxy = info.sender;
_try_update_mixnode_cost_params(deps, env, new_costs, owner, Some(proxy))
_try_update_mixnode_cost_params(deps, new_costs, owner, Some(proxy))
}
pub(crate) fn _try_update_mixnode_cost_params(
deps: DepsMut,
env: Env,
new_costs: MixNodeCostParams,
owner: Addr,
proxy: Option<Addr>,
@@ -273,11 +267,11 @@ pub(crate) fn _try_update_mixnode_cost_params(
);
// push the interval event
let interval_event = PendingIntervalEventKind::ChangeMixCostParams {
let interval_event = PendingIntervalEventData::ChangeMixCostParams {
mix_id: existing_bond.mix_id,
new_costs,
};
push_new_interval_event(deps.storage, &env, interval_event)?;
push_new_interval_event(deps.storage, &interval_event)?;
Ok(Response::new().add_event(cosmos_event))
}
@@ -424,7 +418,7 @@ pub mod tests {
let info = mock_info(sender, &[]);
// trying to remove your mixnode fails if you never had one in the first place
let res = try_remove_mixnode(deps.as_mut(), env.clone(), info.clone());
let res = try_remove_mixnode(deps.as_mut(), info.clone());
assert_eq!(
res,
Err(MixnetContractError::NoAssociatedMixNodeBond {
@@ -432,18 +426,12 @@ pub mod tests {
})
);
let mix_id = test_helpers::add_mixnode(
&mut rng,
deps.as_mut(),
env.clone(),
sender,
good_mixnode_pledge(),
);
let mix_id =
test_helpers::add_mixnode(&mut rng, deps.as_mut(), env, sender, good_mixnode_pledge());
// attempted to remove on behalf with invalid proxy (current is `None`)
let res = try_remove_mixnode_on_behalf(
deps.as_mut(),
env.clone(),
mock_info("proxy", &[]),
sender.to_string(),
);
@@ -456,7 +444,7 @@ pub mod tests {
);
// "normal" unbonding succeeds and unbonding event is pushed to the pending epoch events
let res = try_remove_mixnode(deps.as_mut(), env.clone(), info.clone());
let res = try_remove_mixnode(deps.as_mut(), info.clone());
assert!(res.is_ok());
let mut pending_events = interval_storage::PENDING_EPOCH_EVENTS
.range(deps.as_ref().storage, None, None, Order::Ascending)
@@ -465,13 +453,10 @@ pub mod tests {
assert_eq!(pending_events.len(), 1);
let event = pending_events.pop().unwrap();
assert_eq!(1, event.0);
assert_eq!(
PendingEpochEventKind::UnbondMixnode { mix_id },
event.1.kind
);
assert_eq!(PendingEpochEventData::UnbondMixnode { mix_id }, event.1);
// but fails if repeated (since the node is already in the "unbonding" state)(
let res = try_remove_mixnode(deps.as_mut(), env, info);
let res = try_remove_mixnode(deps.as_mut(), info);
assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id }))
}
@@ -503,7 +488,7 @@ pub mod tests {
let mix_id = test_helpers::add_mixnode(
&mut rng,
deps.as_mut(),
env.clone(),
env,
sender,
tests::fixtures::good_mixnode_pledge(),
);
@@ -536,7 +521,7 @@ pub mod tests {
assert_eq!(mix.mix_node.version, update.version);
// but we cannot perform any updates whilst the mixnode is already unbonding
try_remove_mixnode(deps.as_mut(), env, info.clone()).unwrap();
try_remove_mixnode(deps.as_mut(), info.clone()).unwrap();
let res = try_update_mixnode_config(deps.as_mut(), info, update);
assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id }))
}
@@ -555,12 +540,7 @@ pub mod tests {
};
// try updating a non existing mixnode bond
let res = try_update_mixnode_cost_params(
deps.as_mut(),
env.clone(),
info.clone(),
update.clone(),
);
let res = try_update_mixnode_cost_params(deps.as_mut(), info.clone(), update.clone());
assert_eq!(
res,
Err(MixnetContractError::NoAssociatedMixNodeBond {
@@ -579,7 +559,6 @@ pub mod tests {
// attempted to remove on behalf with invalid proxy (current is `None`)
let res = try_update_mixnode_cost_params_on_behalf(
deps.as_mut(),
env.clone(),
mock_info("proxy", &[]),
update.clone(),
sender.to_string(),
@@ -592,12 +571,7 @@ pub mod tests {
})
);
// "normal" update succeeds
let res = try_update_mixnode_cost_params(
deps.as_mut(),
env.clone(),
info.clone(),
update.clone(),
);
let res = try_update_mixnode_cost_params(deps.as_mut(), info.clone(), update.clone());
assert!(res.is_ok());
// see if the event has been pushed onto the queue
@@ -609,15 +583,15 @@ pub mod tests {
let event = pending_events.pop().unwrap();
assert_eq!(1, event.0);
assert_eq!(
PendingIntervalEventKind::ChangeMixCostParams {
PendingIntervalEventData::ChangeMixCostParams {
mix_id,
new_costs: update.clone()
},
event.1.kind
event.1
);
// execute the event
test_helpers::execute_all_pending_events(deps.as_mut(), env.clone());
test_helpers::execute_all_pending_events(deps.as_mut(), env);
// and see if the config has actually been updated
let mix = get_mixnode_details_by_id(deps.as_ref().storage, mix_id)
@@ -626,8 +600,8 @@ pub mod tests {
assert_eq!(mix.rewarding_details.cost_params, update);
// but we cannot perform any updates whilst the mixnode is already unbonding
try_remove_mixnode(deps.as_mut(), env.clone(), info.clone()).unwrap();
let res = try_update_mixnode_cost_params(deps.as_mut(), env, info, update);
try_remove_mixnode(deps.as_mut(), info.clone()).unwrap();
let res = try_update_mixnode_cost_params(deps.as_mut(), info, update);
assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id }))
}
+12 -18
View File
@@ -4,9 +4,8 @@
use super::storage;
use crate::delegations::storage as delegations_storage;
use crate::interval::storage as interval_storage;
use cosmwasm_std::{Coin, Storage};
use cosmwasm_std::{Coin, Decimal, Storage};
use mixnet_contract_common::error::MixnetContractError;
use mixnet_contract_common::helpers::IntoBaseDecimal;
use mixnet_contract_common::mixnode::{MixNodeDetails, MixNodeRewarding};
use mixnet_contract_common::Delegation;
@@ -22,10 +21,11 @@ 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 epoch_reward_budget = reward_pool / interval.epochs_in_interval().into_base_decimal()?
let epoch_reward_budget = reward_pool
/ Decimal::from_atomics(interval.epochs_in_interval(), 0).unwrap()
* rewarding_params.interval.interval_pool_emission;
let stake_saturation_point =
staking_supply / rewarding_params.rewarded_set_size.into_base_decimal()?;
staking_supply / Decimal::from_atomics(rewarding_params.rewarded_set_size, 0).unwrap();
rewarding_params.interval.reward_pool = reward_pool;
rewarding_params.interval.staking_supply = staking_supply;
@@ -45,7 +45,7 @@ pub(crate) fn withdraw_operator_reward(
let mix_id = mix_details.mix_id();
let mut mix_rewarding = mix_details.rewarding_details;
let original_pledge = mix_details.bond_information.original_pledge;
let reward = mix_rewarding.withdraw_operator_reward(&original_pledge)?;
let reward = mix_rewarding.withdraw_operator_reward(&original_pledge);
// save updated rewarding info
storage::MIXNODE_REWARDING.save(store, mix_id, &mix_rewarding)?;
@@ -86,7 +86,7 @@ mod tests {
let mut test = TestSetup::new();
let epochs_in_interval = test.current_interval().epochs_in_interval();
let epochs_in_interval_dec = epochs_in_interval.into_base_decimal().unwrap();
let epochs_in_interval_dec = Decimal::from_atomics(epochs_in_interval, 0).unwrap();
let start_rewarding_params = test.rewarding_params();
// nothing changes if pending changes are empty
@@ -94,7 +94,7 @@ mod tests {
assert_eq!(start_rewarding_params, test.rewarding_params());
// normal case of having distributed some rewards
let distributed_rewards = 100_000_000u32.into_base_decimal().unwrap();
let distributed_rewards = Decimal::from_atomics(100_000_000u32, 0).unwrap();
storage::PENDING_REWARD_POOL_CHANGE
.save(
test.deps_mut().storage,
@@ -131,10 +131,7 @@ mod tests {
assert_eq!(
updated_rewarding_params.interval.stake_saturation_point,
updated_rewarding_params.interval.staking_supply
/ updated_rewarding_params
.rewarded_set_size
.into_base_decimal()
.unwrap()
/ Decimal::from_atomics(updated_rewarding_params.rewarded_set_size, 0).unwrap()
);
// resets changes back to 0
@@ -146,7 +143,7 @@ mod tests {
);
// future case of having to also increase the reward pool
let added_credentials = 50_000_000u32.into_base_decimal().unwrap();
let added_credentials = Decimal::from_atomics(50_000_000u32, 0).unwrap();
storage::PENDING_REWARD_POOL_CHANGE
.save(
test.deps_mut().storage,
@@ -183,10 +180,7 @@ mod tests {
assert_eq!(
updated_rewarding_params2.interval.stake_saturation_point,
updated_rewarding_params2.interval.staking_supply
/ updated_rewarding_params2
.rewarded_set_size
.into_base_decimal()
.unwrap()
/ Decimal::from_atomics(updated_rewarding_params2.rewarded_set_size, 0).unwrap()
);
// resets changes back to 0
@@ -203,7 +197,7 @@ mod tests {
let mut test = TestSetup::new();
let pledge = Uint128::new(250_000_000);
let pledge_dec = 250_000_000u32.into_base_decimal().unwrap();
let pledge_dec = Decimal::from_atomics(250_000_000u32, 0).unwrap();
let mix_id = test.add_dummy_mixnode("mix-owner", Some(pledge));
// no rewards
@@ -240,7 +234,7 @@ mod tests {
let mut test = TestSetup::new();
let delegation_amount = Uint128::new(2500_000_000);
let delegation_dec = 2500_000_000u32.into_base_decimal().unwrap();
let delegation_dec = Decimal::from_atomics(2500_000_000u32, 0).unwrap();
let mix_id = test.add_dummy_mixnode("mix-owner", None);
let delegator = "delegator";
test.add_immediate_delegation(delegator, delegation_amount, mix_id);
+19 -33
View File
@@ -7,7 +7,6 @@ use crate::interval::storage as interval_storage;
use crate::mixnodes;
use crate::mixnodes::storage as mixnodes_storage;
use cosmwasm_std::{coin, Coin, Decimal, Deps, StdResult};
use mixnet_contract_common::helpers::into_base_decimal;
use mixnet_contract_common::mixnode::MixNodeDetails;
use mixnet_contract_common::reward_params::{NodeRewardParams, Performance, RewardingParams};
use mixnet_contract_common::rewarding::helpers::truncate_reward;
@@ -20,18 +19,16 @@ pub(crate) fn query_rewarding_params(deps: Deps<'_>) -> StdResult<RewardingParam
storage::REWARDING_PARAMS.load(deps.storage)
}
fn pending_operator_reward(
mix_details: Option<MixNodeDetails>,
) -> StdResult<PendingRewardResponse> {
Ok(match mix_details {
fn pending_operator_reward(mix_details: Option<MixNodeDetails>) -> PendingRewardResponse {
match mix_details {
Some(mix_details) => PendingRewardResponse {
amount_staked: Some(mix_details.original_pledge().clone()),
amount_earned: Some(mix_details.pending_operator_reward()),
amount_earned_detailed: Some(mix_details.pending_detailed_operator_reward()?),
amount_earned_detailed: Some(mix_details.pending_detailed_operator_reward()),
mixnode_still_fully_bonded: !mix_details.is_unbonding(),
},
None => PendingRewardResponse::default(),
})
}
}
pub fn query_pending_operator_reward(
@@ -42,7 +39,7 @@ pub fn query_pending_operator_reward(
// in order to determine operator's reward we need to know its original pledge and thus
// we have to load the entire thing
let mix_details = mixnodes::helpers::get_mixnode_details_by_owner(deps.storage, owner_address)?;
pending_operator_reward(mix_details)
Ok(pending_operator_reward(mix_details))
}
pub fn query_pending_mixnode_operator_reward(
@@ -52,7 +49,7 @@ pub fn query_pending_mixnode_operator_reward(
// in order to determine operator's reward we need to know its original pledge and thus
// we have to load the entire thing
let mix_details = mixnodes::helpers::get_mixnode_details_by_id(deps.storage, mix_id)?;
pending_operator_reward(mix_details)
Ok(pending_operator_reward(mix_details))
}
pub fn query_pending_delegator_reward(
@@ -77,8 +74,8 @@ pub fn query_pending_delegator_reward(
None => return Ok(PendingRewardResponse::default()),
};
let detailed_reward = mix_rewarding.determine_delegation_reward(&delegation)?;
let delegator_reward = mix_rewarding.pending_delegator_reward(&delegation)?;
let detailed_reward = mix_rewarding.determine_delegation_reward(&delegation);
let delegator_reward = mix_rewarding.pending_delegator_reward(&delegation);
// check if the mixnode isnt in the process of unbonding (or has already unbonded)
let is_bonded = matches!(mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)?, Some(mix_bond) if !mix_bond.is_unbonding);
@@ -180,8 +177,8 @@ pub(crate) fn query_estimated_current_epoch_delegator_reward(
None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()),
};
let staked_dec = into_base_decimal(delegation.amount.amount)?;
let current_value = staked_dec + mix_rewarding.determine_delegation_reward(&delegation)?;
let staked_dec = Decimal::from_atomics(delegation.amount.amount, 0).unwrap();
let current_value = staked_dec + mix_rewarding.determine_delegation_reward(&delegation);
let amount_staked = delegation.amount;
// check if the mixnode isnt in the process of unbonding (or has already unbonded)
@@ -348,8 +345,7 @@ mod tests {
total_earned += dist.operator;
let sender = mock_info(owner, &[]);
let env = test.env();
try_remove_mixnode(test.deps_mut(), env, sender).unwrap();
try_remove_mixnode(test.deps_mut(), sender).unwrap();
let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap();
let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap();
@@ -375,8 +371,7 @@ mod tests {
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
let sender = mock_info(owner, &[]);
let env = test.env();
try_remove_mixnode(test.deps_mut(), env, sender).unwrap();
try_remove_mixnode(test.deps_mut(), sender).unwrap();
test.execute_all_pending_events();
let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap();
@@ -498,8 +493,7 @@ mod tests {
total_earned += dist.delegates;
let sender = mock_info("mix-owner", &[]);
let env = test.env();
try_remove_mixnode(test.deps_mut(), env, sender).unwrap();
try_remove_mixnode(test.deps_mut(), sender).unwrap();
let res =
query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None).unwrap();
@@ -528,8 +522,7 @@ mod tests {
total_earned += dist.delegates;
let sender = mock_info("mix-owner", &[]);
let env = test.env();
try_remove_mixnode(test.deps_mut(), env, sender).unwrap();
try_remove_mixnode(test.deps_mut(), sender).unwrap();
test.execute_all_pending_events();
let res =
@@ -664,8 +657,7 @@ mod tests {
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
let sender = mock_info(owner, &[]);
let env = test.env();
try_remove_mixnode(test.deps_mut(), env, sender).unwrap();
try_remove_mixnode(test.deps_mut(), sender).unwrap();
let res = query_estimated_current_epoch_operator_reward(
test.deps(),
@@ -690,8 +682,7 @@ mod tests {
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
let sender = mock_info(owner, &[]);
let env = test.env();
try_remove_mixnode(test.deps_mut(), env, sender).unwrap();
try_remove_mixnode(test.deps_mut(), sender).unwrap();
test.execute_all_pending_events();
let res = query_estimated_current_epoch_operator_reward(
@@ -795,10 +786,7 @@ mod tests {
let delegation = test.delegation(mix_id, owner, &None);
let staked_dec = Decimal::from_atomics(delegation.amount.amount, 0).unwrap();
let current_value = staked_dec
+ mix_rewarding
.determine_delegation_reward(&delegation)
.unwrap();
let current_value = staked_dec + mix_rewarding.determine_delegation_reward(&delegation);
let amount_staked = delegation.amount;
EstimatedCurrentEpochRewardResponse {
@@ -845,8 +833,7 @@ mod tests {
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
let sender = mock_info("mix-owner", &[]);
let env = test.env();
try_remove_mixnode(test.deps_mut(), env, sender).unwrap();
try_remove_mixnode(test.deps_mut(), sender).unwrap();
let res = query_estimated_current_epoch_delegator_reward(
test.deps(),
@@ -875,8 +862,7 @@ mod tests {
test.reward_with_distribution(mix_id, test_helpers::performance(100.0));
let sender = mock_info("mix-owner", &[]);
let env = test.env();
try_remove_mixnode(test.deps_mut(), env, sender).unwrap();
try_remove_mixnode(test.deps_mut(), sender).unwrap();
test.execute_all_pending_events();
let res = query_estimated_current_epoch_delegator_reward(
+39 -55
View File
@@ -22,7 +22,7 @@ use mixnet_contract_common::events::{
new_withdraw_delegator_reward_event, new_withdraw_operator_reward_event,
new_zero_uptime_mix_operator_rewarding_event,
};
use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind};
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
use mixnet_contract_common::reward_params::{
IntervalRewardingParamsUpdate, NodeRewardParams, Performance,
};
@@ -288,16 +288,13 @@ pub(crate) fn try_update_active_set_size(
if force_immediately || interval.is_current_epoch_over(&env) {
rewarding_params.try_change_active_set_size(active_set_size)?;
storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?;
Ok(Response::new().add_event(new_active_set_update_event(
env.block.height,
active_set_size,
)))
Ok(Response::new().add_event(new_active_set_update_event(active_set_size)))
} else {
// push the epoch event
let epoch_event = PendingEpochEventKind::UpdateActiveSetSize {
let epoch_event = PendingEpochEventData::UpdateActiveSetSize {
new_size: active_set_size,
};
push_new_epoch_event(deps.storage, &env, epoch_event)?;
push_new_epoch_event(deps.storage, &epoch_event)?;
let time_left = interval.secs_until_current_interval_end(&env);
Ok(
Response::new().add_event(new_pending_active_set_update_event(
@@ -327,16 +324,15 @@ pub(crate) fn try_update_rewarding_params(
rewarding_params.try_apply_updates(updated_params, interval.epochs_in_interval())?;
storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?;
Ok(Response::new().add_event(new_rewarding_params_update_event(
env.block.height,
updated_params,
rewarding_params.interval,
)))
} else {
// push the interval event
let interval_event = PendingIntervalEventKind::UpdateRewardingParams {
let interval_event = PendingIntervalEventData::UpdateRewardingParams {
update: updated_params,
};
push_new_interval_event(deps.storage, &env, interval_event)?;
push_new_interval_event(deps.storage, &interval_event)?;
let time_left = interval.secs_until_current_interval_end(&env);
Ok(
Response::new().add_event(new_pending_rewarding_params_update_event(
@@ -362,8 +358,7 @@ pub mod tests {
use cosmwasm_std::{Decimal, Uint128};
use mixnet_contract_common::events::{
MixnetEventType, BOND_NOT_FOUND_VALUE, DELEGATES_REWARD_KEY, NO_REWARD_REASON_KEY,
OPERATOR_REWARD_KEY, PRIOR_DELEGATES_KEY, PRIOR_UNIT_REWARD_KEY,
ZERO_PERFORMANCE_VALUE,
OPERATOR_REWARD_KEY, PRIOR_DELEGATES_KEY, PRIOR_UNIT_REWARD, ZERO_PERFORMANCE_VALUE,
};
use mixnet_contract_common::helpers::compare_decimals;
use mixnet_contract_common::RewardedSetNodeStatus;
@@ -423,9 +418,9 @@ pub mod tests {
&rewarding_details,
)
.unwrap();
pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded).unwrap();
pending_events::unbond_mixnode(test.deps_mut(), &env, mix_id_unbonded).unwrap();
pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover)
pending_events::unbond_mixnode(test.deps_mut(), &env, mix_id_unbonded_leftover)
.unwrap();
let env = test.env();
@@ -744,7 +739,7 @@ pub mod tests {
performance,
in_active_set: true,
};
let sim_res = sim.simulate_epoch_single_node(node_params).unwrap();
let sim_res = sim.simulate_epoch(node_params);
assert_eq!(sim_res, dist);
}
test.skip_to_next_epoch_end();
@@ -768,7 +763,7 @@ pub mod tests {
performance,
in_active_set: true,
};
let sim_res = sim.simulate_epoch_single_node(node_params).unwrap();
let sim_res = sim.simulate_epoch(node_params);
assert_eq!(sim_res, dist);
}
test.skip_to_next_epoch_end();
@@ -809,8 +804,8 @@ pub mod tests {
in_active_set: true,
};
let dist1 = sim1.simulate_epoch_single_node(node_params).unwrap();
let dist2 = sim2.simulate_epoch_single_node(node_params).unwrap();
let dist1 = sim1.simulate_epoch(node_params);
let dist2 = sim2.simulate_epoch(node_params);
let env = test.env();
@@ -846,7 +841,7 @@ pub mod tests {
let prior_unit_reward: Decimal = find_attribute(
Some(MixnetEventType::MixnodeRewarding),
PRIOR_UNIT_REWARD_KEY,
PRIOR_UNIT_REWARD,
&res1,
)
.parse()
@@ -858,17 +853,15 @@ pub mod tests {
let unit_delegation_base = actual_prior1.unit_delegation;
// recompute the state of fully compounded delegation from before this rewarding was distributed
let pre_rewarding_del11 = del11.dec_amount().unwrap()
+ (prior_unit_reward - del11.cumulative_reward_ratio)
* del11.dec_amount().unwrap()
let pre_rewarding_del11 = del11.dec_amount()
+ (prior_unit_reward - del11.cumulative_reward_ratio) * del11.dec_amount()
/ (del11.cumulative_reward_ratio + unit_delegation_base);
let computed_del11_reward =
pre_rewarding_del11 / prior_delegates1 * delegates_reward1;
let pre_rewarding_del12 = del12.dec_amount().unwrap()
+ (prior_unit_reward - del12.cumulative_reward_ratio)
* del12.dec_amount().unwrap()
let pre_rewarding_del12 = del12.dec_amount()
+ (prior_unit_reward - del12.cumulative_reward_ratio) * del12.dec_amount()
/ (del12.cumulative_reward_ratio + unit_delegation_base);
let computed_del12_reward =
@@ -878,7 +871,6 @@ pub mod tests {
compare_decimals(
computed_del11_reward + computed_del12_reward,
delegates_reward1,
None,
);
let res2 = try_reward_mixnode(
@@ -910,7 +902,7 @@ pub mod tests {
let prior_unit_reward: Decimal = find_attribute(
Some(MixnetEventType::MixnodeRewarding),
PRIOR_UNIT_REWARD_KEY,
PRIOR_UNIT_REWARD,
&res2,
)
.parse()
@@ -922,9 +914,8 @@ pub mod tests {
let unit_delegation_base = actual_prior2.unit_delegation;
// recompute the state of fully compounded delegation from before this rewarding was distributed
let pre_rewarding_del21 = del21.dec_amount().unwrap()
+ (prior_unit_reward - del21.cumulative_reward_ratio)
* del21.dec_amount().unwrap()
let pre_rewarding_del21 = del21.dec_amount()
+ (prior_unit_reward - del21.cumulative_reward_ratio) * del21.dec_amount()
/ (del21.cumulative_reward_ratio + unit_delegation_base);
let computed_del21_reward =
@@ -952,8 +943,8 @@ pub mod tests {
in_active_set: true,
};
let dist1 = sim1.simulate_epoch_single_node(node_params).unwrap();
let dist2 = sim2.simulate_epoch_single_node(node_params).unwrap();
let dist1 = sim1.simulate_epoch(node_params);
let dist2 = sim2.simulate_epoch(node_params);
let env = test.env();
@@ -989,7 +980,7 @@ pub mod tests {
let prior_unit_reward: Decimal = find_attribute(
Some(MixnetEventType::MixnodeRewarding),
PRIOR_UNIT_REWARD_KEY,
PRIOR_UNIT_REWARD,
&res1,
)
.parse()
@@ -1001,25 +992,22 @@ pub mod tests {
let unit_delegation_base = actual_prior1.unit_delegation;
// recompute the state of fully compounded delegation from before this rewarding was distributed
let pre_rewarding_del11 = del11.dec_amount().unwrap()
+ (prior_unit_reward - del11.cumulative_reward_ratio)
* del11.dec_amount().unwrap()
let pre_rewarding_del11 = del11.dec_amount()
+ (prior_unit_reward - del11.cumulative_reward_ratio) * del11.dec_amount()
/ (del11.cumulative_reward_ratio + unit_delegation_base);
let computed_del11_reward =
pre_rewarding_del11 / prior_delegates1 * delegates_reward1;
let pre_rewarding_del12 = del12.dec_amount().unwrap()
+ (prior_unit_reward - del12.cumulative_reward_ratio)
* del12.dec_amount().unwrap()
let pre_rewarding_del12 = del12.dec_amount()
+ (prior_unit_reward - del12.cumulative_reward_ratio) * del12.dec_amount()
/ (del12.cumulative_reward_ratio + unit_delegation_base);
let computed_del12_reward =
pre_rewarding_del12 / prior_delegates1 * delegates_reward1;
let pre_rewarding_del13 = del13.dec_amount().unwrap()
+ (prior_unit_reward - del13.cumulative_reward_ratio)
* del13.dec_amount().unwrap()
let pre_rewarding_del13 = del13.dec_amount()
+ (prior_unit_reward - del13.cumulative_reward_ratio) * del13.dec_amount()
/ (del13.cumulative_reward_ratio + unit_delegation_base);
let computed_del13_reward =
@@ -1029,7 +1017,6 @@ pub mod tests {
compare_decimals(
computed_del11_reward + computed_del12_reward + computed_del13_reward,
delegates_reward1,
None,
);
let res2 = try_reward_mixnode(
@@ -1061,7 +1048,7 @@ pub mod tests {
let prior_unit_reward: Decimal = find_attribute(
Some(MixnetEventType::MixnodeRewarding),
PRIOR_UNIT_REWARD_KEY,
PRIOR_UNIT_REWARD,
&res2,
)
.parse()
@@ -1073,17 +1060,15 @@ pub mod tests {
let unit_delegation_base = actual_prior2.unit_delegation;
// recompute the state of fully compounded delegation from before this rewarding was distributed
let pre_rewarding_del21 = del21.dec_amount().unwrap()
+ (prior_unit_reward - del21.cumulative_reward_ratio)
* del21.dec_amount().unwrap()
let pre_rewarding_del21 = del21.dec_amount()
+ (prior_unit_reward - del21.cumulative_reward_ratio) * del21.dec_amount()
/ (del21.cumulative_reward_ratio + unit_delegation_base);
let computed_del21_reward =
pre_rewarding_del21 / prior_delegates2 * delegates_reward2;
let pre_rewarding_del23 = del23.dec_amount().unwrap()
+ (prior_unit_reward - del23.cumulative_reward_ratio)
* del23.dec_amount().unwrap()
let pre_rewarding_del23 = del23.dec_amount()
+ (prior_unit_reward - del23.cumulative_reward_ratio) * del23.dec_amount()
/ (del23.cumulative_reward_ratio + unit_delegation_base);
let computed_del23_reward =
@@ -1092,7 +1077,6 @@ pub mod tests {
compare_decimals(
computed_del21_reward + computed_del23_reward,
delegates_reward2,
None,
);
test.skip_to_next_epoch_end();
@@ -1236,7 +1220,7 @@ pub mod tests {
.unwrap();
let env = test.env();
pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover)
pending_events::unbond_mixnode(test.deps_mut(), &env, mix_id_unbonded_leftover)
.unwrap();
let res =
@@ -1518,7 +1502,7 @@ pub mod tests {
.unwrap();
let env = test.env();
pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover)
pending_events::unbond_mixnode(test.deps_mut(), &env, mix_id_unbonded_leftover)
.unwrap();
let res = try_withdraw_operator_reward(test.deps_mut(), sender1);
@@ -1688,7 +1672,7 @@ pub mod tests {
// make sure it's actually saved to pending events
let events = test.pending_epoch_events();
assert!(
matches!(events[0].kind, PendingEpochEventKind::UpdateActiveSetSize { new_size } if new_size == 42)
matches!(events[0], PendingEpochEventData::UpdateActiveSetSize { new_size } if new_size == 42)
);
test.execute_all_pending_events();
@@ -1843,7 +1827,7 @@ pub mod tests {
// make sure it's actually saved to pending events
let events = test.pending_interval_events();
assert!(
matches!(events[0].kind,PendingIntervalEventKind::UpdateRewardingParams { update } if update.rewarded_set_size == Some(123))
matches!(events[0],PendingIntervalEventData::UpdateRewardingParams { update } if update.rewarded_set_size == Some(123))
);
test.execute_all_pending_events();
-8
View File
@@ -69,10 +69,6 @@ pub(crate) fn validate_pledge(
});
}
// throughout this function we've been using the value at `pledge[0]` without problems
// (plus we have even validated that the vec is not empty), so the unwrap here is absolutely fine,
// since it cannot possibly fail without UB
#[allow(clippy::unwrap_used)]
Ok(pledge.pop().unwrap())
}
@@ -110,10 +106,6 @@ pub(crate) fn validate_delegation_stake(
return Err(MixnetContractError::EmptyDelegation);
}
// throughout this function we've been using the value at `delegation[0]` without problems
// (plus we have even validated that the vec is not empty), so the unwrap here is absolutely fine,
// since it cannot possibly fail without UB
#[allow(clippy::unwrap_used)]
Ok(delegation.pop().unwrap())
}
+16 -43
View File
@@ -49,7 +49,6 @@ pub mod test_helpers {
use mixnet_contract_common::mixnode::{MixNodeRewarding, UnbondedMixnode};
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
use mixnet_contract_common::reward_params::{Performance, RewardingParams};
use mixnet_contract_common::rewarding::simulator::simulated_node::SimulatedNode;
use mixnet_contract_common::rewarding::simulator::Simulator;
use mixnet_contract_common::rewarding::RewardDistribution;
use mixnet_contract_common::{
@@ -216,19 +215,13 @@ pub mod test_helpers {
.load(self.deps().storage, mix_id)
.unwrap();
let env = self.env();
try_remove_mixnode(
self.deps_mut(),
env,
mock_info(bond_details.owner.as_str(), &[]),
)
.unwrap();
try_remove_mixnode(self.deps_mut(), mock_info(bond_details.owner.as_str(), &[]))
.unwrap();
}
pub fn immediately_unbond_mixnode(&mut self, mix_id: MixId) {
let env = self.env();
pending_events::unbond_mixnode(self.deps_mut(), &env, env.block.height, mix_id)
.unwrap();
pending_events::unbond_mixnode(self.deps_mut(), &env, mix_id).unwrap();
}
pub fn add_immediate_delegation(
@@ -246,7 +239,6 @@ pub mod test_helpers {
pending_events::delegate(
self.deps_mut(),
&env,
env.block.height,
Addr::unchecked(delegator),
target,
amount,
@@ -271,7 +263,6 @@ pub mod test_helpers {
pending_events::delegate(
self.deps_mut(),
&env,
env.block.height,
Addr::unchecked(delegator),
target,
amount,
@@ -292,20 +283,12 @@ pub mod test_helpers {
denom,
amount: amount.into(),
};
let env = self.env();
delegate(self.deps_mut(), env, delegator, vec![amount], target)
delegate(self.deps_mut(), delegator, vec![amount], target)
}
pub fn remove_immediate_delegation(&mut self, delegator: &str, target: MixId) {
let height = self.env.block.height;
pending_events::undelegate(
self.deps_mut(),
height,
Addr::unchecked(delegator),
target,
None,
)
.unwrap();
pending_events::undelegate(self.deps_mut(), Addr::unchecked(delegator), target, None)
.unwrap();
}
pub fn skip_to_next_epoch_end(&mut self) {
@@ -354,7 +337,7 @@ pub mod test_helpers {
}
pub fn instantiate_simulator(&self, node: MixId) -> Simulator {
simulator_from_single_node_state(self.deps(), node)
simulator_from_state(self.deps(), node)
}
pub fn execute_all_pending_events(&mut self) {
@@ -433,7 +416,7 @@ pub mod test_helpers {
}
}
pub fn simulator_from_single_node_state(deps: Deps<'_>, node: MixId) -> Simulator {
pub fn simulator_from_state(deps: Deps<'_>, node: MixId) -> Simulator {
let mix_rewarding = rewards_storage::MIXNODE_REWARDING
.load(deps.storage, node)
.unwrap();
@@ -449,21 +432,12 @@ pub mod test_helpers {
.load(deps.storage)
.unwrap();
let interval = interval_storage::current_interval(deps.storage).unwrap();
let mut simulator = Simulator::new(rewarding_params, interval);
simulator.nodes.insert(
0,
SimulatedNode {
mix_id: 0,
rewarding_details: mix_rewarding,
delegations: delegations
.delegations
.into_iter()
.map(|d| (d.owner.to_string(), d))
.collect(),
},
);
simulator
Simulator {
node_rewarding_details: mix_rewarding,
node_delegations: delegations.delegations,
system_rewarding_params: rewarding_params,
interval,
}
}
pub fn get_bank_send_msg(response: &Response) -> Option<(String, Vec<Coin>)> {
@@ -572,7 +546,6 @@ pub mod test_helpers {
pending_events::delegate(
deps.branch(),
&env,
env.block.height,
Addr::unchecked(&format!("owner{}", i)),
mix_id,
tests::fixtures::good_mixnode_pledge().pop().unwrap(),
@@ -792,9 +765,9 @@ pub mod test_helpers {
deps
}
pub fn delegate(deps: DepsMut<'_>, env: Env, sender: &str, stake: Vec<Coin>, mix_id: MixId) {
pub fn delegate(deps: DepsMut<'_>, sender: &str, stake: Vec<Coin>, mix_id: MixId) {
let info = mock_info(sender, &stake);
try_delegate_to_mixnode(deps, env, info, mix_id).unwrap();
try_delegate_to_mixnode(deps, info, mix_id).unwrap();
}
pub(crate) fn read_delegation(
+2 -2
View File
@@ -579,7 +579,7 @@ pub fn try_get_current_vesting_period(
env: Env,
) -> Result<Period, ContractError> {
let account = account_from_address(address, deps.storage, deps.api)?;
account.get_current_vesting_period(env.block.time)
Ok(account.get_current_vesting_period(env.block.time))
}
/// Loads mixnode bond from vesting contract storage.
@@ -666,7 +666,7 @@ pub fn try_get_original_vesting(
deps: Deps<'_>,
) -> Result<OriginalVestingResponse, ContractError> {
let account = account_from_address(vesting_account_address, deps.storage, deps.api)?;
account.get_original_vesting()
Ok(account.get_original_vesting())
}
/// See [crate::traits::VestingAccount::get_delegated_free]
-2
View File
@@ -48,6 +48,4 @@ pub enum ContractError {
MinVestingFunds { sent: u128, need: u128 },
#[error("VESTING ({}): Maximum amount of locked coins has already been pledged: {current}, cap is {cap}", line!())]
LockedPledgeCapReached { current: Uint128, cap: Uint128 },
#[error("VESTING: ({}: Account owned by {owner} has unpopulated vesting periods!", line!())]
UnpopulatedVestingPeriods { owner: Addr },
}
-3
View File
@@ -1,9 +1,6 @@
#![allow(rustdoc::private_intra_doc_links)]
//! Nym vesting contract, providing vesting accounts with ability to stake unvested tokens
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
pub mod contract;
mod errors;
mod queued_migrations;
@@ -55,7 +55,7 @@ pub trait VestingAccount {
/// Returns amount of coins set at account creation
/// See [/vesting-contract/struct.Account.html/method.get_original_vesting] for impl
fn get_original_vesting(&self) -> Result<OriginalVestingResponse, ContractError>;
fn get_original_vesting(&self) -> OriginalVestingResponse;
/// See [/vesting-contract/struct.Account.html/method.get_delegated_free] for impl
fn get_delegated_free(
+9 -30
View File
@@ -23,6 +23,7 @@ fn generate_storage_key(storage: &mut dyn Storage) -> Result<u32, ContractError>
Ok(key)
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct Account {
pub owner_address: Addr,
@@ -65,13 +66,8 @@ impl Account {
self.periods.len()
}
pub fn period_duration(&self) -> Result<u64, ContractError> {
self.periods
.get(0)
.ok_or(ContractError::UnpopulatedVestingPeriods {
owner: self.owner_address.clone(),
})
.map(|p| p.period_seconds)
pub fn period_duration(&self) -> u64 {
self.periods.get(0).unwrap().period_seconds
}
pub fn storage_key(&self) -> u32 {
@@ -108,28 +104,11 @@ impl Account {
/// Returns the index of the next vesting period. Unless the current time is somehow in the past or vesting has not started yet.
/// In case vesting is over it will always return NUM_VESTING_PERIODS.
pub fn get_current_vesting_period(
&self,
block_time: Timestamp,
) -> Result<Period, ContractError> {
let first_period =
self.periods
.first()
.ok_or(ContractError::UnpopulatedVestingPeriods {
owner: self.owner_address.clone(),
})?;
let last_period = self
.periods
.last()
.ok_or(ContractError::UnpopulatedVestingPeriods {
owner: self.owner_address.clone(),
})?;
if block_time.seconds() < first_period.start_time {
Ok(Period::Before)
} else if last_period.end_time() < block_time {
Ok(Period::After)
pub fn get_current_vesting_period(&self, block_time: Timestamp) -> Period {
if block_time.seconds() < self.periods.first().unwrap().start_time {
Period::Before
} else if self.periods.last().unwrap().end_time() < block_time {
Period::After
} else {
let mut index = 0;
for period in &self.periods {
@@ -138,7 +117,7 @@ impl Account {
}
index += 1;
}
Ok(Period::In(index))
Period::In(index)
}
}
@@ -67,7 +67,7 @@ impl VestingAccount for Account {
storage: &dyn Storage,
) -> Result<Coin, ContractError> {
let block_time = block_time.unwrap_or(env.block.time);
let period = self.get_current_vesting_period(block_time)?;
let period = self.get_current_vesting_period(block_time);
let denom = MIX_DENOM.load(storage)?;
let amount = match period {
@@ -94,7 +94,7 @@ impl VestingAccount for Account {
storage: &dyn Storage,
) -> Result<Coin, ContractError> {
Ok(Coin {
amount: self.get_original_vesting()?.amount().amount
amount: self.get_original_vesting().amount().amount
- self.get_vested_coins(block_time, env, storage)?.amount,
denom: MIX_DENOM.load(storage)?,
})
@@ -108,12 +108,12 @@ impl VestingAccount for Account {
self.periods[(self.num_vesting_periods() - 1) as usize].end_time()
}
fn get_original_vesting(&self) -> Result<OriginalVestingResponse, ContractError> {
Ok(OriginalVestingResponse::new(
fn get_original_vesting(&self) -> OriginalVestingResponse {
OriginalVestingResponse::new(
self.coin.clone(),
self.num_vesting_periods(),
self.period_duration()?,
))
self.period_duration(),
)
}
fn get_delegated_free(
@@ -123,7 +123,7 @@ impl VestingAccount for Account {
storage: &dyn Storage,
) -> Result<Coin, ContractError> {
let block_time = block_time.unwrap_or(env.block.time);
let period = self.get_current_vesting_period(block_time)?;
let period = self.get_current_vesting_period(block_time);
let withdrawn = self.load_withdrawn(storage)?;
let max_available = self
.get_vested_coins(Some(block_time), env, storage)?
@@ -155,7 +155,7 @@ 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 period = self.get_current_vesting_period(block_time);
let start_time = match period {
Period::Before => 0,
Period::After => u64::MAX,
@@ -180,7 +180,7 @@ impl VestingAccount for Account {
storage: &dyn Storage,
) -> Result<Coin, ContractError> {
let block_time = block_time.unwrap_or(env.block.time);
let period = self.get_current_vesting_period(block_time)?;
let period = self.get_current_vesting_period(block_time);
let max_vested = self.get_vested_coins(Some(block_time), env, storage)?;
let start_time = match period {
Period::Before => 0,
+11 -51
View File
@@ -180,14 +180,12 @@ mod tests {
assert_eq!(account.periods().len(), num_vesting_periods as usize);
let current_period = account
.get_current_vesting_period(Timestamp::from_seconds(0))
.unwrap();
let current_period = account.get_current_vesting_period(Timestamp::from_seconds(0));
assert_eq!(Period::Before, current_period);
let block_time =
Timestamp::from_seconds(account.start_time().seconds() + vesting_period + 1);
let current_period = account.get_current_vesting_period(block_time).unwrap();
let current_period = account.get_current_vesting_period(block_time);
assert_eq!(current_period, Period::In(1));
let vested_coins = account
.get_vested_coins(Some(block_time), &env, &deps.storage)
@@ -198,37 +196,21 @@ mod tests {
assert_eq!(
vested_coins.amount,
Uint128::new(
account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
/ num_vesting_periods as u128
account.get_original_vesting().amount().amount.u128() / num_vesting_periods as u128
)
);
assert_eq!(
vesting_coins.amount,
Uint128::new(
account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
- account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
account.get_original_vesting().amount().amount.u128()
- account.get_original_vesting().amount().amount.u128()
/ num_vesting_periods as u128
)
);
let block_time =
Timestamp::from_seconds(account.start_time().seconds() + 5 * vesting_period + 1);
let current_period = account.get_current_vesting_period(block_time).unwrap();
let current_period = account.get_current_vesting_period(block_time);
assert_eq!(current_period, Period::In(5));
let vested_coins = account
.get_vested_coins(Some(block_time), &env, &deps.storage)
@@ -239,30 +221,15 @@ mod tests {
assert_eq!(
vested_coins.amount,
Uint128::new(
5 * account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
5 * account.get_original_vesting().amount().amount.u128()
/ num_vesting_periods as u128
)
);
assert_eq!(
vesting_coins.amount,
Uint128::new(
account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
- 5 * account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
account.get_original_vesting().amount().amount.u128()
- 5 * account.get_original_vesting().amount().amount.u128()
/ num_vesting_periods as u128
)
);
@@ -270,7 +237,7 @@ mod tests {
let block_time = Timestamp::from_seconds(
account.start_time().seconds() + vesting_over_period * vesting_period + 1,
);
let current_period = account.get_current_vesting_period(block_time).unwrap();
let current_period = account.get_current_vesting_period(block_time);
assert_eq!(current_period, Period::After);
let vested_coins = account
.get_vested_coins(Some(block_time), &env, &deps.storage)
@@ -280,14 +247,7 @@ mod tests {
.unwrap();
assert_eq!(
vested_coins.amount,
Uint128::new(
account
.get_original_vesting()
.unwrap()
.amount()
.amount
.u128()
)
Uint128::new(account.get_original_vesting().amount().amount.u128())
);
assert_eq!(vesting_coins.amount, Uint128::zero());
}
+33 -48
View File
@@ -1,4 +1,4 @@
version: "3.7"
version: '3.7'
x-network: &NETWORK
BECH32_PREFIX: nymt
@@ -23,7 +23,7 @@ services:
networks:
localnet:
ipv4_address: 172.168.10.2
command: ["genesis"]
command: [ "genesis" ]
secondary_validator:
build:
context: docker/validator
@@ -41,7 +41,7 @@ services:
ipv4_address: 172.168.10.3
depends_on:
- "genesis_validator"
command: ["secondary"]
command: [ "secondary" ]
# mixnet_contract:
# build: docker/mixnet_contract
# image: contract:latest
@@ -74,49 +74,34 @@ services:
- "genesis_validator"
- "secondary_validator"
# mongo:
# image: mongo:latest
# command:
# - --storageEngine=wiredTiger
# volumes:
# - mongo_data:/data/db
# block_explorer:
# build:
# context: https://github.com/forbole/big-dipper.git#v0.41.x-7
# image: block_explorer:v0.41.x-7
# ports:
# - "3080:3000"
# depends_on:
# - "mongo"
# environment:
# ROOT_URL: ${APP_ROOT_URL:-http://localhost}
# MONGO_URL: mongodb://mongo:27017/meteor
# PORT: 3000
# METEOR_SETTINGS: ${METEOR_SETTINGS}
# explorer:
# build:
# context: docker/explorer
# image: explorer:latest
# ports:
# - "3040:3000"
# depends_on:
# - "genesis_validator"
# - "block_explorer"
# service to update geoip binary database, for explorer-api
geoipupdate:
container_name: geoipupdate
image: maxmindinc/geoipupdate
restart: unless-stopped
environment:
GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID}
GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY}
GEOIPUPDATE_EDITION_IDS: ${GEOIPUPDATE_EDITION_IDS}
GEOIPUPDATE_FREQUENCY: ${GEOIPUPDATE_FREQUENCY}
networks:
- geoipupdate
volumes:
- ${GEOIP_DB_DIRECTORY}:/usr/share/GeoIP
# mongo:
# image: mongo:latest
# command:
# - --storageEngine=wiredTiger
# volumes:
# - mongo_data:/data/db
# block_explorer:
# build:
# context: https://github.com/forbole/big-dipper.git#v0.41.x-7
# image: block_explorer:v0.41.x-7
# ports:
# - "3080:3000"
# depends_on:
# - "mongo"
# environment:
# ROOT_URL: ${APP_ROOT_URL:-http://localhost}
# MONGO_URL: mongodb://mongo:27017/meteor
# PORT: 3000
# METEOR_SETTINGS: ${METEOR_SETTINGS}
# explorer:
# build:
# context: docker/explorer
# image: explorer:latest
# ports:
# - "3040:3000"
# depends_on:
# - "genesis_validator"
# - "block_explorer"
volumes:
genesis_volume:
@@ -126,11 +111,11 @@ volumes:
# contract_volume:
# mongo_data:
networks:
geoipupdate:
localnet:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.168.10.0/25
- subnet: 172.168.10.0/25
+1 -1
View File
@@ -9,7 +9,7 @@ MIX_DENOM_DISPLAY=nym
STAKE_DENOM=unyx
STAKE_DENOM_DISPLAY=nyx
DENOMS_EXPONENT=6
MIXNET_CONTRACT_ADDRESS=n1rjzps6qrmdqmf0xz4cn4x4rcmqeqzq6hnzqg4wcvd0r2lyasdq5sepn5s8
MIXNET_CONTRACT_ADDRESS=n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep
VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav
BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw
-2
View File
@@ -1,2 +0,0 @@
# The path to the geoip database file
GEOIP_DB_PATH=./geo_ip/GeoLite2-Country.mmdb
-1
View File
@@ -1,3 +1,2 @@
target
explorer-api-state.json
/geo_ip
-1
View File
@@ -24,7 +24,6 @@ serde_json = "1.0.66"
thiserror = "1.0.29"
tokio = {version = "1.21.2", features = ["full"] }
maxminddb = "0.23.0"
dotenv = "0.15.0"
mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" }
network-defaults = { path = "../common/network-defaults" }
+14 -42
View File
@@ -1,58 +1,30 @@
# Network Explorer API
Network Explorer API
====================
An API that provides data for the [Network Explorer](../explorer).
Features:
- geolocates mixnodes using https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
- calculates how many nodes are in each country
- proxies mixnode API requests to add HTTPS
## GeoIP db install/update
First we need to install the geoip database.
We use https://github.com/maxmind/geoipupdate to automatically
download and update GeoLite2 binary database. For convenience we
run it as a docker container.
At the root of the repo, inside the `docker-compose.yml`, there
is a docker service `geoipupdate` for it.
Supposed you provided an `.env` file with **all the environment
variables** listed in `.env.sample-dev` (found at the root),
simply run the service through docker:
```shell
docker compose up -d geoipupdate
```
Running this command will automatically install (and update) the
db file inside the directory path provided by `GEOIP_DB_DIRECTORY`
env variable.
- geolocates mixnodes using https://app.ipbase.com/
- calculates how many nodes are in each country
- proxies mixnode API requests to add HTTPS
## Running
When starting the explorer-api, supply the environment variable
`GEOIP_DB_PATH`, pointing to the GeoLite2 binary database file.
It should be previously installed thanks to `geoipupdate` service.
For example:
```shell
GEOIP_DB_PATH=./geo_ip/GeoLite2-Country.mmdb cargo run
```
Note: explorer-api binary reads the provided `.env` file.
Supply the environment variable `GEOIP_DATABASE_PATH` with a path
that points to a GeoIP2 database file in binary format.
Run as a service and reverse proxy with `nginx` to add `https` with Lets Encrypt.
Setup nginx to inject the request IP to the header `X-Real-IP`.
Use https://github.com/maxmind/geoipupdate to automatically
provide and update the GeoIP2 database file.
# TODO / Known Issues
## TODO
- record the number of mixnodes on a given date and write to a file for later retrieval
- dependency injection
- tests
* record the number of mixnodes on a given date and write to a file for later retrieval
* dependency injection
* tests
Binary file not shown.
+4 -4
View File
@@ -6,7 +6,7 @@ use log::warn;
use maxminddb::{geoip2::Country, MaxMindDBError, Reader};
use std::{net::IpAddr, str::FromStr, sync::Arc};
const DEFAULT_DATABASE_PATH: &str = "./geo_ip/GeoLite2-Country.mmdb";
const DEFAULT_DATABASE_PATH: &str = "./src/geo_ip/GeoLite2-Country.mmdb";
#[derive(Debug)]
pub enum GeoIpError {
@@ -17,7 +17,7 @@ pub enum GeoIpError {
// The current State implementation does not allow to fail on state
// creation, ie. returning Result<>. To avoid to use unwrap family,
// as a workaround, wrap the state inside an Option<>
// If Reader::open_readfile fails for some reason db will be set to None
// If Reader::open_readfile fails for some reason db will will be set to None
// and an error will be logged.
pub(crate) struct GeoIp {
pub(crate) db: Option<Reader<Vec<u8>>>,
@@ -37,9 +37,9 @@ pub(crate) struct Location {
impl GeoIp {
pub fn new() -> Self {
let db_path = std::env::var("GEOIP_DB_PATH").unwrap_or_else(|e| {
let db_path = std::env::var("GEOIP_DATABASE_PATH").unwrap_or_else(|e| {
warn!(
"Env variable GEOIP_DB_PATH is not set: {} - Fallback to {}",
"Env variable GEOIP_DATABASE_PATH is not set: {} - Fallback to {}",
e, DEFAULT_DATABASE_PATH
);
DEFAULT_DATABASE_PATH.to_string()
-2
View File
@@ -4,7 +4,6 @@ extern crate rocket;
extern crate rocket_okapi;
use clap::Parser;
use dotenv::dotenv;
use log::info;
use network_defaults::setup_env;
use task::ShutdownNotifier;
@@ -31,7 +30,6 @@ const COUNTRY_DATA_REFRESH_INTERVAL: u64 = 60 * 15; // every 15 minutes
#[tokio::main]
async fn main() {
dotenv().ok();
setup_logging();
let args = commands::Cli::parse();
setup_env(args.config_env_file);
+1
View File
@@ -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.";
+9 -1
View File
@@ -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();
+57 -36
View File
@@ -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>
);
};
+28 -3
View File
@@ -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}%`,
};
}
+2 -2
View File
@@ -23,7 +23,7 @@ import { Footer } from './Footer';
import { DarkLightSwitchDesktop } from './Switch';
import { NavOptionType } from '../context/nav';
const drawerWidth = 255;
const drawerWidth = 300;
const openedMixin = (theme: Theme): CSSObject => ({
width: drawerWidth,
@@ -182,7 +182,7 @@ export const ExpandableButton: React.FC<ExpandableButtonType> = ({
background: isChild ? palette.nym.networkExplorer.nav.selected.nested : 'none',
}}
>
<ListItemIcon sx={{ minWidth: '39px' }}>{Icon}</ListItemIcon>
<ListItemIcon>{Icon}</ListItemIcon>
<ListItemText
primary={title}
sx={{
@@ -70,24 +70,6 @@ export const UniversalDataGrid: React.FC<DataGridProps> = ({ rows, columns, load
width: '100%',
border: 'none',
}}
sx={{
'*::-webkit-scrollbar': {
width: '1em',
},
'*::-webkit-scrollbar-track': {
background: (t) => t.palette.nym.networkExplorer.scroll.backgroud,
outline: (t) => `1px solid ${t.palette.nym.networkExplorer.scroll.border}`,
boxShadow: 'auto',
borderRadius: 'auto',
},
'*::-webkit-scrollbar-thumb': {
backgroundColor: (t) => t.palette.nym.networkExplorer.scroll.color,
borderRadius: '20px',
width: '.4em',
border: (t) => `3px solid ${t.palette.nym.networkExplorer.scroll.backgroud}`,
shadow: 'auto',
},
}}
/>
);
return null;
+59
View File
@@ -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>;
};
+32 -15
View File
@@ -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];
};
+1
View File
@@ -109,6 +109,7 @@ export const MainContextProvider: React.FC = ({ children }) => {
};
const fetchGateways = async () => {
setGateways((d) => ({ ...d, isLoading: true }));
try {
const data = await Api.fetchGateways();
setGateways({ data, isLoading: false });
+1 -44
View File
@@ -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];
}
+179
View File
@@ -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>
);
};
+28 -18
View File
@@ -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 { 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';
@@ -43,29 +44,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>
),
},
{
@@ -109,6 +101,19 @@ export const PageGateways: React.FC = () => {
</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 +138,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>
+57 -57
View File
@@ -81,12 +81,30 @@ export const PageMixnodes: React.FC = () => {
};
const columns: GridColDef[] = [
{
field: 'owner',
headerName: 'Owner',
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
width: 200,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
href={`${BIG_DIPPER}/account/${params.value}`}
target="_blank"
sx={getCellStyles(theme, params.row)}
data-testid="big-dipper-link"
>
{splice(7, 29, params.value)}
</MuiLink>
),
},
{
field: 'identity_key',
headerName: 'Identity Key',
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
headerClassName: 'MuiDataGrid-header-override',
width: 170,
width: 180,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<>
@@ -106,13 +124,49 @@ export const PageMixnodes: React.FC = () => {
</>
),
},
{
field: 'location',
headerName: 'Location',
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
width: 150,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Button
onClick={() => handleSearch(params.value as string)}
sx={{
...getCellStyles(theme, params.row),
justifyContent: 'flex-start',
}}
>
{params.value}
</Button>
),
},
{
field: 'host',
headerName: 'Host',
renderHeader: () => <CustomColumnHeading headingTitle="Host" />,
headerClassName: 'MuiDataGrid-header-override',
width: 130,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={getCellStyles(theme, params.row)}
component={RRDLink}
to={`/network-components/mixnode/${params.row.identity_key}`}
>
{params.value}
</MuiLink>
),
},
{
field: 'bond',
headerName: 'Stake',
renderHeader: () => <CustomColumnHeading headingTitle="Stake" />,
type: 'number',
headerClassName: 'MuiDataGrid-header-override',
width: 170,
width: 200,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
@@ -152,7 +206,7 @@ export const PageMixnodes: React.FC = () => {
{
field: 'pledge_amount',
headerName: 'Bond',
width: 175,
width: 200,
headerClassName: 'MuiDataGrid-header-override',
renderHeader: () => <CustomColumnHeading headingTitle="Bond" tooltipInfo="Node operator's share of stake." />,
type: 'number',
@@ -211,60 +265,6 @@ export const PageMixnodes: React.FC = () => {
</MuiLink>
),
},
{
field: 'owner',
headerName: 'Owner',
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
width: 120,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
href={`${BIG_DIPPER}/account/${params.value}`}
target="_blank"
sx={getCellStyles(theme, params.row)}
data-testid="big-dipper-link"
>
{splice(7, 29, params.value)}
</MuiLink>
),
},
{
field: 'location',
headerName: 'Location',
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
width: 120,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Button
onClick={() => handleSearch(params.value as string)}
sx={{
...getCellStyles(theme, params.row),
justifyContent: 'flex-start',
}}
>
{params.value}
</Button>
),
},
{
field: 'host',
headerName: 'Host',
renderHeader: () => <CustomColumnHeading headingTitle="Host" />,
headerClassName: 'MuiDataGrid-header-override',
width: 130,
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={getCellStyles(theme, params.row)}
component={RRDLink}
to={`/network-components/mixnode/${params.row.identity_key}`}
>
{params.value}
</MuiLink>
),
},
];
const handlePageSize = (event: SelectChangeEvent<string>) => {
@@ -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>
+3 -6
View File
@@ -126,12 +126,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;
+5 -5
View File
@@ -160,9 +160,9 @@ mod qa {
pub(crate) const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6);
pub(crate) const MIXNET_CONTRACT_ADDRESS: &str =
"n1frq2hzkjtatsupc6jtyaz67ytydk9nya437q92qg76ny3y8fcnjsw806vg";
"n1rjzps6qrmdqmf0xz4cn4x4rcmqeqzq6hnzqg4wcvd0r2lyasdq5sepn5s8";
pub(crate) const VESTING_CONTRACT_ADDRESS: &str =
"n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g";
"n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav";
pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str =
"n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0";
pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str =
@@ -172,13 +172,13 @@ mod qa {
hex_literal::hex!("0000000000000000000000000000000000000000");
pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] =
hex_literal::hex!("0000000000000000000000000000000000000000");
//pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n17dcjwykjj9ydd8p9hk65uf2flrhhlfklju8flp";
//pub(crate) const REWARDING_VALIDATOR_ADDRESS: &str = "n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq";
//pub(crate) const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "http://0.0.0.0";
pub(crate) fn validators() -> Vec<ValidatorDetails> {
vec![ValidatorDetails::new(
"https://adv-epoch-qa-validator.qa.nymte.ch/",
Some("https://adv-epoch-qa-val-api.qa.nymte.ch/api"),
"https://qa-validator.nymtech.net",
Some("https://qa-validator-api.nymtech.net/api"),
)]
}
-32
View File
@@ -1,32 +0,0 @@
import React, { useState } from 'react';
import { Alert as MuiAlert, IconButton } from '@mui/material';
import { Close } from '@mui/icons-material';
export const Alert = ({ title, dismissable }: { title: string | React.ReactNode; dismissable?: boolean }) => {
const [displayAlert, setDisplayAlert] = useState(true);
const handleDismiss = () => setDisplayAlert(false);
if (!displayAlert) return null;
return (
<MuiAlert
severity="info"
sx={{
width: '100%',
borderRadius: 0,
bgcolor: 'background.default',
color: (theme) => theme.palette.nym.nymWallet.text.blue,
'& .MuiAlert-icon': { color: 'nym.nymWallet.text.blue', mr: 1 },
}}
action={
dismissable && (
<IconButton aria-label="close" color="inherit" size="small" onClick={handleDismiss}>
<Close fontSize="inherit" />
</IconButton>
)
}
>
{title}
</MuiAlert>
);
};
@@ -33,7 +33,7 @@ const headers: Header[] = [
'The percentage of the node rewards that you as the node operator will take before the rest of the reward is shared between you and the delegators.',
},
{
header: 'Operating cost',
header: 'Operator cost',
id: 'operator-cost',
// tooltipText: 'TODO', // TODO
},
@@ -94,7 +94,7 @@ export const BondedMixnode = ({
id: 'pm-cell',
},
{
cell: operatorCost ? `${operatorCost.amount} ${operatorCost.denom}` : '-',
cell: operatorCost ? `${operatorCost} NYM` : '-',
id: 'operator-cost-cell',
},
{
@@ -1,6 +1,5 @@
import * as Yup from 'yup';
import {
isLessThan,
isValidHostname,
validateAmount,
validateKey,
@@ -57,15 +56,4 @@ export const amountSchema = Yup.object().shape({
return true;
}),
}),
operatorCost: Yup.object().shape({
amount: Yup.string()
.required('An operating cost is required')
.test('valid-operating-cost', 'A valid amount is required (min 40)', async function isValidAmount(this, value) {
if (value && (!Number(value) || isLessThan(+value, 40))) {
return false;
}
return true;
}),
}),
});
@@ -24,7 +24,7 @@ const defaultGatewayValues: GatewayData = {
const defaultAmountValues = (denom: CurrencyDenom) => ({
amount: { amount: '100', denom },
operatorCost: { amount: '40', denom },
operatorCost: { amount: '0', denom },
tokenPool: 'balance',
});
@@ -57,7 +57,6 @@ function descendingComparator(a: any, b: any, orderBy: string) {
}
return 0;
}
// Sorting function needs fixing
function sortPendingDelegation(a: DelegationWithEvent, b: DelegationWithEvent) {
if (isPendingDelegation(a) && isPendingDelegation(b)) return 0;
@@ -155,7 +154,7 @@ export const DelegationList: React.FC<{
<EnhancedTableHead order={order} orderBy={orderBy} onRequestSort={handleRequestSort} />
<TableBody>
{items?.length ? (
items.map((item) => (
items.sort(getComparator(order, orderBy)).map((item) => (
<TableRow key={item.node_identity}>
<TableCell>
<Link
+5 -3
View File
@@ -54,7 +54,7 @@ export type TBondedMixnode = {
delegators: number;
status: MixnodeStatus;
proxy?: string;
operatorCost: DecCoin;
operatorCost?: string;
host: string;
estimatedRewards?: DecCoin;
activeSetProbability?: SelectionChance;
@@ -141,6 +141,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
const additionalDetails: {
status: MixnodeStatus;
stakeSaturation: string;
operatorCost?: string;
estimatedRewards?: DecCoin;
} = {
status: 'not_found',
@@ -162,6 +163,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
}
try {
const rewardEstimation = await getMixnodeRewardEstimation(mixId);
additionalDetails.operatorCost = unymToNym(rewardEstimation.estimation.operating_cost);
const estimatedRewards = unymToNym(rewardEstimation.estimation.total_node_reward);
if (estimatedRewards) {
additionalDetails.estimatedRewards = {
@@ -238,7 +240,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
bond_information: { mix_id },
} = data;
const { status, stakeSaturation, estimatedRewards } = await getAdditionalMixnodeDetails(mix_id);
const { status, stakeSaturation, operatorCost, estimatedRewards } = await getAdditionalMixnodeDetails(mix_id);
const setProbabilities = await getSetProbabilities(mix_id);
const nodeDescription = await getNodeDescription(
bond_information.mix_node.host,
@@ -259,7 +261,7 @@ export const BondingContextProvider = ({ children }: { children?: React.ReactNod
operatorRewards,
status,
stakeSaturation,
operatorCost: rewarding_details.cost_params.interval_operating_cost,
operatorCost,
host: bond_information.mix_node.host.replace(/\s/g, ''),
routingScore,
activeSetProbability: setProbabilities?.in_active,
+1 -1
View File
@@ -16,7 +16,7 @@ const bondedMixnodeMock: TBondedMixnode = {
operatorRewards: { denom: 'nym', amount: '1234' },
delegators: 5423,
status: 'active',
operatorCost: { denom: 'nym', amount: '1234' },
operatorCost: '0.22',
host: '1.2.3.4',
routingScore: 75,
activeSetProbability: 'High',
@@ -15,8 +15,7 @@ import { AppContext, urls } from 'src/context/main';
import { NodeGeneralSettings } from './settings-pages/general-settings';
import { NodeUnbondPage } from './settings-pages/NodeUnbondPage';
import { createNavItems } from './node-settings.constant';
import { isMixnode } from 'src/types';
import { navItems, NodeSettingsNav } from './node-settings.constant';
export const NodeSettings = () => {
const theme = useTheme();
@@ -26,9 +25,9 @@ export const NodeSettings = () => {
const location = useLocation();
const [confirmationDetails, setConfirmationDetails] = useState<ConfirmationDetailProps>();
const [value, setValue] = React.useState('General');
const [value, setValue] = React.useState<NodeSettingsNav>('General');
const handleChange = (event: React.SyntheticEvent, tab: string) => {
setValue(tab);
setValue(tab as NodeSettingsNav);
};
useEffect(() => {
@@ -54,10 +53,6 @@ export const NodeSettings = () => {
});
};
if (!bondedNode) {
return null;
}
return (
<PageLayout>
<NymCard
@@ -81,7 +76,7 @@ export const NodeSettings = () => {
</Box>
<Box sx={{ width: '100%' }}>
<Tabs
tabs={createNavItems(isMixnode(bondedNode))}
tabs={navItems}
selectedTab={value}
onChange={handleChange}
tabSx={{
@@ -1,5 +1,3 @@
export const createNavItems = (isMixnode: boolean) => {
const navItems = ['Unbond'];
if (isMixnode) return ['General', ...navItems];
return navItems;
};
export const navItems = ['General', 'Unbond'] as const;
export type NodeSettingsNav = typeof navItems[number]; // type NodeSettingsNav = 'General' | 'Unbond';
@@ -1,17 +1,18 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import { Button, Divider, Typography, TextField, Grid, CircularProgress, Box } from '@mui/material';
import { Button, Divider, Typography, TextField, Grid, Alert, IconButton, CircularProgress, Box } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import CloseIcon from '@mui/icons-material/Close';
import { isMixnode } from 'src/types';
import { updateMixnodeConfig } from 'src/requests';
import { TBondedMixnode, TBondedGateway } from 'src/context/bonding';
import { SimpleModal } from 'src/components/Modals/SimpleModal';
import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/mixnodeValidationSchema';
import { Console } from 'src/utils/console';
import { Alert } from 'src/components/Alert';
export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => {
const [open, setOpen] = useState(true);
const [openConfirmationModal, setOpenConfirmationModal] = useState<boolean>(false);
const theme = useTheme();
@@ -53,14 +54,33 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon
return (
<Grid container xs item>
<Alert
title={
<Box sx={{ fontWeight: 600 }}>
Your changes will be ONLY saved on the display. Remember to change the values on your nodes config file too
</Box>
}
dismissable
/>
{open && (
<Alert
severity="info"
action={
<IconButton
aria-label="close"
color="inherit"
size="small"
onClick={() => {
setOpen(false);
}}
>
<CloseIcon fontSize="inherit" />
</IconButton>
}
sx={{
px: 2,
borderRadius: 0,
bgcolor: 'background.default',
color: (theme) => theme.palette.nym.nymWallet.text.blue,
'& .MuiAlert-icon': { color: (theme) => theme.palette.nym.nymWallet.text.blue, mr: 1 },
}}
>
<Box sx={{ fontWeight: 600 }}>Your changes will be ONLY saved on the display.</Box> Remember to change the
values on your nodes config file too.
</Alert>
)}
<Grid container>
<Grid item container direction="row" alignItems="left" justifyContent="space-between" padding={3}>
<Grid item>

Some files were not shown because too many files have changed in this diff Show More