Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a9978b81e | |||
| 7cdfdf8437 | |||
| fca32424b4 | |||
| 7ad9c15c9e | |||
| 54716c7c09 | |||
| 2a0b1a3734 | |||
| 7f35b3660b | |||
| 8470288079 | |||
| d5a65841af | |||
| 76144928a0 | |||
| d6ffdb86ce | |||
| 74f9205be1 |
@@ -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
|
||||
|
||||
@@ -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
@@ -1583,7 +1583,6 @@ version = "1.0.1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap 3.2.8",
|
||||
"dotenv",
|
||||
"humantime-serde",
|
||||
"isocountry",
|
||||
"itertools",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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(¶ms_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);
|
||||
}
|
||||
}
|
||||
-81
@@ -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 +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.
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
allow-unwrap-in-tests = true
|
||||
allow-expect-in-tests = true
|
||||
@@ -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());
|
||||
|
||||
@@ -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, ¤t_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(),
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 }))
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 },
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# The path to the geoip database file
|
||||
GEOIP_DB_PATH=./geo_ip/GeoLite2-Country.mmdb
|
||||
@@ -1,3 +1,2 @@
|
||||
target
|
||||
explorer-api-state.json
|
||||
/geo_ip
|
||||
|
||||
@@ -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
@@ -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.
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -14,6 +14,7 @@ export const VALIDATORS_API = `${VALIDATOR_BASE_URL}/validators`;
|
||||
export const BLOCK_API = `${VALIDATOR_API_BASE_URL}/block`;
|
||||
export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
|
||||
export const UPTIME_STORY_API = `${VALIDATOR_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
|
||||
export const UPTIME_STORY_API_GATEWAY = `${VALIDATOR_API_BASE_URL}/api/v1/status/gateway`; // add ID then '/history' or '/report' to this.
|
||||
|
||||
// errors
|
||||
export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us.";
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
BLOCK_API,
|
||||
COUNTRY_DATA_API,
|
||||
GATEWAYS_API,
|
||||
UPTIME_STORY_API_GATEWAY,
|
||||
MIXNODE_API,
|
||||
MIXNODE_PING,
|
||||
MIXNODES_API,
|
||||
@@ -15,6 +16,8 @@ import {
|
||||
DelegationsResponse,
|
||||
UniqDelegationsResponse,
|
||||
GatewayResponse,
|
||||
GatewayReportResponse,
|
||||
UptimeStoryResponse,
|
||||
MixNodeDescriptionResponse,
|
||||
MixNodeResponse,
|
||||
MixNodeResponseItem,
|
||||
@@ -23,7 +26,6 @@ import {
|
||||
StatsResponse,
|
||||
StatusResponse,
|
||||
SummaryOverviewResponse,
|
||||
UptimeStoryResponse,
|
||||
ValidatorsResponse,
|
||||
} from '../typeDefs/explorer-api';
|
||||
|
||||
@@ -92,6 +94,12 @@ export class Api {
|
||||
return res.json();
|
||||
};
|
||||
|
||||
static fetchGatewayUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/history`)).json();
|
||||
|
||||
static fetchGatewayReportById = async (id: string): Promise<GatewayReportResponse> =>
|
||||
(await fetch(`${UPTIME_STORY_API_GATEWAY}/${id}/report`)).json();
|
||||
|
||||
static fetchValidators = async (): Promise<ValidatorsResponse> => {
|
||||
const res = await fetch(VALIDATORS_API);
|
||||
const json = await res.json();
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Tooltip } from '@nymproject/react/tooltip/Tooltip';
|
||||
import { CopyToClipboard } from '@nymproject/react/clipboard/CopyToClipboard';
|
||||
import { Box } from '@mui/system';
|
||||
import { cellStyles } from './Universal-DataGrid';
|
||||
import { currencyToString } from '../utils/currency';
|
||||
import { GatewayEnrichedRowType } from './Gateways';
|
||||
import { MixnodeRowType } from './MixNodes';
|
||||
|
||||
export type ColumnsType = {
|
||||
@@ -43,42 +46,60 @@ function formatCellValues(val: string | number, field: string) {
|
||||
export const DetailTable: React.FC<{
|
||||
tableName: string;
|
||||
columnsData: ColumnsType[];
|
||||
rows: MixnodeRowType[];
|
||||
}> = ({ tableName, columnsData, rows }: UniversalTableProps) => (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columnsData?.map(({ field, title, flex }) => (
|
||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
|
||||
{title}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((eachRow) => (
|
||||
<TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
{columnsData?.map((_, index) => (
|
||||
<TableCell
|
||||
key={_.title}
|
||||
component="th"
|
||||
scope="row"
|
||||
variant="body"
|
||||
sx={{
|
||||
...cellStyles,
|
||||
padding: 2,
|
||||
width: 200,
|
||||
fontSize: 14,
|
||||
}}
|
||||
data-testid={`${_.title.replace(/ /g, '-')}-value`}
|
||||
>
|
||||
{formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)}
|
||||
rows: MixnodeRowType[] | GatewayEnrichedRowType[];
|
||||
}> = ({ tableName, columnsData, rows }: UniversalTableProps) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<TableContainer component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label={tableName}>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columnsData?.map(({ field, title, flex, tooltipInfo }) => (
|
||||
<TableCell key={field} sx={{ fontSize: 14, fontWeight: 600, flex }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{tooltipInfo && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Tooltip
|
||||
title={tooltipInfo}
|
||||
id={field}
|
||||
placement="top-start"
|
||||
textColor={theme.palette.nym.networkExplorer.tooltip.color}
|
||||
bgColor={theme.palette.nym.networkExplorer.tooltip.background}
|
||||
maxWidth={230}
|
||||
arrow
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{title}
|
||||
</Box>
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((eachRow) => (
|
||||
<TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
|
||||
{columnsData?.map((data, index) => (
|
||||
<TableCell
|
||||
key={data.title}
|
||||
component="th"
|
||||
scope="row"
|
||||
variant="body"
|
||||
sx={{
|
||||
...cellStyles,
|
||||
padding: 2,
|
||||
width: 200,
|
||||
fontSize: 14,
|
||||
}}
|
||||
data-testid={`${data.title.replace(/ /g, '-')}-value`}
|
||||
>
|
||||
{formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,48 @@
|
||||
import { GatewayResponse } from '../typeDefs/explorer-api';
|
||||
import { GatewayResponse, GatewayResponseItem, GatewayReportResponse } from '../typeDefs/explorer-api';
|
||||
|
||||
export type GatewayRowType = {
|
||||
id: string;
|
||||
owner: string;
|
||||
identity_key: string;
|
||||
identityKey: string;
|
||||
bond: number;
|
||||
host: string;
|
||||
location: string;
|
||||
};
|
||||
|
||||
export type GatewayEnrichedRowType = GatewayRowType & {
|
||||
routingScore: string;
|
||||
avgUptime: string;
|
||||
clientsPort: number;
|
||||
mixPort: number;
|
||||
};
|
||||
|
||||
export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] {
|
||||
return !arrayOfGateways
|
||||
? []
|
||||
: arrayOfGateways.map((gw) => ({
|
||||
id: gw.owner,
|
||||
owner: gw.owner,
|
||||
identity_key: gw.gateway.identity_key || '',
|
||||
identityKey: gw.gateway.identity_key || '',
|
||||
location: gw?.gateway?.location || '',
|
||||
bond: gw.pledge_amount.amount || 0,
|
||||
host: gw.gateway.host || '',
|
||||
}));
|
||||
}
|
||||
|
||||
export function gatewayEnrichedToGridRow(
|
||||
gateway: GatewayResponseItem,
|
||||
report: GatewayReportResponse,
|
||||
): GatewayEnrichedRowType {
|
||||
return {
|
||||
id: gateway.owner,
|
||||
owner: gateway.owner,
|
||||
identityKey: gateway.gateway.identity_key || '',
|
||||
location: gateway?.gateway?.location || '',
|
||||
bond: gateway.pledge_amount.amount || 0,
|
||||
host: gateway.gateway.host || '',
|
||||
clientsPort: gateway.gateway.clients_port || 0,
|
||||
mixPort: gateway.gateway.mix_port || 0,
|
||||
routingScore: `${report.most_recent}%`,
|
||||
avgUptime: `${report.last_day || report.last_hour}%`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from 'react';
|
||||
import { ApiState, GatewayReportResponse, UptimeStoryResponse } from '../typeDefs/explorer-api';
|
||||
import { Api } from '../api';
|
||||
import { useApiState } from './hooks';
|
||||
|
||||
/**
|
||||
* This context provides the state for a single gateway by identity key.
|
||||
*/
|
||||
|
||||
interface GatewayState {
|
||||
uptimeReport?: ApiState<GatewayReportResponse>;
|
||||
uptimeStory?: ApiState<UptimeStoryResponse>;
|
||||
}
|
||||
|
||||
export const GatewayContext = React.createContext<GatewayState>({});
|
||||
|
||||
export const useGatewayContext = (): React.ContextType<typeof GatewayContext> =>
|
||||
React.useContext<GatewayState>(GatewayContext);
|
||||
|
||||
/**
|
||||
* Provides a state context for a gateway by identity
|
||||
* @param gatewayIdentityKey The identity key of the gateway
|
||||
*/
|
||||
export const GatewayContextProvider = ({
|
||||
gatewayIdentityKey,
|
||||
children,
|
||||
}: {
|
||||
gatewayIdentityKey: string;
|
||||
children: JSX.Element;
|
||||
}) => {
|
||||
const [uptimeReport, fetchUptimeReportById, clearUptimeReportById] = useApiState<GatewayReportResponse>(
|
||||
gatewayIdentityKey,
|
||||
Api.fetchGatewayReportById,
|
||||
'Failed to fetch gateway uptime report by id',
|
||||
);
|
||||
|
||||
const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState<UptimeStoryResponse>(
|
||||
gatewayIdentityKey,
|
||||
Api.fetchGatewayUptimeStoryById,
|
||||
'Failed to fetch gateway uptime history',
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
// when the identity key changes, remove all previous data
|
||||
clearUptimeReportById();
|
||||
clearUptimeHistory();
|
||||
Promise.all([fetchUptimeReportById(), fetchUptimeHistory()]);
|
||||
}, [gatewayIdentityKey]);
|
||||
|
||||
const state = React.useMemo<GatewayState>(
|
||||
() => ({
|
||||
uptimeReport,
|
||||
uptimeStory,
|
||||
}),
|
||||
[uptimeReport, uptimeStory],
|
||||
);
|
||||
|
||||
return <GatewayContext.Provider value={state}>{children}</GatewayContext.Provider>;
|
||||
};
|
||||
@@ -1,30 +1,47 @@
|
||||
import * as React from 'react';
|
||||
import { ApiState } from '../typeDefs/explorer-api';
|
||||
|
||||
type WrappedApiFn<T> = () => Promise<ApiState<T>>;
|
||||
|
||||
/**
|
||||
* Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously
|
||||
* @param id The id to fetch
|
||||
* @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter)
|
||||
* @param errorMessage A static error message, to use when no dynamic error message is returned
|
||||
*/
|
||||
export const useApiState = <T>(
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
fn: Function,
|
||||
id: string,
|
||||
fn: (argId: string) => Promise<T>,
|
||||
errorMessage: string,
|
||||
): [ApiState<T>, WrappedApiFn<T>] => {
|
||||
): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] => {
|
||||
// stores the state
|
||||
const [value, setValue] = React.useState<ApiState<T>>();
|
||||
const wrappedFn = React.useCallback(async () => {
|
||||
|
||||
// clear the value
|
||||
const clearValueFn = () => setValue(undefined);
|
||||
|
||||
// this provides a method to trigger the delegate to fetch data
|
||||
const wrappedFetchFn = React.useCallback(async () => {
|
||||
setValue({ isLoading: true });
|
||||
try {
|
||||
// keep previous state and set to loading
|
||||
setValue((prevState) => ({ ...prevState, isLoading: true }));
|
||||
const data = await fn();
|
||||
setValue({
|
||||
|
||||
// delegate to user function to get data and set if successful
|
||||
const data = await fn(id);
|
||||
const newValue: ApiState<T> = {
|
||||
isLoading: false,
|
||||
data,
|
||||
});
|
||||
return data;
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
setValue({
|
||||
// return the caught error or create a new error with the static error message
|
||||
const newValue: ApiState<T> = {
|
||||
error: error instanceof Error ? error : new Error(errorMessage),
|
||||
isLoading: false,
|
||||
});
|
||||
return undefined;
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
}
|
||||
}, [setValue, fn]);
|
||||
return [value || { isLoading: true }, wrappedFn];
|
||||
}, [setValue, fn, id, errorMessage]);
|
||||
return [value, wrappedFetchFn, clearValueFn];
|
||||
};
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
UptimeStoryResponse,
|
||||
} from '../typeDefs/explorer-api';
|
||||
import { Api } from '../api';
|
||||
import { useApiState } from './hooks';
|
||||
import { mixNodeResponseItemToMixnodeRowType, MixnodeRowType } from '../components/MixNodes';
|
||||
|
||||
/**
|
||||
@@ -153,47 +154,3 @@ export const MixnodeContextProvider: React.FC<MixnodeContextProviderProps> = ({
|
||||
|
||||
return <MixnodeContext.Provider value={state}>{children}</MixnodeContext.Provider>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook to get data from the API by passing an id to a delegate method that fetches the data asynchronously
|
||||
* @param id The id to fetch
|
||||
* @param fn Delegate the fetching to this method (must take `(id: string)` as a parameter)
|
||||
* @param errorMessage A static error message, to use when no dynamic error message is returned
|
||||
*/
|
||||
function useApiState<T>(
|
||||
id: string,
|
||||
fn: (argId: string) => Promise<T>,
|
||||
errorMessage: string,
|
||||
): [ApiState<T> | undefined, () => Promise<ApiState<T>>, () => void] {
|
||||
// stores the state
|
||||
const [value, setValue] = React.useState<ApiState<T> | undefined>();
|
||||
|
||||
// clear the value
|
||||
const clearValueFn = () => setValue(undefined);
|
||||
|
||||
// this provides a method to trigger the delegate to fetch data
|
||||
const wrappedFetchFn = React.useCallback(async () => {
|
||||
try {
|
||||
// keep previous state and set to loading
|
||||
setValue((prevState) => ({ ...prevState, isLoading: true }));
|
||||
|
||||
// delegate to user function to get data and set if successful
|
||||
const data = await fn(id);
|
||||
const newValue: ApiState<T> = {
|
||||
isLoading: false,
|
||||
data,
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
} catch (error) {
|
||||
// return the caught error or create a new error with the static error message
|
||||
const newValue: ApiState<T> = {
|
||||
error: error instanceof Error ? error : new Error(errorMessage),
|
||||
isLoading: false,
|
||||
};
|
||||
setValue(newValue);
|
||||
return newValue;
|
||||
}
|
||||
}, [setValue, fn]);
|
||||
return [value || { isLoading: true }, wrappedFetchFn, clearValueFn];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import * as React from 'react';
|
||||
import { Alert, AlertTitle, Box, CircularProgress, Grid } from '@mui/material';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { GatewayResponseItem } from '../../typeDefs/explorer-api';
|
||||
import { ColumnsType, DetailTable } from '../../components/DetailTable';
|
||||
import { gatewayEnrichedToGridRow, GatewayEnrichedRowType } from '../../components/Gateways';
|
||||
import { ComponentError } from '../../components/ComponentError';
|
||||
import { ContentCard } from '../../components/ContentCard';
|
||||
import { TwoColSmallTable } from '../../components/TwoColSmallTable';
|
||||
import { UptimeChart } from '../../components/UptimeChart';
|
||||
import { GatewayContextProvider, useGatewayContext } from '../../context/gateway';
|
||||
import { useMainContext } from '../../context/main';
|
||||
import { Title } from '../../components/Title';
|
||||
|
||||
const columns: ColumnsType[] = [
|
||||
{
|
||||
field: 'identityKey',
|
||||
title: 'Identity Key',
|
||||
headerAlign: 'left',
|
||||
width: 230,
|
||||
},
|
||||
{
|
||||
field: 'bond',
|
||||
title: 'Bond',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
},
|
||||
{
|
||||
field: 'routingScore',
|
||||
title: 'Routing Score',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo:
|
||||
'Routing score is relative to that of the network. Each time a gateway is tested, the test packets have to go through the full path of the network (gateway + 3 nodes). If a node in the path drop packets it will affect the score of the gateway and other nodes in the test.',
|
||||
},
|
||||
{
|
||||
field: 'avgUptime',
|
||||
title: 'Avg. Score',
|
||||
flex: 1,
|
||||
headerAlign: 'left',
|
||||
tooltipInfo: 'Is the average routing score in the last 24 hours',
|
||||
},
|
||||
{
|
||||
field: 'host',
|
||||
title: 'IP',
|
||||
headerAlign: 'left',
|
||||
width: 99,
|
||||
},
|
||||
{
|
||||
field: 'location',
|
||||
title: 'Location',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: 'owner',
|
||||
title: 'Owner',
|
||||
headerAlign: 'left',
|
||||
flex: 1,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Shows gateway details
|
||||
*/
|
||||
const PageGatewayDetailsWithState = ({ selectedGateway }: { selectedGateway: GatewayResponseItem | undefined }) => {
|
||||
const [enrichGateway, setEnrichGateway] = React.useState<GatewayEnrichedRowType>();
|
||||
const [status, setStatus] = React.useState<number[] | undefined>();
|
||||
const { uptimeReport, uptimeStory } = useGatewayContext();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (uptimeReport?.data && selectedGateway) {
|
||||
setEnrichGateway(gatewayEnrichedToGridRow(selectedGateway, uptimeReport.data));
|
||||
}
|
||||
}, [uptimeReport, selectedGateway]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (enrichGateway) {
|
||||
setStatus([enrichGateway.mixPort, enrichGateway.clientsPort]);
|
||||
}
|
||||
}, [enrichGateway]);
|
||||
|
||||
return (
|
||||
<Box component="main">
|
||||
<Title text="Gateway Detail" />
|
||||
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<DetailTable
|
||||
columnsData={columns}
|
||||
tableName="Gateway detail table"
|
||||
rows={enrichGateway ? [enrichGateway] : []}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid container spacing={2} mt={0}>
|
||||
<Grid item xs={12} md={4}>
|
||||
{status && (
|
||||
<ContentCard title="Gateway Status">
|
||||
<TwoColSmallTable
|
||||
loading={false}
|
||||
keys={['Mix port', 'Client WS API Port']}
|
||||
values={status.map((each) => each)}
|
||||
icons={status.map((elem) => !!elem)}
|
||||
/>
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid item xs={12} md={8}>
|
||||
{uptimeStory && (
|
||||
<ContentCard title="Routing Score">
|
||||
{uptimeStory.error && <ComponentError text="There was a problem retrieving routing score." />}
|
||||
<UptimeChart loading={uptimeStory.isLoading} xLabel="date" uptimeStory={uptimeStory} />
|
||||
</ContentCard>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Guard component to handle loading and not found states
|
||||
*/
|
||||
const PageGatewayDetailGuard: React.FC = () => {
|
||||
const [selectedGateway, setSelectedGateway] = React.useState<GatewayResponseItem | undefined>();
|
||||
const { gateways } = useMainContext();
|
||||
const { id } = useParams<{ id: string | undefined }>();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (gateways?.data) {
|
||||
setSelectedGateway(gateways.data.find((gateway) => gateway.gateway.identity_key === id));
|
||||
}
|
||||
}, [gateways, id]);
|
||||
|
||||
if (gateways?.isLoading) {
|
||||
return <CircularProgress />;
|
||||
}
|
||||
|
||||
if (gateways?.error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(gateways?.error);
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Oh no! Could not load mixnode <code>{id || ''}</code>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
// loaded, but not found
|
||||
if (gateways && !gateways.isLoading && !gateways.data) {
|
||||
return (
|
||||
<Alert severity="warning">
|
||||
<AlertTitle>Gateway not found</AlertTitle>
|
||||
Sorry, we could not find a mixnode with id <code>{id || ''}</code>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return <PageGatewayDetailsWithState selectedGateway={selectedGateway} />;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper component that adds the mixnode content based on the `id` in the address URL
|
||||
*/
|
||||
export const PageGatewayDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string | undefined }>();
|
||||
|
||||
if (!id) {
|
||||
return <Alert severity="error">Oh no! No mixnode identity key specified</Alert>;
|
||||
}
|
||||
|
||||
return (
|
||||
<GatewayContextProvider gatewayIdentityKey={id}>
|
||||
<PageGatewayDetailGuard />
|
||||
</GatewayContextProvider>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { Button, Card, Grid, Typography } from '@mui/material';
|
||||
import { Link as RRDLink } from 'react-router-dom';
|
||||
import { 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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"),
|
||||
)]
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
+31
-11
@@ -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 node’s 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 node’s 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
Reference in New Issue
Block a user