Compare commits

..

21 Commits

Author SHA1 Message Date
tommy 0881d4cbc3 Revert "Validator Command Binary - Draft"
This reverts commit 3429e0a8e9.
2022-08-05 12:19:47 +02:00
tommy 3429e0a8e9 Validator Command Binary - Draft
- validator-action rust cli tool
- enables ease of use functionality to do commands to respective environments
2022-08-04 12:57:56 +02:00
Bogdan-Ștefan Neacşu 9c19ae322d Bump regex version to fix dependabot (#1488) 2022-08-03 12:45:21 +03:00
Bogdan-Ștefan Neacşu 07893828d8 Fix NC filter for domains suffix-only domains (#1487)
* Fix NC filter for domains suffix-only domains

* Update CHANGELOG

* Fix unit test for filter

Some domains might be composed of the suffix only.

There are no nonsense domains, as they can be defined even on the local
machine. The underlying library doesn't resolve them, but rather uses a
fixed list of public suffixes to assess the domains.

* Fix clippy
2022-08-03 11:58:20 +03:00
Pierre Dommerc 1167f50543 [Wallet] move Receive page in modal (#1484)
* feat(wallet): move receive page in modal

* feat(wallet-receive): some ui work

* feat(wallet): simple modal component

show or not the Ok button based on onOk props

* feat(wallet): fix sx props type imports
2022-08-02 12:04:47 +02:00
Drazen Urch ba1818a903 Chitchat example (#1464)
* Chitchat example

* Cleanup
2022-08-01 14:19:04 +02:00
Bogdan-Ștefan Neacşu e631219a73 explorer-api: handle SIGTERM (#1482)
* explorer-api: handle SIGTERM

* Update CHANGELOG
2022-08-01 13:30:31 +03:00
rachyandco 207c6cf2c7 Correcting a typo (#1475)
Co-authored-by: Rachyandco <alexis@nymtech.net>
2022-07-29 09:13:07 +01:00
Drazen Urch c5ece97872 Stake inflation mitigations (#1480)
* Return Err from compound transactions

* Remove malicious nodes migration

* Reduce total delegation, before adding to it

* Blacklist malicious nodes, prevent future bonding

* Blacklisted gets no reward, enable compound

* Add GetBlacklistedNodes message

* Rebase on develop

* Remove TODO
2022-07-28 15:19:31 +02:00
Bogdan-Ștefan Neacșu 8a2c95d044 Mixnode option doc fix 2022-07-26 15:00:27 +03:00
Bogdan-Ștefan Neacşu ba5e3d4efa Remove service entries instead of zeroing them (#1479) 2022-07-26 11:53:10 +03:00
Bogdan-Ștefan Neacşu c81623a61a Add gateway id in the gateway stats (#1478)
* Add gateway id in the gateway stats

* Update CHANGELOG
2022-07-25 16:59:45 +03:00
Bogdan-Ștefan Neacşu 8bb42c2b1b Add migration code for mixnet and vesting contracts (#1477) 2022-07-25 14:05:22 +03:00
Mark Sinclair 33e161bd59 GitHub Actions: make explorer deployment only a manual step 2022-07-22 12:19:58 +01:00
Mark Sinclair 0233499036 GitHub Actions: add prod deploy step for Network Explorer UI 2022-07-22 10:34:20 +01:00
Mark Sinclair a059a29173 nym-connect: update CHANGELOG and bump version to 1.0.1 2022-07-22 10:15:18 +01:00
Mark Sinclair 83c3398570 nym-connect: copy changes 2022-07-22 10:09:13 +01:00
Fouad 93f931459a fix type update issues with actions (bonding, delegating etc.) (#1469) 2022-07-22 09:40:06 +01:00
Bogdan-Ștefan Neacşu 5a7b19aeb6 Nym connect use config from env (mainnet defaulted (#1471) 2022-07-21 17:03:14 +03:00
Bogdan-Ștefan Neacşu b901655591 Fix message deserialization in socks5 client (#1470) 2022-07-21 15:36:38 +03:00
Drazen Urch a9fdbccb82 Universal compound rewards message that anyone can call (#1387) 2022-07-21 10:51:33 +02:00
110 changed files with 2957 additions and 16432 deletions
+12
View File
@@ -1,6 +1,7 @@
name: CI for Network Explorer
on:
workflow_dispatch:
push:
paths:
- 'explorer/**'
@@ -75,3 +76,14 @@ jobs:
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
- name: Deploy
if: github.event_name == 'workflow_dispatch'
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CD_PROD_NE_SSH_PRIVATE_KEY }}
ARGS: "-rltgoDzvO --delete"
SOURCE: "explorer/dist/"
REMOTE_HOST: ${{ secrets.CD_PROD_NE_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CD_PROD_NE_REMOTE_USER }}
TARGET: ${{ secrets.CD_PROD_NE_REMOTE_TARGET }}
EXCLUDE: "/dist/, /node_modules/"
+21 -6
View File
@@ -8,10 +8,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
### Added
- socks5 client/websocket client: add `--force-register-gateway` flag, useful when rerunning init ([#1353])
- nym-connect: initial proof-of-concept of a UI around the socks5 client was added
- nym-connect: add ability to select network requester and gateway ([#1427])
- nym-connect: add ability to export gateway keys as JSON
- nym-connect: add auto updater
- all: added network compilation target to `--help` (or `--version`) commands ([#1256]).
- explorer-api: learned how to sum the delegations by owner in a new endpoint.
- explorer-api: add apy values to `mix_nodes` endpoint
@@ -39,10 +35,11 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- native & socks5 clients: rerun init will now reuse previous gateway configuration instead of failing ([#1353])
- native & socks5 clients: deduplicate big chunks of init logic
- validator: fixed local docker-compose setup to work on Apple M1 ([#1329])
- explorer-api: listen out for SIGTERM and SIGQUIT too, making it play nicely as a system service ([#1482]).
- network-requester: fix filter for suffix-only domains ([#1487])
### Changed
- nym-connect: reuse config id instead of creating a new id on each connection
- validator-client: created internal `Coin` type that replaces coins from `cosmrs` and `cosmwasm` for API entrypoints [[#1295]]
- all: updated all `cosmwasm`-related dependencies to `1.0.0` and `cw-storage-plus` to `0.13.4` [[#1318]]
- all: updated `rocket` to `0.5.0-rc.2`.
@@ -53,6 +50,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- validator-api: fee payment for multisig operations comes from the gateway account instead of the validator APIs' accounts ([#1419])
- multisig-contract: Limit the proposal creating functionality to one address (coconut-bandwidth-contract address) ([#1457])
- All binaries and cosmwasm blobs are configured at runtime now; binaries are configured using environment variables or .env files and contracts keep the configuration parameters in storage ([#1463])
- gateway, network-statistics: include gateway id in the sent statistical data ([#1478])
[#1249]: https://github.com/nymtech/nym/pull/1249
@@ -74,9 +72,26 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
[#1393]: https://github.com/nymtech/nym/pull/1393
[#1404]: https://github.com/nymtech/nym/pull/1404
[#1419]: https://github.com/nymtech/nym/pull/1419
[#1427]: https://github.com/nymtech/nym/pull/1427
[#1457]: https://github.com/nymtech/nym/pull/1457
[#1463]: https://github.com/nymtech/nym/pull/1463
[#1478]: https://github.com/nymtech/nym/pull/1478
[#1482]: https://github.com/nymtech/nym/pull/1482
[#1487]: https://github.com/nymtech/nym/pull/1487
## [nym-connect-v1.0.1](https://github.com/nymtech/nym/tree/nym-connect-v1.0.1) (2022-07-22)
### Added
- nym-connect: initial proof-of-concept of a UI around the socks5 client was added
- nym-connect: add ability to select network requester and gateway ([#1427])
- nym-connect: add ability to export gateway keys as JSON
- nym-connect: add auto updater
### Changed
- nym-connect: reuse config id instead of creating a new id on each connection
[#1427]: https://github.com/nymtech/nym/pull/1427
## [nym-wallet-v1.0.7](https://github.com/nymtech/nym/tree/nym-wallet-v1.0.7) (2022-07-11)
Generated
+1
View File
@@ -1606,6 +1606,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"task",
"thiserror",
"tokio",
"validator-client",
+1 -1
View File
@@ -1,5 +1,5 @@
test: clippy-all cargo-test wasm fmt
test-all: test cargo-test-expensive
test: build clippy-all cargo-test wasm fmt
no-clippy: build cargo-test wasm fmt
happy: fmt clippy-happy test
clippy-all: clippy-all-main clippy-all-contracts clippy-all-wallet clippy-all-connect
+1 -1
View File
@@ -96,7 +96,7 @@ pub(crate) async fn execute(args: &Cli) {
}
}
fn parse_validators(raw: &str) -> Vec<Url> {
pub fn parse_validators(raw: &str) -> Vec<Url> {
raw.split(',')
.map(|raw_validator| {
raw_validator
+7 -3
View File
@@ -5,7 +5,7 @@ use futures::StreamExt;
use log::*;
use nymsphinx::receiver::ReconstructedMessage;
use proxy_helpers::connection_controller::{ControllerCommand, ControllerSender};
use socks5_requests::Response;
use socks5_requests::Message;
pub(crate) struct MixnetResponseListener {
buffer_requester: ReceivedBufferRequestSender,
@@ -44,12 +44,16 @@ impl MixnetResponseListener {
warn!("this message had a surb - we didn't do anything with it");
}
let response = match Response::try_from_bytes(&raw_message) {
let response = match Message::try_from_bytes(&raw_message) {
Err(err) => {
warn!("failed to parse received response - {:?}", err);
return;
}
Ok(data) => data,
Ok(Message::Request(_)) => {
warn!("unexpected request");
return;
}
Ok(Message::Response(data)) => data,
};
self.controller_sender
@@ -1006,6 +1006,29 @@ impl<C> NymdClient<C> {
.await
}
#[execute("mixnet")]
fn _compound_reward(
&self,
operator: Option<String>,
delegator: Option<String>,
mix_identity: Option<IdentityKey>,
proxy: Option<String>,
fee: Option<Fee>,
) -> (ExecuteMsg, Option<Fee>)
where
C: SigningCosmWasmClient + Sync,
{
(
ExecuteMsg::CompoundReward {
operator,
delegator,
mix_identity,
proxy,
},
fee,
)
}
#[execute("mixnet")]
fn _compound_operator_reward(&self, fee: Option<Fee>) -> (ExecuteMsg, Option<Fee>)
where
@@ -33,6 +33,12 @@ pub enum ExecuteMsg {
CompoundDelegatorReward {
mix_identity: IdentityKey,
},
CompoundReward {
operator: Option<String>,
delegator: Option<String>,
mix_identity: Option<IdentityKey>,
proxy: Option<String>,
},
BondMixnode {
mix_node: MixNode,
owner_signature: String,
@@ -116,6 +122,7 @@ pub enum ExecuteMsg {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum QueryMsg {
GetBlacklistedNodes {},
GetCurrentOperatorCost {},
GetRewardingValidatorAddress {},
GetAllDelegationKeys {},
@@ -206,6 +213,31 @@ pub enum QueryMsg {
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct MigrateMsg {}
pub struct MigrateMsg {
pub mixnet_denom: String,
nodes_to_remove: Option<Vec<NodeToRemove>>,
}
impl MigrateMsg {
pub fn nodes_to_remove(&self) -> Vec<NodeToRemove> {
self.nodes_to_remove.clone().unwrap_or_default()
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct NodeToRemove {
owner: String,
proxy: Option<String>,
}
impl NodeToRemove {
pub fn owner(&self) -> &str {
&self.owner
}
pub fn proxy(&self) -> Option<&String> {
self.proxy.as_ref()
}
}
@@ -12,7 +12,9 @@ pub struct InitMsg {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct MigrateMsg {}
pub struct MigrateMsg {
pub mix_denom: String,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema, Default)]
pub struct VestingSpecification {
+6 -2
View File
@@ -34,12 +34,16 @@ pub enum StatsData {
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct StatsGatewayData {
pub gateway_id: String,
pub inbox_count: u32,
}
impl StatsGatewayData {
pub fn new(inbox_count: u32) -> Self {
StatsGatewayData { inbox_count }
pub fn new(gateway_id: String, inbox_count: u32) -> Self {
StatsGatewayData {
gateway_id,
inbox_count,
}
}
}
+4 -4
View File
@@ -1392,18 +1392,18 @@ dependencies = [
[[package]]
name = "regex"
version = "1.5.4"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
dependencies = [
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.25"
version = "0.6.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]]
name = "rfc6979"
+76 -7
View File
@@ -27,16 +27,18 @@ use crate::mixnodes::bonding_queries::{
query_checkpoints_for_mixnode, query_mixnode_at_height, query_mixnodes_paged,
};
use crate::mixnodes::layer_queries::query_layer_distribution;
use crate::mixnodes::transactions::_try_remove_mixnode;
use crate::queued_migrations::migrate_config_from_env;
use crate::rewards::queries::{
query_circulating_supply, query_reward_pool, query_rewarding_status, query_staking_supply,
};
use crate::rewards::storage as rewards_storage;
use cosmwasm_std::{
entry_point, to_binary, Addr, Api, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response,
Uint128,
Storage, Uint128,
};
use mixnet_contract_common::{
ContractStateParams, ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg,
ContractStateParams, ExecuteMsg, InstantiateMsg, MigrateMsg, NodeToRemove, QueryMsg,
};
use time::OffsetDateTime;
@@ -112,6 +114,19 @@ pub fn execute(
msg: ExecuteMsg,
) -> Result<Response, ContractError> {
match msg {
ExecuteMsg::CompoundReward {
operator,
delegator,
mix_identity,
proxy,
} => crate::rewards::transactions::try_compound_reward(
deps,
env,
operator,
delegator,
mix_identity,
proxy,
),
ExecuteMsg::UpdateRewardingValidatorAddress { address } => {
try_update_rewarding_validator_address(deps, info, address)
}
@@ -127,7 +142,7 @@ pub fn execute(
owner_signature,
),
ExecuteMsg::UnbondMixnode {} => {
crate::mixnodes::transactions::try_remove_mixnode(env, deps, info)
crate::mixnodes::transactions::try_remove_mixnode(&env, deps.storage, deps.api, info)
}
ExecuteMsg::UpdateMixnodeConfig {
profit_margin_percent,
@@ -221,7 +236,13 @@ pub fn execute(
owner_signature,
),
ExecuteMsg::UnbondMixnodeOnBehalf { owner } => {
crate::mixnodes::transactions::try_remove_mixnode_on_behalf(env, deps, info, owner)
crate::mixnodes::transactions::try_remove_mixnode_on_behalf(
&env,
deps.storage,
deps.api,
info,
owner,
)
}
ExecuteMsg::BondGatewayOnBehalf {
gateway,
@@ -280,7 +301,7 @@ pub fn execute(
)
}
ExecuteMsg::ReconcileDelegations {} => {
crate::delegations::transactions::try_reconcile_all_delegation_events(deps, info)
crate::delegations::transactions::try_reconcile_all_delegation_events(deps)
}
ExecuteMsg::CheckpointMixnodes {} => {
crate::mixnodes::transactions::try_checkpoint_mixnodes(
@@ -321,6 +342,9 @@ pub fn execute(
#[entry_point]
pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
let query_res = match msg {
QueryMsg::GetBlacklistedNodes {} => to_binary(
&crate::mixnodes::bonding_queries::get_blacklisted_nodes(deps),
),
QueryMsg::GetRewardingValidatorAddress {} => {
to_binary(&query_rewarding_validator_address(deps)?)
}
@@ -448,9 +472,54 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
Ok(query_res?)
}
fn blacklist_malicious_node(storage: &mut dyn Storage, owner: &Addr) -> Result<(), ContractError> {
let mixnode_bond = match crate::mixnodes::storage::mixnodes()
.idx
.owner
.item(storage, owner.clone())?
{
Some(record) => record.1,
None => {
return Err(ContractError::NoAssociatedMixNodeBond {
owner: owner.to_owned(),
})
}
};
crate::mixnodes::storage::MIXNODES_BOND_BLACKLIST.save(storage, mixnode_bond.identity(), &0)?;
Ok(())
}
// Removes nodes we've deemed malicious, returns the pledge to the owners, but does not send any rewards
fn remove_malicious_node(
storage: &mut dyn Storage,
api: &dyn Api,
env: &Env,
node: &NodeToRemove,
) -> Result<Response, ContractError> {
let proxy = node.proxy().map(|p| {
api.addr_validate(p)
.unwrap_or_else(|_| panic!("Invalid address: {}", p))
});
let owner_addr = api.addr_validate(node.owner())?;
blacklist_malicious_node(storage, &owner_addr)?;
_try_remove_mixnode(env, storage, api, node.owner(), proxy, false)
}
#[entry_point]
pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
Ok(Default::default())
pub fn migrate(deps: DepsMut<'_>, env: Env, msg: MigrateMsg) -> Result<Response, ContractError> {
migrate_config_from_env(deps.storage, &msg)?;
let mut response = Response::new();
for node in msg.nodes_to_remove().iter() {
let mut sub_response = remove_malicious_node(deps.storage, deps.api, &env, node)
.unwrap_or_else(|_| panic!("Could not remove node: {:?}", node));
response.messages.append(&mut sub_response.messages);
response.attributes.append(&mut sub_response.attributes);
response.events.append(&mut sub_response.events);
}
Ok(response)
}
#[cfg(test)]
+35 -183
View File
@@ -19,16 +19,7 @@ use mixnet_contract_common::{Delegation, IdentityKey};
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
use vesting_contract_common::one_ucoin;
pub fn try_reconcile_all_delegation_events(
deps: DepsMut<'_>,
info: MessageInfo,
) -> Result<Response, ContractError> {
let state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?;
// check if this is executed by the permitted validator, if not reject the transaction
if info.sender != state.rewarding_validator_address {
return Err(ContractError::Unauthorized);
}
pub fn try_reconcile_all_delegation_events(deps: DepsMut<'_>) -> Result<Response, ContractError> {
_try_reconcile_all_delegation_events(deps.storage, deps.api)
}
@@ -630,7 +621,14 @@ mod tests {
deps.as_mut(),
);
let delegation_owner = Addr::unchecked("sender");
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
let api = deps.api.clone();
try_remove_mixnode(
&mock_env(),
deps.as_mut().storage,
&api,
mock_info(mixnode_owner, &[]),
)
.unwrap();
assert_eq!(
Err(ContractError::MixNodeBondNotFound {
identity: identity.clone()
@@ -653,7 +651,14 @@ mod tests {
tests::fixtures::good_mixnode_pledge(),
deps.as_mut(),
);
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
let api = deps.api.clone();
try_remove_mixnode(
&mock_env(),
deps.as_mut().storage,
&api,
mock_info(mixnode_owner, &[]),
)
.unwrap();
let identity = test_helpers::add_mixnode(
mixnode_owner,
tests::fixtures::good_mixnode_pledge(),
@@ -917,7 +922,14 @@ mod tests {
identity.clone(),
)
.unwrap();
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
let api = deps.api.clone();
try_remove_mixnode(
&mock_env(),
deps.as_mut().storage,
&api,
mock_info(mixnode_owner, &[]),
)
.unwrap();
assert_eq!(
Err(ContractError::MixNodeBondNotFound {
identity: identity.clone()
@@ -1058,8 +1070,14 @@ mod tests {
.unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
let api = deps.api.clone();
try_remove_mixnode(
&mock_env(),
deps.as_mut().storage,
&api,
mock_info(mixnode_owner, &[]),
)
.unwrap();
let expected = Delegation::new(
delegation_owner.clone(),
@@ -1084,179 +1102,13 @@ mod tests {
#[cfg(test)]
mod removing_mix_stake_delegation {
use super::*;
use crate::support::tests;
use cosmwasm_std::coin;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::testing::mock_info;
use cosmwasm_std::Addr;
use crate::support::tests;
use super::*;
// TODO: Probably delete due to reconciliation logic
//#[ignore]
//#[test]
//fn fails_if_delegation_never_existed() {
// let mut deps = test_helpers::init_contract();
// let env = mock_env();
// let mixnode_owner = "bob";
// let identity = test_helpers::add_mixnode(
// mixnode_owner,
// tests::fixtures::good_mixnode_pledge(),
// deps.as_mut(),
// );
// let delegation_owner = Addr::unchecked("sender");
// assert_eq!(
// Err(ContractError::NoMixnodeDelegationFound {
// identity: identity.clone(),
// address: delegation_owner.to_string(),
// }),
// try_remove_delegation_from_mixnode(
// deps.as_mut(),
// env,
// mock_info(delegation_owner.as_str(), &[]),
// identity,
// )
// );
//}
// TODO: Update to work with reconciliation
//#[ignore]
//#[test]
//fn succeeds_if_delegation_existed() {
// let mut deps = test_helpers::init_contract();
// let mixnode_owner = "bob";
// let env = mock_env();
// let identity = test_helpers::add_mixnode(
// mixnode_owner,
// tests::fixtures::good_mixnode_pledge(),
// deps.as_mut(),
// );
// let delegation_owner = Addr::unchecked("sender");
// try_delegate_to_mixnode(
// deps.as_mut(),
// mock_env(),
// mock_info(delegation_owner.as_str(), &coins(100, TEST_COIN_DENOM)),
// identity.clone(),
// )
// .unwrap();
// _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
// let _delegation = query_mixnode_delegation(
// &deps.storage,
// &deps.api,
// identity.clone(),
// delegation_owner.clone().into_string(),
// None,
// )
// .unwrap();
// let expected_response = Response::new()
// .add_message(BankMsg::Send {
// to_address: delegation_owner.clone().into(),
// amount: coins(100, TEST_COIN_DENOM),
// })
// .add_event(new_undelegation_event(
// &delegation_owner,
// &None,
// &identity,
// Uint128::new(100),
// ));
// assert_eq!(
// Ok(expected_response),
// try_remove_delegation_from_mixnode(
// deps.as_mut(),
// env,
// mock_info(delegation_owner.as_str(), &[]),
// identity.clone(),
// )
// );
// assert!(storage::delegations()
// .may_load(
// &deps.storage,
// (identity.clone(), delegation_owner.as_bytes().to_vec(), 0),
// )
// .unwrap()
// .is_none());
// // and total delegation is cleared
// assert_eq!(
// Uint128::zero(),
// mixnodes_storage::TOTAL_DELEGATION
// .load(&deps.storage, &identity)
// .unwrap()
// )
//}
// TODO: Update to work with reconciliation
//#[ignore]
//#[test]
//fn succeeds_if_delegation_existed_even_if_node_unbonded() {
// let mut deps = test_helpers::init_contract();
// let mixnode_owner = "bob";
// let env = mock_env();
// let identity = test_helpers::add_mixnode(
// mixnode_owner,
// tests::fixtures::good_mixnode_pledge(),
// deps.as_mut(),
// );
// let delegation_owner = Addr::unchecked("sender");
// try_delegate_to_mixnode(
// deps.as_mut(),
// mock_env(),
// mock_info(delegation_owner.as_str(), &coins(100, TEST_COIN_DENOM)),
// identity.clone(),
// )
// .unwrap();
// _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
// let _delegation = query_mixnode_delegation(
// &deps.storage,
// &deps.api,
// identity.clone(),
// delegation_owner.clone().into_string(),
// None,
// )
// .unwrap();
// let expected_response = Response::new()
// .add_message(BankMsg::Send {
// to_address: delegation_owner.clone().into(),
// amount: coins(100, TEST_COIN_DENOM),
// })
// .add_event(new_undelegation_event(
// &delegation_owner,
// &None,
// &identity,
// Uint128::new(100),
// ));
// try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
// assert_eq!(
// Ok(expected_response),
// try_remove_delegation_from_mixnode(
// deps.as_mut(),
// env,
// mock_info(delegation_owner.as_str(), &[]),
// identity.clone(),
// )
// );
// _try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
// assert!(test_helpers::read_delegation(
// &deps.storage,
// identity,
// delegation_owner.as_bytes(),
// mock_env().block.height
// )
// .is_none());
//}
#[test]
fn total_delegation_is_preserved_if_only_some_undelegate() {
let mut deps = test_helpers::init_contract();
+7
View File
@@ -178,4 +178,11 @@ pub enum ContractError {
last_update_time: u64,
current_block_time: u64,
},
#[error("`mix_identity` is required when `delegator` is set")]
MissingMixIdentity,
#[error("Compounding has been disabled temporarily")]
CompoundDisabled,
#[error("Mixnode {identity} has been blacklisted on the network")]
MixnodeBlacklisted { identity: String },
}
+1
View File
@@ -9,5 +9,6 @@ mod gateways;
mod interval;
mod mixnet_contract_settings;
mod mixnodes;
mod queued_migrations;
mod rewards;
mod support;
@@ -8,6 +8,13 @@ use mixnet_contract_common::{
IdentityKey, MixNodeBond, MixOwnershipResponse, MixnodeBondResponse, PagedMixnodeResponse,
};
pub fn get_blacklisted_nodes(deps: Deps<'_>) -> Vec<IdentityKey> {
storage::MIXNODES_BOND_BLACKLIST
.keys(deps.storage, None, None, Order::Ascending)
.filter_map(|i| i.ok())
.collect()
}
pub fn query_mixnode_at_height(
deps: Deps<'_>,
mix_identity: String,
@@ -252,10 +259,13 @@ pub(crate) mod tests {
let res = query_owns_mixnode(deps.as_ref(), "fred".to_string()).unwrap();
assert!(res.mixnode.is_some());
let api = deps.api.clone();
// but after unbonding it, he doesn't own one anymore
crate::mixnodes::transactions::try_remove_mixnode(
env,
deps.as_mut(),
&env,
deps.as_mut().storage,
&api,
mock_info("fred", &[]),
)
.unwrap();
+4
View File
@@ -19,6 +19,7 @@ const MIXNODES_PK_CHECKPOINTS: &str = "mn__check";
const MIXNODES_PK_CHANGELOG: &str = "mn__change";
const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno";
const MIXNODES_SPHINX_IDX_NAMESPACE: &str = "mns";
const MIXNODES_BOND_BLACKLIST_NAMESPACE: &str = "mbb";
const LAST_PM_UPDATE_NAMESPACE: &str = "lpm";
@@ -32,6 +33,9 @@ pub(crate) const TOTAL_DELEGATION: Map<'_, IdentityKeyRef<'_>, Uint128> =
pub(crate) const LAST_PM_UPDATE_TIME: Map<'_, IdentityKeyRef<'_>, u64> =
Map::new(LAST_PM_UPDATE_NAMESPACE);
pub(crate) const MIXNODES_BOND_BLACKLIST: Map<'_, IdentityKeyRef<'_>, u8> =
Map::new(MIXNODES_BOND_BLACKLIST_NAMESPACE);
pub(crate) struct MixnodeBondIndex<'a> {
pub(crate) owner: UniqueIndex<'a, Addr, StoredMixnodeBond>,
+43 -27
View File
@@ -8,12 +8,12 @@ use crate::mixnodes::layer_queries::query_layer_distribution;
use crate::mixnodes::storage::StoredMixnodeBond;
use crate::support::helpers::{ensure_no_existing_bond, validate_node_identity_signature};
use cosmwasm_std::{
wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Storage, Uint128,
wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Storage, Uint128,
};
use mixnet_contract_common::events::{
new_checkpoint_mixnodes_event, new_mixnode_bonding_event, new_mixnode_unbonding_event,
};
use mixnet_contract_common::MixNode;
use mixnet_contract_common::{IdentityKeyRef, MixNode};
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
use vesting_contract_common::one_ucoin;
@@ -87,6 +87,15 @@ pub fn try_add_mixnode_on_behalf(
)
}
pub fn is_blacklisted(
storage: &dyn Storage,
identity: IdentityKeyRef,
) -> Result<bool, ContractError> {
Ok(storage::MIXNODES_BOND_BLACKLIST
.may_load(storage, identity)?
.is_some())
}
fn _try_add_mixnode(
deps: DepsMut<'_>,
env: Env,
@@ -100,6 +109,12 @@ fn _try_add_mixnode(
// if the client has an active bonded mixnode or gateway, don't allow bonding
ensure_no_existing_bond(deps.storage, &owner)?;
if is_blacklisted(deps.storage, &mix_node.identity_key)? {
return Err(ContractError::MixnodeBlacklisted {
identity: mix_node.identity_key,
});
};
// We don't have to check lower bound as its an u8
if mix_node.profit_margin_percent > 100 {
return Err(ContractError::InvalidProfitMarginPercent(
@@ -167,45 +182,47 @@ fn _try_add_mixnode(
}
pub fn try_remove_mixnode_on_behalf(
env: Env,
deps: DepsMut<'_>,
env: &Env,
storage: &mut dyn Storage,
api: &dyn Api,
info: MessageInfo,
owner: String,
) -> Result<Response, ContractError> {
let proxy = info.sender;
_try_remove_mixnode(env, deps, &owner, Some(proxy))
_try_remove_mixnode(env, storage, api, &owner, Some(proxy), true)
}
pub fn try_remove_mixnode(
env: Env,
deps: DepsMut<'_>,
env: &Env,
storage: &mut dyn Storage,
api: &dyn Api,
info: MessageInfo,
) -> Result<Response, ContractError> {
_try_remove_mixnode(env, deps, info.sender.as_ref(), None)
_try_remove_mixnode(env, storage, api, info.sender.as_ref(), None, true)
}
pub(crate) fn _try_remove_mixnode(
env: Env,
deps: DepsMut<'_>,
env: &Env,
storage: &mut dyn Storage,
api: &dyn Api,
owner: &str,
proxy: Option<Addr>,
collect_rewards: bool,
) -> Result<Response, ContractError> {
let owner = deps.api.addr_validate(owner)?;
let owner = api.addr_validate(owner)?;
crate::rewards::transactions::_try_compound_operator_reward(
deps.storage,
deps.api,
env.block.height,
&owner,
None,
)?;
if collect_rewards {
crate::rewards::transactions::_try_compound_operator_reward(
storage,
api,
env.block.height,
&owner,
None,
)?;
}
// try to find the node of the sender
let mixnode_bond = match storage::mixnodes()
.idx
.owner
.item(deps.storage, owner.clone())?
{
let mixnode_bond = match storage::mixnodes().idx.owner.item(storage, owner.clone())? {
Some(record) => record.1,
None => return Err(ContractError::NoAssociatedMixNodeBond { owner }),
};
@@ -225,10 +242,10 @@ pub(crate) fn _try_remove_mixnode(
};
// remove the bond
storage::mixnodes().remove(deps.storage, mixnode_bond.identity(), env.block.height)?;
storage::mixnodes().remove(storage, mixnode_bond.identity(), env.block.height)?;
// decrement layer count
mixnet_params_storage::decrement_layer_count(deps.storage, mixnode_bond.layer)?;
mixnet_params_storage::decrement_layer_count(storage, mixnode_bond.layer)?;
let mut response = Response::new();
@@ -238,8 +255,7 @@ pub(crate) fn _try_remove_mixnode(
amount: mixnode_bond.pledge_amount(),
};
let track_unbond_message =
wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom(deps.storage)?)])?;
let track_unbond_message = wasm_execute(proxy, &msg, vec![one_ucoin(mix_denom(storage)?)])?;
response = response.add_message(track_unbond_message);
}
+38
View File
@@ -0,0 +1,38 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{Addr, Response, Storage};
use cw_storage_plus::Item;
use mixnet_contract_common::{ContractStateParams, MigrateMsg};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::error::ContractError;
use crate::mixnet_contract_settings::models::ContractState;
use crate::mixnet_contract_settings::storage::CONTRACT_STATE;
pub fn migrate_config_from_env(
storage: &mut dyn Storage,
msg: &MigrateMsg,
) -> Result<Response, ContractError> {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)]
pub struct OldContractState {
pub owner: Addr,
pub mix_denom: String,
pub rewarding_validator_address: Addr,
pub params: ContractStateParams,
}
const OLD_CONTRACT_STATE: Item<'_, OldContractState> = Item::new("config");
let old_state = OLD_CONTRACT_STATE.load(storage)?;
let new_state = ContractState {
owner: old_state.owner,
mix_denom: msg.mixnet_denom.clone(),
rewarding_validator_address: old_state.rewarding_validator_address,
params: old_state.params,
};
CONTRACT_STATE.save(storage, &new_state)?;
Ok(Default::default())
}
+72 -2
View File
@@ -13,6 +13,7 @@ use crate::error::ContractError;
use crate::mixnet_contract_settings::storage::mix_denom;
use crate::mixnodes::storage::mixnodes;
use crate::mixnodes::storage::{self as mixnodes_storage, StoredMixnodeBond};
use crate::mixnodes::transactions::is_blacklisted;
use crate::rewards::helpers;
use crate::support::helpers::{is_authorized, operator_cost_at_epoch};
use cosmwasm_std::{
@@ -372,6 +373,56 @@ pub fn try_compound_delegator_reward_on_behalf(
)
}
pub fn try_compound_reward(
deps: DepsMut<'_>,
env: Env,
operator: Option<String>,
delegator: Option<String>,
mix_identity: Option<IdentityKey>,
proxy: Option<String>,
) -> Result<Response, ContractError> {
let proxy = proxy.and_then(|p| deps.api.addr_validate(&p).ok());
if let Some(operator_address) = operator {
let operator_address = deps.api.addr_validate(&operator_address)?;
let reward = _try_compound_operator_reward(
deps.storage,
deps.api,
env.block.height,
&operator_address,
proxy,
)?;
Ok(
Response::default().add_event(new_compound_operator_reward_event(
&operator_address,
reward,
)),
)
} else if let Some(delegator_address) = delegator {
if mix_identity.is_none() {
return Err(ContractError::MissingMixIdentity);
}
let delegator_address = deps.api.addr_validate(&delegator_address)?;
let reward = _try_compound_delegator_reward(
env.block.height,
deps,
delegator_address.as_str(),
mix_identity.as_ref().unwrap(),
proxy,
)?;
Ok(
Response::default().add_event(new_compound_delegator_reward_event(
&delegator_address,
&None,
reward,
&mix_identity.unwrap(),
)),
)
} else {
Ok(Response::default())
}
}
pub fn try_compound_delegator_reward(
deps: DepsMut<'_>,
env: Env,
@@ -412,7 +463,7 @@ pub fn _try_compound_delegator_reward(
proxy.as_ref(),
);
let reward = calculate_delegator_reward(deps.storage, deps.api, key.clone(), mix_identity)?;
let mut compounded_delegation = reward;
let mut total_delegation_delegate = Uint128::zero();
// Might want to introduce paging here
let delegation_heights = delegation_map
@@ -424,7 +475,7 @@ pub fn _try_compound_delegator_reward(
for h in delegation_heights {
let delegation =
delegation_map.load(deps.storage, (mix_identity.to_string(), key.clone(), h))?;
compounded_delegation += delegation.amount.amount;
total_delegation_delegate += delegation.amount.amount;
delegation_map.replace(
deps.storage,
(mix_identity.to_string(), key.clone(), h),
@@ -433,7 +484,22 @@ pub fn _try_compound_delegator_reward(
)?;
}
let compounded_delegation = total_delegation_delegate + reward;
if compounded_delegation != Uint128::zero() {
mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>(
deps.storage,
mix_identity,
|total_delegation| {
// since we know that the target node exists and because the total_delegation bucket
// entry is created whenever the node itself is added, the unwrap here is fine
// as the entry MUST exist
Ok(total_delegation
.unwrap()
.saturating_sub(total_delegation_delegate))
},
)?;
_try_delegate_to_mixnode(
deps.branch(),
block_height,
@@ -472,6 +538,10 @@ pub fn calculate_delegator_reward(
key: Vec<u8>,
mix_identity: &str,
) -> Result<Uint128, ContractError> {
if is_blacklisted(storage, mix_identity)? {
return Ok(Uint128::zero());
};
let last_claimed_height = storage::DELEGATOR_REWARD_CLAIMED_HEIGHT
.load(storage, (key.clone(), mix_identity.to_string()))
.unwrap_or(0);
+2
View File
@@ -1,4 +1,5 @@
use crate::errors::ContractError;
use crate::queued_migrations::migrate_config_from_env;
use crate::storage::{
account_from_address, locked_pledge_cap, update_locked_pledge_cap, ADMIN,
MIXNET_CONTRACT_ADDRESS, MIX_DENOM,
@@ -41,6 +42,7 @@ pub fn instantiate(
#[entry_point]
pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
migrate_config_from_env(_deps, _env, _msg)?;
Ok(Response::default())
}
+1
View File
@@ -1,5 +1,6 @@
pub mod contract;
mod errors;
mod queued_migrations;
mod storage;
mod support;
mod traits;
@@ -0,0 +1,17 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{DepsMut, Env, Response};
use vesting_contract_common::MigrateMsg;
use crate::{errors::ContractError, storage::MIX_DENOM};
pub fn migrate_config_from_env(
deps: DepsMut<'_>,
_env: Env,
msg: MigrateMsg,
) -> Result<Response, ContractError> {
MIX_DENOM.save(deps.storage, &msg.mix_denom)?;
Ok(Default::default())
}
+1
View File
@@ -26,4 +26,5 @@ tokio = {version = "1.19.1", features = ["full"] }
mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" }
network-defaults = { path = "../common/network-defaults" }
task = { path = "../common/task" }
validator-client = { path = "../common/client-libs/validator-client", features=["nymd-client"] }
@@ -1,4 +1,5 @@
use log::info;
use task::ShutdownListener;
use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution;
use crate::COUNTRY_DATA_REFRESH_INTERVAL;
@@ -7,11 +8,12 @@ use crate::state::ExplorerApiStateContext;
pub(crate) struct CountryStatisticsDistributionTask {
state: ExplorerApiStateContext,
shutdown: ShutdownListener,
}
impl CountryStatisticsDistributionTask {
pub(crate) fn new(state: ExplorerApiStateContext) -> Self {
CountryStatisticsDistributionTask { state }
pub(crate) fn new(state: ExplorerApiStateContext, shutdown: ShutdownListener) -> Self {
CountryStatisticsDistributionTask { state, shutdown }
}
pub(crate) fn start(mut self) {
@@ -20,10 +22,15 @@ impl CountryStatisticsDistributionTask {
let mut interval_timer = tokio::time::interval(std::time::Duration::from_secs(
COUNTRY_DATA_REFRESH_INTERVAL,
));
loop {
// wait for the next interval tick
interval_timer.tick().await;
self.calculate_nodes_per_country().await;
while !self.shutdown.is_shutdown() {
tokio::select! {
_ = interval_timer.tick() => {
self.calculate_nodes_per_country().await;
}
_ = self.shutdown.recv() => {
trace!("Listener: Received shutdown");
}
}
}
});
}
@@ -5,15 +5,17 @@ use crate::mix_nodes::location::{GeoLocation, Location};
use crate::state::ExplorerApiStateContext;
use log::{info, warn};
use reqwest::Error as ReqwestError;
use task::ShutdownListener;
use thiserror::Error;
pub(crate) struct GeoLocateTask {
state: ExplorerApiStateContext,
shutdown: ShutdownListener,
}
impl GeoLocateTask {
pub(crate) fn new(state: ExplorerApiStateContext) -> Self {
GeoLocateTask { state }
pub(crate) fn new(state: ExplorerApiStateContext, shutdown: ShutdownListener) -> Self {
GeoLocateTask { state, shutdown }
}
pub(crate) fn start(mut self) {
@@ -27,10 +29,15 @@ impl GeoLocateTask {
info!("Spawning mix node locator task runner...");
tokio::spawn(async move {
let mut interval_timer = tokio::time::interval(std::time::Duration::from_millis(50));
loop {
// wait for the next interval tick
interval_timer.tick().await;
self.locate_mix_nodes().await;
while !self.shutdown.is_shutdown() {
tokio::select! {
_ = interval_timer.tick() => {
self.locate_mix_nodes().await;
}
_ = self.shutdown.recv() => {
trace!("Listener: Received shutdown");
}
}
}
});
}
+47 -12
View File
@@ -6,6 +6,7 @@ extern crate rocket_okapi;
use clap::Parser;
use log::info;
use network_defaults::setup_env;
use task::ShutdownNotifier;
pub(crate) mod cache;
mod client;
@@ -50,29 +51,63 @@ impl ExplorerApi {
let validator_api_url = self.state.inner.validator_client.api_endpoint();
info!("Using validator API - {}", validator_api_url);
let shutdown = ShutdownNotifier::default();
// spawn concurrent tasks
crate::tasks::ExplorerApiTasks::new(self.state.clone()).start();
crate::tasks::ExplorerApiTasks::new(self.state.clone(), shutdown.subscribe()).start();
country_statistics::distribution::CountryStatisticsDistributionTask::new(
self.state.clone(),
shutdown.subscribe(),
)
.start();
country_statistics::geolocate::GeoLocateTask::new(self.state.clone()).start();
country_statistics::geolocate::GeoLocateTask::new(self.state.clone(), shutdown.subscribe())
.start();
// Rocket handles shutdown on it's own, but its shutdown handling should be incorporated
// with that of the rest of the tasks.
// Currently it's runtime is forcefully terminated once the explorer-api exits.
http::start(self.state.clone());
// wait for user to press ctrl+C
self.wait_for_interrupt().await
self.wait_for_interrupt(shutdown).await
}
async fn wait_for_interrupt(&self) {
if let Err(e) = tokio::signal::ctrl_c().await {
error!(
"There was an error while capturing SIGINT - {:?}. We will terminate regardless",
e
);
async fn wait_for_interrupt(&self, mut shutdown: ShutdownNotifier) {
wait_for_signal().await;
log::info!("Sending shutdown");
shutdown.signal_shutdown().ok();
log::info!("Waiting for tasks to finish... (Press ctrl-c to force)");
shutdown.wait_for_shutdown().await;
log::info!("Stopping explorer API");
}
}
#[cfg(unix)]
async fn wait_for_signal() {
use tokio::signal::unix::{signal, SignalKind};
let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM channel");
let mut sigquit = signal(SignalKind::quit()).expect("Failed to setup SIGQUIT channel");
tokio::select! {
_ = tokio::signal::ctrl_c() => {
log::info!("Received SIGINT");
},
_ = sigterm.recv() => {
log::info!("Received SIGTERM");
}
info!(
"Received SIGINT - the mixnode will terminate now (threads are not yet nicely stopped, if you see stack traces that's alright)."
);
_ = sigquit.recv() => {
log::info!("Received SIGQUIT");
}
}
}
#[cfg(not(unix))]
async fn wait_for_signal() {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
log::info!("Received SIGINT");
},
}
}
+21 -15
View File
@@ -4,6 +4,7 @@
use std::future::Future;
use mixnet_contract_common::GatewayBond;
use task::ShutdownListener;
use validator_client::models::MixNodeBondAnnotated;
use validator_client::nymd::error::NymdError;
use validator_client::nymd::{Paging, QueryNymdClient, ValidatorResponse};
@@ -14,11 +15,12 @@ use crate::state::ExplorerApiStateContext;
pub(crate) struct ExplorerApiTasks {
state: ExplorerApiStateContext,
shutdown: ShutdownListener,
}
impl ExplorerApiTasks {
pub(crate) fn new(state: ExplorerApiStateContext) -> Self {
ExplorerApiTasks { state }
pub(crate) fn new(state: ExplorerApiStateContext, shutdown: ShutdownListener) -> Self {
ExplorerApiTasks { state, shutdown }
}
// a helper to remove duplicate code when grabbing active/rewarded/all mixnodes
@@ -128,24 +130,28 @@ impl ExplorerApiTasks {
}
}
pub(crate) fn start(self) {
pub(crate) fn start(mut self) {
info!("Spawning mix nodes task runner...");
tokio::spawn(async move {
let mut interval_timer = tokio::time::interval(CACHE_REFRESH_RATE);
loop {
// wait for the next interval tick
interval_timer.tick().await;
while !self.shutdown.is_shutdown() {
tokio::select! {
_ = interval_timer.tick() => {
info!("Updating validator cache...");
self.update_validators_cache().await;
info!("Done");
info!("Updating validator cache...");
self.update_validators_cache().await;
info!("Done");
info!("Updating gateway cache...");
self.update_gateways_cache().await;
info!("Done");
info!("Updating gateway cache...");
self.update_gateways_cache().await;
info!("Done");
info!("Updating mix node cache...");
self.update_mixnode_cache().await;
info!("Updating mix node cache...");
self.update_mixnode_cache().await;
}
_ = self.shutdown.recv() => {
trace!("Listener: Received shutdown");
}
}
}
});
}
+5 -5
View File
@@ -1,5 +1,5 @@
EXPLORER_API_URL=https://sandbox-explorer.nymtech.net/api/v1
VALIDATOR_API_URL=https://sandbox-validator.nymtech.net
BIG_DIPPER_URL=https://sandbox-blocks.nymtech.net
CURRENCY_DENOM=unymt
CURRENCY_STAKING_DENOM=unyxt
EXPLORER_API_URL=https://explorer.nymtech.net/api/v1
VALIDATOR_API_URL=https://validator.nymtech.net
BIG_DIPPER_URL=https://blocks.nymtech.net
CURRENCY_DENOM=unym
CURRENCY_STAKING_DENOM=unyx
+1
View File
@@ -336,6 +336,7 @@ where
if self.config.get_enabled_statistics() {
let statistics_service_url = self.config.get_statistics_service_url();
let stats_collector = GatewayStatisticsCollector::new(
self.identity_keypair.public_key().to_base58_string(),
active_clients_store.clone(),
statistics_service_url,
);
+11 -2
View File
@@ -14,13 +14,19 @@ use statistics_common::{
use crate::node::client_handling::active_clients::ActiveClientsStore;
pub(crate) struct GatewayStatisticsCollector {
gateway_id: String,
active_clients_store: ActiveClientsStore,
statistics_service_url: Url,
}
impl GatewayStatisticsCollector {
pub fn new(active_clients_store: ActiveClientsStore, statistics_service_url: Url) -> Self {
pub fn new(
gateway_id: String,
active_clients_store: ActiveClientsStore,
statistics_service_url: Url,
) -> Self {
GatewayStatisticsCollector {
gateway_id,
active_clients_store,
statistics_service_url,
}
@@ -35,7 +41,10 @@ impl StatisticsCollector for GatewayStatisticsCollector {
timestamp: DateTime<Utc>,
) -> StatsMessage {
let inbox_count = self.active_clients_store.size() as u32;
let stats_data = vec![StatsData::Gateway(StatsGatewayData { inbox_count })];
let stats_data = vec![StatsData::Gateway(StatsGatewayData::new(
self.gateway_id.clone(),
inbox_count,
))];
StatsMessage {
stats_data,
interval_seconds: interval.as_secs() as u32,
+1 -1
View File
@@ -24,7 +24,7 @@ fn long_version_static() -> &'static str {
#[derive(Parser)]
#[clap(author = "Nymtech", version, about, long_version = long_version_static())]
struct Cli {
/// Path pointing to an env file that configures the gateway.
/// Path pointing to an env file that configures the mixnode.
#[clap(long)]
pub(crate) config_env_file: Option<std::path::PathBuf>,
+1983
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "chitchat-test"
version = "0.3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chitchat = "0.4"
poem = "1"
poem-openapi = {version="2", features = ["swagger-ui"] }
structopt = "0.3"
tokio = { version = "1.14.0", features = ["net", "sync", "rt-multi-thread", "macros", "time"] }
serde = { version="1", features=["derive"] }
serde_json = "1"
anyhow = "1"
once_cell = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
cool-id-generator = "1"
env_logger = "0.9"
[dev-dependencies]
assert_cmd = "2"
predicates = "2"
reqwest = { version = "0.11", default-features=false, features = ["blocking", "json"] }
[workspace]
+15
View File
@@ -0,0 +1,15 @@
# Chitchat test
Runs simple chitchat servers, mostly copied over from https://github.com/quickwit-oss/chitchat
## Example
```bash
# Starts 5 servers and joins them into a cluster on localhost ports 10000-10004
# All servers print cluster state on `/` ie 127.0.0.1:10000
# `/docs` endpoint has an open api with a key value setter, set it on one node and observe how the state propagates to the other nodes
# NodeState is a regular BTreeMap
./run-servers.sh
# run killall chitchat-test after you're done, as the servers will continue to run forever in the background
```
+15
View File
@@ -0,0 +1,15 @@
#!/bin/bash
killall chitchat-test
cargo build --release
for i in $(seq 10000 10004)
do
listen_addr="127.0.0.1:$i";
echo ${listen_addr};
cargo run --release -- --listen_addr ${listen_addr} --seed 127.0.0.1:10000 --node_id node_$i &
done;
read
kill 0
+123
View File
@@ -0,0 +1,123 @@
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use chitchat::transport::UdpTransport;
use chitchat::{spawn_chitchat, Chitchat, ChitchatConfig, FailureDetectorConfig, NodeId};
use cool_id_generator::Size;
use poem::listener::TcpListener;
use poem::{Route, Server};
use poem_openapi::param::Query;
use poem_openapi::payload::Json;
use poem_openapi::{OpenApi, OpenApiService};
use structopt::StructOpt;
use tokio::sync::Mutex;
use chitchat::ClusterStateSnapshot;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct ApiResponse {
pub cluster_id: String,
pub cluster_state: ClusterStateSnapshot,
pub live_nodes: Vec<NodeId>,
pub dead_nodes: Vec<NodeId>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SetKeyValueResponse {
pub status: bool,
}
struct Api {
chitchat: Arc<Mutex<Chitchat>>,
}
#[OpenApi]
impl Api {
/// Chitchat state
#[oai(path = "/", method = "get")]
async fn index(&self) -> Json<serde_json::Value> {
let chitchat_guard = self.chitchat.lock().await;
let response = ApiResponse {
cluster_id: chitchat_guard.cluster_id().to_string(),
cluster_state: chitchat_guard.state_snapshot(),
live_nodes: chitchat_guard.live_nodes().cloned().collect::<Vec<_>>(),
dead_nodes: chitchat_guard.dead_nodes().cloned().collect::<Vec<_>>(),
};
Json(serde_json::to_value(&response).unwrap())
}
/// Set a key & value on this node (with no validation).
#[oai(path = "/set_kv/", method = "get")]
async fn set_kv(&self, key: Query<String>, value: Query<String>) -> Json<serde_json::Value> {
let mut chitchat_guard = self.chitchat.lock().await;
let cc_state = chitchat_guard.self_node_state();
cc_state.set(key.as_str(), value.as_str());
Json(serde_json::to_value(&SetKeyValueResponse { status: true }).unwrap())
}
}
#[derive(Debug, StructOpt)]
#[structopt(name = "chitchat", about = "Chitchat test server.")]
struct Opt {
/// Defines the socket addr on which we should listen to.
#[structopt(long = "listen_addr", default_value = "127.0.0.1:10000")]
listen_addr: SocketAddr,
/// Defines the socket_address (host:port) other servers should use to
/// reach this server.
///
/// It defaults to the listen address, but this is only valid
/// when all server are running on the same server.
#[structopt(long = "public_addr")]
public_addr: Option<SocketAddr>,
/// Node id. Has to be unique. If None, the node_id will be generated from
/// the public_addr and a random suffix.
#[structopt(long = "node_id")]
node_id: Option<String>,
#[structopt(long = "seed")]
seeds: Vec<String>,
#[structopt(long = "interval_ms", default_value = "500")]
interval: u64,
}
fn generate_server_id(public_addr: SocketAddr) -> String {
let cool_id = cool_id_generator::get_id(Size::Medium);
format!("server:{}-{}", public_addr, cool_id)
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt::init();
let opt = Opt::from_args();
println!("{:?}", opt);
let public_addr = opt.public_addr.unwrap_or(opt.listen_addr);
let node_id_str = opt
.node_id
.unwrap_or_else(|| generate_server_id(public_addr));
let node_id = NodeId::new(node_id_str, public_addr);
let config = ChitchatConfig {
node_id,
cluster_id: "testing".to_string(),
gossip_interval: Duration::from_millis(opt.interval),
listen_addr: opt.listen_addr,
seed_nodes: opt.seeds.clone(),
failure_detector_config: FailureDetectorConfig::default(),
};
let chitchat_handler = spawn_chitchat(config, Vec::new(), &UdpTransport).await?;
let chitchat = chitchat_handler.chitchat();
let api = Api { chitchat };
let api_service = OpenApiService::new(api, "Hello World", "1.0")
.server(&format!("http://{}/", opt.listen_addr));
let docs = api_service.swagger_ui();
let app = Route::new().nest("/", api_service).nest("/docs", docs);
Server::new(TcpListener::bind(&opt.listen_addr))
.run(app)
.await?;
Ok(())
}
+1 -2
View File
@@ -3199,7 +3199,6 @@ dependencies = [
"cosmwasm-std",
"fixed",
"log",
"network-defaults",
"schemars",
"serde",
"serde_repr",
@@ -3405,7 +3404,7 @@ dependencies = [
[[package]]
name = "nym-connect"
version = "1.0.0"
version = "1.0.1"
dependencies = [
"bip39",
"client-core",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-connect"
version = "1.0.0"
version = "1.0.1"
description = "nym-connect"
authors = ["Nym Technologies SA"]
license = ""
@@ -39,7 +39,7 @@ tokio = { version = "1.19.1", features = ["sync", "time"] }
url = "2.2"
client-core = { path = "../../clients/client-core" }
config = { path = "../../common/config" }
config-common = { path = "../../common/config", package = "config" }
nym-socks5-client = { path = "../../clients/socks5" }
topology = { path = "../../common/topology" }
+7 -2
View File
@@ -6,8 +6,8 @@ use tap::TapFallible;
use tokio::sync::RwLock;
use client_core::config::Config as BaseConfig;
use config::NymConfig;
use nym_socks5::client::config::Config as Socks5Config;
use config_common::NymConfig;
use nym_socks5::{client::config::Config as Socks5Config, commands::parse_validators};
use crate::{
error::{BackendError, Result},
@@ -134,6 +134,11 @@ pub async fn init_socks5_config(provider_address: String, chosen_gateway_id: Str
config
.get_base_mut()
.with_eth_private_key(DEFAULT_ETH_PRIVATE_KEY);
if let Ok(raw_validators) = std::env::var(config_common::defaults::var_names::API_VALIDATOR) {
config
.get_base_mut()
.set_custom_validator_apis(parse_validators(&raw_validators));
}
let gateway = setup_gateway(
&id,
+2
View File
@@ -5,6 +5,7 @@
use std::sync::Arc;
use config_common::defaults::setup_env;
use tauri::Menu;
use tokio::sync::RwLock;
@@ -24,6 +25,7 @@ mod window;
fn main() {
setup_logging();
setup_env(None);
println!("Starting up...");
// As per breaking change description here
+1 -1
View File
@@ -1,6 +1,6 @@
use std::time::Duration;
use ::config::NymConfig;
use ::config_common::NymConfig;
use futures::SinkExt;
use tap::TapFallible;
use tauri::Manager;
+1 -1
View File
@@ -4,7 +4,7 @@ use std::sync::Arc;
use tap::TapFallible;
use tokio::sync::RwLock;
use config::NymConfig;
use config_common::NymConfig;
#[cfg(not(feature = "coconut"))]
use nym_socks5::client::NymClient as Socks5NymClient;
use nym_socks5::client::{config::Config as Socks5Config, Socks5ControlMessageSender};
+1 -1
View File
@@ -1,7 +1,7 @@
{
"package": {
"productName": "nym-connect",
"version": "1.0.0"
"version": "1.0.1"
},
"build": {
"distDir": "../dist",
+7 -4
View File
@@ -22,11 +22,14 @@ export const DefaultLayout: React.FC<{
};
return (
<AppWindowFrame>
<Typography fontWeight="700" fontSize="14px" textAlign="center">
Connect, your privacy will be 100% protected thanks to the Nym Mixnet
<Typography fontWeight="400" fontSize="12px" textAlign="center" sx={{ opacity: 0.6 }}>
This is experimental software. <br />
Do not rely on it for strong anonymity (yet).
</Typography>
<Typography fontWeight="700" fontSize="14px" textAlign="center" color="#60D6EF" pt={2}>
You are not protected now
<Typography fontWeight="700" fontSize="14px" textAlign="center" pt={2}>
Connect to the
<br />
Nym mixnet for privacy.
</Typography>
<ServiceProviderSelector services={services} onChange={handleServiceProviderChange} />
<ConnectionButton
+4 -4
View File
@@ -3996,9 +3996,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.5.4"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
dependencies = [
"aho-corasick",
"memchr",
@@ -4016,9 +4016,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.6.25"
version = "0.6.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]]
name = "remove_dir_all"
@@ -1,6 +1,5 @@
import React, { useState } from 'react';
import { Box, Typography } from '@mui/material';
import { SxProps } from '@mui/system';
import { Box, Typography, SxProps } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { CurrencyDenom, FeeDetails, DecCoin } from '@nymproject/types';
@@ -31,7 +30,7 @@ export const DelegateModal: React.FC<{
estimatedReward?: number;
profitMarginPercentage?: number | null;
nodeUptimePercentage?: number | null;
currency: string;
denom: CurrencyDenom;
initialAmount?: string;
hasVestingContract: boolean;
sx?: SxProps;
@@ -48,7 +47,7 @@ export const DelegateModal: React.FC<{
rewardInterval,
accountBalance,
estimatedReward,
currency,
denom,
profitMarginPercentage,
nodeUptimePercentage,
initialAmount,
@@ -103,7 +102,7 @@ export const DelegateModal: React.FC<{
}
if (amount && Number(amount) < MIN_AMOUNT_TO_DELEGATE) {
errorAmountMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${currency}`;
errorAmountMessage = `Min. delegation amount: ${MIN_AMOUNT_TO_DELEGATE} ${denom.toUpperCase()}`;
newValidatedValue = false;
}
@@ -118,7 +117,7 @@ export const DelegateModal: React.FC<{
const handleOk = async () => {
if (onOk && amount && identityKey) {
onOk(identityKey, { amount, denom: currency as CurrencyDenom }, tokenPool, fee);
onOk(identityKey, { amount, denom }, tokenPool, fee);
}
};
@@ -170,7 +169,7 @@ export const DelegateModal: React.FC<{
onConfirm={handleOk}
>
<ModalListItem label="Node identity key" value={identityKey} divider />
<ModalListItem label="Amount" value={`${amount} ${currency}`} divider />
<ModalListItem label="Amount" value={`${amount} ${denom.toUpperCase()}`} divider />
</ConfirmTx>
);
}
@@ -181,7 +180,7 @@ export const DelegateModal: React.FC<{
onClose={onClose}
onOk={async () => {
if (identityKey && amount) {
handleConfirm({ identity: identityKey, value: { amount, denom: currency as CurrencyDenom } });
handleConfirm({ identity: identityKey, value: { amount, denom } });
}
}}
header={header || 'Delegate'}
@@ -219,6 +218,7 @@ export const DelegateModal: React.FC<{
initialValue={amount}
autoFocus={Boolean(initialIdentityKey)}
onChanged={handleAmountChanged}
denom={denom}
/>
</Box>
<Typography
@@ -247,7 +247,12 @@ export const DelegateModal: React.FC<{
divider
/>
<ModalListItem label="Node est. reward per epoch" value={`${estimatedReward} ${currency}`} hidden divider />
<ModalListItem
label="Node est. reward per epoch"
value={`${estimatedReward} ${denom.toUpperCase()}`}
hidden
divider
/>
</SimpleModal>
);
};
@@ -144,7 +144,7 @@ export const DelegationsActionsMenu: React.FC<{
/>
<DelegationActionsMenuItem
title="Redeem"
description="Trasfer your rewards to your balance"
description="Transfer your rewards to your balance"
Icon={<Typography sx={{ pl: 1 }}>R</Typography>}
onClick={() => handleActionSelect?.('redeem')}
disabled={disableRedeemingRewards}
@@ -61,7 +61,7 @@ export const Delegate = () => {
open={open}
onClose={() => setOpen(false)}
onOk={async () => setOpen(false)}
currency="nym"
denom="nym"
estimatedReward={50.423}
accountBalance="425.2345053"
nodeUptimePercentage={99.28394}
@@ -84,7 +84,7 @@ export const DelegateBelowMinimum = () => {
open={open}
onClose={() => setOpen(false)}
onOk={async () => setOpen(false)}
currency="nym"
denom="nym"
estimatedReward={425.2345053}
nodeUptimePercentage={99.28394}
profitMarginPercentage={11.12334234}
@@ -109,7 +109,7 @@ export const DelegateMore = () => {
onOk={async () => setOpen(false)}
header="Delegate more"
buttonText="Delegate more"
currency="nym"
denom="nym"
estimatedReward={50.423}
accountBalance="425.2345053"
nodeUptimePercentage={99.28394}
@@ -1,6 +1,5 @@
import React from 'react';
import { Box, CircularProgress, Modal, Stack, Typography } from '@mui/material';
import { SxProps } from '@mui/system';
import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material';
const modalStyle: SxProps = {
position: 'absolute',
@@ -60,12 +60,16 @@ export const SimpleModal: React.FC<{
{children}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 2 }}>
{onBack && <StyledBackButton onBack={onBack} />}
<Button variant="contained" fullWidth size="large" onClick={onOk} disabled={okDisabled}>
{okLabel}
</Button>
</Box>
{(onOk || onBack) && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mt: 2 }}>
{onBack && <StyledBackButton onBack={onBack} />}
{onOk && (
<Button variant="contained" fullWidth size="large" onClick={onOk} disabled={okDisabled}>
{okLabel}
</Button>
)}
</Box>
)}
</Box>
</Modal>
);
+3 -4
View File
@@ -2,14 +2,14 @@ import React, { useState, useContext } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { List, ListItem, ListItemIcon, ListItemText } from '@mui/material';
import { AccountBalanceWalletOutlined, ArrowBack, ArrowForward, Description, Settings } from '@mui/icons-material';
import { AppContext } from '../context/main';
import { AppContext } from '../context';
import { Bond, Delegate, Unbond } from '../svg-icons';
export const Nav = () => {
const location = useLocation();
const navigate = useNavigate();
const { isAdminAddress, handleShowSendModal } = useContext(AppContext);
const { isAdminAddress, handleShowSendModal, handleShowReceiveModal } = useContext(AppContext);
const [routesSchema] = useState([
{
@@ -25,9 +25,8 @@ export const Nav = () => {
},
{
label: 'Receive',
route: '/receive',
Icon: ArrowBack,
onClick: () => navigate('/receive'),
onClick: handleShowReceiveModal,
},
{
label: 'Bond',
@@ -0,0 +1,42 @@
import React, { useContext } from 'react';
import { AppContext } from 'src/context';
import { Box, Stack, Typography, SxProps } from '@mui/material';
import QRCode from 'qrcode.react';
import { SimpleModal } from '../Modals/SimpleModal';
import { ClientAddress } from '../ClientAddress';
export const ReceiveModal = ({
onClose,
open,
sx,
backdropProps,
}: {
onClose: () => void;
open: boolean;
sx?: SxProps;
backdropProps?: object;
}) => {
const { clientDetails } = useContext(AppContext);
return (
<SimpleModal
header="Receive"
okLabel="Ok"
onClose={onClose}
open={open}
sx={{ width: 'small', ...sx }}
backdropProps={backdropProps}
>
<Stack spacing={3} sx={{ mt: 1.6 }}>
<Stack direction="row" alignItems="center" gap={4}>
<Typography>Your address:</Typography>
<ClientAddress withCopy showEntireAddress />
</Stack>
<Stack alignItems="center">
<Box sx={{ border: (t) => `1px solid ${t.palette.nym.highlight}`, borderRadius: 2, p: 2 }}>
{clientDetails && <QRCode data-testid="qr-code" value={clientDetails?.client_address} />}
</Box>
</Stack>
</Stack>
</SimpleModal>
);
};
@@ -0,0 +1,12 @@
import React, { useContext } from 'react';
import { AppContext } from 'src/context';
import { ReceiveModal } from './ReceiveModal';
export const Receive = ({ hasStorybookStyles }: { hasStorybookStyles?: {} }) => {
const { showReceiveModal, handleShowReceiveModal } = useContext(AppContext);
if (showReceiveModal)
return <ReceiveModal onClose={handleShowReceiveModal} open={showReceiveModal} {...hasStorybookStyles} />;
return null;
};
@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { Stack, Typography } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { FeeDetails } from '@nymproject/types';
import { CurrencyDenom, FeeDetails } from '@nymproject/types';
import { simulateCompoundDelgatorReward, simulateVestingCompoundDelgatorReward } from 'src/requests';
import { useGetFee } from 'src/hooks/useGetFee';
import { SimpleModal } from '../Modals/SimpleModal';
@@ -14,10 +14,10 @@ export const CompoundModal: React.FC<{
onOk?: (identityKey: string, fee?: FeeDetails) => void;
identityKey: string;
amount: number;
currency: string;
denom: CurrencyDenom;
message: string;
usesVestingTokens: boolean;
}> = ({ open, onClose, onOk, identityKey, amount, currency, message, usesVestingTokens }) => {
}> = ({ open, onClose, onOk, identityKey, amount, denom, message, usesVestingTokens }) => {
const { fee, isFeeLoading, feeError, getFee } = useGetFee();
const handleOk = async () => {
@@ -47,7 +47,7 @@ export const CompoundModal: React.FC<{
<Stack direction="row" justifyContent="space-between" mb={4} mt={identityKey && 4}>
<Typography>Rewards amount:</Typography>
<Typography>
{amount} {currency}
{amount} {denom.toUpperCase()}
</Typography>
</Stack>
@@ -61,7 +61,7 @@ export const RedeemAllRewards = () => {
onClose={() => setOpen(false)}
onOk={async () => setOpen(false)}
message="Redeem all rewards"
currency="nym"
denom="nym"
identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa"
amount={425.65843}
{...storybookStyles(theme)}
@@ -82,7 +82,7 @@ export const RedeemRewardForMixnode = () => {
onClose={() => setOpen(false)}
onOk={async () => setOpen(false)}
message="Redeem rewards"
currency="nym"
denom="nym"
identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa"
amount={425.65843}
{...storybookStyles(theme)}
@@ -103,7 +103,7 @@ export const FeeIsMoreThanAllRewards = () => {
onClose={() => setOpen(false)}
onOk={() => setOpen(false)}
message="Redeem all rewards"
currency="nym"
denom="nym"
identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa"
amount={0.001}
{...storybookStyles(theme)}
@@ -125,7 +125,7 @@ export const FeeIsMoreThanMixnodeReward = () => {
onOk={async () => setOpen(false)}
identityKey="D88RfeY8DttMD3CQKoayV6mss5a5FC3RoH75Kmcujaaa"
message="Redeem rewards"
currency="nym"
denom="nym"
amount={0.001}
{...storybookStyles(theme)}
usesVestingTokens={false}
@@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { Stack, Typography, SxProps } from '@mui/material';
import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField';
import { FeeDetails } from '@nymproject/types';
import { CurrencyDenom, FeeDetails } from '@nymproject/types';
import { useGetFee } from 'src/hooks/useGetFee';
import { simulateClaimDelgatorReward, simulateVestingClaimDelgatorReward } from 'src/requests';
import { ModalFee } from '../Modals/ModalFee';
@@ -14,12 +14,12 @@ export const RedeemModal: React.FC<{
onOk?: (identityKey: string, fee?: FeeDetails) => void;
identityKey: string;
amount: number;
currency: string;
denom: CurrencyDenom;
message: string;
sx?: SxProps;
backdropProps?: Object;
usesVestingTokens: boolean;
}> = ({ open, onClose, onOk, identityKey, amount, currency, message, usesVestingTokens, sx, backdropProps }) => {
}> = ({ open, onClose, onOk, identityKey, amount, denom, message, usesVestingTokens, sx, backdropProps }) => {
const { fee, isFeeLoading, feeError, getFee } = useGetFee();
const handleOk = async () => {
@@ -52,7 +52,7 @@ export const RedeemModal: React.FC<{
<Stack direction="row" justifyContent="space-between" mb={4} mt={identityKey && 4}>
<Typography sx={{ color: 'text.primary' }}>Rewards amount:</Typography>
<Typography sx={{ color: 'text.primary' }}>
{amount} {currency}
{amount} {denom.toUpperCase()}
</Typography>
</Stack>
@@ -46,6 +46,7 @@ export const SendDetails = () => {
fromAddress="nymt1w8qp7zsxggvtxhpqpt6e329j42wtv07dm5ts8u"
toAddress="nymt1w8qp7zsxggvtxhpqpt6e329j42wtv07dm5ts8u"
fee={{ amount: { amount: '0.01', denom: 'nym' }, fee: { Auto: null } }}
denom="nym"
amount={{ amount: '100', denom: 'nym' }}
onPrev={() => {}}
onSend={() => {}}
@@ -1,6 +1,6 @@
import React from 'react';
import { Stack } from '@mui/material';
import { SxProps } from '@mui/system';
import { Stack, SxProps } from '@mui/material';
import { CurrencyDenom } from '@nymproject/types';
import { FeeDetails, DecCoin } from '@nymproject/types';
import { SimpleModal } from '../Modals/SimpleModal';
import { ModalListItem } from '../Modals/ModalListItem';
@@ -10,6 +10,7 @@ export const SendDetailsModal = ({
toAddress,
fromAddress,
fee,
denom,
onClose,
onPrev,
onSend,
@@ -20,6 +21,7 @@ export const SendDetailsModal = ({
toAddress: string;
fee?: FeeDetails;
amount?: DecCoin;
denom: CurrencyDenom;
onClose: () => void;
onPrev: () => void;
onSend: (data: { val: DecCoin; to: string }) => void;
@@ -39,7 +41,7 @@ export const SendDetailsModal = ({
<Stack gap={0.5} sx={{ mt: 4 }}>
<ModalListItem label="From" value={fromAddress} divider />
<ModalListItem label="To" value={toAddress} divider />
<ModalListItem label="Amount" value={`${amount?.amount} ${amount?.denom}`} divider />
<ModalListItem label="Amount" value={`${amount?.amount} ${denom.toUpperCase()}`} divider />
<ModalListItem
label="Fee for this transaction"
value={!fee ? 'n/a' : `${fee.amount?.amount} ${fee.amount?.denom}`}
@@ -1,5 +1,5 @@
import React from 'react';
import { SxProps } from '@mui/system';
import { SxProps } from '@mui/material';
import { SimpleModal } from '../Modals/SimpleModal';
export const SendErrorModal = ({
@@ -1,8 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Stack, TextField, Typography } from '@mui/material';
import { SxProps } from '@mui/system';
import { Stack, TextField, Typography, SxProps } from '@mui/material';
import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField';
import { DecCoin } from '@nymproject/types';
import { CurrencyDenom, DecCoin } from '@nymproject/types';
import { validateAmount } from 'src/utils';
import { SimpleModal } from '../Modals/SimpleModal';
import { ModalListItem } from '../Modals/ModalListItem';
@@ -12,6 +11,7 @@ export const SendInputModal = ({
toAddress,
amount,
balance,
denom,
error,
onNext,
onClose,
@@ -24,6 +24,7 @@ export const SendInputModal = ({
toAddress: string;
amount?: DecCoin;
balance?: string;
denom?: CurrencyDenom;
error?: string;
onNext: () => void;
onClose: () => void;
@@ -69,6 +70,7 @@ export const SendInputModal = ({
validate(value);
}}
initialValue={amount?.amount}
denom={denom}
/>
<Typography fontSize="smaller" sx={{ color: 'error.main' }}>
{error}
+4 -2
View File
@@ -21,7 +21,7 @@ export const SendModal = ({ onClose, hasStorybookStyles }: { onClose: () => void
const [isLoading, setIsLoading] = useState(false);
const [txDetails, setTxDetails] = useState<TTransactionDetails>();
const { clientDetails, userBalance, network, denom } = useContext(AppContext);
const { clientDetails, userBalance, network } = useContext(AppContext);
const { fee, getFee } = useGetFee();
const handleOnNext = async () => {
@@ -47,7 +47,7 @@ export const SendModal = ({ onClose, hasStorybookStyles }: { onClose: () => void
try {
const txResponse = await send({ amount: val, address: to, memo: '', fee: fee?.fee });
setTxDetails({
amount: `${amount?.amount} ${denom}`,
amount: `${amount?.amount} ${clientDetails?.display_mix_denom.toUpperCase()}`,
txUrl: `${urls(network).blockExplorer}/transaction/${txResponse.tx_hash}`,
});
} catch (e) {
@@ -74,6 +74,7 @@ export const SendModal = ({ onClose, hasStorybookStyles }: { onClose: () => void
onClose={onClose}
onPrev={() => setModal('send')}
onSend={handleSend}
denom={clientDetails?.display_mix_denom || 'nym'}
{...hasStorybookStyles}
/>
);
@@ -87,6 +88,7 @@ export const SendModal = ({ onClose, hasStorybookStyles }: { onClose: () => void
onClose={onClose}
onNext={handleOnNext}
error={error}
denom={clientDetails?.display_mix_denom}
onAmountChange={(value) => setAmount(value)}
onAddressChange={(value) => setToAddress(value)}
{...hasStorybookStyles}
@@ -1,6 +1,5 @@
import React from 'react';
import { Stack, Typography } from '@mui/material';
import { SxProps } from '@mui/system';
import { Stack, Typography, SxProps } from '@mui/material';
import { Link } from '@nymproject/react/link/Link';
import { TTransactionDetails } from './types';
import { ConfirmationModal } from '../Modals/ConfirmationModal';
@@ -11,7 +11,7 @@ export const TokenPoolSelector: React.FC<{ disabled: boolean; onSelect: (pool: T
const [value, setValue] = useState<TPoolOption>('balance');
const {
userBalance: { tokenAllocation, balance, fetchBalance, fetchTokenAllocation },
denom,
clientDetails,
} = useContext(AppContext);
useEffect(() => {
@@ -48,7 +48,9 @@ export const TokenPoolSelector: React.FC<{ disabled: boolean; onSelect: (pool: T
{tokenAllocation && (
<ListItemText
primary="Locked"
secondary={`${+tokenAllocation.locked + +tokenAllocation.spendable} ${denom}`}
secondary={`${
+tokenAllocation.locked + +tokenAllocation.spendable
} ${clientDetails?.display_mix_denom.toUpperCase()}`}
sx={{ textTransform: 'uppercase' }}
/>
)}
+1 -1
View File
@@ -74,7 +74,6 @@ export const DelegationContextProvider: FC<{
};
const resetState = () => {
setIsLoading(true);
setError(undefined);
setTotalDelegations(undefined);
setTotalRewards(undefined);
@@ -82,6 +81,7 @@ export const DelegationContextProvider: FC<{
};
const refresh = useCallback(async () => {
setIsLoading(true);
try {
const data = await getDelegationSummary();
const pending = await getAllPendingDelegations();
+8 -6
View File
@@ -2,7 +2,7 @@ import React, { createContext, useEffect, useMemo, useState } from 'react';
import { forage } from '@tauri-apps/tauri-forage';
import { useNavigate } from 'react-router-dom';
import { useSnackbar } from 'notistack';
import { Account, AccountEntry, CurrencyDenom, MixNodeBond } from '@nymproject/types';
import { Account, AccountEntry, MixNodeBond } from '@nymproject/types';
import { getVersion } from '@tauri-apps/api/app';
import { AppEnv, Network } from '../types';
import { TUseuserBalance, useGetBalance } from '../hooks/useGetBalance';
@@ -49,9 +49,10 @@ export type TAppContext = {
loginType?: TLoginType;
showSettings: boolean;
showSendModal: boolean;
denom: Uppercase<CurrencyDenom>;
showReceiveModal: boolean;
handleShowSettings: () => void;
handleShowSendModal: () => void;
handleShowReceiveModal: () => void;
setIsLoading: (isLoading: boolean) => void;
setError: (value?: string) => void;
switchNetwork: (network: Network) => void;
@@ -68,7 +69,6 @@ export const AppContext = createContext({} as TAppContext);
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [clientDetails, setClientDetails] = useState<Account>();
const [denom, setDenom] = useState<Uppercase<CurrencyDenom>>('NYM');
const [storedAccounts, setStoredAccounts] = useState<AccountEntry[]>();
const [mixnodeDetails, setMixnodeDetails] = useState<MixNodeBond | null>(null);
const [network, setNetwork] = useState<Network | undefined>();
@@ -83,6 +83,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const [isAdminAddress, setIsAdminAddress] = useState<boolean>(false);
const [showSettings, setShowSettings] = useState(false);
const [showSendModal, setShowSendModal] = useState(false);
const [showReceiveModal, setShowReceiveModal] = useState(false);
const userBalance = useGetBalance(clientDetails);
const navigate = useNavigate();
@@ -101,7 +102,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
try {
const client = await selectNetwork(n);
setClientDetails(client);
setDenom(client.display_mix_denom.toUpperCase() as Uppercase<CurrencyDenom>);
} catch (e) {
enqueueSnackbar('Error loading account', { variant: 'error' });
Console.error(e as string);
@@ -237,6 +237,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const switchNetwork = (_network: Network) => setNetwork(_network);
const handleShowSettings = () => setShowSettings((show) => !show);
const handleShowSendModal = () => setShowSendModal((show) => !show);
const handleShowReceiveModal = () => setShowReceiveModal((show) => !show);
const handleSwitchMode = () =>
setMode((currentMode) => {
const newMode = currentMode === 'light' ? 'dark' : 'light';
@@ -256,7 +257,6 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
storedAccounts,
mixnodeDetails,
userBalance,
denom,
showAdmin,
showTerminal,
showSettings,
@@ -274,7 +274,9 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
onAccountChange,
handleShowSettings,
showSendModal,
showReceiveModal,
handleShowSendModal,
handleShowReceiveModal,
handleSwitchMode,
}),
[
@@ -294,7 +296,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
showTerminal,
showSettings,
showSendModal,
denom,
showReceiveModal,
],
);
+3 -1
View File
@@ -35,11 +35,12 @@ export const MockMainContextProvider: FC<{}> = ({ children }) => {
fetchTokenAllocation: async () => undefined,
refreshBalances: async () => {},
},
denom: 'NYM',
displayDenom: 'NYM',
showAdmin: false,
showTerminal: false,
showSettings: false,
showSendModal: true,
showReceiveModal: false,
network: 'SANDBOX',
loginType: 'mnemonic',
setIsLoading: () => undefined,
@@ -54,6 +55,7 @@ export const MockMainContextProvider: FC<{}> = ({ children }) => {
onAccountChange: () => undefined,
handleShowSettings: () => undefined,
handleShowSendModal: () => undefined,
handleShowReceiveModal: () => undefined,
}),
[],
);
@@ -16,18 +16,19 @@ export const TransferModal = ({ onClose }: { onClose: () => void }) => {
const [fee, setFee] = useState<FeeDetails>();
const [tx, setTx] = useState<TTransactionDetails>();
const { userBalance, denom, network } = useContext(AppContext);
const { userBalance, clientDetails, network } = useContext(AppContext);
const getFee = async () => {
if (userBalance.tokenAllocation?.spendable && denom) {
if (userBalance.tokenAllocation?.spendable && clientDetails?.display_mix_denom) {
try {
const simulatedFee = await simulateWithdrawVestedCoins({
amount: { amount: userBalance.tokenAllocation?.spendable, denom },
amount: { amount: userBalance.tokenAllocation?.spendable, denom: clientDetails?.display_mix_denom },
});
setFee(simulatedFee);
await userBalance.refreshBalances();
} catch (e) {
setFee({
amount: { amount: 'n/a', denom: denom as CurrencyDenom },
amount: { amount: 'n/a', denom: clientDetails?.display_mix_denom.toUpperCase() as CurrencyDenom },
fee: { Auto: null },
});
Console.error(e);
@@ -40,16 +41,19 @@ export const TransferModal = ({ onClose }: { onClose: () => void }) => {
}, []);
const handleTransfer = async () => {
if (userBalance.tokenAllocation?.spendable && denom) {
if (userBalance.tokenAllocation?.spendable && clientDetails?.display_mix_denom) {
setState('loading');
try {
const txResponse = await withdrawVestedCoins({
amount: userBalance.tokenAllocation?.spendable,
denom: denom as CurrencyDenom,
});
const txResponse = await withdrawVestedCoins(
{
amount: userBalance.tokenAllocation?.spendable,
denom: clientDetails?.display_mix_denom,
},
fee?.fee,
);
setState('success');
setTx({
amount: `${userBalance.tokenAllocation?.spendable} ${denom}`,
amount: `${userBalance.tokenAllocation?.spendable} ${clientDetails.display_mix_denom.toUpperCase()}`,
url: `${urls(network).blockExplorer}/transaction/${txResponse.transaction_hash}`,
});
await userBalance.refreshBalances();
@@ -84,7 +88,7 @@ export const TransferModal = ({ onClose }: { onClose: () => void }) => {
<>
<ModalListItem
label="Unlocked transferrable tokens"
value={`${userBalance.tokenAllocation?.spendable} ${denom}`}
value={`${userBalance.tokenAllocation?.spendable} ${clientDetails?.display_mix_denom.toUpperCase()}`}
divider
/>
<ModalListItem
+7 -5
View File
@@ -35,7 +35,7 @@ const vestingPeriod = (current?: Period, original?: number) => {
};
const VestingSchedule = () => {
const { userBalance, denom } = useContext(AppContext);
const { userBalance, clientDetails } = useContext(AppContext);
const [vestedPercentage, setVestedPercentage] = useState(0);
const calculatePercentage = () => {
@@ -66,7 +66,8 @@ const VestingSchedule = () => {
</TableRow>
<TableRow>
<TableCell sx={{ borderBottom: 'none', textTransform: 'uppercase' }}>
{userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount} {denom}
{userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
{clientDetails?.display_mix_denom.toUpperCase()}
</TableCell>
<TableCell align="left" sx={{ borderBottom: 'none' }}>
{vestingPeriod(userBalance.currentVestingPeriod, userBalance.originalVesting?.number_of_periods)}
@@ -78,7 +79,8 @@ const VestingSchedule = () => {
</Box>
</TableCell>
<TableCell sx={{ borderBottom: 'none', textTransform: 'uppercase' }} align="right">
{userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount} {denom}
{userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
{clientDetails?.display_mix_denom.toUpperCase()}
</TableCell>
</TableRow>
</TableHead>
@@ -88,7 +90,7 @@ const VestingSchedule = () => {
};
const TokenTransfer = () => {
const { userBalance, denom } = useContext(AppContext);
const { userBalance, clientDetails } = useContext(AppContext);
const icon = useCallback(
() => (
<Box sx={{ display: 'flex', mr: 1 }}>
@@ -114,7 +116,7 @@ const TokenTransfer = () => {
fontWeight="700"
textTransform="uppercase"
>
{userBalance.tokenAllocation?.spendable || 'n/a'} {denom}
{userBalance.tokenAllocation?.spendable || 'n/a'} {clientDetails?.display_mix_denom.toUpperCase()}
</Typography>
</Grid>
</Grid>
@@ -63,7 +63,7 @@ export const GatewayForm = ({
resolver: yupResolver(gatewayValidationSchema),
defaultValues,
});
const { userBalance, clientDetails, denom } = useContext(AppContext);
const { userBalance, clientDetails } = useContext(AppContext);
const { fee, getFee, resetFeeState, feeError } = useGetFee();
@@ -209,7 +209,7 @@ export const GatewayForm = ({
fullWidth
label="Amount"
onChanged={(val) => setValue('amount', val, { shouldValidate: true })}
denom={denom}
denom={clientDetails?.display_mix_denom}
validationError={errors.amount?.amount?.message}
/>
</Grid>
@@ -66,7 +66,7 @@ export const MixnodeForm = ({
defaultValues,
});
const { userBalance, clientDetails, denom } = useContext(AppContext);
const { userBalance, clientDetails } = useContext(AppContext);
const { fee, getFee, resetFeeState, feeError } = useGetFee();
@@ -216,7 +216,7 @@ export const MixnodeForm = ({
fullWidth
label="Amount"
onChanged={(val) => setValue('amount', val, { shouldValidate: true })}
denom={denom}
denom={clientDetails?.display_mix_denom}
validationError={errors.amount?.amount?.message}
/>
</Grid>
@@ -5,7 +5,7 @@ import { AppContext } from '../../../context/main';
import { useCheckOwnership } from '../../../hooks/useCheckOwnership';
export const SuccessView: React.FC<{ details?: { amount: string; address: string } }> = ({ details }) => {
const { userBalance, denom } = useContext(AppContext);
const { userBalance, clientDetails } = useContext(AppContext);
const { ownership } = useCheckOwnership();
return (
@@ -15,7 +15,9 @@ export const SuccessView: React.FC<{ details?: { amount: string; address: string
subtitle="Successfully bonded to node with following details"
caption={
ownership.vestingPledge
? `Your current locked balance is: ${userBalance.tokenAllocation?.locked}${denom}`
? `Your current locked balance is: ${
userBalance.tokenAllocation?.locked
} ${clientDetails?.display_mix_denom.toUpperCase()}`
: `Your current balance is: ${userBalance.balance?.printable_balance.toUpperCase()}`
}
/>
@@ -26,7 +28,7 @@ export const SuccessView: React.FC<{ details?: { amount: string; address: string
{ primary: 'Node', secondary: details.address },
{
primary: 'Amount',
secondary: `${details.amount} ${denom}`,
secondary: `${details.amount} ${clientDetails?.display_mix_denom.toUpperCase()}`,
},
]}
/>
+4 -5
View File
@@ -44,7 +44,6 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
const {
clientDetails,
network,
denom,
userBalance: { balance, originalVesting, fetchBalance },
} = useContext(AppContext);
@@ -343,7 +342,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
onOk={handleNewDelegation}
header="Delegate"
buttonText="Delegate stake"
currency={denom}
denom={clientDetails?.display_mix_denom || 'nym'}
accountBalance={balance?.printable_balance}
rewardInterval="weekly"
hasVestingContract={Boolean(originalVesting)}
@@ -359,7 +358,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
header="Delegate more"
buttonText="Delegate more"
identityKey={currentDelegationListActionItem.node_identity}
currency={denom}
denom={clientDetails?.display_mix_denom || 'nym'}
accountBalance={balance?.printable_balance}
nodeUptimePercentage={currentDelegationListActionItem.avg_uptime_percent}
profitMarginPercentage={currentDelegationListActionItem.profit_margin_percent}
@@ -386,7 +385,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
onClose={() => setShowRedeemRewardsModal(false)}
onOk={(identity, fee) => handleRedeem(identity, fee)}
message="Redeem rewards"
currency={denom}
denom={clientDetails?.display_mix_denom || 'nym'}
identityKey={currentDelegationListActionItem?.node_identity}
amount={+currentDelegationListActionItem.accumulated_rewards.amount}
usesVestingTokens={currentDelegationListActionItem.uses_vesting_contract_tokens}
@@ -399,7 +398,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => {
onClose={() => setShowCompoundRewardsModal(false)}
onOk={(identity, fee) => handleCompound(identity, fee)}
message="Compound rewards"
currency={denom}
denom={clientDetails?.display_mix_denom || 'nym'}
identityKey={currentDelegationListActionItem?.node_identity}
amount={+currentDelegationListActionItem.accumulated_rewards.amount}
usesVestingTokens={currentDelegationListActionItem.uses_vesting_contract_tokens}
-1
View File
@@ -2,7 +2,6 @@ export * from './Admin';
export * from './balance';
export * from './bond';
export * from './internal-docs';
export * from './receive';
export * from './auth';
export * from './settings';
export * from './unbond';
-26
View File
@@ -1,26 +0,0 @@
import React, { useContext } from 'react';
import QRCode from 'qrcode.react';
import { Alert, Box, Stack } from '@mui/material';
import { ClientAddress, NymCard } from '../../components';
import { AppContext } from '../../context/main';
import { PageLayout } from '../../layouts';
export const Receive = () => {
const { clientDetails, denom } = useContext(AppContext);
return (
<PageLayout>
<NymCard title={`Receive ${denom}`}>
<Stack spacing={3} alignItems="center">
<Alert severity="info" data-testid="receive-nym" sx={{ width: '100%' }}>
You can receive tokens by providing this address to the sender
</Alert>
<Box>
<ClientAddress withCopy showEntireAddress />
</Box>
{clientDetails && <QRCode data-testid="qr-code" value={clientDetails?.client_address} />}
</Stack>
</NymCard>
</PageLayout>
);
};
+2 -2
View File
@@ -57,8 +57,8 @@ export const vestingBondMixNode = async ({
export const vestingUnbondMixnode = async (fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('vesting_unbond_mixnode', { fee });
export const withdrawVestedCoins = async (amount: DecCoin) =>
invokeWrapper<TransactionExecuteResult>('withdraw_vested_coins', { amount });
export const withdrawVestedCoins = async (amount: DecCoin, fee?: Fee) =>
invokeWrapper<TransactionExecuteResult>('withdraw_vested_coins', { amount, fee });
export const vestingUpdateMixnode = async (profitMarginPercent: number) =>
invokeWrapper<TransactionExecuteResult>('vesting_update_mixnode', { profitMarginPercent });
+3 -2
View File
@@ -3,16 +3,17 @@ import { Route, Routes } from 'react-router-dom';
import { ApplicationLayout } from 'src/layouts';
import { Terminal } from 'src/pages/terminal';
import { Send } from 'src/components/Send';
import { Bond, Balance, InternalDocs, Receive, Unbond, DelegationPage, Admin, Settings } from '../pages';
import { Receive } from '../components/Receive';
import { Bond, Balance, InternalDocs, Unbond, DelegationPage, Admin, Settings } from '../pages';
export const AppRoutes = () => (
<ApplicationLayout>
<Terminal />
<Settings />
<Send />
<Receive />
<Routes>
<Route path="/balance" element={<Balance />} />
<Route path="/receive" element={<Receive />} />
<Route path="/bond" element={<Bond />} />
<Route path="/unbond" element={<Unbond />} />
<Route path="/delegation" element={<DelegationPage />} />
@@ -109,9 +109,14 @@ impl OutboundRequestFilter {
}
/// Attempts to get the root domain, shorn of subdomains, using publicsuffix.
/// If the domain is itself a suffix, then just use the full address as root.
fn get_domain_root(&self, host: &str) -> Option<String> {
match self.domain_list.parse_domain(host) {
Ok(d) => d.root().map(|root| root.to_string()),
Ok(d) => Some(
d.root()
.map(|root| root.to_string())
.unwrap_or_else(|| d.full().to_string()),
),
Err(_) => {
log::warn!("Error parsing domain: {:?}", host);
None // domain couldn't be parsed
@@ -348,9 +353,12 @@ mod tests {
}
#[test]
fn returns_none_on_nonsense_domains() {
fn returns_full_on_suffix_domains() {
let filter = setup();
assert_eq!(None, filter.get_domain_root("flappappa"));
assert_eq!(
Some("s3.amazonaws.com".to_string()),
filter.get_domain_root("s3.amazonaws.com")
);
}
}
@@ -194,17 +194,10 @@ impl StatisticsCollector for ServiceStatisticsCollector {
}
async fn reset_stats(&mut self) {
self.request_stats_data
.write()
.await
.client_processed_bytes
.iter_mut()
.for_each(|(_, b)| *b = 0);
self.request_stats_data.write().await.client_processed_bytes = HashMap::new();
self.response_stats_data
.write()
.await
.client_processed_bytes
.iter_mut()
.for_each(|(_, b)| *b = 0);
.client_processed_bytes = HashMap::new();
}
}
@@ -16,6 +16,7 @@ CREATE TABLE service_statistics
CREATE TABLE gateway_statistics
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
gateway_id VARCHAR NOT NULL,
inbox_count INTEGER NOT NULL,
timestamp DATETIME NOT NULL
);
@@ -35,6 +35,7 @@ pub struct ServiceStatistic {
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct GatewayStatistic {
pub gateway_id: String,
pub inbox_count: u32,
pub timestamp: String,
}
@@ -70,6 +71,7 @@ pub(crate) async fn post_all_statistics(
.into_iter()
.map(|data| {
GenericStatistic::Gateway(GatewayStatistic {
gateway_id: data.gateway_id,
inbox_count: data.inbox_count as u32,
timestamp: data.timestamp.to_string(),
})
@@ -52,11 +52,13 @@ impl StorageManager {
/// * `timestamp`: The moment in time when the data started being collected.
pub(super) async fn insert_gateway_statistics(
&self,
gateway_id: String,
inbox_count: u32,
timestamp: DateTime<Utc>,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO gateway_statistics(inbox_count, timestamp) VALUES (?, ?)",
"INSERT INTO gateway_statistics(gateway_id, inbox_count, timestamp) VALUES (?, ?, ?)",
gateway_id,
inbox_count,
timestamp,
)
@@ -71,7 +71,11 @@ impl NetworkStatisticsStorage {
}
statistics_common::StatsData::Gateway(gateway_data) => {
self.manager
.insert_gateway_statistics(gateway_data.inbox_count, timestamp)
.insert_gateway_statistics(
gateway_data.gateway_id,
gateway_data.inbox_count,
timestamp,
)
.await?
}
}
@@ -17,6 +17,7 @@ pub(crate) struct ServiceStatistics {
pub(crate) struct GatewayStatistics {
#[allow(dead_code)]
pub(crate) id: i64,
pub(crate) gateway_id: String,
pub(crate) inbox_count: i64,
pub(crate) timestamp: NaiveDateTime,
}
-3
View File
@@ -1,3 +0,0 @@
{
"presets": ["@babel/env", "@babel/react"]
}
-98
View File
@@ -1,98 +0,0 @@
# Example with React + Typescript + Webpack 5 + MUI
An example of using default Webpack and Typescript settings with React and MUI, including theming.
You can use this example as a seed for a new project.
Remember to build the dependency packages from the root of this repo by running:
```
yarn
yarn build
```
If you need to make changes to the dependency packages, you can run `yarn watch` in that package to watch for chagnes and build them. This project will pick up the changes in the built package and hot-reload / recompile.
## Features
### Yarn workspaces
Packages from `ts-packages` are shared using Yarn workspaces. Make sure you add you new project to [package.json](../../package.json) to use the shared packages.
> ⚠️ **Warning**: Yarn workspaces will share all dependencies between projects and works by falling back to parent directories until a `node_modules` directory is found. So be careful when messing around with `node_modules` and resolution, because unexpected things could happen - for example, if you do not run `yarn` from the root and you have a `node_modules` in a directory that is a parent of the directory where you checkout out this repository, that `node_modules` will be used for resolving packages 🙀.
### Typescript
Shared Typescript config is in [tsconfig.json](./tsconfig.json), with specific production settings in [tsconfig.prod.json](./tsconfig.prod.json) that:
- exclude Storybook stories and Jest tests
- do not output typing `*.d.ts` files
### Webpack
Inherit config for Webpack 5 with additional tweaks including:
- favicon generation from [favicon asset files](../../assets/favicon/favicon.png)
- asset handling (svg, png, fonts, css, etc)
- minification
The development settings include:
- `ts-loader` for quick transpilation
- threaded type checking using `tsc`
- hot reloading using `react-refresh`
### Storybook
Storybook is available in [@nymproject/react](../react-components/src/stories/Introduction.stories.mdx) and can be run using `yarn storybook`.
### MUI and theming
The [Nym theme](../mui-theme/src/theme/theme.ts) provides a theme provider that you can add as follows:
```typescript jsx
export const App: React.FC = () => (
<AppContextProvider>
<AppTheme>
<Content />
</AppTheme>
</AppContextProvider>
);
export const AppTheme: React.FC = ({ children }) => {
const { mode } = useAppContext();
return <NymThemeProvider mode={mode}>{children}</NymThemeProvider>;
};
export const Content: React.FC = () => {
...
}
```
And augment typings for the Theme by adding [mui-theme.d.ts](./src/theme/mui-theme.d.ts):
```typescript
import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles';
import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme';
declare module '@mui/material/styles' {
interface Theme extends NymTheme {}
interface ThemeOptions extends Partial<NymTheme> {}
interface Palette extends NymPaletteWithExtensions {}
interface PaletteOptions extends NymPaletteWithExtensionsOptions {}
}
```
Adding the above, means that any component now has the correct typings, for example, below the Nym palette interface is available for all MUI `Theme` instances with code completion for VSCode and IntelliJ:
```typescript jsx
import { Typography } from '@mui/material';
...
<Typography sx={{ color: (theme) => theme.palette.nym.networkExplorer.mixnodes.status.active }}>
The quick brown fox jumps over the white fence
</Typography>
```
@@ -1,5 +0,0 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
-85
View File
@@ -1,85 +0,0 @@
{
"name": "@nymproject/apy-playground",
"description": "APY calculator and playground",
"version": "1.0.0",
"license": "Apache-2.0",
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2",
"@mui/material": "^5.0.1",
"@mui/styles": "^5.0.1",
"@mui/icons-material": "^5.5.0",
"@mui/lab": "^5.0.0-alpha.72",
"@nymproject/mui-theme" : "^1.0.0",
"@nymproject/react" : "^1.0.0",
"@cosmjs/math": "^0.27.1"
},
"devDependencies": {
"@babel/core": "^7.15.0",
"@babel/plugin-transform-async-to-generator": "^7.14.5",
"@babel/preset-env": "^7.15.0",
"@babel/preset-react": "^7.14.5",
"@babel/preset-typescript": "^7.15.0",
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@nymproject/webpack": "^1.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@svgr/webpack": "^6.1.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@types/jest": "^27.0.1",
"@types/node": "^16.7.13",
"@types/react": "^17.0.34",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"babel-loader": "^8.2.2",
"babel-plugin-root-import": "^5.1.0",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"dotenv-webpack": "^7.0.3",
"eslint": "^8.10.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-root-import": "^1.0.4",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.1.1",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.2",
"eslint-plugin-react-hooks": "^4.3.0",
"favicons": "^6.2.2",
"favicons-webpack-plugin": "^5.0.2",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^7.2.1",
"html-webpack-plugin": "^5.3.2",
"jest": "^27.1.0",
"mini-css-extract-plugin": "^2.2.2",
"prettier": "^2.5.1",
"react-refresh-typescript": "^2.0.3",
"style-loader": "^3.2.1",
"thread-loader": "^3.0.4",
"ts-jest": "^27.0.5",
"ts-loader": "^9.2.5",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.2",
"url-loader": "^4.1.1",
"webpack": "^5.64.3",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.5.0",
"webpack-favicons": "^1.3.8",
"webpack-merge": "^5.8.0"
},
"scripts": {
"start": "webpack serve --progress --port 3000",
"build": "webpack build --progress --config webpack.prod.js",
"build:dev": "webpack build --progress",
"build:serve": "npx serve dist",
"test": "jest",
"test:watch": "jest --watch",
"tsc": "tsc",
"tsc:watch": "tsc --watch",
"lint": "eslint src",
"lint:fix": "eslint src --fix"
}
}
-51
View File
@@ -1,51 +0,0 @@
import * as React from 'react';
import { Box, Container, Grid, Typography } from '@mui/material';
import { NymLogo } from '@nymproject/react/logo/NymLogo';
import { Playground } from '@nymproject/react/playground/Playground';
import { useIsMounted } from '@nymproject/react/hooks/useIsMounted';
import { NymThemeProvider } from '@nymproject/mui-theme';
import { useTheme } from '@mui/material/styles';
import { ThemeToggle } from './ThemeToggle';
import { AppContextProvider, useAppContext } from './context';
import { MixNodes } from './components/MixNodes';
export const AppTheme: React.FC = ({ children }) => {
const { mode } = useAppContext();
return <NymThemeProvider mode={mode}>{children}</NymThemeProvider>;
};
export const Content: React.FC = () => {
const { mode } = useAppContext();
const theme = useTheme();
const isMounted = useIsMounted();
if (isMounted()) {
console.log('Content is mounted');
}
return (
<Box sx={{ px: 4, py: 4 }}>
<Box display="flex" justifyContent="space-between" pb={2}>
<Box display="flex" alignItems="center">
<NymLogo height={50} />
<Box ml={2}>
<h1>APY Playground</h1>
</Box>
</Box>
<Box>
<ThemeToggle />
</Box>
</Box>
<MixNodes />
</Box>
);
};
export const App: React.FC = () => (
<AppContextProvider>
<AppTheme>
<Content />
</AppTheme>
</AppContextProvider>
);
@@ -1,21 +0,0 @@
import * as React from 'react';
import { Button, Typography } from '@mui/material';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
import { useAppContext } from './context';
export const ThemeToggle: React.FC = () => {
const { mode, toggleMode } = useAppContext();
return (
<Button variant="outlined" color="secondary" onClick={toggleMode} sx={{ display: 'flex', alignItems: 'centre' }}>
{mode === 'dark' ? (
<DarkModeIcon sx={{ color: (theme) => theme.palette.text.secondary }} />
) : (
<LightModeIcon sx={{ color: (theme) => theme.palette.text.secondary }} />
)}
<Typography ml={1} color={(theme) => theme.palette.primary.light}>
Switch to {mode === 'dark' ? 'light mode' : 'dark mode'}
</Typography>
</Button>
);
};
@@ -1,716 +0,0 @@
import * as React from 'react';
import CircularProgress from '@mui/material/CircularProgress';
import {
Checkbox,
Stack,
Box,
IconButton,
Paper,
Slider,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
tableCellClasses,
TableContainer,
Typography,
Link,
Chip,
} from '@mui/material';
import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown';
import ArrowDropUpIcon from '@mui/icons-material/ArrowDropUp';
import { Currency } from '@nymproject/react/currency/Currency';
import { CurrencyAmountString } from '@nymproject/react/currency/CurrencyAmount';
import RestartAltIcon from '@mui/icons-material/RestartAlt';
import { useTheme } from '@mui/material/styles';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline';
import { Api, useAppContext } from '../context';
import { toMajorCurrencyFromCoin } from '../utils/coin';
import { round } from '../utils/round';
import {
MixNodeBondWithDetails,
RewardEstimation,
RewardEstimationParamsForSliders,
RewardEstimationWithAPY,
} from '../context/types';
const NETWORK_EXPLORER_BASE_URL = 'https://explorer.nymtech.net';
const MAJOR_AMOUNT_FOR_CALCS = 1000;
const selectionChanceToProb = (value: string): number => {
switch (value.toLowerCase()) {
case 'veryhigh':
return 0.95;
case 'high':
return 0.8;
case 'moderate':
return 0.6;
case 'low':
return 0.25;
default:
return 0.05;
}
};
const MinorValue: React.FC<{
value?: number;
decimals?: number;
}> = ({ value, decimals = 3 }) =>
// <CurrencyAmountString
// majorAmount={value ? round(value / 1_000_000, decimals).toString() : undefined}
// sx={{ flexDirection: 'row-reverse' }}
// />
value ? <span>{round(value / 1_000_000, decimals)}</span> : <span>-</span>;
const TableCellValue: React.FC<{
value?: number;
decimals?: number;
suffix?: string;
}> = ({ value, suffix, decimals = 0 }) => (
<TableCell align="right">
{value ? round(value, decimals) : '-'}
{suffix && ` ${suffix}`}
</TableCell>
);
const ResultValue: React.FC<{
value?: number;
decimals?: number;
}> = ({ value, decimals = 0 }) => (
<>
<TableCell align="right">
<MinorValue value={value ? value * 24 : undefined} decimals={decimals} />
</TableCell>
<TableCell align="right">
<MinorValue value={value ? value * 24 * 30 : undefined} decimals={decimals} />
</TableCell>
<TableCell align="right">
<MinorValue value={value ? value * 24 * 365 : undefined} decimals={decimals} />
</TableCell>
</>
);
const SliderWithValue: React.FC<{
label: string;
value?: number;
min?: number;
max?: number;
scaleValue?: number;
onChange: (value?: number) => void;
onReset: () => void;
display: React.ReactNode;
}> = ({ label, value, min, max, onChange, onReset, display, scaleValue = 1 }) => {
const minScaled = min !== undefined ? min * scaleValue : undefined;
const maxScaled = max !== undefined ? max * scaleValue : undefined;
const valueScaled = value !== undefined ? value * scaleValue : undefined;
console.log({ label, minScaled, maxScaled, valueScaled });
return (
<TableRow>
<TableCell width="20%">{label}</TableCell>
<TableCell width="30%" align="left">
<Stack spacing={2} direction="row">
<Slider
value={valueScaled}
min={minScaled}
max={maxScaled}
onChange={(_event, newValue) => {
const scaledNewValue = (newValue as number) / scaleValue;
console.log({ label, minScaled, maxScaled, valueScaled, scaledNewValue });
onChange(scaledNewValue);
}}
/>
<IconButton>
<RestartAltIcon opacity={0.15} onClick={onReset} />
</IconButton>
</Stack>
</TableCell>
<TableCell width="50%">{display}</TableCell>
</TableRow>
);
};
export const InclusionProbabilityDisplay: React.FC<{
isActive?: boolean;
value: string;
}> = ({ isActive, value }) => (
<Stack
direction="row"
spacing={1}
color={(theme) =>
isActive
? theme.palette.nym.networkExplorer.mixnodes.status.active
: theme.palette.nym.networkExplorer.mixnodes.status.standby
}
>
{isActive ? (
<Box color="inherit">
<CheckCircleOutlineIcon fontSize="small" color="inherit" />
</Box>
) : (
<Box color="inherit">
<PauseCircleOutlineIcon fontSize="small" color="inherit" />
</Box>
)}
<Box color="inherit">{value}</Box>
</Stack>
);
export const MixNodeRow: React.FC<{ index: number; mixnode: MixNodeBondWithDetails }> = ({ index, mixnode }) => {
const theme = useTheme();
const [open, setOpen] = React.useState<boolean>(false);
const [showRaw, setShowRaw] = React.useState<boolean>(false);
const ref = React.useRef<NodeJS.Timeout | null>(null);
const [result, setResult] = React.useState<RewardEstimationWithAPY | undefined>();
const [defaultResult, setDefaultResult] = React.useState<RewardEstimation | undefined>();
const defaultParams: RewardEstimationParamsForSliders = {
pledge_amount: +(Number.parseFloat(mixnode.mixnode_bond.pledge_amount.amount) / 1_000_000),
uptime: mixnode.uptime,
total_delegation: +(Number.parseFloat(mixnode.mixnode_bond.total_delegation.amount) / 1_000_000),
is_active: true,
};
const [params, setParams] = React.useState<RewardEstimationParamsForSliders>(defaultParams);
const handleChange = (prop: string) => (value: any) => {
setParams((prevState) => ({ ...prevState, [prop]: value }));
};
const handleReset = (prop: string) => () =>
setParams((prevState) => ({ ...prevState, [prop]: (defaultParams as any)[prop] }));
React.useEffect(() => {
if (ref.current) {
clearTimeout(ref.current);
}
ref.current = setTimeout(() => calculate(), 250);
}, [params.is_active, params.pledge_amount, params.uptime, params.total_delegation]);
const calculate = async () => {
const res = await Api.computeRewardEstimation(mixnode.mixnode_bond.mix_node.identity_key, {
...params,
total_delegation: Math.floor(params.total_delegation * 1_000_000),
pledge_amount: Math.floor(params.pledge_amount * 1_000_000),
});
const majorAmountToUseInCalcs = MAJOR_AMOUNT_FOR_CALCS;
const operatorReward = (res.estimated_operator_reward / 1_000_000) * 24; // epoch_reward * 1 epoch_per_hour * 24 hours
const delegatorsReward = (res.estimated_delegators_reward / 1_000_000) * 24;
const totalPledge = Number.parseFloat(mixnode.mixnode_bond.pledge_amount.amount) / 1_000_000;
// const totalDelegations = Number.parseFloat(mixnode.mixnode_bond.total_delegation.amount) / 1_000_000;
const operatorRewardScaled = majorAmountToUseInCalcs * (operatorReward / params.pledge_amount);
const delegatorReward = majorAmountToUseInCalcs * (delegatorsReward / params.total_delegation);
const nodeApy = ((operatorReward + delegatorsReward) / (totalPledge + params.total_delegation)) * 365 * 100;
const res2: RewardEstimationWithAPY = {
...res,
estimates: {
majorAmountToUseInCalcs,
nodeApy,
operator: {
apy: (operatorRewardScaled / majorAmountToUseInCalcs) * 365 * 100,
rewardMajorAmount: {
daily: operatorRewardScaled,
monthly: operatorRewardScaled * 30,
yearly: operatorRewardScaled * 365,
},
},
delegator: {
apy: (delegatorReward / majorAmountToUseInCalcs) * 365 * 100,
rewardMajorAmount: {
daily: delegatorReward,
monthly: delegatorReward * 30,
yearly: delegatorReward * 365,
},
},
},
};
if (!defaultResult) {
setDefaultResult(res);
} else {
setResult(res2);
}
};
React.useEffect(() => {
if (open && !result) {
calculate();
}
}, [open, result]);
const bond = toMajorCurrencyFromCoin(mixnode.mixnode_bond.pledge_amount);
const totalDelegation = toMajorCurrencyFromCoin(mixnode.mixnode_bond.total_delegation);
const totalDelegationFloat = Number.parseFloat(totalDelegation?.amount || '1');
let color;
// eslint-disable-next-line default-case
switch (mixnode.status) {
case 'active':
color = theme.palette.nym.networkExplorer.mixnodes.status.active;
break;
case 'standby':
color = theme.palette.nym.networkExplorer.mixnodes.status.standby;
break;
}
return (
<>
<TableRow>
<TableCell>
{open ? (
<IconButton onClick={() => setOpen(false)}>
<ArrowDropUpIcon />
</IconButton>
) : (
<IconButton onClick={() => setOpen(true)}>
<ArrowDropDownIcon />
</IconButton>
)}
<Chip sx={{ ml: 1 }} label={`${index + 1}`} variant="outlined" />
</TableCell>
<TableCell>
<Link
href={`${NETWORK_EXPLORER_BASE_URL}/network-components/mixnode/${mixnode.mixnode_bond.mix_node.identity_key}`}
target="_blank"
>
{mixnode.mixnode_bond.mix_node.identity_key.slice(0, 6)}
...
{mixnode.mixnode_bond.mix_node.identity_key.slice(-6)}
</Link>
</TableCell>
<TableCell>
<Currency majorAmount={bond} showCoinMark coinMarkPrefix hideFractions sx={{ fontSize: 14 }} />
</TableCell>
<TableCell>
<Currency majorAmount={totalDelegation} showCoinMark coinMarkPrefix hideFractions sx={{ fontSize: 14 }} />
</TableCell>
<TableCell>
<Typography color={(theme) => (mixnode.stake_saturation > 1 ? theme.palette.warning.main : undefined)}>
{round(mixnode.stake_saturation * 100, 1)}%
</Typography>
</TableCell>
<TableCell>
<Typography fontSize="inherit" color={color}>
{mixnode.status}
</Typography>
</TableCell>
<TableCell>{round(mixnode.uptime, 0)}%</TableCell>
<TableCell>{mixnode.mixnode_bond.mix_node.profit_margin_percent}%</TableCell>
<TableCell>
{mixnode.inclusion_probability && (
<InclusionProbabilityDisplay isActive value={mixnode.inclusion_probability.in_active} />
)}
</TableCell>
<TableCell>{round(mixnode.estimated_operator_apy, 0)}%</TableCell>
<TableCell>
<Currency
majorAmount={{
amount: defaultResult
? ((defaultResult.estimated_operator_reward / 1_000_000) * 24 * 365).toString()
: '',
denom: 'NYM',
}}
showCoinMark
coinMarkPrefix
hideFractions
sx={{ fontSize: 14 }}
/>
</TableCell>
<TableCell>{round(mixnode.estimated_delegators_apy, 0)}%</TableCell>
<TableCell>
<Currency
majorAmount={{
amount: defaultResult
? (
(MAJOR_AMOUNT_FOR_CALCS * ((defaultResult.estimated_delegators_reward / 1_000_000) * 24 * 365)) /
totalDelegationFloat
).toString()
: '',
denom: 'NYM',
}}
showCoinMark
coinMarkPrefix
hideFractions
sx={{ fontSize: 14 }}
/>
</TableCell>
<TableCell>
{mixnode.inclusion_probability &&
round(mixnode.estimated_delegators_apy * selectionChanceToProb(mixnode.inclusion_probability.in_active), 0)}
%
</TableCell>
<TableCell>
<Currency
majorAmount={{
amount:
defaultResult && mixnode.inclusion_probability
? (
((MAJOR_AMOUNT_FOR_CALCS * ((defaultResult.estimated_delegators_reward / 1_000_000) * 24 * 365)) /
totalDelegationFloat) *
selectionChanceToProb(mixnode.inclusion_probability.in_active)
).toString()
: '',
denom: 'NYM',
}}
showCoinMark
coinMarkPrefix
hideFractions
sx={{ fontSize: 14 }}
/>
</TableCell>
</TableRow>
{open && (
<TableRow>
<TableCell colSpan={12}>
<Paper elevation={3} sx={{ px: 2, py: 2 }}>
<Box>
<Table size="small">
<TableBody>
<SliderWithValue
label="Pledge"
value={params.pledge_amount}
min={0}
max={Math.max(1_000_000, (defaultParams.pledge_amount || 0) * 2)}
// max={Math.max(1_000_000_000_000, (params.pledge_amount || 0) * 1.2)}
onChange={handleChange('pledge_amount')}
onReset={handleReset('pledge_amount')}
display={
<Stack direction="row" spacing={2}>
<CurrencyAmountString majorAmount={params.pledge_amount?.toString()} hideFractions />
<span>nym</span>
</Stack>
}
/>
<SliderWithValue
label="Total delegations"
min={0}
max={Math.max(1_000_000, (defaultParams.total_delegation || 0) * 2)}
value={params.total_delegation}
onChange={handleChange('total_delegation')}
onReset={handleReset('total_delegation')}
display={
<Stack direction="row" spacing={2}>
<CurrencyAmountString majorAmount={params.total_delegation?.toString()} hideFractions />
<span>nym</span>
</Stack>
}
/>
<SliderWithValue
label="Uptime"
min={0}
max={100}
value={params.uptime}
onChange={handleChange('uptime')}
onReset={handleReset('uptime')}
display={<span>{params.uptime}%</span>}
/>
<TableRow>
<TableCell width="20%">In active set?</TableCell>
<TableCell width="30%" align="left">
<Checkbox
checked={params.is_active === true}
onChange={(_, checked) => {
handleChange('is_active')(checked);
}}
/>
<IconButton>
<RestartAltIcon opacity={0.15} onClick={handleReset('is_active')} />
</IconButton>
</TableCell>
<TableCell width="50%">{params.is_active === undefined ? '-' : `${params.is_active}`}</TableCell>
</TableRow>
{result && (
<>
<TableRow>
<TableCell colSpan={4}>
<Box>
<TableContainer>
<Table
sx={{
'& .MuiTableRow-root:hover': {
backgroundColor: 'grey.800',
},
[`& .${tableCellClasses.root}`]: {
borderBottom: 'none',
},
}}
>
<TableHead>
<TableRow>
<TableCell colSpan={1} />
<TableCell colSpan={5} align="center">
<strong>Total rewards</strong>
</TableCell>
<TableCell colSpan={4} align="center">
<strong>
When {result.estimates.majorAmountToUseInCalcs} NYM is staked,
<br />
estimated rewards in NYM are:
</strong>
</TableCell>
<TableCell />
</TableRow>
<TableRow>
<TableCell />
<TableCell align="right" sx={{ opacity: 0.2 }}>
<strong>Current per day</strong>
</TableCell>
<TableCell align="right">
<strong>Est. per day</strong>
</TableCell>
<TableCell align="right">
<strong>Est. per month</strong>
</TableCell>
<TableCell align="right">
<strong>Est. per year</strong>
</TableCell>
<TableCell />
<TableCell align="right">
<strong>Daily</strong>
</TableCell>
<TableCell align="right">
<strong>Monthly</strong>
</TableCell>
<TableCell align="right">
<strong>Annual</strong>
</TableCell>
<TableCell align="right">
<strong>APY</strong>
</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>Total node reward</TableCell>
<TableCell sx={{ opacity: 0.3 }} align="right">
<MinorValue value={defaultResult?.estimated_total_node_reward} />
</TableCell>
<ResultValue value={result.estimated_total_node_reward} />
<TableCell sx={{ opacity: 0.3 }}>nym</TableCell>
<TableCell />
<TableCell />
<TableCell />
<TableCellValue value={result.estimates.nodeApy} decimals={0} suffix="%" />
</TableRow>
<TableRow>
<TableCell>Operator reward</TableCell>
<TableCell sx={{ opacity: 0.3 }} align="right">
<MinorValue value={defaultResult?.estimated_operator_reward} />
</TableCell>
<ResultValue value={result.estimated_operator_reward} />
<TableCell sx={{ opacity: 0.3 }}>nym</TableCell>
<TableCellValue
value={result.estimates.operator.rewardMajorAmount.daily}
decimals={3}
/>
<TableCellValue value={result.estimates.operator.rewardMajorAmount.monthly} />
<TableCellValue value={result.estimates.operator.rewardMajorAmount.yearly} />
<TableCellValue value={result.estimates.operator.apy} suffix="%" />
</TableRow>
<TableRow>
<TableCell>All delegators reward</TableCell>
<TableCell sx={{ opacity: 0.3 }} align="right">
<MinorValue value={defaultResult?.estimated_delegators_reward} />
</TableCell>
<ResultValue value={result.estimated_delegators_reward} />
<TableCell sx={{ opacity: 0.3 }}>nym</TableCell>
<TableCellValue
value={result.estimates.delegator.rewardMajorAmount.daily}
decimals={3}
/>
<TableCellValue value={result.estimates.delegator.rewardMajorAmount.monthly} />
<TableCellValue value={result.estimates.delegator.rewardMajorAmount.yearly} />
<TableCellValue value={result.estimates.delegator.apy} suffix="%" />
</TableRow>
<TableRow>
<TableCell>Node profit</TableCell>
<TableCell sx={{ opacity: 0.3 }} align="right">
<MinorValue value={defaultResult?.estimated_node_profit} />
</TableCell>
<ResultValue value={result.estimated_node_profit} />
<TableCell sx={{ opacity: 0.3 }}>nym</TableCell>
</TableRow>
<TableRow>
<TableCell>Operator cost</TableCell>
<TableCell sx={{ opacity: 0.3 }} align="right">
<MinorValue value={defaultResult?.estimated_operator_cost} />
</TableCell>
<ResultValue value={result.estimated_operator_cost} />
<TableCell sx={{ opacity: 0.3 }}>nym</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Box>
<Box mt={2}>
Raw values
{showRaw ? (
<IconButton onClick={() => setShowRaw(false)}>
<ArrowDropUpIcon />
</IconButton>
) : (
<IconButton onClick={() => setShowRaw(true)}>
<ArrowDropDownIcon />
</IconButton>
)}
</Box>
</TableCell>
</TableRow>
{showRaw && (
<TableRow>
<TableCell>Raw Result</TableCell>
<TableCell>
<pre>
{JSON.stringify(
{
result,
mixnode,
},
null,
2,
)}
</pre>
</TableCell>
</TableRow>
)}
</>
)}
</TableBody>
</Table>
</Box>
</Paper>
</TableCell>
</TableRow>
)}
</>
);
};
export const MixNodes: React.FC = () => {
const { loading, mixnodes, rewardParams } = useAppContext();
if (loading) {
return <CircularProgress />;
}
return (
<>
<TableContainer>
<Table
sx={{
'& .MuiTableRow-root:hover': {
backgroundColor: 'grey.A700',
},
}}
>
<TableHead>
<TableRow>
<TableCell colSpan={9} />
<TableCell colSpan={4} align="center" sx={{ background: (theme) => theme.palette.divider }}>
Maximum achievable values
<br />
(when always in the active set)
</TableCell>
<TableCell colSpan={2} align="center">
More realistic values
<br />
(scaled by selection probability)
</TableCell>
</TableRow>
<TableRow>
<TableCell />
<TableCell>Identity</TableCell>
<TableCell>Pledge</TableCell>
<TableCell>Total delegations</TableCell>
<TableCell>Saturation</TableCell>
<TableCell>Status</TableCell>
<TableCell>Uptime</TableCell>
<TableCell>Profit Margin</TableCell>
<TableCell>Selection Probability</TableCell>
<TableCell sx={{ background: (theme) => theme.palette.divider }}>Est. Operator APY</TableCell>
<TableCell sx={{ background: (theme) => theme.palette.divider }}>Annual operator rewards</TableCell>
<TableCell sx={{ background: (theme) => theme.palette.divider }}>Est. All Delegators APY</TableCell>
<TableCell sx={{ background: (theme) => theme.palette.divider }}>
Annual delegator rewards
<br />
for staking {MAJOR_AMOUNT_FOR_CALCS} NYM
</TableCell>
<TableCell>Est. All Delegators APY</TableCell>
<TableCell>
Annual delegator rewards
<br />
for staking {MAJOR_AMOUNT_FOR_CALCS} NYM
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{(mixnodes || []).map((m, i) => (
<MixNodeRow key={m.mixnode_bond.mix_node.identity_key} index={i} mixnode={m} />
))}
</TableBody>
</Table>
</TableContainer>
<Box mt={6}>
<h3>Reward Params (for epoch)</h3>
</Box>
<Table>
<TableRow>
<TableCell>Epoch reward pool</TableCell>
<TableCell>
<Currency
coinMarkPrefix
showCoinMark
hideFractions
majorAmount={
rewardParams?.epoch_reward_pool
? toMajorCurrencyFromCoin({
amount: rewardParams.epoch_reward_pool,
denom: 'unym',
})
: undefined
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Rewarded set size</TableCell>
<TableCell>{rewardParams?.rewarded_set_size}</TableCell>
</TableRow>
<TableRow>
<TableCell>Active set size</TableCell>
<TableCell>{rewardParams?.active_set_size}</TableCell>
</TableRow>
<TableRow>
<TableCell>Staking supply</TableCell>
<TableCell>
<Currency
coinMarkPrefix
showCoinMark
hideFractions
majorAmount={
rewardParams?.staking_supply
? toMajorCurrencyFromCoin({
amount: rewardParams.staking_supply,
denom: 'unym',
})
: undefined
}
/>
</TableCell>
</TableRow>
<TableRow>
<TableCell>Sybil resistance percent</TableCell>
<TableCell>{rewardParams?.sybil_resistance_percent}</TableCell>
</TableRow>
<TableRow>
<TableCell>Active set work factor</TableCell>
<TableCell>{rewardParams?.active_set_work_factor}</TableCell>
</TableRow>
</Table>
</>
);
};
@@ -1,109 +0,0 @@
import { PaletteMode } from '@mui/material';
import * as React from 'react';
import {
InclusionProbability,
MixNodeBondWithDetails,
RewardEstimation,
RewardEstimationParams,
RewardParams,
} from './types';
// const API_BASE = 'https://qa-validator-api.nymtech.net/api';
const API_BASE = 'https://validator-apy.dev.nymte.ch/api';
interface State {
mode: PaletteMode;
toggleMode: () => void;
loading: boolean;
mixnodes: MixNodeBondWithDetails[] | undefined;
rewardParams: RewardParams | undefined;
}
const AppContext = React.createContext<State | undefined>(undefined);
export const Api = {
computeRewardEstimation: async (identityKey: string, params: RewardEstimationParams): Promise<RewardEstimation> => {
const response = await fetch(`${API_BASE}/v1/status/mixnode/${identityKey}/compute-reward-estimation`, {
method: 'POST',
body: JSON.stringify(params),
});
return response.json();
},
getMixnodesDetailed: async (): Promise<MixNodeBondWithDetails[]> => {
const response = await fetch(`${API_BASE}/v1/mixnodes/detailed`);
const items = (await response.json()) as MixNodeBondWithDetails[];
const page = items
.sort((a, b) => {
const amountA = Number.parseFloat(a.mixnode_bond.total_delegation.amount);
const amountB = Number.parseFloat(b.mixnode_bond.total_delegation.amount);
return amountB - amountA;
})
.slice(0, 100);
await Promise.all(
page.map(async (item) => {
const status = await Api.getMixnodeStatus(item.mixnode_bond.mix_node.identity_key);
const probability = await Api.getMixnodeInclusionProbability(item.mixnode_bond.mix_node.identity_key);
// eslint-disable-next-line no-param-reassign
item.status = status;
// eslint-disable-next-line no-param-reassign
item.inclusion_probability = probability;
}),
);
return page;
},
getRewardParams: async (): Promise<RewardParams> => {
const response = await fetch(`${API_BASE}/v1/epoch/reward_params`);
const params = (await response.json()) as RewardParams;
return params;
},
getMixnodeStatus: async (identityKey: string): Promise<string> => {
const response = await fetch(`${API_BASE}/v1/status/mixnode/${identityKey}/status`);
return (await response.json()).status;
},
getMixnodeInclusionProbability: async (identityKey: string): Promise<InclusionProbability> => {
const response = await fetch(`${API_BASE}/v1/status/mixnode/${identityKey}/inclusion-probability`);
return (await response.json()) as InclusionProbability;
},
};
export const useAppContext = (): State => {
const context = React.useContext<State | undefined>(AppContext);
if (!context) {
throw new Error('Please include a `import { AppContextProvider } from "./context"` before using this hook');
}
return context;
};
export const AppContextProvider: React.FC = ({ children }) => {
// light/dark mode
const [mode, setMode] = React.useState<PaletteMode>('dark');
const [loading, setLoading] = React.useState<boolean>(false);
const [mixnodes, setMixnodes] = React.useState<MixNodeBondWithDetails[] | undefined>();
const [rewardParams, setRewardParams] = React.useState<RewardParams | undefined>();
const refresh = async () => {
setMixnodes(await Api.getMixnodesDetailed());
setRewardParams(await Api.getRewardParams());
};
React.useEffect(() => {
setLoading(true);
refresh().finally(() => setLoading(false));
}, []);
const value = React.useMemo<State>(
() => ({
mode,
toggleMode: () => setMode((prevMode) => (prevMode !== 'light' ? 'light' : 'dark')),
loading,
mixnodes,
rewardParams,
}),
[mode, mixnodes, loading, rewardParams],
);
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
};
@@ -1,101 +0,0 @@
export interface RewardParams {
epoch_reward_pool: string;
rewarded_set_size: string;
active_set_size: string;
staking_supply: string;
sybil_resistance_percent: number;
active_set_work_factor: number;
}
export interface RewardEstimationParams {
uptime?: number;
is_active?: boolean;
pledge_amount?: number;
total_delegation?: number;
}
export interface RewardEstimationParamsForSliders {
uptime: number;
is_active: boolean;
pledge_amount: number;
total_delegation: number;
}
export interface RewardEstimation {
estimated_total_node_reward: number;
estimated_operator_reward: number;
estimated_delegators_reward: number;
estimated_node_profit: number;
estimated_operator_cost: number;
reward_params: any;
as_at: number;
}
export interface RewardEstimationWithAPY {
estimated_total_node_reward: number;
estimated_operator_reward: number;
estimated_delegators_reward: number;
estimated_node_profit: number;
estimated_operator_cost: number;
reward_params: any;
as_at: number;
estimates: {
majorAmountToUseInCalcs: number;
nodeApy: number;
operator: {
apy: number;
rewardMajorAmount: {
daily: number;
monthly: number;
yearly: number;
};
};
delegator: {
apy: number;
rewardMajorAmount: {
daily: number;
monthly: number;
yearly: number;
};
};
};
}
export interface MixNodeBondWithDetails {
mixnode_bond: {
pledge_amount: {
denom: string;
amount: string;
};
total_delegation: {
denom: string;
amount: string;
};
owner: string;
layer: number;
block_height: number;
mix_node: {
host: string;
mix_port: number;
verloc_port: number;
http_api_port: number;
sphinx_key: string;
identity_key: string;
version: string;
profit_margin_percent: number;
};
proxy: null;
accumulated_rewards: string;
};
stake_saturation: number;
uptime: number;
estimated_operator_apy: number;
estimated_delegators_apy: number;
status?: string;
inclusion_probability?: InclusionProbability;
}
export interface InclusionProbability {
in_active: string;
in_reserve: string;
}
-14
View File
@@ -1,14 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Nym APY Playground</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<div id="app"></div>
</body>
</html>
-5
View File
@@ -1,5 +0,0 @@
import * as React from 'react';
import ReactDOM from 'react-dom';
import { App } from './App';
ReactDOM.render(<App />, document.getElementById('app'));
@@ -1,13 +0,0 @@
import React, { render } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { App } from '../App';
describe('App', () => {
beforeEach(() => {
render(<App />);
});
it('should render without exploding', () => {
const { container } = render(<App />);
expect(container.firstChild).toBeInTheDocument();
});
});
-37
View File
@@ -1,37 +0,0 @@
/* eslint-disable no-shadow,@typescript-eslint/no-unused-vars,@typescript-eslint/no-empty-interface,import/no-extraneous-dependencies */
import { Theme, ThemeOptions, Palette, PaletteOptions } from '@mui/material/styles';
import { NymTheme, NymPaletteWithExtensions, NymPaletteWithExtensionsOptions } from '@nymproject/mui-theme';
/**
* If you are unfamiliar with Material UI theming, please read the following first:
* - https://mui.com/customization/theming/
* - https://mui.com/customization/palette/
* - https://mui.com/customization/dark-mode/#dark-mode-with-custom-palette
*
* This file adds typings to the theme using Typescript's module augmentation.
*
* Read the following if you are unfamiliar with module augmentation and declaration merging. Then
* look at the recommendations from Material UI docs for implementation:
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
* - https://www.typescriptlang.org/docs/handbook/declaration-merging.html#merging-interfaces
* - https://mui.com/customization/palette/#adding-new-colors
*
*
* IMPORTANT:
*
* The type augmentation must match MUI's definitions. So, notice the use of `interface` rather than
* `type Foo = { ... }` - this is necessary to merge the definitions.
*/
declare module '@mui/material/styles' {
/**
* This augments the definitions of the MUI Theme with the Nym theme, as well as
* a partial `ThemeOptions` type used by `createTheme`
*
* IMPORTANT: only add extensions to the interfaces above, do not modify the lines below
*/
interface Theme extends NymTheme {}
interface ThemeOptions extends Partial<NymTheme> {}
interface Palette extends NymPaletteWithExtensions {}
interface PaletteOptions extends NymPaletteWithExtensionsOptions {}
}
@@ -1,23 +0,0 @@
import { Decimal } from '@cosmjs/math';
import { MajorCurrencyAmount } from '@nymproject/types';
export const toMajorCurrency = (amount: string, denom: string): MajorCurrencyAmount => {
if (denom[0].toLowerCase() !== 'u') {
return {
amount,
denom: denom as any,
};
}
const decimal = Decimal.fromAtomics(amount, 6);
return {
amount: decimal.toString(),
denom: denom.slice(1) as any,
};
};
export const toMajorCurrencyFromCoin = (coin?: { amount: string; denom: string }): MajorCurrencyAmount | undefined => {
if (!coin) {
return undefined;
}
return toMajorCurrency(coin.amount, coin.denom);
};
@@ -1,23 +0,0 @@
/**
* Reproduce the behaviour of Python's round method
* @param value The floating point number to round
* @param decimals The number of decimals to round to, e.g. 11.4999 to 2 decimals is 11.50
*/
export const round = (value: number, decimals: number = 0): number => {
if (decimals === 0) {
return Math.round(value);
}
const pow = 10 ** decimals;
return +Math.round(value * pow) / pow;
// return +(Math.round(Number.parseFloat(value + `e+${decimals}`)) + `e-${decimals}`)
};
/**
* Round returning 0 when value is undefined
*/
export const roundWithDefault = (value?: number, decimals: number = 0): number => {
if (!value) {
return 0;
}
return round(value, decimals);
};
-16
View File
@@ -1,16 +0,0 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"jsx": "react-jsx",
"outDir": "./dist"
},
"include": [
"src/**/*.ts",
"src/**/*.tsx"
],
"exclude": [
"node_modules",
"build",
"dist"
]
}

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