Compare commits

...

29 Commits

Author SHA1 Message Date
tommy a6ac1cf5e7 one more removal 2022-08-08 15:20:14 +02:00
tommy 0ba2a3b724 update text to reflect the correct opts args 2022-08-08 15:18:54 +02:00
tommy 96d5fd7612 update the version 2022-08-08 15:15:52 +02:00
tommy a1b7b43da2 Resolve PR comments 2022-08-08 15:11:24 +02:00
tommy 123cbf7aff add change log entry 2022-08-05 15:42:40 +02:00
tommy c4d12e7a8e cargo fmt - and the project to cargo.toml 2022-08-05 15:34:24 +02:00
tommy bf22a13b8f Revert "Validator Command Binary - Draft"
This reverts commit 3429e0a8e9.
2022-08-05 12:19:12 +02:00
tommy 8f5dd00027 minor changes 2022-08-04 13:41:53 +02:00
tommy 84c43ebf54 Draft - Validator CLI
- validator  binary - to enable easy to use commands on the network
- contains all operations (vesting / normal)
2022-08-04 13:38:59 +02:00
Dave Hrycyszyn b957b939cf Typo fix 2022-08-04 11:01:36 +01: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
123 changed files with 4439 additions and 444 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/"
+22 -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
@@ -29,6 +25,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
- validator-client: add `denom` argument and add simple test for querying an account balance
- gateway, validator-api: Checks for coconut credential double spending attempts, taking the coconut bandwidth contract as source of truth ([#1457])
- coconut-bandwidth-contract: Record the state of a coconut credential; create specific proposal for releasing funds ([#1457])
- add validator-client script rust binary /tools/validator-client-scripts
### Fixed
@@ -39,10 +36,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 +51,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 +73,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
+33
View File
@@ -1606,6 +1606,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"task",
"thiserror",
"tokio",
"validator-client",
@@ -5396,6 +5397,15 @@ dependencies = [
"tokio",
]
[[package]]
name = "streaming-stats"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0d670ce4e348a2081843569e0f79b21c99c91bb9028b3b3ecb0f050306de547"
dependencies = [
"num-traits",
]
[[package]]
name = "stringprep"
version = "0.1.2"
@@ -6283,6 +6293,29 @@ dependencies = [
"vesting-contract-common",
]
[[package]]
name = "validator-client-scripts"
version = "0.1.0"
dependencies = [
"base64",
"bip39",
"bs58",
"clap 3.2.8",
"csv",
"dotenv",
"log",
"mixnet-contract-common",
"network-defaults",
"pretty_env_logger",
"serde",
"serde_json",
"streaming-stats",
"tokio",
"url",
"validator-client",
"vesting-contract-common",
]
[[package]]
name = "valuable"
version = "0.1.0"
+2 -1
View File
@@ -68,7 +68,8 @@ members = [
"service-providers/network-statistics",
"validator-api",
"validator-api/validator-api-requests",
"tools/ts-rs-cli"
"tools/ts-rs-cli",
"tools/validator-client-scripts"
]
default-members = [
+4 -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
@@ -69,6 +69,9 @@ build-wallet:
build-connect:
cargo build --manifest-path nym-connect/Cargo.toml --workspace
build-validator-scripts:
cargo build --release --manifest-path tools/validator-client-scripts/Cargo.toml
fmt-main:
cargo fmt --all
+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",
+1 -1
View File
@@ -32,7 +32,7 @@ yarn install
## Development mode
You can compile nym-connectin development mode by running the following command inside the `nym-connect` directory:
You can compile nym-connect in development mode by running the following command inside the `nym-connect` directory:
```
yarn dev
+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,
}
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "validator-client-scripts"
version = "1.0.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
base64 = "0.13.0"
bs58 = "0.4"
clap = { version = "3.2.8", features = ["derive"] }
dotenv = "0.15.0"
log = "0.4"
pretty_env_logger = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1.11", features = [ "net", "rt-multi-thread", "macros", "signal"] }
bip39 = "1.0.1"
validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] }
network-defaults = { path = "../../common/network-defaults" }
mixnet-contract-common ={ path = "../../common/cosmwasm-smart-contracts/mixnet-contract" }
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract" }
+32
View File
@@ -0,0 +1,32 @@
# NYM - CLI TOOL
## Overview
Set of simple CLI commands to perform basic actions, such as upload contract, bond mixnode, etc.
Note: to work it requires rust 1.60+
## The binary
- Build the binary
- cargo build --release --bin validator-client-scripts
- this will compile the binary to "your/main/nym/path/target/release"
- the binary is named ./validator-client-scripts
## Executing commands
-./validator-client-scripts --help
- will produce a list of it's capabilites
The binary takes optional args which are as follows -> OPTIONS:
```
--config-env-file <CONFIG_ENV_FILE>
--mixnet-contract <MIXNET_CONTRACT>
--mnemonic <MNEMONIC>
--nymd-url <NYMD_URL>
--vesting-contract <VESTING_CONTRACT>
```
If you specify --config-env-file it will read the values from the envs/directory:
`./validator-client-scripts --config-env-file env/qa.env .....` and you don't need to supply the
mixnet-contract nor the vesting-contract argument
An example of a command is as follows:
```
Disclaimer the amount is in UNYMs
./validator-client-scripts --config-env-file ../../envs/qa.env --nymd-url https://qa-validator.nymtech.net --mnemonic "INPUT YOUR MNEMONIC" send --amount 100000000 --recipient <NYM_ADDRESS>
```
@@ -0,0 +1,20 @@
CONFIGURED=true
RUST_LOG=info
RUST_BACKTRACE=1
BECH32_PREFIX=n
MIX_DENOM=unym
MIX_DENOM_DISPLAY=nym
STAKE_DENOM=unyx
STAKE_DENOM_DISPLAY=nyx
DENOMS_EXPONENT=6
MIXNET_CONTRACT_ADDRESS=n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g
VESTING_CONTRACT_ADDRESS=n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw
BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
MULTISIG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://127.0.0.1:8090"
NYMD_VALIDATOR="https://rpc.nyx.nodes.guru/"
API_VALIDATOR="https://validator.nymtech.net/api/"
@@ -0,0 +1,20 @@
CONFIGURED=true
RUST_LOG=info
RUST_BACKTRACE=1
BECH32_PREFIX=n
MIX_DENOM=unym
MIX_DENOM_DISPLAY=nym
STAKE_DENOM=unyx
STAKE_DENOM_DISPLAY=nyx
DENOMS_EXPONENT=6
MIXNET_CONTRACT_ADDRESS=n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep
VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav
BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0
COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw
MULTISIG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs
REWARDING_VALIDATOR_ADDRESS=n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
NYMD_VALIDATOR="https://qa-validator.nymtech.net"
API_VALIDATOR="https://qa-validator-api.nymtech.net/api"
@@ -0,0 +1,78 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::Client;
use clap::Parser;
use log::{info, warn};
use mixnet_contract_common::Coin;
use network_defaults::{DEFAULT_CLIENT_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT};
#[derive(Debug, Parser)]
pub(crate) struct Args {
#[clap(long)]
pub host: String,
#[clap(long)]
pub signature: String,
#[clap(long)]
pub mix_port: Option<u16>,
#[clap(long)]
pub clients_port: Option<u16>,
#[clap(long)]
pub location: Option<String>,
#[clap(long)]
pub sphinx_key: String,
#[clap(long)]
pub identity_key: String,
#[clap(long)]
pub version: String,
#[clap(
long,
help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')"
)]
pub amount: u128,
#[clap(long)]
pub gas: Option<u64>,
#[clap(short, long)]
pub force: bool,
}
pub(crate) async fn bond_gateway(client: Client, args: Args, denom: &str) {
info!("Starting gateway bonding!");
// if we're trying to bond less than 1 token
if args.amount < 1_000_000 && !args.force {
warn!("You're trying to bond only {}{} which is less than 1 full token. Are you sure that's what you want? If so, run with `--force` or `-f` flag", args.amount, denom);
return;
}
let gateway = mixnet_contract_common::Gateway {
host: args.host,
mix_port: args.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT),
clients_port: args.clients_port.unwrap_or(DEFAULT_CLIENT_LISTENING_PORT),
location: args
.location
.unwrap_or_else(|| "secret gateway location".to_owned()),
sphinx_key: args.sphinx_key,
identity_key: args.identity_key,
version: args.version,
};
let coin = Coin::new(args.amount, denom);
let res = client
.bond_gateway(gateway, args.signature, coin.into(), None)
.await
.expect("failed to bond gateway!");
info!("Bonding result: {:?}", res)
}
@@ -0,0 +1,84 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::Client;
use clap::Parser;
use log::{info, warn};
use mixnet_contract_common::Coin;
use network_defaults::{
DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT,
};
#[derive(Debug, Parser)]
pub(crate) struct Args {
#[clap(long)]
pub host: String,
#[clap(long)]
pub signature: String,
#[clap(long)]
pub mix_port: Option<u16>,
#[clap(long)]
pub verloc_port: Option<u16>,
#[clap(long)]
pub http_api_port: Option<u16>,
#[clap(long)]
pub sphinx_key: String,
#[clap(long)]
pub identity_key: String,
#[clap(long)]
pub version: String,
#[clap(long)]
pub profit_margin_percent: Option<u8>,
#[clap(
long,
help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')"
)]
pub amount: u128,
#[clap(long)]
pub gas: Option<u64>,
#[clap(short, long)]
pub force: bool,
}
pub(crate) async fn bond_mixnode(client: Client, args: Args, denom: &str) {
info!("Starting mixnode bonding!");
// if we're trying to bond less than 1 token
if args.amount < 1_000_000 && !args.force {
warn!("You're trying to bond only {}{} which is less than 1 full token. Are you sure that's what you want? If so, run with `--force` or `-f` flag", args.amount, denom);
return;
}
let mixnode = mixnet_contract_common::MixNode {
host: args.host,
mix_port: args.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT),
verloc_port: args.verloc_port.unwrap_or(DEFAULT_VERLOC_LISTENING_PORT),
http_api_port: args
.http_api_port
.unwrap_or(DEFAULT_HTTP_API_LISTENING_PORT),
sphinx_key: args.sphinx_key,
identity_key: args.identity_key,
version: args.version,
profit_margin_percent: args.profit_margin_percent.unwrap_or(10),
};
let coin = Coin::new(args.amount, denom);
let res = client
.bond_mixnode(mixnode, args.signature, coin.into(), None)
.await
.expect("failed to bond mixnode!");
info!("Bonding result: {:?}", res)
}
@@ -0,0 +1,26 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::Client;
use clap::Parser;
use log::info;
#[derive(Debug, Parser)]
pub(crate) struct Args {
#[clap(long)]
pub gas: Option<u64>,
#[clap(long)]
pub identity: String,
}
pub(crate) async fn claim_delegator_reward(client: Client, args: Args) {
info!("Claim delegator reward");
let res = client
.execute_claim_delegator_reward(args.identity, None)
.await
.expect("failed to claim delegator-reward");
info!("Claiming delegator reward: {:?}", res)
}
@@ -0,0 +1,23 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::Client;
use clap::Parser;
use log::info;
#[derive(Debug, Parser)]
pub(crate) struct Args {
#[clap(long)]
pub gas: Option<u64>,
}
pub(crate) async fn claim_operator_reward(client: Client) {
info!("Claim operator reward");
let res = client
.execute_claim_operator_reward(None)
.await
.expect("failed to claim operator reward");
info!("Claiming operator reward: {:?}", res)
}
@@ -0,0 +1,26 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::Client;
use clap::Parser;
use log::info;
#[derive(Debug, Parser)]
pub(crate) struct Args {
#[clap(long)]
pub gas: Option<u64>,
#[clap(long)]
pub identity: String,
}
pub(crate) async fn compound_delegator_reward(client: Client, args: Args) {
info!("compound delegator reward");
let res = client
.execute_compound_delegator_reward(args.identity, None)
.await
.expect("failed to claim delegator-reward");
info!("Claiming delegator reward: {:?}", res)
}
@@ -0,0 +1,23 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::Client;
use clap::Parser;
use log::info;
#[derive(Debug, Parser)]
pub(crate) struct Args {
#[clap(long)]
pub gas: Option<u64>,
}
pub(crate) async fn compound_operator_reward(client: Client, _args: Args) {
info!("compounding operator reward");
let res = client
.execute_compound_operator_reward(None)
.await
.expect("failed to compound operator-reward");
info!("Claiming compound operator reward: {:?}", res)
}
@@ -0,0 +1,24 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Parser;
use validator_client::nymd::wallet::DirectSecp256k1HdWallet;
#[derive(Debug, Parser)]
pub(crate) struct Args {
#[clap(long)]
// allowed values are 12, 18 or 24
pub word_count: Option<usize>,
}
pub(crate) fn create_account(args: Args, prefix: &str) {
let word_count = args.word_count.unwrap_or(24);
let mnemonic = bip39::Mnemonic::generate(word_count).expect("failed to generate mnemonic!");
let wallet =
DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic).expect("failed to build wallet!");
// Output address and mnemonics into separate lines for easier parsing
println!("{}", wallet.mnemonic());
println!("{}", wallet.try_derive_accounts().unwrap()[0].address());
}
@@ -0,0 +1,17 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::Parser;
#[derive(Debug, Parser)]
pub(crate) struct Args {
#[clap(short, long)]
pub key: String,
}
pub(crate) fn decode_mixnode_key(args: Args) {
let b64_decoded = base64::decode(args.key).expect("failed to decode base64 string");
let b58_encoded = bs58::encode(&b64_decoded).into_string();
println!("{}", b58_encoded)
}
@@ -0,0 +1,32 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::Client;
use clap::Parser;
use log::info;
use mixnet_contract_common::Coin;
#[derive(Debug, Parser)]
pub(crate) struct Args {
#[clap(long)]
pub gas: Option<u64>,
#[clap(long)]
pub identity_key: String,
#[clap(long)]
pub amount: u128,
}
pub(crate) async fn delegate_to_mixnode(client: Client, args: Args, denom: &str) {
info!("Starting delegation to mixnode");
let coin = Coin::new(args.amount, denom);
let res = client
.delegate_to_mixnode(&*args.identity_key, coin.into(), None)
.await
.expect("failed to delegate to mixnode!");
info!("delegating to mixnode: {:?}", res);
}
@@ -0,0 +1,74 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::Client;
use clap::Parser;
use log::info;
use mixnet_contract_common::InstantiateMsg;
use validator_client::nymd::cosmwasm_client::types::{ContractCodeId, InstantiateOptions};
#[derive(Debug, Parser)]
pub(crate) struct Args {
#[clap(long)]
pub code_id: ContractCodeId,
#[clap(long)]
pub memo: Option<String>,
#[clap(long)]
pub label: Option<String>,
#[clap(long)]
pub init_message: Option<String>,
#[clap(long)]
pub gas: Option<u64>,
}
pub(crate) async fn init(client: Client, args: Args, denom: &str) {
info!("Starting contract instantiation!");
let memo = args
.memo
.unwrap_or_else(|| "contract instantiation".to_owned());
let label = args
.label
.unwrap_or_else(|| "Nym mixnet smart contract".to_owned());
// by default we make ourselves an admin, let me know if you don't like that behaviour
let opts = Some(InstantiateOptions {
funds: vec![],
admin: Some(client.address().clone()),
});
// the EmptyMsg{} argument is equivalent to `--init-message='{}'`
let res = if let Some(raw_msg) = args.init_message {
let msg: serde_json::Value =
serde_json::from_str(&raw_msg).expect("failed to parse init message");
client
.instantiate(args.code_id, &msg, label, memo, opts, None)
.await
.expect("failed to instantiate the contract!")
} else {
let address = client.address().to_string();
client
.instantiate(
args.code_id,
&InstantiateMsg {
rewarding_validator_address: address,
mixnet_denom: denom.to_string(),
},
label,
memo,
opts,
None,
)
.await
.expect("failed to instantiate the contract!")
};
info!("Init result: {:?}", res);
println!("{}", res.contract_address)
}

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