Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3c9b11258 | |||
| e7179eaa46 | |||
| 84cf1c6a62 | |||
| f5e874f8d9 | |||
| 183f2779f0 | |||
| 92e56c0121 | |||
| 550e0ce856 | |||
| 3b0215ccee | |||
| 38a8621032 | |||
| caa0bc4e1e | |||
| abcb0cbf5e | |||
| cdbcfbe3bf | |||
| b8ee730561 | |||
| 26a067c14d | |||
| 69fe8e5cce | |||
| 4fbf1cd876 | |||
| c693412258 | |||
| 72825a2ad3 | |||
| 89a19815fc | |||
| 394f0d30bf |
Generated
+8
-8
@@ -798,9 +798,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-crypto"
|
||||
version = "1.0.0-beta8"
|
||||
version = "1.0.0-beta7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37e70111e9701c3ec43bfbff0e523cd4cb115876b4d3433813436dd0934ee962"
|
||||
checksum = "88c2565b1e73a816fb659ef4838fc356143fbd35f43c48a51d2d7d4e5d6679d3"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"ed25519-zebra",
|
||||
@@ -811,18 +811,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-derive"
|
||||
version = "1.0.0-beta8"
|
||||
version = "1.0.0-beta7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58bc2ad5d86be5f6068833f63e20786768db6890019c095dd7775232184fb7b3"
|
||||
checksum = "fa89fcdf8dbbe0088e663d0a814aa7368e7ebe8fb045a3a150fb5fdc2ffe3b45"
|
||||
dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-std"
|
||||
version = "1.0.0-beta8"
|
||||
version = "1.0.0-beta7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "915ca82bd944f116f3a9717481f3fa657e4a73f28c4887288761ebb24e6fbe10"
|
||||
checksum = "bcb8f99a61d0b9069e1afc80a4ffea87dcc3523edd992080923870b13a677da0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cosmwasm-crypto",
|
||||
@@ -5966,9 +5966,9 @@ checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
|
||||
|
||||
[[package]]
|
||||
name = "uint"
|
||||
version = "0.9.3"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12f03af7ccf01dd611cc450a0d10dbc9b745770d096473e2faf0ca6e2d66d1e0"
|
||||
checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crunchy",
|
||||
|
||||
@@ -7,7 +7,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cosmwasm-std = "1.0.0-beta8"
|
||||
cosmwasm-std = "1.0.0-beta6"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_repr = "0.1"
|
||||
|
||||
@@ -26,7 +26,7 @@ pub use mixnode::{
|
||||
pub use msg::*;
|
||||
pub use types::*;
|
||||
|
||||
pub type U128 = fixed::types::U50F78;
|
||||
pub type U128 = fixed::types::U75F53;
|
||||
|
||||
fixed::const_fixed_from_int! {
|
||||
const ONE: U128 = 1;
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::reward_params::RewardParams;
|
||||
use crate::{Delegation, IdentityKey, SphinxKey};
|
||||
use crate::{ONE, U128};
|
||||
use az::CheckedCast;
|
||||
use cosmwasm_std::{coin, Addr, Coin, Decimal, Uint128};
|
||||
use cosmwasm_std::{coin, Addr, Coin, Uint128};
|
||||
use log::error;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -195,16 +195,22 @@ pub struct DelegatorRewardParams {
|
||||
|
||||
// to be completely honest I don't understand all consequences of using `#[schemars(with = "String")]`
|
||||
// for U128 here, but it seems that CosmWasm is using the same attribute for their Uint128
|
||||
sigma: Decimal,
|
||||
profit_margin: Decimal,
|
||||
node_profit: Uint128,
|
||||
#[schemars(with = "String")]
|
||||
#[serde(with = "fixed_U128_as_string")]
|
||||
sigma: U128,
|
||||
#[schemars(with = "String")]
|
||||
#[serde(with = "fixed_U128_as_string")]
|
||||
profit_margin: U128,
|
||||
#[schemars(with = "String")]
|
||||
#[serde(with = "fixed_U128_as_string")]
|
||||
node_profit: U128,
|
||||
}
|
||||
|
||||
impl DelegatorRewardParams {
|
||||
pub fn new(
|
||||
sigma: Decimal,
|
||||
profit_margin: Decimal,
|
||||
node_profit: Uint128,
|
||||
sigma: U128,
|
||||
profit_margin: U128,
|
||||
node_profit: U128,
|
||||
reward_params: RewardParams,
|
||||
) -> Self {
|
||||
DelegatorRewardParams {
|
||||
@@ -217,14 +223,23 @@ impl DelegatorRewardParams {
|
||||
|
||||
pub fn determine_delegation_reward(&self, delegation_amount: Uint128) -> u128 {
|
||||
// change all values into their fixed representations
|
||||
let delegation_amount = U128::from_num(delegation_amount.u128());
|
||||
let circulating_supply = U128::from_num(self.reward_params.circulating_supply());
|
||||
|
||||
let scaled_delegation_amount = delegation_amount / Uint128::new(self.reward_params.circulating_supply());
|
||||
let scaled_delegation_amount = delegation_amount / circulating_supply;
|
||||
let delegator_reward =
|
||||
(Decimal::one() - self.profit_margin) * (scaled_delegation_amount / self.sigma.atomics()) * self.node_profit;
|
||||
(ONE - self.profit_margin) * scaled_delegation_amount / self.sigma * self.node_profit;
|
||||
|
||||
let reward = delegator_reward.max(Uint128::zero());
|
||||
|
||||
reward.u128()
|
||||
let reward = delegator_reward.max(U128::ZERO);
|
||||
if let Some(int_reward) = reward.checked_cast() {
|
||||
int_reward
|
||||
} else {
|
||||
error!(
|
||||
"Could not cast delegator reward ({}) to u128, returning 0",
|
||||
reward,
|
||||
);
|
||||
0u128
|
||||
}
|
||||
}
|
||||
|
||||
pub fn node_reward_params(&self) -> RewardParams {
|
||||
@@ -235,8 +250,8 @@ impl DelegatorRewardParams {
|
||||
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
|
||||
pub struct StoredNodeRewardResult {
|
||||
reward: Uint128,
|
||||
lambda: Decimal,
|
||||
sigma: Decimal,
|
||||
lambda: Uint128,
|
||||
sigma: Uint128,
|
||||
}
|
||||
|
||||
impl StoredNodeRewardResult {
|
||||
@@ -244,11 +259,11 @@ impl StoredNodeRewardResult {
|
||||
self.reward
|
||||
}
|
||||
|
||||
pub fn lambda(&self) -> Decimal {
|
||||
pub fn lambda(&self) -> Uint128 {
|
||||
self.lambda
|
||||
}
|
||||
|
||||
pub fn sigma(&self) -> Decimal {
|
||||
pub fn sigma(&self) -> Uint128 {
|
||||
self.sigma
|
||||
}
|
||||
}
|
||||
@@ -258,30 +273,45 @@ impl TryFrom<NodeRewardResult> for StoredNodeRewardResult {
|
||||
|
||||
fn try_from(node_reward_result: NodeRewardResult) -> Result<Self, Self::Error> {
|
||||
Ok(StoredNodeRewardResult {
|
||||
reward: node_reward_result.reward(),
|
||||
lambda: node_reward_result.lambda(),
|
||||
sigma: node_reward_result.sigma(),
|
||||
reward: Uint128::new(
|
||||
node_reward_result
|
||||
.reward()
|
||||
.checked_cast()
|
||||
.ok_or(MixnetContractError::CastError)?,
|
||||
),
|
||||
lambda: Uint128::new(
|
||||
node_reward_result
|
||||
.lambda()
|
||||
.checked_cast()
|
||||
.ok_or(MixnetContractError::CastError)?,
|
||||
),
|
||||
sigma: Uint128::new(
|
||||
node_reward_result
|
||||
.sigma()
|
||||
.checked_cast()
|
||||
.ok_or(MixnetContractError::CastError)?,
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct NodeRewardResult {
|
||||
reward: Uint128,
|
||||
lambda: Decimal,
|
||||
sigma: Decimal,
|
||||
reward: U128,
|
||||
lambda: U128,
|
||||
sigma: U128,
|
||||
}
|
||||
|
||||
impl NodeRewardResult {
|
||||
pub fn reward(&self) -> Uint128 {
|
||||
pub fn reward(&self) -> U128 {
|
||||
self.reward
|
||||
}
|
||||
|
||||
pub fn lambda(&self) -> Decimal {
|
||||
pub fn lambda(&self) -> U128 {
|
||||
self.lambda
|
||||
}
|
||||
|
||||
pub fn sigma(&self) -> Decimal {
|
||||
pub fn sigma(&self) -> U128 {
|
||||
self.sigma
|
||||
}
|
||||
}
|
||||
@@ -327,10 +357,6 @@ impl MixNodeBond {
|
||||
U128::from_num(self.mix_node.profit_margin_percent) / U128::from_num(100)
|
||||
}
|
||||
|
||||
pub fn profit_margin_dec(&self) -> Decimal {
|
||||
Decimal::from_ratio(self.mix_node.profit_margin_percent, 100u128)
|
||||
}
|
||||
|
||||
pub fn identity(&self) -> &String {
|
||||
&self.mix_node.identity_key
|
||||
}
|
||||
@@ -364,61 +390,53 @@ impl MixNodeBond {
|
||||
self.total_delegation.clone()
|
||||
}
|
||||
|
||||
pub fn stake_saturation(&self, circulating_supply: u128, rewarded_set_size: u32) -> Decimal {
|
||||
pub fn stake_saturation(&self, circulating_supply: u128, rewarded_set_size: u32) -> U128 {
|
||||
self.total_bond_to_circulating_supply(circulating_supply)
|
||||
* Decimal::from_atomics(rewarded_set_size, 0).unwrap()
|
||||
* U128::from_num(rewarded_set_size)
|
||||
}
|
||||
|
||||
// TODO: There is an effect here when adding accumulted rewards to the total bond, ie accumulated rewards will not
|
||||
// affect lambda, but will affect sigma, in turn over time, if left unclaimed operator rewards will not compound, but
|
||||
// behave similarly to delegations.
|
||||
// The question is should this be taken into account when calculating operator rewards?
|
||||
pub fn pledge_to_circulating_supply(&self, circulating_supply: u128) -> Decimal {
|
||||
// U128::from_num(self.pledge_amount().amount.u128()) / U128::from_num(circulating_supply);
|
||||
|
||||
Decimal::from_atomics(self.pledge_amount().amount.u128(), 0).unwrap()
|
||||
/ Uint128::from(circulating_supply)
|
||||
pub fn pledge_to_circulating_supply(&self, circulating_supply: u128) -> U128 {
|
||||
U128::from_num(self.pledge_amount().amount.u128()) / U128::from_num(circulating_supply)
|
||||
}
|
||||
|
||||
pub fn total_bond_to_circulating_supply(&self, circulating_supply: u128) -> Decimal {
|
||||
// U128::from_num(self.pledge_amount().amount.u128() + self.total_delegation().amount.u128())
|
||||
// / U128::from_num(circulating_supply)
|
||||
|
||||
Decimal::from_atomics(
|
||||
self.total_bond().unwrap_or(0) + self.pledge_amount().amount.u128(),
|
||||
0,
|
||||
)
|
||||
.unwrap()
|
||||
/ Uint128::from(circulating_supply)
|
||||
pub fn total_bond_to_circulating_supply(&self, circulating_supply: u128) -> U128 {
|
||||
U128::from_num(self.pledge_amount().amount.u128() + self.total_delegation().amount.u128())
|
||||
/ U128::from_num(circulating_supply)
|
||||
}
|
||||
|
||||
pub fn lambda(&self, params: &RewardParams) -> Decimal {
|
||||
pub fn lambda(&self, params: &RewardParams) -> U128 {
|
||||
// Ratio of a bond to the token circulating supply
|
||||
let pledge_to_circulating_supply_ratio =
|
||||
self.pledge_to_circulating_supply(params.circulating_supply());
|
||||
pledge_to_circulating_supply_ratio.min(params.one_over_k_dec())
|
||||
pledge_to_circulating_supply_ratio.min(params.one_over_k())
|
||||
}
|
||||
|
||||
pub fn sigma(&self, params: &RewardParams) -> Decimal {
|
||||
pub fn sigma(&self, params: &RewardParams) -> U128 {
|
||||
// Ratio of a delegation to the the token circulating supply
|
||||
let total_bond_to_circulating_supply_ratio =
|
||||
self.total_bond_to_circulating_supply(params.circulating_supply());
|
||||
total_bond_to_circulating_supply_ratio.min(params.one_over_k_dec())
|
||||
total_bond_to_circulating_supply_ratio.min(params.one_over_k())
|
||||
}
|
||||
|
||||
pub fn estimate_reward(
|
||||
&self,
|
||||
params: &RewardParams,
|
||||
) -> Result<(u64, u64, u64), MixnetContractError> {
|
||||
let total_node_reward = self
|
||||
.reward(params)
|
||||
.reward();
|
||||
let total_node_reward = self.reward(params);
|
||||
let operator_reward = self.operator_reward(params);
|
||||
// Total reward has to be the sum of operator and delegator rewards
|
||||
let delegators_reward = total_node_reward.u128() - operator_reward;
|
||||
// TODO: This overestimates the reward by a lot, it should take a Uint128 and return estiamte for that
|
||||
let delegators_reward = self.reward_delegation(self.total_delegation().amount, params);
|
||||
|
||||
Ok((
|
||||
total_node_reward.u128().try_into()?,
|
||||
total_node_reward
|
||||
.reward()
|
||||
.checked_to_num::<u128>()
|
||||
.unwrap_or_default()
|
||||
.try_into()?,
|
||||
operator_reward.try_into()?,
|
||||
delegators_reward.try_into()?,
|
||||
))
|
||||
@@ -428,16 +446,11 @@ impl MixNodeBond {
|
||||
let lambda = self.lambda(params);
|
||||
let sigma = self.sigma(params);
|
||||
|
||||
println!("performance: {}", params.performance_dec().atomics());
|
||||
println!("reward pool: {}", Uint128::from(params.epoch_reward_pool()));
|
||||
println!("omega: {}", params.omega());
|
||||
|
||||
|
||||
let reward = params.performance_dec()
|
||||
* Uint128::from(params.epoch_reward_pool())
|
||||
let reward = params.performance()
|
||||
* params.epoch_reward_pool()
|
||||
* (sigma * params.omega()
|
||||
+ params.alpha_dec() * lambda * sigma * Uint128::from(params.rewarded_set_size()))
|
||||
/ (Decimal::one() + params.alpha_dec()).atomics();
|
||||
+ params.alpha() * lambda * sigma * params.rewarded_set_size())
|
||||
/ (ONE + params.alpha());
|
||||
|
||||
NodeRewardResult {
|
||||
reward,
|
||||
@@ -446,45 +459,53 @@ impl MixNodeBond {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn node_profit(&self, params: &RewardParams) -> Uint128 {
|
||||
if self.reward(params).reward() < params.node.operator_cost_dec() {
|
||||
Uint128::zero()
|
||||
pub fn node_profit(&self, params: &RewardParams) -> U128 {
|
||||
if self.reward(params).reward() < params.node.operator_cost() {
|
||||
U128::from_num(0u128)
|
||||
} else {
|
||||
self.reward(params).reward() - params.node.operator_cost_dec()
|
||||
self.reward(params).reward() - params.node.operator_cost()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn operator_reward(&self, params: &RewardParams) -> u128 {
|
||||
let reward = self.reward(params);
|
||||
println!("{:?}", reward);
|
||||
let profit = if reward.reward < params.node.operator_cost_dec() {
|
||||
Uint128::zero()
|
||||
let profit = if reward.reward < params.node.operator_cost() {
|
||||
U128::from_num(0u128)
|
||||
} else {
|
||||
reward.reward - params.node.operator_cost_dec()
|
||||
reward.reward - params.node.operator_cost()
|
||||
};
|
||||
let operator_base_reward = reward.reward.min(params.node.operator_cost_dec());
|
||||
let operator_reward = (self.profit_margin_dec()
|
||||
+ (Decimal::one() - self.profit_margin_dec()) * reward.lambda / reward.sigma.atomics())
|
||||
let operator_base_reward = reward.reward.min(params.node.operator_cost());
|
||||
let operator_reward = (self.profit_margin()
|
||||
+ (ONE - self.profit_margin()) * reward.lambda / reward.sigma)
|
||||
* profit;
|
||||
|
||||
let reward = (operator_reward + operator_base_reward).max(Uint128::new(0));
|
||||
let reward = (operator_reward + operator_base_reward).max(U128::from_num(0));
|
||||
|
||||
reward.u128()
|
||||
if let Some(int_reward) = reward.checked_cast() {
|
||||
int_reward
|
||||
} else {
|
||||
error!(
|
||||
"Could not cast reward ({}) to u128, returning 0 - mixnode {}",
|
||||
reward,
|
||||
self.identity()
|
||||
);
|
||||
0u128
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn sigma_ratio(&self, params: &RewardParams) -> U128 {
|
||||
// if self.total_bond_to_circulating_supply(params.circulating_supply()) < params.one_over_k()
|
||||
// {
|
||||
// self.total_bond_to_circulating_supply(params.circulating_supply())
|
||||
// } else {
|
||||
// params.one_over_k()
|
||||
// }
|
||||
// }
|
||||
pub fn sigma_ratio(&self, params: &RewardParams) -> U128 {
|
||||
if self.total_bond_to_circulating_supply(params.circulating_supply()) < params.one_over_k()
|
||||
{
|
||||
self.total_bond_to_circulating_supply(params.circulating_supply())
|
||||
} else {
|
||||
params.one_over_k()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reward_delegation(&self, delegation_amount: Uint128, params: &RewardParams) -> u128 {
|
||||
let reward_params = DelegatorRewardParams::new(
|
||||
self.sigma(params),
|
||||
self.profit_margin_dec(),
|
||||
self.profit_margin(),
|
||||
self.node_profit(params),
|
||||
params.to_owned(),
|
||||
);
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
use crate::{error::MixnetContractError, mixnode::StoredNodeRewardResult, ONE, U128};
|
||||
use az::CheckedCast;
|
||||
use cosmwasm_std::{Decimal, Uint128};
|
||||
use cosmwasm_std::Uint128;
|
||||
use network_defaults::DEFAULT_OPERATOR_INTERVAL_COST;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
fn sane_decimal(value: &Uint128) -> Decimal {
|
||||
Decimal::new(value * Uint128::new(1_000_000_000_000_000_000u128))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
|
||||
pub struct NodeEpochRewards {
|
||||
params: NodeRewardParams,
|
||||
@@ -29,11 +25,11 @@ impl NodeEpochRewards {
|
||||
self.epoch_id
|
||||
}
|
||||
|
||||
pub fn sigma(&self) -> Decimal {
|
||||
pub fn sigma(&self) -> Uint128 {
|
||||
self.result.sigma()
|
||||
}
|
||||
|
||||
pub fn lambda(&self) -> Decimal {
|
||||
pub fn lambda(&self) -> Uint128 {
|
||||
self.result.lambda()
|
||||
}
|
||||
|
||||
@@ -49,19 +45,6 @@ impl NodeEpochRewards {
|
||||
U128::from_num(self.params.uptime.u128() / 100u128 * DEFAULT_OPERATOR_INTERVAL_COST as u128)
|
||||
}
|
||||
|
||||
pub fn operator_cost_dec(&self) -> Uint128 {
|
||||
Decimal::from_ratio(self.params.uptime, 100u128)
|
||||
* Uint128::from(DEFAULT_OPERATOR_INTERVAL_COST)
|
||||
}
|
||||
|
||||
pub fn node_profit_dec(&self) -> Uint128 {
|
||||
if self.reward() < self.operator_cost_dec() {
|
||||
Uint128::zero()
|
||||
} else {
|
||||
self.reward() - self.operator_cost_dec()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn node_profit(&self) -> U128 {
|
||||
let reward = U128::from_num(self.reward().u128());
|
||||
if reward < self.operator_cost() {
|
||||
@@ -71,34 +54,44 @@ impl NodeEpochRewards {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn operator_reward(&self, profit_margin: Decimal) -> Result<Uint128, MixnetContractError> {
|
||||
let reward = self.node_profit_dec();
|
||||
let operator_base_reward = reward.min(self.operator_cost_dec());
|
||||
pub fn operator_reward(&self, profit_margin: U128) -> Result<Uint128, MixnetContractError> {
|
||||
let reward = self.node_profit();
|
||||
let operator_base_reward = reward.min(self.operator_cost());
|
||||
let operator_reward = (profit_margin
|
||||
+ (Decimal::one() - profit_margin) * self.lambda() / self.sigma().atomics())
|
||||
+ (ONE - profit_margin) * U128::from_num(self.lambda().u128())
|
||||
/ U128::from_num(self.sigma().u128()))
|
||||
* reward;
|
||||
|
||||
let reward = (operator_reward + operator_base_reward).max(Uint128::zero());
|
||||
let reward = (operator_reward + operator_base_reward).max(U128::from_num(0u128));
|
||||
|
||||
Ok(reward)
|
||||
if let Some(int_reward) = reward.checked_cast() {
|
||||
Ok(Uint128::new(int_reward))
|
||||
} else {
|
||||
Err(MixnetContractError::CastError)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delegation_reward(
|
||||
&self,
|
||||
delegation_amount: Uint128,
|
||||
profit_margin: Decimal,
|
||||
profit_margin: U128,
|
||||
epoch_reward_params: EpochRewardParams,
|
||||
) -> Result<Uint128, MixnetContractError> {
|
||||
// change all values into their fixed representations;
|
||||
// change all values into their fixed representations
|
||||
let delegation_amount = U128::from_num(delegation_amount.u128());
|
||||
let circulating_supply = U128::from_num(epoch_reward_params.circulating_supply());
|
||||
|
||||
let scaled_delegation_amount =
|
||||
delegation_amount / Uint128::from(epoch_reward_params.circulating_supply());
|
||||
let delegator_reward = (Decimal::one() - profit_margin) * scaled_delegation_amount
|
||||
/ self.sigma().atomics()
|
||||
* self.node_profit_dec();
|
||||
let scaled_delegation_amount = delegation_amount / circulating_supply;
|
||||
let delegator_reward = (ONE - profit_margin) * scaled_delegation_amount
|
||||
/ U128::from_num(self.sigma().u128())
|
||||
* self.node_profit();
|
||||
|
||||
let reward = delegator_reward.max(Uint128::zero());
|
||||
Ok(reward)
|
||||
let reward = delegator_reward.max(U128::ZERO);
|
||||
if let Some(int_reward) = reward.checked_cast() {
|
||||
Ok(Uint128::new(int_reward))
|
||||
} else {
|
||||
Err(MixnetContractError::CastError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,11 +176,6 @@ impl NodeRewardParams {
|
||||
U128::from_num(self.uptime.u128() / 100u128 * DEFAULT_OPERATOR_INTERVAL_COST as u128)
|
||||
}
|
||||
|
||||
pub fn operator_cost_dec(&self) -> Uint128 {
|
||||
Decimal::from_ratio(self.uptime.u128(), 100u128)
|
||||
* Uint128::new(DEFAULT_OPERATOR_INTERVAL_COST as u128)
|
||||
}
|
||||
|
||||
pub fn uptime(&self) -> u128 {
|
||||
self.uptime.u128()
|
||||
}
|
||||
@@ -208,36 +196,18 @@ impl RewardParams {
|
||||
RewardParams { epoch, node }
|
||||
}
|
||||
|
||||
pub fn omega(&self) -> Uint128 {
|
||||
pub fn omega(&self) -> U128 {
|
||||
// As per keybase://chat/nymtech#tokeneconomics/1179
|
||||
// let denom = self.active_set_work_factor() * U128::from_num(self.rewarded_set_size())
|
||||
// - (self.active_set_work_factor() - ONE) * U128::from_num(self.idle_nodes().u128());
|
||||
let denom = self.active_set_work_factor() * U128::from_num(self.rewarded_set_size())
|
||||
- (self.active_set_work_factor() - ONE) * U128::from_num(self.idle_nodes().u128());
|
||||
|
||||
let active_set_work_factor = self.active_set_work_factor_dec();
|
||||
let rewarded_set_size = sane_decimal(&self.epoch.rewarded_set_size);
|
||||
let idle_nodes = sane_decimal(&self.idle_nodes());
|
||||
|
||||
println!("active_set_work_factor: {}", active_set_work_factor);
|
||||
println!("rewarded_set_size: {}", rewarded_set_size);
|
||||
println!("idle_nodes: {}", idle_nodes);
|
||||
|
||||
let denom = active_set_work_factor * rewarded_set_size
|
||||
- (active_set_work_factor - Decimal::one()) * idle_nodes;
|
||||
|
||||
println!("denom: {}", denom);
|
||||
|
||||
let result = if self.in_active_set() {
|
||||
if self.in_active_set() {
|
||||
// work_active = factor / (factor * self.network.k[month] - (factor - 1) * idle_nodes)
|
||||
active_set_work_factor
|
||||
* Decimal::from_ratio(Decimal::one().atomics(), denom.atomics())
|
||||
* rewarded_set_size
|
||||
self.active_set_work_factor() / denom * self.rewarded_set_size()
|
||||
} else {
|
||||
// work_idle = 1 / (factor * self.network.k[month] - (factor - 1) * idle_nodes)
|
||||
Decimal::one() / denom.atomics() * rewarded_set_size
|
||||
};
|
||||
|
||||
println!("omega_result: {}", result);
|
||||
result.atomics()
|
||||
ONE / denom * self.rewarded_set_size()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn idle_nodes(&self) -> Uint128 {
|
||||
@@ -248,10 +218,6 @@ impl RewardParams {
|
||||
U128::from_num(self.epoch.active_set_work_factor)
|
||||
}
|
||||
|
||||
pub fn active_set_work_factor_dec(&self) -> Decimal {
|
||||
sane_decimal(&Uint128::new(self.epoch.active_set_work_factor as u128))
|
||||
}
|
||||
|
||||
pub fn in_active_set(&self) -> bool {
|
||||
self.node.in_active_set
|
||||
}
|
||||
@@ -260,10 +226,6 @@ impl RewardParams {
|
||||
U128::from_num(self.node.uptime.u128()) / U128::from_num(100)
|
||||
}
|
||||
|
||||
pub fn performance_dec(&self) -> Decimal {
|
||||
Decimal::from_ratio(self.node.uptime, 100u128)
|
||||
}
|
||||
|
||||
pub fn set_reward_blockstamp(&mut self, blockstamp: u64) {
|
||||
self.node.reward_blockstamp = blockstamp;
|
||||
}
|
||||
@@ -292,15 +254,7 @@ impl RewardParams {
|
||||
ONE / U128::from_num(self.epoch.rewarded_set_size.u128())
|
||||
}
|
||||
|
||||
pub fn one_over_k_dec(&self) -> Decimal {
|
||||
Decimal::one() / self.epoch.rewarded_set_size
|
||||
}
|
||||
|
||||
pub fn alpha(&self) -> U128 {
|
||||
U128::from_num(self.epoch.sybil_resistance_percent) / U128::from_num(100)
|
||||
}
|
||||
|
||||
pub fn alpha_dec(&self) -> Decimal {
|
||||
Decimal::from_atomics(self.epoch.sybil_resistance_percent, 2).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,5 +22,11 @@ pub(crate) fn validators() -> Vec<ValidatorDetails> {
|
||||
vec![ValidatorDetails::new(
|
||||
"https://rpc.nyx.nodes.guru/",
|
||||
Some("https://api.nyx.nodes.guru/"),
|
||||
), ValidatorDetails::new(
|
||||
"https://test2.nyx.nodes.guru/",
|
||||
Some("https://test1.nyx.nodes.guru/"),
|
||||
), ValidatorDetails::new(
|
||||
"https://test2.nyx.nodes.guru/",
|
||||
Some("https://test1.nyx.nodes.guru/"),
|
||||
)]
|
||||
}
|
||||
|
||||
Generated
+10
-10
@@ -241,9 +241,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-crypto"
|
||||
version = "1.0.0-beta8"
|
||||
version = "1.0.0-beta7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37e70111e9701c3ec43bfbff0e523cd4cb115876b4d3433813436dd0934ee962"
|
||||
checksum = "88c2565b1e73a816fb659ef4838fc356143fbd35f43c48a51d2d7d4e5d6679d3"
|
||||
dependencies = [
|
||||
"digest 0.9.0",
|
||||
"ed25519-zebra",
|
||||
@@ -254,9 +254,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-derive"
|
||||
version = "1.0.0-beta8"
|
||||
version = "1.0.0-beta7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58bc2ad5d86be5f6068833f63e20786768db6890019c095dd7775232184fb7b3"
|
||||
checksum = "fa89fcdf8dbbe0088e663d0a814aa7368e7ebe8fb045a3a150fb5fdc2ffe3b45"
|
||||
dependencies = [
|
||||
"syn",
|
||||
]
|
||||
@@ -273,9 +273,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-std"
|
||||
version = "1.0.0-beta8"
|
||||
version = "1.0.0-beta7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "915ca82bd944f116f3a9717481f3fa657e4a73f28c4887288761ebb24e6fbe10"
|
||||
checksum = "bcb8f99a61d0b9069e1afc80a4ffea87dcc3523edd992080923870b13a677da0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"cosmwasm-crypto",
|
||||
@@ -290,9 +290,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cosmwasm-storage"
|
||||
version = "1.0.0-beta8"
|
||||
version = "1.0.0-beta7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c4be9fd8c9d3ae7d0c32a925ecbc20707007ce0cba1f7538c0d78b7a2d3729b"
|
||||
checksum = "07f856099c824aa8f2488e62d1da3fc06383d3fdbc764573595f451be43441a2"
|
||||
dependencies = [
|
||||
"cosmwasm-std",
|
||||
"serde",
|
||||
@@ -1467,9 +1467,9 @@ checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
|
||||
|
||||
[[package]]
|
||||
name = "uint"
|
||||
version = "0.9.3"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12f03af7ccf01dd611cc450a0d10dbc9b745770d096473e2faf0ca6e2d66d1e0"
|
||||
checksum = "6470ab50f482bde894a037a57064480a246dbfdd5960bd65a44824693f08da5f"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crunchy",
|
||||
|
||||
@@ -20,8 +20,8 @@ mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-
|
||||
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" }
|
||||
config = { path = "../../common/config"}
|
||||
|
||||
cosmwasm-std = "1.0.0-beta8"
|
||||
cosmwasm-storage = "1.0.0-beta8"
|
||||
cosmwasm-std = "1.0.0-beta6"
|
||||
cosmwasm-storage = "1.0.0-beta6"
|
||||
cw-storage-plus = "0.13.1"
|
||||
|
||||
az = "1.2.0"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::{StdResult, Storage, Uint128, Decimal};
|
||||
use cosmwasm_std::{StdResult, Storage, Uint128};
|
||||
use cw_storage_plus::{Index, IndexList, IndexedSnapshotMap, Map, Strategy, UniqueIndex};
|
||||
use mixnet_contract_common::U128;
|
||||
use mixnet_contract_common::{
|
||||
@@ -135,10 +135,6 @@ impl StoredMixnodeBond {
|
||||
pub fn profit_margin(&self) -> U128 {
|
||||
U128::from_num(self.mix_node.profit_margin_percent) / U128::from_num(100)
|
||||
}
|
||||
|
||||
pub fn profit_margin_dec(&self) -> Decimal {
|
||||
Decimal::from_ratio(self.mix_node.profit_margin_percent, 100u128)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for StoredMixnodeBond {
|
||||
|
||||
@@ -117,7 +117,7 @@ pub fn calculate_operator_reward(
|
||||
// Compound rewards from previous heights
|
||||
let reward_at_height = epoch_rewards.delegation_reward(
|
||||
bond.pledge_amount().amount + accumulated_reward,
|
||||
bond.profit_margin_dec(),
|
||||
bond.profit_margin(),
|
||||
epoch_reward_params,
|
||||
)?;
|
||||
return Ok(accumulated_reward + reward_at_height);
|
||||
@@ -281,7 +281,7 @@ pub fn calculate_delegator_reward(
|
||||
epoch_reward_params_for_id(storage, epoch_rewards.epoch_id())?;
|
||||
let reward_at_height = epoch_rewards.delegation_reward(
|
||||
delegation_at_height + accumulated_reward,
|
||||
bond.profit_margin_dec(),
|
||||
bond.profit_margin(),
|
||||
epoch_reward_params,
|
||||
)?;
|
||||
return Ok(accumulated_reward + reward_at_height);
|
||||
@@ -440,9 +440,10 @@ pub mod tests {
|
||||
use crate::rewards::transactions::try_reward_mixnode;
|
||||
use crate::support::tests;
|
||||
use crate::support::tests::test_helpers;
|
||||
use az::CheckedCast;
|
||||
use config::defaults::DENOM;
|
||||
use cosmwasm_std::testing::{mock_env, mock_info};
|
||||
use cosmwasm_std::{coin, coins, Addr, Timestamp, Uint128, Decimal};
|
||||
use cosmwasm_std::{coin, coins, Addr, Timestamp, Uint128};
|
||||
use mixnet_contract_common::events::{
|
||||
must_find_attribute, BOND_TOO_FRESH_VALUE, NO_REWARD_REASON_KEY,
|
||||
OPERATOR_REWARDING_EVENT_TYPE,
|
||||
@@ -917,25 +918,25 @@ pub mod tests {
|
||||
|
||||
assert_eq!(
|
||||
mix_1_reward_result.sigma(),
|
||||
Decimal::zero()
|
||||
U128::from_num(0.0000266666666666f64)
|
||||
);
|
||||
assert_eq!(
|
||||
mix_1_reward_result.lambda(),
|
||||
Decimal::zero()
|
||||
U128::from_num(0.0000133333333333f64)
|
||||
);
|
||||
// assert_eq!(mix_1_reward_result.reward().int(), 259114u128);
|
||||
assert_eq!(mix_1_reward_result.reward().int(), 259114u128);
|
||||
|
||||
let mix_2_reward_result = mix_2.reward(¶ms2);
|
||||
|
||||
// assert_eq!(
|
||||
// mix_2_reward_result.sigma(),
|
||||
// U128::from_num(0.0000266666666666f64)
|
||||
// );
|
||||
// assert_eq!(
|
||||
// mix_2_reward_result.lambda(),
|
||||
// U128::from_num(0.0000133333333333f64)
|
||||
// );
|
||||
// assert_eq!(mix_2_reward_result.reward().int(), 129557u128);
|
||||
assert_eq!(
|
||||
mix_2_reward_result.sigma(),
|
||||
U128::from_num(0.0000266666666666f64)
|
||||
);
|
||||
assert_eq!(
|
||||
mix_2_reward_result.lambda(),
|
||||
U128::from_num(0.0000133333333333f64)
|
||||
);
|
||||
assert_eq!(mix_2_reward_result.reward().int(), 129557u128);
|
||||
|
||||
let mix_3_reward_result = mix_3.reward(¶ms3);
|
||||
|
||||
@@ -946,7 +947,8 @@ pub mod tests {
|
||||
fn test_tokenomics_rewarding() {
|
||||
use crate::constants::INTERVAL_REWARD_PERCENT;
|
||||
use crate::contract::INITIAL_REWARD_POOL;
|
||||
use mixnet_contract_common::U128;
|
||||
|
||||
type U128 = fixed::types::U75F53;
|
||||
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mut env = mock_env();
|
||||
@@ -1015,37 +1017,28 @@ pub mod tests {
|
||||
|
||||
assert_eq!(
|
||||
mix_1_reward_result.sigma(),
|
||||
Decimal::new(Uint128::new(40000000000000))
|
||||
U128::from_num(0.0000266666666666f64)
|
||||
);
|
||||
assert_eq!(
|
||||
mix_1_reward_result.lambda(),
|
||||
Decimal::new(Uint128::new(13333333333333))
|
||||
U128::from_num(0.0000133333333333f64)
|
||||
);
|
||||
// assert_eq!(mix_1_reward_result.reward().int(), 259114u128);
|
||||
|
||||
// assert_eq!(mix_1.node_profit(¶ms).int(), 203558u128);
|
||||
assert_eq!(mix_1_reward_result.reward().int(), 259114u128);
|
||||
|
||||
let mix1_operator_reward = mix_1.operator_reward(¶ms);
|
||||
|
||||
let mix1_delegator1_reward = mix_1.reward_delegation(Uint128::new(8000_000000), ¶ms);
|
||||
|
||||
let mix1_delegator2_reward = mix_1.reward_delegation(Uint128::new(2000_000000), ¶ms);
|
||||
let mix1_total_delegator_reward = mix_1.reward_delegation(Uint128::new(10000_000000), ¶ms);
|
||||
|
||||
assert_eq!(mix1_operator_reward, 167513);
|
||||
assert_eq!(mix1_delegator1_reward, 73280);
|
||||
assert_eq!(mix1_delegator2_reward, 18320);
|
||||
// Rounding errors make this not be equal
|
||||
assert!(mix1_total_delegator_reward > mix1_delegator1_reward + mix1_delegator2_reward);
|
||||
|
||||
// assert_eq!(
|
||||
// mix_1_reward_result.reward().int(),
|
||||
// mix1_operator_reward + mix1_delegator1_reward + mix1_delegator2_reward + 1
|
||||
// );
|
||||
|
||||
// assert_eq!(
|
||||
// mix1_operator_reward + mix1_delegator1_reward + mix1_delegator2_reward + 1,
|
||||
// mix_1_reward_result.reward().int()
|
||||
// );
|
||||
assert_eq!(
|
||||
mix1_operator_reward + mix1_delegator1_reward + mix1_delegator2_reward + 1,
|
||||
mix_1_reward_result.reward().int()
|
||||
);
|
||||
|
||||
let pre_reward_bond =
|
||||
test_helpers::read_mixnode_pledge_amount(&deps.storage, &node_identity)
|
||||
@@ -1099,18 +1092,18 @@ pub mod tests {
|
||||
);
|
||||
|
||||
// it's all correctly saved
|
||||
// match storage::REWARDING_STATUS
|
||||
// .load(deps.as_ref().storage, (0u32, node_identity))
|
||||
// .unwrap()
|
||||
// {
|
||||
// RewardingStatus::Complete(result) => assert_eq!(
|
||||
// RewardingResult {
|
||||
// node_reward: Uint128::new(mix_1_reward_result.reward().checked_cast().unwrap()),
|
||||
// },
|
||||
// result
|
||||
// ),
|
||||
// _ => unreachable!(),
|
||||
// }
|
||||
match storage::REWARDING_STATUS
|
||||
.load(deps.as_ref().storage, (0u32, node_identity))
|
||||
.unwrap()
|
||||
{
|
||||
RewardingStatus::Complete(result) => assert_eq!(
|
||||
RewardingResult {
|
||||
node_reward: Uint128::new(mix_1_reward_result.reward().checked_cast().unwrap()),
|
||||
},
|
||||
result
|
||||
),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+12
-12
@@ -358,9 +358,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-align/node_modules/ansi-regex": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
|
||||
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -403,9 +403,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -2309,9 +2309,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
|
||||
"integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "7.0.3",
|
||||
@@ -2344,9 +2344,9 @@
|
||||
}
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
|
||||
@@ -4,7 +4,17 @@
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
invoke: (operation) => {
|
||||
invoke: (operation, ...args) => {
|
||||
switch(operation) {
|
||||
case 'get_validator_nymd_urls': {
|
||||
const { network } = args;
|
||||
return new Promise(resolve => {
|
||||
resolve({
|
||||
urls: ['foo', 'bar'],
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
console.error(`Tauri cannot be used in Storybook. The operation requested was "${operation}". You can add mock responses to "nym_wallet/.storybook/mocks/tauri.js" if you need. The default response is "void".`);
|
||||
return new Promise((resolve, reject) => {
|
||||
reject(new Error(`Tauri operation ${operation} not available in storybook.`));
|
||||
|
||||
@@ -4,9 +4,11 @@ import { Logout } from '@mui/icons-material';
|
||||
import { ClientContext } from '../context/main';
|
||||
import { NetworkSelector } from './NetworkSelector';
|
||||
import { Node as NodeIcon } from '../svg-icons/node';
|
||||
import { Delegate as DelegateIcon } from '../svg-icons';
|
||||
|
||||
|
||||
export const AppBar = () => {
|
||||
const { showSettings, logOut, handleShowSettings } = useContext(ClientContext);
|
||||
const { showSettings, showValidatorSettings, logOut, handleShowSettings, handleShowValidatorSettings } = useContext(ClientContext);
|
||||
|
||||
return (
|
||||
<MuiAppBar position="sticky" sx={{ boxShadow: 'none', bgcolor: 'transparent' }}>
|
||||
@@ -16,6 +18,15 @@ export const AppBar = () => {
|
||||
<NetworkSelector />
|
||||
</Grid>
|
||||
<Grid item container justifyContent="flex-end" md={12} lg={5} spacing={2}>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
onClick={handleShowValidatorSettings}
|
||||
sx={{ color: showValidatorSettings ? 'primary.main' : 'nym.background.dark' }}
|
||||
size="small"
|
||||
>
|
||||
<DelegateIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<IconButton
|
||||
onClick={handleShowSettings}
|
||||
|
||||
@@ -2,10 +2,10 @@ import React, { useMemo, createContext, useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { TLoginType } from 'src/pages/sign-in/types';
|
||||
import { Account, Network, TCurrency, TMixnodeBondDetails } from '../types';
|
||||
import { Account, Network, TCurrency, TMixnodeBondDetails, ValidatorUrls } from '../types';
|
||||
import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance';
|
||||
import { config } from '../../config';
|
||||
import { getMixnodeBondDetails, selectNetwork, signInWithMnemonic, signInWithPassword, signOut } from '../requests';
|
||||
import { getMixnodeBondDetails, selectNetwork, signInWithMnemonic, signInWithPassword, signOut, getValidatorUrls, selectValidatorNymdUrl } from '../requests';
|
||||
import { currencyMap } from '../utils';
|
||||
import { Console } from '../utils/console';
|
||||
|
||||
@@ -14,13 +14,13 @@ export const { ADMIN_ADDRESS, IS_DEV_MODE } = config;
|
||||
export const urls = (networkName?: Network) =>
|
||||
networkName === 'MAINNET'
|
||||
? {
|
||||
blockExplorer: 'https://blocks.nymtech.net',
|
||||
networkExplorer: 'https://explorer.nymtech.net',
|
||||
}
|
||||
blockExplorer: 'https://blocks.nymtech.net',
|
||||
networkExplorer: 'https://explorer.nymtech.net',
|
||||
}
|
||||
: {
|
||||
blockExplorer: `https://${networkName}-blocks.nymtech.net`,
|
||||
networkExplorer: `https://${networkName}-explorer.nymtech.net`,
|
||||
};
|
||||
blockExplorer: `https://${networkName}-blocks.nymtech.net`,
|
||||
networkExplorer: `https://${networkName}-explorer.nymtech.net`,
|
||||
};
|
||||
|
||||
type TClientContext = {
|
||||
mode: 'light' | 'dark';
|
||||
@@ -29,6 +29,8 @@ type TClientContext = {
|
||||
userBalance: TUseuserBalance;
|
||||
showAdmin: boolean;
|
||||
showSettings: boolean;
|
||||
showValidatorSettings: boolean;
|
||||
validatorsUrl?: ValidatorUrls;
|
||||
network?: Network;
|
||||
currency?: TCurrency;
|
||||
isLoading: boolean;
|
||||
@@ -37,7 +39,10 @@ type TClientContext = {
|
||||
setError: (value?: string) => void;
|
||||
switchNetwork: (network: Network) => void;
|
||||
getBondDetails: () => Promise<void>;
|
||||
fetchValidatorsUrl: (network: Network) => Promise<void>;
|
||||
selectValidatorNymd: (validatorNymd: string, network: Network) => Promise<void>;
|
||||
handleShowSettings: () => void;
|
||||
handleShowValidatorSettings: () => void;
|
||||
handleShowAdmin: () => void;
|
||||
logIn: (opts: { type: 'mnemonic' | 'password'; value: string }) => void;
|
||||
signInWithPassword: (password: string) => void;
|
||||
@@ -49,10 +54,12 @@ export const ClientContext = createContext({} as TClientContext);
|
||||
export const ClientContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [clientDetails, setClientDetails] = useState<Account>();
|
||||
const [mixnodeDetails, setMixnodeDetails] = useState<TMixnodeBondDetails | null>();
|
||||
const [validatorsUrl, setValidatorsUrl] = useState<ValidatorUrls>();
|
||||
const [network, setNetwork] = useState<Network | undefined>();
|
||||
const [currency, setCurrency] = useState<TCurrency>();
|
||||
const [showAdmin, setShowAdmin] = useState(false);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
const [showValidatorSettings, setShowValidatorSettings] = useState(false);
|
||||
const [mode] = useState<'light' | 'dark'>('light');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string>();
|
||||
@@ -83,17 +90,49 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
}
|
||||
};
|
||||
|
||||
const fetchValidatorsUrl = async (network: Network) => {
|
||||
try {
|
||||
if (network) {
|
||||
const urls = await getValidatorUrls(network);
|
||||
setValidatorsUrl(urls);
|
||||
}
|
||||
} catch (e) {
|
||||
Console.error(e as string);
|
||||
}
|
||||
};
|
||||
|
||||
const selectValidatorNymd = async (validatorNymd: string, network: Network) => {
|
||||
try {
|
||||
if (network) {
|
||||
const response = await selectValidatorNymdUrl(validatorNymd, network);
|
||||
return response;
|
||||
}
|
||||
} catch (e) {
|
||||
Console.error(e as string);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const refreshAccount = async () => {
|
||||
if (network) {
|
||||
await loadAccount(network);
|
||||
await getBondDetails();
|
||||
await userBalance.fetchBalance();
|
||||
await fetchValidatorsUrl(network);
|
||||
}
|
||||
};
|
||||
refreshAccount();
|
||||
}, [network]);
|
||||
|
||||
useEffect(() => {
|
||||
const refreshValidators = async () => {
|
||||
if (!validatorsUrl && network) {
|
||||
await fetchValidatorsUrl(network);
|
||||
}
|
||||
};
|
||||
refreshValidators();
|
||||
}, [showValidatorSettings]);
|
||||
|
||||
const logIn = async ({ type, value }: { type: TLoginType; value: string }) => {
|
||||
if (value.length === 0) {
|
||||
setError(`A ${type} must be provided`);
|
||||
@@ -127,7 +166,14 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
};
|
||||
|
||||
const handleShowAdmin = () => setShowAdmin((show) => !show);
|
||||
const handleShowSettings = () => setShowSettings((show) => !show);
|
||||
const handleShowSettings = () => {
|
||||
setShowSettings((show) => !show)
|
||||
setShowValidatorSettings(false);
|
||||
};
|
||||
const handleShowValidatorSettings = () => {
|
||||
setShowValidatorSettings((show) => !show)
|
||||
setShowSettings(false);
|
||||
};
|
||||
const switchNetwork = (_network: Network) => setNetwork(_network);
|
||||
|
||||
const memoizedValue = useMemo(
|
||||
@@ -140,6 +186,8 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
userBalance,
|
||||
showAdmin,
|
||||
showSettings,
|
||||
showValidatorSettings,
|
||||
validatorsUrl,
|
||||
network,
|
||||
currency,
|
||||
setIsLoading,
|
||||
@@ -147,12 +195,15 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode
|
||||
signInWithPassword,
|
||||
switchNetwork,
|
||||
getBondDetails,
|
||||
fetchValidatorsUrl,
|
||||
selectValidatorNymd,
|
||||
handleShowSettings,
|
||||
handleShowValidatorSettings,
|
||||
handleShowAdmin,
|
||||
logIn,
|
||||
logOut,
|
||||
}),
|
||||
[mode, isLoading, error, clientDetails, mixnodeDetails, userBalance, showAdmin, showSettings, network, currency],
|
||||
[mode, isLoading, error, clientDetails, mixnodeDetails, userBalance, showAdmin, showSettings, showValidatorSettings, validatorsUrl, selectValidatorNymd, network, currency],
|
||||
);
|
||||
|
||||
return <ClientContext.Provider value={memoizedValue}>{children}</ClientContext.Provider>;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SnackbarProvider } from 'notistack';
|
||||
import { AppRoutes, SignInRoutes } from './routes';
|
||||
import { ClientContext, ClientContextProvider } from './context/main';
|
||||
import { ApplicationLayout } from './layouts';
|
||||
import { Admin, Settings } from './pages';
|
||||
import { Admin, Settings, ValidatorSettingsModal } from './pages';
|
||||
import { ErrorFallback } from './components';
|
||||
import { NymWalletTheme, WelcomeTheme } from './theme';
|
||||
import { maximizeWindow } from './utils';
|
||||
@@ -29,6 +29,7 @@ const App = () => {
|
||||
<NymWalletTheme>
|
||||
<ApplicationLayout>
|
||||
<Settings />
|
||||
<ValidatorSettingsModal />
|
||||
<Admin />
|
||||
<AppRoutes />
|
||||
</ApplicationLayout>
|
||||
|
||||
@@ -9,3 +9,4 @@ export * from './sign-in';
|
||||
export * from './settings';
|
||||
export * from './unbond';
|
||||
export * from './undelegate';
|
||||
export * from './validators';
|
||||
@@ -0,0 +1 @@
|
||||
export { ValidatorSettingsModal } from './validatorModal';
|
||||
@@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
|
||||
import { Button, Paper } from '@mui/material';
|
||||
import { ValidatorSettingsModal } from './validatorModal';
|
||||
|
||||
export default {
|
||||
title: 'Modals/Validator Setting Modal',
|
||||
component: ValidatorSettingsModal,
|
||||
} as ComponentMeta<typeof ValidatorSettingsModal>;
|
||||
|
||||
export const Default = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(true);
|
||||
return (
|
||||
<>
|
||||
<Paper elevation={0} sx={{ px: 4, pt: 2, pb: 4 }}>
|
||||
<h2>Lorem ipsum</h2>
|
||||
<Button variant="contained" onClick={() => setOpen(true)}>
|
||||
Show modal
|
||||
</Button>
|
||||
<p>
|
||||
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis
|
||||
sunt velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur
|
||||
culpa exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa.
|
||||
</p>
|
||||
<p>
|
||||
Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eiusmod occaecat aliquip
|
||||
ad tempor. Labore amet laborum magna amet consequat dolor cupidatat in consequat sunt aliquip magna laboris
|
||||
tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna
|
||||
elit ut mollit.
|
||||
</p>
|
||||
<p>
|
||||
Labore voluptate elit amet ipsum qui officia duis in et occaecat culpa ex do non labore mollit. Cillum
|
||||
cupidatat duis ea dolore laboris laboris sunt duis anim consectetur cupidatat nulla ad minim sunt ea. Aliqua
|
||||
amet commodo est irure sint magna sunt. Pariatur dolore commodo labore quis incididunt proident duis voluptate
|
||||
exercitation in duis. Occaecat aliqua laboris reprehenderit nostrud est aute pariatur fugiat anim. Dolore sunt
|
||||
cillum ea aliquip consectetur laborum ipsum qui veniam Lorem consectetur adipisicing velit magna aute. Amet
|
||||
tempor quis excepteur minim culpa velit Lorem enim ad.
|
||||
</p>
|
||||
<p>
|
||||
Mollit laborum exercitation excepteur laboris adipisicing ipsum veniam cillum mollit voluptate do. Amet et
|
||||
anim Lorem mollit minim duis cupidatat non. Consectetur sit deserunt nisi nisi non excepteur dolor eiusmod
|
||||
aute aute irure anim dolore ipsum et veniam.
|
||||
</p>
|
||||
</Paper>
|
||||
<ValidatorSettingsModal/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const NoSubheader = () => {
|
||||
const [open, setOpen] = React.useState<boolean>(true);
|
||||
return (
|
||||
<>
|
||||
<Paper elevation={0} sx={{ px: 4, pt: 2, pb: 4 }}>
|
||||
<h2>Lorem ipsum</h2>
|
||||
<Button variant="contained" onClick={() => setOpen(true)}>
|
||||
Show modal
|
||||
</Button>
|
||||
<p>
|
||||
Veniam dolor laborum labore sit reprehenderit enim mollit magna nulla adipisicing fugiat. Est ex irure quis
|
||||
sunt velit elit do minim mollit non duis reprehenderit. Eiusmod dolore adipisicing ex nostrud consectetur
|
||||
culpa exercitation do. Ad elit esse ipsum aliqua labore irure laborum qui culpa.
|
||||
</p>
|
||||
<p>
|
||||
Occaecat commodo excepteur anim ut officia dolor laboris dolore id occaecat enim qui eiusmod occaecat aliquip
|
||||
ad tempor. Labore amet laborum magna amet consequat dolor cupidatat in consequat sunt aliquip magna laboris
|
||||
tempor culpa est magna. Sit tempor cillum culpa sint ipsum nostrud ullamco voluptate exercitation dolore magna
|
||||
elit ut mollit.
|
||||
</p>
|
||||
<p>
|
||||
Labore voluptate elit amet ipsum qui officia duis in et occaecat culpa ex do non labore mollit. Cillum
|
||||
cupidatat duis ea dolore laboris laboris sunt duis anim consectetur cupidatat nulla ad minim sunt ea. Aliqua
|
||||
amet commodo est irure sint magna sunt. Pariatur dolore commodo labore quis incididunt proident duis voluptate
|
||||
exercitation in duis. Occaecat aliqua laboris reprehenderit nostrud est aute pariatur fugiat anim. Dolore sunt
|
||||
cillum ea aliquip consectetur laborum ipsum qui veniam Lorem consectetur adipisicing velit magna aute. Amet
|
||||
tempor quis excepteur minim culpa velit Lorem enim ad.
|
||||
</p>
|
||||
<p>
|
||||
Mollit laborum exercitation excepteur laboris adipisicing ipsum veniam cillum mollit voluptate do. Amet et
|
||||
anim Lorem mollit minim duis cupidatat non. Consectetur sit deserunt nisi nisi non excepteur dolor eiusmod
|
||||
aute aute irure anim dolore ipsum et veniam.
|
||||
</p>
|
||||
</Paper>
|
||||
<ValidatorSettingsModal />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,142 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Button, Box, Dialog, CircularProgress, Typography } from '@mui/material';
|
||||
import { NymCard } from '../../components';
|
||||
import { ClientContext } from '../../context/main';
|
||||
import { ValidatorSelector } from './validatorSelector';
|
||||
import { Delegate as DelegateIcon } from '../../svg-icons';
|
||||
import { Console } from '../../utils/console';
|
||||
|
||||
export const ValidatorSettingsModal = () => {
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [validatorSelectedSuccessfully, setValidatorSelectedSuccessfully] = useState(false);
|
||||
const [validator, setValidator] = useState('');
|
||||
|
||||
const { showValidatorSettings, getBondDetails, handleShowValidatorSettings, network, selectValidatorNymd } = useContext(ClientContext);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (showValidatorSettings) {
|
||||
getBondDetails();
|
||||
} else {
|
||||
setValidatorSelectedSuccessfully(false);
|
||||
};
|
||||
}, [showValidatorSettings]);
|
||||
|
||||
const onDataChanged = (selectedValidator: string) => {
|
||||
if (selectedValidator) {
|
||||
setValidator(selectedValidator);
|
||||
};
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
if (network) {
|
||||
selectValidatorNymd(validator, network).then(res => console.log('res', res));
|
||||
setValidatorSelectedSuccessfully(true);
|
||||
}
|
||||
} catch (e) {
|
||||
Console.error(e as string);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return showValidatorSettings ? (
|
||||
<Dialog open onClose={handleShowValidatorSettings} maxWidth="md" fullWidth>
|
||||
<NymCard
|
||||
title={
|
||||
<Box display="flex" alignItems="center">
|
||||
<DelegateIcon sx={{ mr: 1 }} />
|
||||
Settings
|
||||
</Box>
|
||||
}
|
||||
noPadding
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: 3,
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'grey.300',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
Wallet Settings
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
p: 2,
|
||||
pl: 3,
|
||||
borderTop: '1px solid',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'grey.300',
|
||||
bgcolor: 'grey.200',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
Validators
|
||||
</Typography>
|
||||
</Box>
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'start',
|
||||
minHeight: 300,
|
||||
padding: 3,
|
||||
}}
|
||||
>
|
||||
<ValidatorSelector
|
||||
type="Validator API Url"
|
||||
onChangeValidatorSelection={(selectedValidator) => onDataChanged(selectedValidator)}
|
||||
/>
|
||||
|
||||
{validatorSelectedSuccessfully && (
|
||||
<Typography sx={{ pt: 2, fontSize: 12, color: (theme) => theme.palette.success.light }}>
|
||||
Successfully selected the validator: {validator}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
padding: 3,
|
||||
bgcolor: 'grey.200',
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'grey.300',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
size="large"
|
||||
variant="contained"
|
||||
data-testid="validatorsSettings-button"
|
||||
color="primary"
|
||||
disableElevation
|
||||
onClick={() => handleSubmit()}
|
||||
disabled={isSubmitting}
|
||||
endIcon={isSubmitting && <CircularProgress size={20} />}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</Box>
|
||||
</NymCard>
|
||||
</Dialog>
|
||||
) : null;
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { FormControl, InputLabel, ListItemText, MenuItem, Select, SelectChangeEvent, Typography, useMediaQuery } from '@mui/material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { ClientContext } from '../../context/main';
|
||||
|
||||
type TValidatorUrl = string;
|
||||
|
||||
export const ValidatorSelector: React.FC<{ onChangeValidatorSelection: (validator: TValidatorUrl) => void, type: string }> = ({
|
||||
onChangeValidatorSelection,
|
||||
}) => {
|
||||
const [selectedValidator, setSelectedValidator] = useState<TValidatorUrl>('');
|
||||
|
||||
const {
|
||||
validatorsUrl
|
||||
} = useContext(ClientContext);
|
||||
const theme = useTheme();
|
||||
const matches = useMediaQuery(theme.breakpoints.down('sm'));
|
||||
|
||||
useEffect(() => {
|
||||
onChangeValidatorSelection(selectedValidator);
|
||||
}, [selectedValidator]);
|
||||
|
||||
return (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="validatorSelect_label">Validator API Url</InputLabel>
|
||||
<Select
|
||||
labelId="validatorSelect_label"
|
||||
id="validatorSelect"
|
||||
sx={{
|
||||
width: '100%'
|
||||
}}
|
||||
value={selectedValidator || ''}
|
||||
label="Choose a Validator"
|
||||
onChange={(e: SelectChangeEvent) => {
|
||||
setSelectedValidator(e.target.value as TValidatorUrl);
|
||||
}}
|
||||
renderValue={(value) => <Typography sx={{ textTransform: 'capitalize' }}>{value}</Typography>}
|
||||
>
|
||||
{
|
||||
validatorsUrl && validatorsUrl.urls.map((validator) => (
|
||||
<MenuItem value={validator} key={validator}>
|
||||
<ListItemText>{validator}</ListItemText>
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)
|
||||
};
|
||||
@@ -5,3 +5,4 @@ export * from './contract';
|
||||
export * from './vesting';
|
||||
export * from './network';
|
||||
export * from './queries';
|
||||
export * from './validators';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { Network } from '../types';
|
||||
|
||||
import {
|
||||
ValidatorUrls
|
||||
} from '../types';
|
||||
|
||||
export const getValidatorUrls = async (network: Network): Promise<ValidatorUrls> => {
|
||||
const res: ValidatorUrls = await invoke('get_validator_nymd_urls', { network });
|
||||
return res;
|
||||
};
|
||||
|
||||
export const selectValidatorNymdUrl = async (validator: string, network: Network): Promise<void> => {
|
||||
const res: void = await invoke('select_validator_nymd_url', { url: validator, network });
|
||||
return res;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta } from '@storybook/react';
|
||||
import { Playground } from '@nymproject/react';
|
||||
|
||||
export default {
|
||||
title: 'Playground',
|
||||
component: Playground,
|
||||
} as ComponentMeta<typeof Playground>;
|
||||
|
||||
export const AllControls = () => <Playground />;
|
||||
@@ -23,3 +23,4 @@ export * from './vestingperiod';
|
||||
export * from './pendingundelegate';
|
||||
export * from './delegationevent';
|
||||
export * from './epoch';
|
||||
export * from './validatorurls';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import bs58 from 'bs58';
|
||||
import { valid } from 'semver';
|
||||
import { userBalance, majorToMinor, getLockedCoins, getSpendableCoins } from '../requests';
|
||||
import { userBalance, majorToMinor, getLockedCoins, getSpendableCoins, getValidatorUrls } from '../requests';
|
||||
import { Coin, Network, TCurrency } from '../types';
|
||||
import { Console } from './console';
|
||||
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './components';
|
||||
export * from './hooks';
|
||||
export { Playground } from './playground';
|
||||
export { Playground } from './playground/Playground';
|
||||
|
||||
+1
-1
@@ -22,4 +22,4 @@ export const Playground: React.FC = () => (
|
||||
<h2>Fonts</h2>
|
||||
<PlaygroundFonts />
|
||||
</>
|
||||
);
|
||||
);
|
||||
Reference in New Issue
Block a user