Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f77e350211 |
@@ -9,6 +9,7 @@
|
|||||||
target
|
target
|
||||||
.env
|
.env
|
||||||
.env.dev
|
.env.dev
|
||||||
|
envs/devnet.env
|
||||||
/.vscode/settings.json
|
/.vscode/settings.json
|
||||||
validator/.vscode
|
validator/.vscode
|
||||||
sample-configs/validator-config.toml
|
sample-configs/validator-config.toml
|
||||||
|
|||||||
+3
-1
@@ -103,6 +103,7 @@ members = [
|
|||||||
"nym-outfox",
|
"nym-outfox",
|
||||||
"tools/internal/ssl-inject",
|
"tools/internal/ssl-inject",
|
||||||
"tools/internal/sdk-version-bump",
|
"tools/internal/sdk-version-bump",
|
||||||
|
"tools/internal/dkg-manager",
|
||||||
"tools/nym-cli",
|
"tools/nym-cli",
|
||||||
"tools/nym-nr-query",
|
"tools/nym-nr-query",
|
||||||
"tools/ts-rs-cli",
|
"tools/ts-rs-cli",
|
||||||
@@ -160,7 +161,8 @@ serde = "1.0.152"
|
|||||||
serde_json = "1.0.91"
|
serde_json = "1.0.91"
|
||||||
tap = "1.0.1"
|
tap = "1.0.1"
|
||||||
thiserror = "1.0.48"
|
thiserror = "1.0.48"
|
||||||
tokio = "1.24.1"
|
tokio = "1.33.0"
|
||||||
|
tokio-util = "0.7.10"
|
||||||
tokio-tungstenite = "0.20.1"
|
tokio-tungstenite = "0.20.1"
|
||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tungstenite = { version = "0.20.1", default-features = false }
|
tungstenite = { version = "0.20.1", default-features = false }
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
tokio = { version = "1.24.1", features = ["time", "net", "rt"] }
|
tokio = { workspace = true, features = ["time", "net", "rt"] }
|
||||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
tokio-util = { workspace = true, features = ["codec"] }
|
||||||
|
|
||||||
# internal
|
# internal
|
||||||
nym-sphinx = { path = "../../nymsphinx" }
|
nym-sphinx = { path = "../../nymsphinx" }
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@ pub trait CoconutBandwidthSigningClient {
|
|||||||
fee: Option<Fee>,
|
fee: Option<Fee>,
|
||||||
) -> Result<ExecuteResult, NyxdError> {
|
) -> Result<ExecuteResult, NyxdError> {
|
||||||
let req = CoconutBandwidthExecuteMsg::DepositFunds {
|
let req = CoconutBandwidthExecuteMsg::DepositFunds {
|
||||||
data: DepositData::new(info.to_string(), verification_key, encryption_key),
|
data: DepositData::new(info, verification_key, encryption_key),
|
||||||
};
|
};
|
||||||
self.execute_coconut_bandwidth_contract(
|
self.execute_coconut_bandwidth_contract(
|
||||||
fee,
|
fee,
|
||||||
|
|||||||
+22
-2
@@ -6,8 +6,8 @@ use crate::nyxd::error::NyxdError;
|
|||||||
use crate::nyxd::CosmWasmClient;
|
use crate::nyxd::CosmWasmClient;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use cw3::{
|
use cw3::{
|
||||||
ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterListResponse,
|
ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterDetail,
|
||||||
VoterResponse,
|
VoterListResponse, VoterResponse,
|
||||||
};
|
};
|
||||||
use cw_utils::ThresholdResponse;
|
use cw_utils::ThresholdResponse;
|
||||||
use nym_multisig_contract_common::msg::QueryMsg as MultisigQueryMsg;
|
use nym_multisig_contract_common::msg::QueryMsg as MultisigQueryMsg;
|
||||||
@@ -114,6 +114,26 @@ pub trait PagedMultisigQueryClient: MultisigQueryClient {
|
|||||||
|
|
||||||
Ok(proposals)
|
Ok(proposals)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_all_voters(&self) -> Result<Vec<VoterDetail>, NyxdError> {
|
||||||
|
let mut voters = Vec::new();
|
||||||
|
let mut start_after = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut paged_response = self.list_voters(start_after.take(), None).await?;
|
||||||
|
|
||||||
|
let last_voter = paged_response.voters.last().map(|prop| prop.addr.clone());
|
||||||
|
voters.append(&mut paged_response.voters);
|
||||||
|
|
||||||
|
if let Some(start_after_res) = last_voter {
|
||||||
|
start_after = Some(start_after_res)
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(voters)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ pub use cosmrs::Coin as CosmosCoin;
|
|||||||
pub use cosmrs::Gas;
|
pub use cosmrs::Gas;
|
||||||
pub use cosmrs::{bip32, AccountId, Denom};
|
pub use cosmrs::{bip32, AccountId, Denom};
|
||||||
pub use cosmwasm_std::Coin as CosmWasmCoin;
|
pub use cosmwasm_std::Coin as CosmWasmCoin;
|
||||||
|
pub use cw2;
|
||||||
|
pub use cw3;
|
||||||
|
pub use cw4;
|
||||||
|
pub use cw_controllers;
|
||||||
pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
|
pub use fee::{gas_price::GasPrice, GasAdjustable, GasAdjustment};
|
||||||
pub use tendermint_rpc::{
|
pub use tendermint_rpc::{
|
||||||
endpoint::{tx::Response as TxResponse, validators::Response as ValidatorResponse},
|
endpoint::{tx::Response as TxResponse, validators::Response as ValidatorResponse},
|
||||||
|
|||||||
@@ -174,17 +174,19 @@ impl Display for EpochState {
|
|||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
EpochState::PublicKeySubmission { resharing } => {
|
EpochState::PublicKeySubmission { resharing } => {
|
||||||
write!(f, "PublicKeySubmission with resharing {resharing}")
|
write!(f, "PublicKeySubmission (resharing: {resharing})")
|
||||||
|
}
|
||||||
|
EpochState::DealingExchange { resharing } => {
|
||||||
|
write!(f, "DealingExchange (resharing: {resharing})")
|
||||||
}
|
}
|
||||||
EpochState::DealingExchange { resharing } => write!(f, "DealingExchange {resharing}"),
|
|
||||||
EpochState::VerificationKeySubmission { resharing } => {
|
EpochState::VerificationKeySubmission { resharing } => {
|
||||||
write!(f, "VerificationKeySubmission with resharing {resharing}")
|
write!(f, "VerificationKeySubmission (resharing: {resharing})")
|
||||||
}
|
}
|
||||||
EpochState::VerificationKeyValidation { resharing } => {
|
EpochState::VerificationKeyValidation { resharing } => {
|
||||||
write!(f, "VerificationKeyValidation with resharing {resharing}")
|
write!(f, "VerificationKeyValidation (resharing: {resharing})")
|
||||||
}
|
}
|
||||||
EpochState::VerificationKeyFinalization { resharing } => {
|
EpochState::VerificationKeyFinalization { resharing } => {
|
||||||
write!(f, "VerificationKeyFinalization with resharing {resharing}")
|
write!(f, "VerificationKeyFinalization (resharing: {resharing})")
|
||||||
}
|
}
|
||||||
EpochState::InProgress => write!(f, "InProgress"),
|
EpochState::InProgress => write!(f, "InProgress"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ tokio = { version = "1.24.1", features = [
|
|||||||
"net",
|
"net",
|
||||||
"io-util",
|
"io-util",
|
||||||
] }
|
] }
|
||||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
tokio-util = { workspace = true, features = ["codec"] }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ repository = { workspace = true }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytes = "1.0"
|
bytes = "1.0"
|
||||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
tokio-util = { workspace = true, features = ["codec"] }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] }
|
nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] }
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytes = "1.0"
|
bytes = "1.0"
|
||||||
tokio = { version = "1.24.1", features = [ "net", "io-util", "sync", "macros", "time", "rt-multi-thread" ] }
|
tokio = { workspace = true, features = [ "net", "io-util", "sync", "macros", "time", "rt-multi-thread" ] }
|
||||||
tokio-util = { version = "0.7.4", features = [ "io" ] } # reason for getting this guy is to to able to port to tokio 1.X more quickly by being able to use
|
tokio-util = { workspace = true, features = [ "io" ] } # reason for getting this guy is to to able to port to tokio 1.X more quickly by being able to use
|
||||||
# their `read_buf` [from the util crate] replacement rather than having to rethink/reimplement `AvailableReader` with the new AsyncRead trait definition.
|
# their `read_buf` [from the util crate] replacement rather than having to rethink/reimplement `AvailableReader` with the new AsyncRead trait definition.
|
||||||
# In the long run, the dependency should probably get removed in favour of pure-tokio implementation, but for time being it's fine.
|
# In the long run, the dependency should probably get removed in favour of pure-tokio implementation, but for time being it's fine.
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@ serde_json = "1.0.91"
|
|||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { version = "1", features = ["macros", "net","rt-multi-thread"] }
|
tokio = { version = "1", features = ["macros", "net","rt-multi-thread"] }
|
||||||
tokio-tungstenite = { workspace = true }
|
tokio-tungstenite = { workspace = true }
|
||||||
tokio-util = { version = "0.7.4", features = ["full"] }
|
tokio-util = { workspace = true, features = ["full"] }
|
||||||
toml = "0.7.0"
|
toml = "0.7.0"
|
||||||
unsigned-varint = "0.7.1"
|
unsigned-varint = "0.7.1"
|
||||||
utoipa = { workspace = true, features = ["actix_extras"] }
|
utoipa = { workspace = true, features = ["actix_extras"] }
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ tokio = { workspace = true, features = [
|
|||||||
] }
|
] }
|
||||||
tokio-stream = { version = "0.1.11", features = ["fs"] }
|
tokio-stream = { version = "0.1.11", features = ["fs"] }
|
||||||
tokio-tungstenite = { version = "0.20.1" }
|
tokio-tungstenite = { version = "0.20.1" }
|
||||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
tokio-util = { workspace = true, features = ["codec"] }
|
||||||
url = { workspace = true, features = ["serde"] }
|
url = { workspace = true, features = ["serde"] }
|
||||||
zeroize = { workspace = true }
|
zeroize = { workspace = true }
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -33,8 +33,8 @@ rand = "0.7.3"
|
|||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
sysinfo = "0.27.7"
|
sysinfo = "0.27.7"
|
||||||
tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] }
|
tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] }
|
||||||
tokio-util = { version = "0.7.3", features = ["codec"] }
|
tokio-util = { workspace = true, features = ["codec"] }
|
||||||
toml = "0.5.8"
|
toml = "0.5.8"
|
||||||
url = { workspace = true, features = ["serde"] }
|
url = { workspace = true, features = ["serde"] }
|
||||||
cfg-if = "1.0.0"
|
cfg-if = "1.0.0"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ nym-bin-common = { path = "../../../common/bin-common" }
|
|||||||
# extra dependencies for libp2p examples
|
# extra dependencies for libp2p examples
|
||||||
libp2p = { git = "https://github.com/ChainSafe/rust-libp2p.git", rev = "e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6", features = [ "identify", "macros", "ping", "tokio", "tcp", "dns", "websocket", "noise", "mplex", "yamux", "gossipsub" ]}
|
libp2p = { git = "https://github.com/ChainSafe/rust-libp2p.git", rev = "e3440d25681df380c9f0f8cfdcfd5ecc0a4f2fb6", features = [ "identify", "macros", "ping", "tokio", "tcp", "dns", "websocket", "noise", "mplex", "yamux", "gossipsub" ]}
|
||||||
tokio-stream = "0.1.12"
|
tokio-stream = "0.1.12"
|
||||||
tokio-util = { version = "0.7", features = ["codec"] }
|
tokio-util = { workspace = true, features = ["codec"] }
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
[package]
|
||||||
|
name = "dkg-manager"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
documentation.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
bip39 = { workspace = true }
|
||||||
|
clap = { workspace = true }
|
||||||
|
futures = { workspace = true }
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
log = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
tokio-util = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||||
|
url = { workspace = true }
|
||||||
|
time = "0.3.30"
|
||||||
|
|
||||||
|
ratatui = { version = "0.24.0", features = ["serde", "macros"] }
|
||||||
|
crossterm = { version = "0.27.0", features = ["serde", "event-stream"] }
|
||||||
|
tui-input = { version = "0.8.0", features = ["serde"] }
|
||||||
|
tui-logger = { version = "0.10", features = ["tracing-support"] }
|
||||||
|
throbber-widgets-tui = "0.3.0"
|
||||||
|
|
||||||
|
# panic handler:
|
||||||
|
better-panic = "0.3.0"
|
||||||
|
color-eyre = "0.6.2"
|
||||||
|
strip-ansi-escapes = "0.2.0"
|
||||||
|
|
||||||
|
nym-validator-client = { path = "../../../common/client-libs/validator-client" }
|
||||||
|
nym-bin-common = { path = "../../../common/bin-common" }
|
||||||
|
nym-network-defaults = { path = "../../../common/network-defaults" }
|
||||||
|
nym-coconut-dkg-common = { path = "../../../common/cosmwasm-smart-contracts/coconut-dkg" }
|
||||||
|
nym-contracts-common = { path = "../../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||||
|
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::nyxd::DkgState;
|
||||||
|
use nym_coconut_dkg_common::dealer::{ContractDealing, DealerDetails};
|
||||||
|
use nym_coconut_dkg_common::types::Epoch;
|
||||||
|
use nym_validator_client::nyxd::{cw4, cw_controllers};
|
||||||
|
use serde::{Serialize, Serializer};
|
||||||
|
|
||||||
|
use crate::components::basic_contract_info::BasicContractInfo;
|
||||||
|
use nym_coconut_dkg_common::verification_key::ContractVKShare;
|
||||||
|
use tokio::sync::mpsc::error::SendError;
|
||||||
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
|
use tui_logger::TuiWidgetEvent;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
pub struct ContractsInfo {
|
||||||
|
pub dkg: DkgInfo,
|
||||||
|
pub group: GroupInfo,
|
||||||
|
pub bandwidth: BandwidthInfo,
|
||||||
|
pub multisig: MultisigInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
pub struct DkgInfo {
|
||||||
|
pub base: BasicContractInfo,
|
||||||
|
|
||||||
|
pub debug_state: DkgState,
|
||||||
|
pub epoch: Epoch,
|
||||||
|
pub threshold: Option<u64>,
|
||||||
|
pub dealers: Vec<DealerDetails>,
|
||||||
|
pub past_dealers: Vec<DealerDetails>,
|
||||||
|
pub epoch_dealings: Vec<ContractDealing>,
|
||||||
|
pub vk_shares: Vec<ContractVKShare>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
pub struct GroupInfo {
|
||||||
|
pub base: BasicContractInfo,
|
||||||
|
|
||||||
|
pub admin: cw_controllers::AdminResponse,
|
||||||
|
pub members: Vec<cw4::Member>,
|
||||||
|
pub total_weight: cw4::TotalWeightResponse,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
pub struct BandwidthInfo {
|
||||||
|
pub base: BasicContractInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
pub struct MultisigInfo {
|
||||||
|
pub base: BasicContractInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
pub enum Action {
|
||||||
|
Quit,
|
||||||
|
Error(String),
|
||||||
|
NextTab,
|
||||||
|
PreviousTab,
|
||||||
|
|
||||||
|
HomeAction(HomeAction),
|
||||||
|
LoggerAction(LoggerAction),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HomeAction> for Action {
|
||||||
|
fn from(value: HomeAction) -> Self {
|
||||||
|
Action::HomeAction(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LoggerAction> for Action {
|
||||||
|
fn from(value: LoggerAction) -> Self {
|
||||||
|
Action::LoggerAction(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TuiWidgetEvent> for Action {
|
||||||
|
fn from(value: TuiWidgetEvent) -> Self {
|
||||||
|
LoggerAction::WidgetKeyEvent(value).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
pub enum HomeAction {
|
||||||
|
ToggleShowHelp,
|
||||||
|
StartInput,
|
||||||
|
ScheduleContractRefresh,
|
||||||
|
RefreshDkgContract(Box<ContractsInfo>),
|
||||||
|
ProcessInput(String),
|
||||||
|
SetLastContractError(String),
|
||||||
|
EnterNormal,
|
||||||
|
|
||||||
|
NextInputMode,
|
||||||
|
PreviousInputMode,
|
||||||
|
|
||||||
|
EnterProcessing,
|
||||||
|
ExitProcessing,
|
||||||
|
FinishContractUpdate,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum LoggerAction {
|
||||||
|
WidgetKeyEvent(TuiWidgetEvent),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for LoggerAction {
|
||||||
|
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ActionSender(pub UnboundedSender<Action>);
|
||||||
|
|
||||||
|
impl ActionSender {
|
||||||
|
pub fn send(&self, action: Action) -> Result<(), SendError<Action>> {
|
||||||
|
self.0.send(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_home_action(&self, action: HomeAction) -> Result<(), SendError<Action>> {
|
||||||
|
self.send(Action::HomeAction(action))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unchecked_send_home_action(&self, action: HomeAction) {
|
||||||
|
self.send_home_action(action)
|
||||||
|
.expect("failed to send home action")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unchecked_send(&self, action: Action) {
|
||||||
|
self.send(action).expect("failed to send action")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//
|
||||||
|
// impl<'de> Deserialize<'de> for Action {
|
||||||
|
// fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
// where
|
||||||
|
// D: Deserializer<'de>,
|
||||||
|
// {
|
||||||
|
// struct ActionVisitor;
|
||||||
|
//
|
||||||
|
// impl<'de> Visitor<'de> for ActionVisitor {
|
||||||
|
// type Value = Action;
|
||||||
|
//
|
||||||
|
// fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
// formatter.write_str("a valid string representation of Action")
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// fn visit_str<E>(self, value: &str) -> Result<Action, E>
|
||||||
|
// where
|
||||||
|
// E: de::Error,
|
||||||
|
// {
|
||||||
|
// match value {
|
||||||
|
// "Tick" => Ok(Action::Tick),
|
||||||
|
// "Quit" => Ok(Action::Quit),
|
||||||
|
// "ScheduleContractRefresh" => Ok(Action::ScheduleContractRefresh),
|
||||||
|
// "ToggleShowHelp" => Ok(Action::ToggleShowHelp),
|
||||||
|
// // "ProcessInput" => Ok(Action::ProcessInput),
|
||||||
|
// "EnterNormal" => Ok(Action::EnterNormal),
|
||||||
|
// data if data.starts_with("Error(") => {
|
||||||
|
// let error_msg = data.trim_start_matches("Error(").trim_end_matches(")");
|
||||||
|
// Ok(Action::Error(error_msg.to_string()))
|
||||||
|
// }
|
||||||
|
// data if data.starts_with("Resize(") => {
|
||||||
|
// let parts: Vec<&str> = data
|
||||||
|
// .trim_start_matches("Resize(")
|
||||||
|
// .trim_end_matches(")")
|
||||||
|
// .split(',')
|
||||||
|
// .collect();
|
||||||
|
// if parts.len() == 2 {
|
||||||
|
// let width: u16 = parts[0].trim().parse().map_err(E::custom)?;
|
||||||
|
// let height: u16 = parts[1].trim().parse().map_err(E::custom)?;
|
||||||
|
// Ok(Action::Resize(width, height))
|
||||||
|
// } else {
|
||||||
|
// Err(E::custom(format!("Invalid Resize format: {}", value)))
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// _ => Err(E::custom(format!("Unknown Action variant: {}", value))),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// deserializer.deserialize_str(ActionVisitor)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::action::ActionSender;
|
||||||
|
use crate::cli::Args;
|
||||||
|
use crate::components::chain_history::ContractChainHistory;
|
||||||
|
use crate::components::logger::Logger;
|
||||||
|
use crate::keybindings::KeyBindings;
|
||||||
|
use crate::nyxd::setup_nyxd_client;
|
||||||
|
use crate::utils::key_event_to_string;
|
||||||
|
use crate::{action::Action, components::home::Home};
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
use ratatui::prelude::*;
|
||||||
|
use ratatui::widgets::{Block, Borders, Tabs};
|
||||||
|
use ratatui::Frame;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use tokio::sync::mpsc::UnboundedReceiver;
|
||||||
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum Mode {
|
||||||
|
#[default]
|
||||||
|
Home,
|
||||||
|
ChainHistory,
|
||||||
|
Logger,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mode {
|
||||||
|
pub fn next(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Mode::Home => Mode::ChainHistory,
|
||||||
|
Mode::ChainHistory => Mode::Logger,
|
||||||
|
Mode::Logger => Mode::Home,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Mode::Home => Mode::Logger,
|
||||||
|
Mode::ChainHistory => Mode::Home,
|
||||||
|
Mode::Logger => Mode::ChainHistory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct App {
|
||||||
|
pub keybindings: KeyBindings,
|
||||||
|
pub home: Home,
|
||||||
|
pub logger: Logger,
|
||||||
|
pub chain_history: ContractChainHistory,
|
||||||
|
|
||||||
|
pub action_tx: ActionSender,
|
||||||
|
|
||||||
|
pub should_quit: bool,
|
||||||
|
pub mode: Mode,
|
||||||
|
|
||||||
|
pub active_tab: usize,
|
||||||
|
pub tab_titles: Vec<&'static str>,
|
||||||
|
|
||||||
|
pub last_tick_key_events: Vec<KeyEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
pub async fn new(args: Args) -> anyhow::Result<(Self, UnboundedReceiver<Action>)> {
|
||||||
|
let (nyxd_client, upstream) = setup_nyxd_client(args)?;
|
||||||
|
|
||||||
|
let keybindings = KeyBindings::default();
|
||||||
|
|
||||||
|
let (action_tx, action_rx) = mpsc::unbounded_channel();
|
||||||
|
let action_sender = ActionSender(action_tx);
|
||||||
|
|
||||||
|
let home = Home::new(nyxd_client.clone(), upstream, action_sender.clone()).await?;
|
||||||
|
let chain_history = ContractChainHistory::new(nyxd_client);
|
||||||
|
let logger = Logger::new();
|
||||||
|
|
||||||
|
let mode = Mode::Home;
|
||||||
|
Ok((
|
||||||
|
Self {
|
||||||
|
keybindings,
|
||||||
|
home,
|
||||||
|
|
||||||
|
logger,
|
||||||
|
chain_history,
|
||||||
|
action_tx: action_sender,
|
||||||
|
should_quit: false,
|
||||||
|
mode,
|
||||||
|
last_tick_key_events: Vec::new(),
|
||||||
|
active_tab: 0,
|
||||||
|
tab_titles: vec![
|
||||||
|
"🥥 Contract Information 🥥",
|
||||||
|
"🔗 Transactions 🔗",
|
||||||
|
"📝 Logs 📝",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
action_rx,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_tab(&mut self) {
|
||||||
|
self.active_tab = (self.active_tab + 1) % self.tab_titles.len();
|
||||||
|
|
||||||
|
self.mode = self.mode.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous_tab(&mut self) {
|
||||||
|
if self.active_tab > 0 {
|
||||||
|
self.active_tab -= 1;
|
||||||
|
} else {
|
||||||
|
self.active_tab = self.tab_titles.len() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.mode = self.mode.previous();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn action_sender(&self) -> ActionSender {
|
||||||
|
self.action_tx.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&mut self, f: &mut Frame<'_>) -> anyhow::Result<()> {
|
||||||
|
let frame_size = f.size();
|
||||||
|
|
||||||
|
let chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints([Constraint::Length(3), Constraint::Min(0)])
|
||||||
|
.split(frame_size);
|
||||||
|
|
||||||
|
let block = Block::default();
|
||||||
|
f.render_widget(block, frame_size);
|
||||||
|
let titles = self
|
||||||
|
.tab_titles
|
||||||
|
.iter()
|
||||||
|
.map(|t| Line::from(Span::styled(*t, Style::default().bold())))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let tabs = Tabs::new(titles)
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("Tabs"))
|
||||||
|
.select(self.active_tab)
|
||||||
|
.style(Style::default().cyan())
|
||||||
|
.highlight_style(Style::default().bold().light_cyan().on_black());
|
||||||
|
// .highlight_style(Style::default().bold().on_black());
|
||||||
|
f.render_widget(tabs, chunks[0]);
|
||||||
|
|
||||||
|
match self.active_tab {
|
||||||
|
0 => {
|
||||||
|
self.home.render_tick();
|
||||||
|
self.home.draw(f, chunks[1])?;
|
||||||
|
}
|
||||||
|
1 => self.chain_history.draw(f, chunks[1])?,
|
||||||
|
2 => self.logger.draw(f, chunks[1])?,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
f.render_widget(
|
||||||
|
self.history_widget(),
|
||||||
|
Rect {
|
||||||
|
x: frame_size.x + 1,
|
||||||
|
y: frame_size.height.saturating_sub(1),
|
||||||
|
width: frame_size.width.saturating_sub(2),
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn history_widget(&self) -> Block {
|
||||||
|
Block::default()
|
||||||
|
.title(
|
||||||
|
ratatui::widgets::block::Title::from(format!(
|
||||||
|
"{:?}",
|
||||||
|
&self
|
||||||
|
.last_tick_key_events
|
||||||
|
.iter()
|
||||||
|
.map(key_event_to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
))
|
||||||
|
.alignment(Alignment::Right),
|
||||||
|
)
|
||||||
|
.title_style(Style::default().bold())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn key_event(&mut self, key: KeyEvent) -> anyhow::Result<()> {
|
||||||
|
if let Some(keymap) = self.keybindings.get(&self.mode) {
|
||||||
|
if let Some(action) = keymap.get(&vec![key]) {
|
||||||
|
info!("Got action: {action:?}");
|
||||||
|
self.action_tx.send(action.clone())?;
|
||||||
|
} else {
|
||||||
|
// If the key was not handled as a single key action,
|
||||||
|
// then consider it for multi-key combinations.
|
||||||
|
self.last_tick_key_events.push(key);
|
||||||
|
|
||||||
|
// Check for multi-key combinations
|
||||||
|
if let Some(action) = keymap.get(&self.last_tick_key_events) {
|
||||||
|
info!("Got action: {action:?}");
|
||||||
|
self.action_tx.send(action.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(action) = self.home.handle_key_events(key)? {
|
||||||
|
self.action_tx.send(action)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self) {
|
||||||
|
self.last_tick_key_events.drain(..);
|
||||||
|
self.home.tick();
|
||||||
|
self.logger.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, action: Action) -> anyhow::Result<()> {
|
||||||
|
debug!("handling action {action:?}");
|
||||||
|
|
||||||
|
let next = match action {
|
||||||
|
Action::Quit => {
|
||||||
|
self.should_quit = true;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Action::NextTab => {
|
||||||
|
self.next_tab();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Action::PreviousTab => {
|
||||||
|
self.previous_tab();
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Action::Error(err) => {
|
||||||
|
error!("unhandled error action: {err}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Action::HomeAction(home_action) => self.home.update(home_action)?,
|
||||||
|
Action::LoggerAction(logger_action) => self.logger.update(logger_action)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(action) = next {
|
||||||
|
self.action_sender().send(action)?
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use clap::Parser;
|
||||||
|
use nym_bin_common::bin_info;
|
||||||
|
use nym_validator_client::nyxd::AccountId;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
lazy_static::lazy_static! {
|
||||||
|
pub static ref PRETTY_BUILD_INFORMATION: String = bin_info!().pretty_print();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper for passing LONG_VERSION to clap
|
||||||
|
fn pretty_build_info_static() -> &'static str {
|
||||||
|
&PRETTY_BUILD_INFORMATION
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author = "Nymtech", version, about, long_version = pretty_build_info_static())]
|
||||||
|
pub struct Args {
|
||||||
|
/// Path pointing to an env file that configures the environment.
|
||||||
|
#[clap(short, long)]
|
||||||
|
pub(crate) config_env_file: Option<std::path::PathBuf>,
|
||||||
|
|
||||||
|
#[clap(long)]
|
||||||
|
pub(crate) admin_mnemonic: bip39::Mnemonic,
|
||||||
|
|
||||||
|
#[clap(long)]
|
||||||
|
pub(crate) dkg_contract_address: Option<AccountId>,
|
||||||
|
|
||||||
|
#[clap(long)]
|
||||||
|
pub(crate) nyxd_validator: Option<Url>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use nym_contracts_common::ContractBuildInformation;
|
||||||
|
use nym_validator_client::nyxd::{cw2, Coin};
|
||||||
|
use ratatui::prelude::*;
|
||||||
|
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
pub struct BasicContractInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub address: String,
|
||||||
|
pub balance: Coin,
|
||||||
|
pub cw2_version: Option<cw2::ContractVersion>,
|
||||||
|
pub build_info: Option<ContractBuildInformation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BasicContractInfo {
|
||||||
|
pub fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> anyhow::Result<()> {
|
||||||
|
// is it generic for any currency? no. but it's an internal tool so the approximation is good enough
|
||||||
|
let amount_nym = format!(
|
||||||
|
"{:.6}{}",
|
||||||
|
self.balance.amount as f64 / 1000000.,
|
||||||
|
self.balance.denom
|
||||||
|
);
|
||||||
|
|
||||||
|
let cw2_formatted = if let Some(cw2_version) = &self.cw2_version {
|
||||||
|
format!("{} {}", cw2_version.contract, cw2_version.version)
|
||||||
|
} else {
|
||||||
|
"<unspecified>".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut text = vec![
|
||||||
|
"Address (part might be hidden): ".into(),
|
||||||
|
Span::styled(&self.address, Style::default().yellow().bold()).into(),
|
||||||
|
Line::from(vec![
|
||||||
|
"Balance: ".into(),
|
||||||
|
Span::styled(amount_nym, Style::default().yellow().bold()),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
"CW2 version: ".into(),
|
||||||
|
Span::styled(cw2_formatted, Style::default().yellow().bold()),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
if let Some(build_info) = &self.build_info {
|
||||||
|
text.push(Line::from(vec![
|
||||||
|
"build branch: ".into(),
|
||||||
|
Span::styled(&build_info.commit_branch, Style::default().yellow().bold()),
|
||||||
|
]));
|
||||||
|
text.push(Line::from(vec![
|
||||||
|
"build sha: ".into(),
|
||||||
|
Span::styled(&build_info.commit_sha, Style::default().yellow().bold()),
|
||||||
|
]));
|
||||||
|
} else {
|
||||||
|
text.push(Line::from(vec![
|
||||||
|
"Build Information: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
"<unspecified>".to_string(),
|
||||||
|
Style::default().yellow().bold(),
|
||||||
|
),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let block = Block::default()
|
||||||
|
.title(Line::from(vec![Span::styled(
|
||||||
|
&self.name,
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
)]))
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded);
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.block(block)
|
||||||
|
.style(Style::default().fg(Color::Cyan))
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
|
||||||
|
f.render_widget(paragraph, rect);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::action::Action;
|
||||||
|
use crate::nyxd::NyxdClient;
|
||||||
|
use ratatui::prelude::*;
|
||||||
|
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
|
||||||
|
|
||||||
|
pub struct ContractChainHistory {
|
||||||
|
pub nyxd_client: NyxdClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContractChainHistory {
|
||||||
|
pub fn new(nyxd_client: NyxdClient) -> Self {
|
||||||
|
ContractChainHistory { nyxd_client }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> anyhow::Result<()> {
|
||||||
|
let block = Block::default()
|
||||||
|
.title("Contract Chain History")
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded);
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new("Unimplemented")
|
||||||
|
.block(block)
|
||||||
|
.style(Style::default().fg(Color::Cyan))
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
|
||||||
|
f.render_widget(paragraph, rect);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, _action: ()) -> anyhow::Result<Option<Action>> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::action::DkgInfo;
|
||||||
|
use nym_coconut_dkg_common::dealer::ContractDealing;
|
||||||
|
use nym_coconut_dkg_common::types::DealerDetails;
|
||||||
|
use nym_coconut_dkg_common::verification_key::ContractVKShare;
|
||||||
|
use ratatui::prelude::*;
|
||||||
|
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
struct FullDealer<'a> {
|
||||||
|
info: &'a DealerDetails,
|
||||||
|
dealing: Option<&'a ContractDealing>,
|
||||||
|
vk_share: Option<&'a ContractVKShare>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> FullDealer<'a> {
|
||||||
|
fn new(info: &'a DealerDetails) -> Self {
|
||||||
|
FullDealer {
|
||||||
|
info,
|
||||||
|
dealing: None,
|
||||||
|
vk_share: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format(self) -> Vec<Line<'a>> {
|
||||||
|
let dealing = if let Some(_dealing) = self.dealing {
|
||||||
|
Span::styled("Submitted", Style::default().bold().green())
|
||||||
|
} else {
|
||||||
|
Span::styled("UNSUBMITTED", Style::default().bold().red())
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut vk_share = if let Some(vk_share) = self.vk_share {
|
||||||
|
let verified = if vk_share.verified {
|
||||||
|
Span::styled(" share has been verified", Style::default().bold().green())
|
||||||
|
} else {
|
||||||
|
Span::styled(" share has been UNVERIFIED", Style::default().bold().red())
|
||||||
|
};
|
||||||
|
|
||||||
|
vec![
|
||||||
|
Line::from(vec![
|
||||||
|
" - VK share: ".into(),
|
||||||
|
Span::styled("Submitted", Style::default().bold().green()),
|
||||||
|
" for Epoch ".into(),
|
||||||
|
Span::styled(
|
||||||
|
vk_share.epoch_id.to_string(),
|
||||||
|
Style::default().bold().yellow(),
|
||||||
|
),
|
||||||
|
verified,
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
" - Assigned index: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
vk_share.node_index.to_string(),
|
||||||
|
Style::default().bold().yellow(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![Line::from(vec![
|
||||||
|
" - VK share: ".into(),
|
||||||
|
Span::styled("UNSUBMITTED", Style::default().bold().red()),
|
||||||
|
])]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut lines = vec![
|
||||||
|
Line::from(vec![
|
||||||
|
Span::styled(format!("{}: ", self.info.address), Style::default().bold()),
|
||||||
|
"\tindex: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}", self.info.assigned_index),
|
||||||
|
Style::default().bold().yellow(),
|
||||||
|
),
|
||||||
|
"\t announce address: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
self.info.announce_address.to_string(),
|
||||||
|
Style::default().bold().yellow(),
|
||||||
|
),
|
||||||
|
"\t BTE key: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}...", &self.info.bte_public_key_with_proof[..16]),
|
||||||
|
Style::default().bold().yellow(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
Line::from(vec![" - dealing: ".into(), dealing]),
|
||||||
|
];
|
||||||
|
|
||||||
|
lines.append(&mut vk_share);
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn format_contract_bytes(bytes: &ContractSafeBytes) -> String {
|
||||||
|
// const MAX_LEN: usize = 32;
|
||||||
|
// let mut output = "0x".to_string();
|
||||||
|
// for byte in bytes.0.iter().take(MAX_LEN) {
|
||||||
|
// output.push_str(&format!("{byte:02X}"));
|
||||||
|
// }
|
||||||
|
// output.push_str("...");
|
||||||
|
// output
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub fn draw_current_dealers_info(
|
||||||
|
info: &DkgInfo,
|
||||||
|
f: &mut Frame<'_>,
|
||||||
|
rect: Rect,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let block = Block::default()
|
||||||
|
.title(Line::from(vec![Span::styled(
|
||||||
|
"Current Dealers",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
)]))
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded);
|
||||||
|
|
||||||
|
let mut dealers = HashMap::new();
|
||||||
|
for dealer in &info.dealers {
|
||||||
|
dealers.insert(dealer.address.clone(), FullDealer::new(dealer));
|
||||||
|
}
|
||||||
|
for dealing in &info.epoch_dealings {
|
||||||
|
if let Some(dealer) = dealers.get_mut(&dealing.dealer) {
|
||||||
|
dealer.dealing = Some(dealing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for vk_share in &info.vk_shares {
|
||||||
|
if let Some(dealer) = dealers.get_mut(&vk_share.owner) {
|
||||||
|
dealer.vk_share = Some(vk_share)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut text = Vec::new();
|
||||||
|
|
||||||
|
// order dealers by their index
|
||||||
|
let mut dealers_vec = dealers.into_iter().map(|d| d.1).collect::<Vec<_>>();
|
||||||
|
dealers_vec.sort_by_key(|d| d.info.assigned_index);
|
||||||
|
|
||||||
|
for dealer_details in dealers_vec {
|
||||||
|
text.append(&mut dealer_details.format())
|
||||||
|
}
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.block(block)
|
||||||
|
.style(Style::default().fg(Color::Cyan))
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
|
||||||
|
f.render_widget(paragraph, rect);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::action::DkgInfo;
|
||||||
|
use ratatui::prelude::*;
|
||||||
|
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
|
||||||
|
use time::format_description::well_known::Rfc3339;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
pub fn draw_dkg_info(info: &DkgInfo, f: &mut Frame<'_>, rect: Rect) -> anyhow::Result<()> {
|
||||||
|
let block = Block::default()
|
||||||
|
.title(Line::from(vec![Span::styled(
|
||||||
|
"DKG Information",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
)]))
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded);
|
||||||
|
|
||||||
|
let end =
|
||||||
|
OffsetDateTime::from_unix_timestamp(info.epoch.finish_timestamp.seconds() as i64).unwrap();
|
||||||
|
|
||||||
|
let remaining = end - OffsetDateTime::now_utc();
|
||||||
|
|
||||||
|
let threshold_str = if let Some(val) = info.threshold {
|
||||||
|
val.to_string()
|
||||||
|
} else {
|
||||||
|
"not yet determined".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let dealers = info.dealers.len();
|
||||||
|
let dealings = info.epoch_dealings.len();
|
||||||
|
let dealings_color = if dealings < dealers {
|
||||||
|
Color::Red
|
||||||
|
} else {
|
||||||
|
Color::Green
|
||||||
|
};
|
||||||
|
|
||||||
|
let shares = info.vk_shares.len();
|
||||||
|
let shares_color = if shares < dealings {
|
||||||
|
Color::Red
|
||||||
|
} else {
|
||||||
|
Color::Green
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = vec![
|
||||||
|
Line::from(vec![
|
||||||
|
format!("DKG Epoch {} State: ", info.epoch.epoch_id).into(),
|
||||||
|
Span::styled(info.epoch.state.to_string(), Style::default().bold()),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
"Epoch end time: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
end.format(&Rfc3339).unwrap().to_string(),
|
||||||
|
Style::default().bold(),
|
||||||
|
),
|
||||||
|
" (".into(),
|
||||||
|
Span::styled(
|
||||||
|
remaining.whole_seconds().to_string(),
|
||||||
|
Style::default().light_green(),
|
||||||
|
),
|
||||||
|
" remaining)".into(),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
"Threshold: ".into(),
|
||||||
|
Span::styled(threshold_str, Style::default().bold()),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
"Registered Dealers: ".into(),
|
||||||
|
Span::styled(dealers.to_string(), Style::default().bold()),
|
||||||
|
" (".into(),
|
||||||
|
Span::styled(info.past_dealers.len().to_string(), Style::default().bold()),
|
||||||
|
" in the past)".into(),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
"Submitted Dealings: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
dealings.to_string(),
|
||||||
|
Style::default().bold().fg(dealings_color),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
"Submitted VK Shares: ".into(),
|
||||||
|
Span::styled(shares.to_string(), Style::default().bold().fg(shares_color)),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.block(block)
|
||||||
|
.style(Style::default().fg(Color::Cyan))
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
|
||||||
|
f.render_widget(paragraph, rect);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use nym_coconut_dkg_common::types::TimeConfiguration;
|
||||||
|
use ratatui::prelude::*;
|
||||||
|
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
|
||||||
|
|
||||||
|
pub fn draw_dkg_timeconfig(
|
||||||
|
config: TimeConfiguration,
|
||||||
|
f: &mut Frame<'_>,
|
||||||
|
rect: Rect,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let block = Block::default()
|
||||||
|
.title(Line::from(vec![Span::styled(
|
||||||
|
"DKG Time Configuration",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
)]))
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded);
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(format_time_configuration(config))
|
||||||
|
.block(block)
|
||||||
|
.style(Style::default().fg(Color::Cyan))
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
|
||||||
|
f.render_widget(paragraph, rect);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_time_configuration<'a>(tc: TimeConfiguration) -> Vec<Line<'a>> {
|
||||||
|
vec![
|
||||||
|
Span::styled("Time Configuration", Style::default().bold()).into(),
|
||||||
|
Line::from(vec![
|
||||||
|
"Public Key Submission: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}secs", tc.public_key_submission_time_secs),
|
||||||
|
Style::default().yellow(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
"Dealing Exchange: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}secs", tc.dealing_exchange_time_secs),
|
||||||
|
Style::default().yellow(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
"Verification Key Submission: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}secs", tc.verification_key_submission_time_secs),
|
||||||
|
Style::default().yellow(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
"Verification Key Validation: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}secs", tc.verification_key_validation_time_secs),
|
||||||
|
Style::default().yellow(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
"Verification Key Finalization: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}secs", tc.verification_key_finalization_time_secs),
|
||||||
|
Style::default().yellow(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
Line::from(vec![
|
||||||
|
"In Progress: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
format!("{}secs", tc.in_progress_time_secs),
|
||||||
|
Style::default().yellow(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::action::GroupInfo;
|
||||||
|
use ratatui::prelude::*;
|
||||||
|
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
|
||||||
|
|
||||||
|
// TODO: move to separate component and get rid of that matching_admin bool
|
||||||
|
pub fn draw_group_info(
|
||||||
|
info: &GroupInfo,
|
||||||
|
f: &mut Frame<'_>,
|
||||||
|
rect: Rect,
|
||||||
|
matching_admin: bool,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let block = Block::default()
|
||||||
|
.title(Line::from(vec![Span::styled(
|
||||||
|
"Group Information",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
)]))
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded);
|
||||||
|
|
||||||
|
let group_admin = match &info.admin.admin {
|
||||||
|
None => Span::styled("<no admin>", Style::default().light_red().bold()),
|
||||||
|
Some(admin) => {
|
||||||
|
let style = if matching_admin {
|
||||||
|
Style::default().light_green().bold()
|
||||||
|
} else {
|
||||||
|
Style::default().light_red().bold()
|
||||||
|
};
|
||||||
|
Span::styled(admin, style)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut text = vec![
|
||||||
|
Line::from(vec!["Admin: ".into(), group_admin]),
|
||||||
|
Line::from(vec![
|
||||||
|
"Total Members: ".into(),
|
||||||
|
Span::styled(info.members.len().to_string(), Style::new().bold()),
|
||||||
|
" (total weight: ".into(),
|
||||||
|
Span::styled(
|
||||||
|
info.total_weight.weight.to_string(),
|
||||||
|
Style::default().yellow().bold(),
|
||||||
|
),
|
||||||
|
"):".into(),
|
||||||
|
]),
|
||||||
|
];
|
||||||
|
|
||||||
|
for member in &info.members {
|
||||||
|
text.push(Line::from(vec![
|
||||||
|
" - ".into(),
|
||||||
|
Span::styled(&member.addr, Style::default().bold()),
|
||||||
|
" (weight: ".into(),
|
||||||
|
Span::styled(member.weight.to_string(), Style::default().yellow()),
|
||||||
|
")".into(),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(text)
|
||||||
|
.block(block)
|
||||||
|
.style(Style::default().fg(Color::Cyan))
|
||||||
|
.alignment(Alignment::Left);
|
||||||
|
|
||||||
|
f.render_widget(paragraph, rect);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,609 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use super::Frame;
|
||||||
|
use crate::action::{Action, ActionSender};
|
||||||
|
use crate::action::{ContractsInfo, HomeAction};
|
||||||
|
use crate::components::home::dealers_info::draw_current_dealers_info;
|
||||||
|
use crate::components::home::dkg_info::draw_dkg_info;
|
||||||
|
use crate::components::home::dkg_timeconfig::draw_dkg_timeconfig;
|
||||||
|
use crate::components::home::group_info::draw_group_info;
|
||||||
|
use crate::nyxd::NyxdClient;
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
use ratatui::{prelude::*, widgets::*};
|
||||||
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
use throbber_widgets_tui::ThrobberState;
|
||||||
|
use tokio::time::Instant;
|
||||||
|
use tracing::error;
|
||||||
|
use tui_input::{backend::crossterm::EventHandler, Input};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub(crate) mod dealers_info;
|
||||||
|
pub(crate) mod dkg_info;
|
||||||
|
pub(crate) mod dkg_timeconfig;
|
||||||
|
pub(crate) mod group_info;
|
||||||
|
pub(crate) mod utils;
|
||||||
|
|
||||||
|
pub const REFRESH_RATE: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
|
#[derive(Default, Clone, PartialEq, Eq)]
|
||||||
|
pub enum InputState {
|
||||||
|
#[default]
|
||||||
|
Normal,
|
||||||
|
AddCW4Member {
|
||||||
|
address: String,
|
||||||
|
},
|
||||||
|
RemoveCW4Member,
|
||||||
|
AdvanceEpochState,
|
||||||
|
SurpassedThreshold,
|
||||||
|
|
||||||
|
Processing,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
// will consume provided characters
|
||||||
|
pub fn expects_text_input(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
InputState::Normal => false,
|
||||||
|
InputState::AddCW4Member { .. } => true,
|
||||||
|
InputState::RemoveCW4Member => true,
|
||||||
|
InputState::Processing => false,
|
||||||
|
InputState::AdvanceEpochState => false,
|
||||||
|
InputState::SurpassedThreshold => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn expects_user_input(&self) -> bool {
|
||||||
|
!matches!(self, InputState::Normal | InputState::Processing)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
InputState::Normal => InputState::Normal,
|
||||||
|
InputState::Processing => InputState::Processing,
|
||||||
|
InputState::AddCW4Member { .. } => InputState::RemoveCW4Member,
|
||||||
|
InputState::RemoveCW4Member => InputState::AdvanceEpochState,
|
||||||
|
InputState::AdvanceEpochState => InputState::SurpassedThreshold,
|
||||||
|
InputState::SurpassedThreshold => InputState::AddCW4Member {
|
||||||
|
address: "".to_string(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
InputState::Normal => InputState::Normal,
|
||||||
|
InputState::Processing => InputState::Processing,
|
||||||
|
InputState::AddCW4Member { .. } => InputState::SurpassedThreshold,
|
||||||
|
InputState::RemoveCW4Member => InputState::AddCW4Member {
|
||||||
|
address: "".to_string(),
|
||||||
|
},
|
||||||
|
InputState::AdvanceEpochState => InputState::RemoveCW4Member,
|
||||||
|
InputState::SurpassedThreshold => InputState::AdvanceEpochState,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Home {
|
||||||
|
pub show_help: bool,
|
||||||
|
pub last_contract_error_message: String,
|
||||||
|
pub refreshing: bool,
|
||||||
|
throbber_state: Option<throbber_widgets_tui::ThrobberState>,
|
||||||
|
|
||||||
|
pub nyxd_client: NyxdClient,
|
||||||
|
pub manager_address: String,
|
||||||
|
|
||||||
|
pub contracts: ContractsInfo,
|
||||||
|
pub upstream: String,
|
||||||
|
|
||||||
|
pub mode: InputState,
|
||||||
|
pub input: Input,
|
||||||
|
pub action_tx: ActionSender,
|
||||||
|
pub keymap: HashMap<KeyEvent, Action>,
|
||||||
|
pub last_contract_update: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Home {
|
||||||
|
pub async fn new(
|
||||||
|
nyxd_client: NyxdClient,
|
||||||
|
upstream: Url,
|
||||||
|
action_tx: ActionSender,
|
||||||
|
) -> anyhow::Result<Self> {
|
||||||
|
let manager_address = nyxd_client.address().await.to_string();
|
||||||
|
|
||||||
|
let contracts = nyxd_client.get_contract_update().await?;
|
||||||
|
|
||||||
|
Ok(Home {
|
||||||
|
show_help: false,
|
||||||
|
|
||||||
|
nyxd_client,
|
||||||
|
mode: Default::default(),
|
||||||
|
input: Default::default(),
|
||||||
|
action_tx,
|
||||||
|
keymap: Default::default(),
|
||||||
|
contracts,
|
||||||
|
last_contract_update: Instant::now(),
|
||||||
|
manager_address,
|
||||||
|
last_contract_error_message: "".to_string(),
|
||||||
|
refreshing: false,
|
||||||
|
upstream: upstream.to_string(),
|
||||||
|
throbber_state: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keymap(mut self, keymap: HashMap<KeyEvent, Action>) -> Self {
|
||||||
|
self.keymap = keymap;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self) {
|
||||||
|
// let state_end = OffsetDateTime::from_unix_timestamp(
|
||||||
|
// self.dkg_info.dkg_epoch.finish_timestamp.seconds() as i64,
|
||||||
|
// )
|
||||||
|
// .unwrap();
|
||||||
|
// let until_epoch_state_end = state_end - OffsetDateTime::now_utc();
|
||||||
|
// let epoch_should_move = until_epoch_state_end.as_seconds_f32() < -5.;
|
||||||
|
|
||||||
|
// let should_refresh = !self.mode.expects_text_input()
|
||||||
|
// && (self.last_contract_update.elapsed() >= REFRESH_RATE || epoch_should_move);
|
||||||
|
|
||||||
|
let should_refresh =
|
||||||
|
!self.mode.expects_text_input() && self.last_contract_update.elapsed() >= REFRESH_RATE;
|
||||||
|
|
||||||
|
if should_refresh && !self.refreshing {
|
||||||
|
self.last_contract_update = Instant::now();
|
||||||
|
self.refreshing = true;
|
||||||
|
self.schedule_contract_refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(throbber) = &mut self.throbber_state {
|
||||||
|
throbber.calc_next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_tick(&mut self) {}
|
||||||
|
|
||||||
|
pub fn handle_input(&mut self, s: String) {
|
||||||
|
match &self.mode {
|
||||||
|
InputState::Normal | InputState::Processing => {
|
||||||
|
panic!("received input whilst it shouldn't have been possible!")
|
||||||
|
}
|
||||||
|
InputState::AddCW4Member { address } => {
|
||||||
|
if address.is_empty() {
|
||||||
|
self.mode = InputState::AddCW4Member { address: s };
|
||||||
|
self.input.reset();
|
||||||
|
} else {
|
||||||
|
let address_owned = address.clone();
|
||||||
|
self.mode = InputState::Processing;
|
||||||
|
self.input.reset();
|
||||||
|
self.schedule_add_cw4_member(address_owned, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputState::RemoveCW4Member => {
|
||||||
|
self.mode = InputState::Processing;
|
||||||
|
self.input.reset();
|
||||||
|
self.schedule_remove_cw4_member(s)
|
||||||
|
}
|
||||||
|
InputState::AdvanceEpochState => {
|
||||||
|
self.mode = InputState::Processing;
|
||||||
|
self.input.reset();
|
||||||
|
self.schedule_call_advance_epoch_state();
|
||||||
|
}
|
||||||
|
InputState::SurpassedThreshold => {
|
||||||
|
self.mode = InputState::Processing;
|
||||||
|
self.input.reset();
|
||||||
|
self.schedule_call_surpassed_threshold();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn schedule_call_advance_epoch_state(&self) {
|
||||||
|
let tx = self.action_tx.clone();
|
||||||
|
let client = self.nyxd_client.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match client.try_advance_epoch_state().await {
|
||||||
|
Ok(_) => tx.unchecked_send_home_action(HomeAction::ScheduleContractRefresh),
|
||||||
|
Err(err) => tx.unchecked_send_home_action(HomeAction::SetLastContractError(
|
||||||
|
format!("failed to advance epoch state: {err}"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.unchecked_send_home_action(HomeAction::ExitProcessing)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn schedule_call_surpassed_threshold(&self) {
|
||||||
|
let tx = self.action_tx.clone();
|
||||||
|
let client = self.nyxd_client.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match client.try_surpass_threshold().await {
|
||||||
|
Ok(_) => {
|
||||||
|
tx.unchecked_send_home_action(HomeAction::ScheduleContractRefresh);
|
||||||
|
}
|
||||||
|
Err(err) => tx.unchecked_send_home_action(HomeAction::SetLastContractError(
|
||||||
|
format!("failed to surpass threshold: {err}"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.unchecked_send_home_action(HomeAction::ExitProcessing);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn schedule_add_cw4_member(&self, member_address: String, member_weight_raw: String) {
|
||||||
|
let tx = self.action_tx.clone();
|
||||||
|
let client = self.nyxd_client.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Ok(weight) = member_weight_raw.parse() {
|
||||||
|
match client.add_group_member(member_address, weight).await {
|
||||||
|
Ok(_) => {
|
||||||
|
tx.unchecked_send_home_action(HomeAction::ScheduleContractRefresh);
|
||||||
|
}
|
||||||
|
Err(err) => tx.unchecked_send_home_action(HomeAction::SetLastContractError(
|
||||||
|
format!("failed to add group member: {err}"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("could not parse '{member_weight_raw}' into a valid weight")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.unchecked_send_home_action(HomeAction::ExitProcessing);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn schedule_remove_cw4_member(&self, member_address: String) {
|
||||||
|
let tx = self.action_tx.clone();
|
||||||
|
let client = self.nyxd_client.clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
match client.remove_group_member(member_address).await {
|
||||||
|
Ok(_) => tx.unchecked_send_home_action(HomeAction::ScheduleContractRefresh),
|
||||||
|
Err(err) => tx.unchecked_send_home_action(HomeAction::SetLastContractError(
|
||||||
|
format!("failed to remove group member: {err}"),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.unchecked_send_home_action(HomeAction::ExitProcessing)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn schedule_contract_refresh(&self) {
|
||||||
|
let tx = self.action_tx.clone();
|
||||||
|
let client = self.nyxd_client.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
tx.unchecked_send_home_action(HomeAction::EnterProcessing);
|
||||||
|
match client.get_contract_update().await {
|
||||||
|
Ok(info) => {
|
||||||
|
tx.unchecked_send_home_action(HomeAction::RefreshDkgContract(Box::new(info)))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to get dkg updates: {err}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.unchecked_send_home_action(HomeAction::ExitProcessing);
|
||||||
|
tx.unchecked_send_home_action(HomeAction::FinishContractUpdate)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_dkg_contract_info(&mut self, update_info: ContractsInfo) {
|
||||||
|
self.contracts = update_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
// fn contracts_info_lines(&self) -> Vec<Line> {
|
||||||
|
// vec![]
|
||||||
|
// //
|
||||||
|
// // if !self.last_contract_error_message.is_empty() {
|
||||||
|
// // lines.push("".into());
|
||||||
|
// // lines.push("".into());
|
||||||
|
// // lines.push(Line::from(vec![
|
||||||
|
// // Span::styled(
|
||||||
|
// // "CONTRACT EXECUTION FAILURE: ",
|
||||||
|
// // Style::default().red().bold(),
|
||||||
|
// // ),
|
||||||
|
// // Span::styled(&self.last_contract_error_message, Style::default().white()),
|
||||||
|
// // ]))
|
||||||
|
// // }
|
||||||
|
// //
|
||||||
|
// // lines
|
||||||
|
// }
|
||||||
|
|
||||||
|
fn draw_main_widget(&mut self, f: &mut Frame<'_>, rect: Rect) -> anyhow::Result<()> {
|
||||||
|
let info_chunks = Layout::default()
|
||||||
|
.direction(Direction::Vertical)
|
||||||
|
.constraints(vec![Constraint::Percentage(33), Constraint::Percentage(33)])
|
||||||
|
.split(rect);
|
||||||
|
|
||||||
|
let contract_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(vec![
|
||||||
|
Constraint::Percentage(33),
|
||||||
|
Constraint::Percentage(33),
|
||||||
|
Constraint::Percentage(33),
|
||||||
|
])
|
||||||
|
.split(info_chunks[0]);
|
||||||
|
|
||||||
|
draw_group_info(
|
||||||
|
&self.contracts.group,
|
||||||
|
f,
|
||||||
|
contract_chunks[0],
|
||||||
|
self.matching_admin(),
|
||||||
|
)?;
|
||||||
|
draw_dkg_info(&self.contracts.dkg, f, contract_chunks[1])?;
|
||||||
|
draw_dkg_timeconfig(
|
||||||
|
self.contracts.dkg.epoch.time_configuration,
|
||||||
|
f,
|
||||||
|
contract_chunks[2],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
draw_current_dealers_info(&self.contracts.dkg, f, info_chunks[1])?;
|
||||||
|
|
||||||
|
// f.render_widget(self.main_widget(), rects[1]);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matching_admin(&self) -> bool {
|
||||||
|
if let Some(group_admin) = &self.contracts.group.admin.admin {
|
||||||
|
return group_admin == &self.manager_address;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_span<S: Into<String>>(&self, text: S) -> Span {
|
||||||
|
let style = if self.matching_admin() {
|
||||||
|
Style::default().light_green().bold()
|
||||||
|
} else {
|
||||||
|
Style::default().light_red().bold()
|
||||||
|
};
|
||||||
|
Span::styled(text.into(), style)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn title_widget(&self) -> Block {
|
||||||
|
Block::default()
|
||||||
|
.title(Line::from(vec![
|
||||||
|
"Nym DKG Contract manager (managed via ".into(),
|
||||||
|
self.admin_span(&self.manager_address),
|
||||||
|
") @ ".into(),
|
||||||
|
Span::styled(&self.upstream, Style::new().light_blue()),
|
||||||
|
]))
|
||||||
|
.title_alignment(Alignment::Center)
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(match self.mode {
|
||||||
|
InputState::Processing => Style::default().fg(Color::Yellow),
|
||||||
|
_ => Style::default(),
|
||||||
|
})
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn input_widget(&self, scroll: usize) -> Paragraph {
|
||||||
|
let mode_name = match &self.mode {
|
||||||
|
InputState::Normal | InputState::Processing => Span::raw("Enter Input Mode "),
|
||||||
|
InputState::AddCW4Member { address } => {
|
||||||
|
if address.is_empty() {
|
||||||
|
Span::raw("Enter address of CW4 member to add to the group ")
|
||||||
|
} else {
|
||||||
|
Span::raw(format!("Enter voting weight of new member '{address}' "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputState::RemoveCW4Member => Span::raw("Enter address of CW4 member to remove it "),
|
||||||
|
InputState::AdvanceEpochState => {
|
||||||
|
Span::raw("press <ENTER> to attempt to advance the epoch state ")
|
||||||
|
}
|
||||||
|
InputState::SurpassedThreshold => {
|
||||||
|
Span::raw("press <ENTER> to attempt to surpass the threshold ")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Paragraph::new(self.input.value())
|
||||||
|
.style(if self.mode.expects_user_input() {
|
||||||
|
Style::default().fg(Color::Yellow)
|
||||||
|
} else {
|
||||||
|
Style::default()
|
||||||
|
})
|
||||||
|
.scroll((0, scroll as u16))
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.title(Line::from(vec![
|
||||||
|
mode_name,
|
||||||
|
Span::styled("(Press ", Style::default().fg(Color::DarkGray)),
|
||||||
|
Span::styled(
|
||||||
|
"/",
|
||||||
|
Style::default()
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
.fg(Color::Gray),
|
||||||
|
),
|
||||||
|
Span::styled(" to start, ", Style::default().fg(Color::DarkGray)),
|
||||||
|
Span::styled(
|
||||||
|
"ESC",
|
||||||
|
Style::default()
|
||||||
|
.add_modifier(Modifier::BOLD)
|
||||||
|
.fg(Color::Gray),
|
||||||
|
),
|
||||||
|
Span::styled(" to finish)", Style::default().fg(Color::DarkGray)),
|
||||||
|
])),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn help_table(&self) -> Table {
|
||||||
|
let rows = vec![
|
||||||
|
Row::new(vec!["/", "Enter Input"]),
|
||||||
|
Row::new(vec!["ESC", "Exit Input"]),
|
||||||
|
Row::new(vec!["Enter", "Submit Input"]),
|
||||||
|
Row::new(vec!["<Ctrl-c>", "Quit"]),
|
||||||
|
Row::new(vec!["<Ctrl-d>", "Quit"]),
|
||||||
|
Row::new(vec!["<Ctrl-h>", "Open Help"]),
|
||||||
|
Row::new(vec!["<Ctrl-r>", "Force refresh contract state"]),
|
||||||
|
];
|
||||||
|
Table::new(rows)
|
||||||
|
.header(
|
||||||
|
Row::new(vec!["Key", "Action"])
|
||||||
|
.bottom_margin(1)
|
||||||
|
.style(Style::default().add_modifier(Modifier::BOLD)),
|
||||||
|
)
|
||||||
|
.widths(&[Constraint::Percentage(10), Constraint::Percentage(90)])
|
||||||
|
.column_spacing(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_key_events(&mut self, key: KeyEvent) -> anyhow::Result<Option<Action>> {
|
||||||
|
let action = match self.mode {
|
||||||
|
InputState::Normal | InputState::Processing => None,
|
||||||
|
_ => match key.code {
|
||||||
|
KeyCode::Esc => Some(Action::HomeAction(HomeAction::EnterNormal)),
|
||||||
|
KeyCode::Enter => Some(Action::HomeAction(HomeAction::ProcessInput(
|
||||||
|
self.input.value().to_string(),
|
||||||
|
))),
|
||||||
|
KeyCode::Left => Some(Action::HomeAction(HomeAction::PreviousInputMode)),
|
||||||
|
KeyCode::Right => Some(Action::HomeAction(HomeAction::NextInputMode)),
|
||||||
|
_ => {
|
||||||
|
self.input.handle_event(&crossterm::event::Event::Key(key));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
Ok(action)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, action: HomeAction) -> anyhow::Result<Option<Action>> {
|
||||||
|
match action {
|
||||||
|
HomeAction::ToggleShowHelp => self.show_help = !self.show_help,
|
||||||
|
HomeAction::ScheduleContractRefresh => self.schedule_contract_refresh(),
|
||||||
|
HomeAction::RefreshDkgContract(update_info) => {
|
||||||
|
self.refresh_dkg_contract_info(*update_info)
|
||||||
|
}
|
||||||
|
HomeAction::ProcessInput(s) => self.handle_input(s),
|
||||||
|
HomeAction::SetLastContractError(err) => self.last_contract_error_message = err,
|
||||||
|
HomeAction::EnterNormal => {
|
||||||
|
self.mode = InputState::Normal;
|
||||||
|
self.last_contract_error_message = "".to_string();
|
||||||
|
}
|
||||||
|
HomeAction::StartInput => {
|
||||||
|
// make sure we're not already in the input mode
|
||||||
|
if !self.mode.expects_user_input() {
|
||||||
|
self.mode = InputState::AddCW4Member {
|
||||||
|
address: "".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HomeAction::PreviousInputMode => {
|
||||||
|
self.mode = self.mode.previous();
|
||||||
|
self.last_contract_error_message = "".to_string();
|
||||||
|
}
|
||||||
|
HomeAction::NextInputMode => {
|
||||||
|
self.mode = self.mode.next();
|
||||||
|
self.last_contract_error_message = "".to_string();
|
||||||
|
}
|
||||||
|
HomeAction::EnterProcessing => {
|
||||||
|
self.mode = InputState::Processing;
|
||||||
|
self.throbber_state = Some(ThrobberState::default());
|
||||||
|
self.last_contract_error_message = "".to_string();
|
||||||
|
}
|
||||||
|
HomeAction::ExitProcessing => {
|
||||||
|
self.mode = InputState::Normal;
|
||||||
|
self.throbber_state = None;
|
||||||
|
}
|
||||||
|
HomeAction::FinishContractUpdate => {
|
||||||
|
self.refreshing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> anyhow::Result<()> {
|
||||||
|
let block = self.title_widget();
|
||||||
|
let inner_area = block.inner(rect);
|
||||||
|
f.render_widget(block, rect);
|
||||||
|
|
||||||
|
let rects = Layout::default()
|
||||||
|
.constraints(
|
||||||
|
[
|
||||||
|
Constraint::Min(8),
|
||||||
|
Constraint::Percentage(100),
|
||||||
|
Constraint::Min(3),
|
||||||
|
]
|
||||||
|
.as_ref(),
|
||||||
|
)
|
||||||
|
.split(inner_area);
|
||||||
|
|
||||||
|
let info_chunks = Layout::default()
|
||||||
|
.direction(Direction::Horizontal)
|
||||||
|
.constraints(vec![
|
||||||
|
Constraint::Percentage(25),
|
||||||
|
Constraint::Percentage(25),
|
||||||
|
Constraint::Percentage(25),
|
||||||
|
Constraint::Percentage(25),
|
||||||
|
])
|
||||||
|
.split(rects[0]);
|
||||||
|
|
||||||
|
self.contracts.bandwidth.base.draw(f, info_chunks[0])?;
|
||||||
|
self.contracts.dkg.base.draw(f, info_chunks[1])?;
|
||||||
|
self.contracts.group.base.draw(f, info_chunks[2])?;
|
||||||
|
self.contracts.multisig.base.draw(f, info_chunks[3])?;
|
||||||
|
|
||||||
|
self.draw_main_widget(f, rects[1])?;
|
||||||
|
|
||||||
|
// old input logic (I haven't touched it yet)
|
||||||
|
|
||||||
|
let width = rects[2].width.max(3) - 3; // keep 2 for borders and 1 for cursor
|
||||||
|
let scroll = self.input.visual_scroll(width as usize);
|
||||||
|
|
||||||
|
let input = self.input_widget(scroll);
|
||||||
|
f.render_widget(input, rects[2]);
|
||||||
|
|
||||||
|
if self.mode.expects_text_input() {
|
||||||
|
f.set_cursor(
|
||||||
|
(rects[2].x + 1 + self.input.cursor() as u16).min(rects[2].x + rects[2].width - 2),
|
||||||
|
rects[2].y + 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(throbber_state) = &mut self.throbber_state {
|
||||||
|
let full = throbber_widgets_tui::Throbber::default()
|
||||||
|
.label("Processing...")
|
||||||
|
.style(Style::default().yellow())
|
||||||
|
.throbber_style(Style::default().red().bold())
|
||||||
|
.throbber_set(throbber_widgets_tui::BRAILLE_SIX_DOUBLE)
|
||||||
|
.use_type(throbber_widgets_tui::WhichUse::Spin);
|
||||||
|
|
||||||
|
f.render_stateful_widget(
|
||||||
|
full,
|
||||||
|
Rect {
|
||||||
|
x: rect.x + 2,
|
||||||
|
y: rect.height,
|
||||||
|
width: rect.width.saturating_sub(2),
|
||||||
|
height: 1,
|
||||||
|
},
|
||||||
|
throbber_state,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.show_help {
|
||||||
|
let rect = rect.inner(&Margin {
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 2,
|
||||||
|
});
|
||||||
|
f.render_widget(Clear, rect);
|
||||||
|
let block = Block::default()
|
||||||
|
.title(Line::from(vec![Span::styled(
|
||||||
|
"Key Bindings",
|
||||||
|
Style::default().add_modifier(Modifier::BOLD),
|
||||||
|
)]))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Yellow));
|
||||||
|
f.render_widget(block, rect);
|
||||||
|
|
||||||
|
f.render_widget(
|
||||||
|
self.help_table(),
|
||||||
|
rect.inner(&Margin {
|
||||||
|
vertical: 4,
|
||||||
|
horizontal: 2,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::action::{Action, LoggerAction};
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
use ratatui::prelude::*;
|
||||||
|
use ratatui::widgets::{Block, Borders};
|
||||||
|
use ratatui::Frame;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tui_logger::{TuiLoggerLevelOutput, TuiLoggerSmartWidget, TuiWidgetState};
|
||||||
|
|
||||||
|
pub struct Logger {
|
||||||
|
state: TuiWidgetState,
|
||||||
|
pub keymap: HashMap<KeyEvent, Action>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Logger {
|
||||||
|
#[allow(clippy::new_without_default)]
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Logger {
|
||||||
|
state: TuiWidgetState::new(),
|
||||||
|
keymap: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keymap(mut self, keymap: HashMap<KeyEvent, Action>) -> Self {
|
||||||
|
self.keymap = keymap;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick(&mut self) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&mut self, f: &mut Frame<'_>, rect: Rect) -> anyhow::Result<()> {
|
||||||
|
let block = Block::default().borders(Borders::ALL);
|
||||||
|
let inner_area = block.inner(rect);
|
||||||
|
f.render_widget(block, rect);
|
||||||
|
|
||||||
|
let tui_sm = TuiLoggerSmartWidget::default()
|
||||||
|
.style_error(Style::default().fg(Color::Red))
|
||||||
|
.style_warn(Style::default().fg(Color::Yellow))
|
||||||
|
.style_info(Style::default().fg(Color::Green))
|
||||||
|
.style_debug(Style::default().fg(Color::Cyan))
|
||||||
|
.style_trace(Style::default().fg(Color::Magenta))
|
||||||
|
.output_separator(':')
|
||||||
|
.output_timestamp(Some("%F %H:%M:%S%.3f".to_string()))
|
||||||
|
.output_level(Some(TuiLoggerLevelOutput::Long))
|
||||||
|
.output_target(true)
|
||||||
|
.output_file(true)
|
||||||
|
.output_line(true)
|
||||||
|
.state(&self.state);
|
||||||
|
f.render_widget(tui_sm, inner_area);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, action: LoggerAction) -> anyhow::Result<Option<Action>> {
|
||||||
|
match action {
|
||||||
|
LoggerAction::WidgetKeyEvent(event) => self.state.transition(&event),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_key_events(&mut self, _key: KeyEvent) -> anyhow::Result<Option<Action>> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use ratatui::Frame;
|
||||||
|
|
||||||
|
pub mod basic_contract_info;
|
||||||
|
pub mod chain_history;
|
||||||
|
pub mod home;
|
||||||
|
pub mod logger;
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::action::HomeAction;
|
||||||
|
use crate::{action::Action, app::Mode};
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use tui_logger::TuiWidgetEvent;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct KeyBindings(pub HashMap<Mode, HashMap<Vec<KeyEvent>, Action>>);
|
||||||
|
|
||||||
|
impl Deref for KeyBindings {
|
||||||
|
type Target = HashMap<Mode, HashMap<Vec<KeyEvent>, Action>>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for KeyBindings {
|
||||||
|
fn default() -> Self {
|
||||||
|
let mut inner_home = HashMap::new();
|
||||||
|
let mut inner_logger = HashMap::new();
|
||||||
|
let mut inner_chain = HashMap::new();
|
||||||
|
|
||||||
|
// those are disgusting but can't be bothered to refactor it for global keybindings
|
||||||
|
// GLOBAL:
|
||||||
|
inner_home.insert(unchecked_keys("<tab>"), Action::NextTab);
|
||||||
|
inner_logger.insert(unchecked_keys("<tab>"), Action::NextTab);
|
||||||
|
inner_chain.insert(unchecked_keys("<tab>"), Action::NextTab);
|
||||||
|
|
||||||
|
inner_home.insert(unchecked_keys("<Ctrl-d>"), Action::Quit);
|
||||||
|
inner_home.insert(unchecked_keys("<Ctrl-c>"), Action::Quit);
|
||||||
|
inner_logger.insert(unchecked_keys("<Ctrl-d>"), Action::Quit);
|
||||||
|
inner_logger.insert(unchecked_keys("<Ctrl-c>"), Action::Quit);
|
||||||
|
inner_chain.insert(unchecked_keys("<Ctrl-d>"), Action::Quit);
|
||||||
|
inner_chain.insert(unchecked_keys("<Ctrl-c>"), Action::Quit);
|
||||||
|
|
||||||
|
// HOME
|
||||||
|
inner_home.insert(
|
||||||
|
unchecked_keys("<Ctrl-h>"),
|
||||||
|
HomeAction::ToggleShowHelp.into(),
|
||||||
|
);
|
||||||
|
inner_home.insert(
|
||||||
|
unchecked_keys("<Ctrl-r>"),
|
||||||
|
HomeAction::ScheduleContractRefresh.into(),
|
||||||
|
);
|
||||||
|
inner_home.insert(unchecked_keys("</>"), HomeAction::StartInput.into());
|
||||||
|
|
||||||
|
// LOGGER
|
||||||
|
inner_logger.insert(unchecked_keys("<space>"), TuiWidgetEvent::SpaceKey.into());
|
||||||
|
inner_logger.insert(unchecked_keys("<esc>"), TuiWidgetEvent::EscapeKey.into());
|
||||||
|
inner_logger.insert(
|
||||||
|
unchecked_keys("<pageup>"),
|
||||||
|
TuiWidgetEvent::PrevPageKey.into(),
|
||||||
|
);
|
||||||
|
inner_logger.insert(
|
||||||
|
unchecked_keys("<pagedown>"),
|
||||||
|
TuiWidgetEvent::NextPageKey.into(),
|
||||||
|
);
|
||||||
|
inner_logger.insert(unchecked_keys("<up>"), TuiWidgetEvent::UpKey.into());
|
||||||
|
inner_logger.insert(unchecked_keys("<down>"), TuiWidgetEvent::DownKey.into());
|
||||||
|
inner_logger.insert(unchecked_keys("<left>"), TuiWidgetEvent::LeftKey.into());
|
||||||
|
inner_logger.insert(unchecked_keys("<right>"), TuiWidgetEvent::RightKey.into());
|
||||||
|
inner_logger.insert(unchecked_keys("<+>"), TuiWidgetEvent::PlusKey.into());
|
||||||
|
inner_logger.insert(unchecked_keys("<->"), TuiWidgetEvent::MinusKey.into());
|
||||||
|
inner_logger.insert(unchecked_keys("<h>"), TuiWidgetEvent::HideKey.into());
|
||||||
|
inner_logger.insert(unchecked_keys("<f>"), TuiWidgetEvent::FocusKey.into());
|
||||||
|
|
||||||
|
let mut inner = HashMap::new();
|
||||||
|
inner.insert(Mode::Home, inner_home);
|
||||||
|
inner.insert(Mode::Logger, inner_logger);
|
||||||
|
inner.insert(Mode::ChainHistory, inner_chain);
|
||||||
|
KeyBindings(inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unchecked_keys(raw: &str) -> Vec<KeyEvent> {
|
||||||
|
parse_key_sequence(raw).expect("failed to parse the key sequence")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_key_sequence(raw: &str) -> Result<Vec<KeyEvent>, String> {
|
||||||
|
if raw.chars().filter(|c| *c == '>').count() != raw.chars().filter(|c| *c == '<').count() {
|
||||||
|
return Err(format!("Unable to parse `{}`", raw));
|
||||||
|
}
|
||||||
|
let raw = if !raw.contains("><") {
|
||||||
|
let raw = raw.strip_prefix('<').unwrap_or(raw);
|
||||||
|
let raw = raw.strip_prefix('>').unwrap_or(raw);
|
||||||
|
raw
|
||||||
|
} else {
|
||||||
|
raw
|
||||||
|
};
|
||||||
|
let sequences = raw
|
||||||
|
.split("><")
|
||||||
|
.map(|seq| {
|
||||||
|
if let Some(s) = seq.strip_prefix('<') {
|
||||||
|
s
|
||||||
|
} else if let Some(s) = seq.strip_suffix('>') {
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
seq
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
sequences.into_iter().map(parse_key_event).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_key_event(raw: &str) -> Result<KeyEvent, String> {
|
||||||
|
let raw_lower = raw.to_ascii_lowercase();
|
||||||
|
let (remaining, modifiers) = extract_modifiers(&raw_lower);
|
||||||
|
parse_key_code_with_modifiers(remaining, modifiers)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_key_code_with_modifiers(
|
||||||
|
raw: &str,
|
||||||
|
mut modifiers: KeyModifiers,
|
||||||
|
) -> Result<KeyEvent, String> {
|
||||||
|
let c = match raw {
|
||||||
|
"esc" => KeyCode::Esc,
|
||||||
|
"enter" => KeyCode::Enter,
|
||||||
|
"left" => KeyCode::Left,
|
||||||
|
"right" => KeyCode::Right,
|
||||||
|
"up" => KeyCode::Up,
|
||||||
|
"down" => KeyCode::Down,
|
||||||
|
"home" => KeyCode::Home,
|
||||||
|
"end" => KeyCode::End,
|
||||||
|
"pageup" => KeyCode::PageUp,
|
||||||
|
"pagedown" => KeyCode::PageDown,
|
||||||
|
"backtab" => {
|
||||||
|
modifiers.insert(KeyModifiers::SHIFT);
|
||||||
|
KeyCode::BackTab
|
||||||
|
}
|
||||||
|
"backspace" => KeyCode::Backspace,
|
||||||
|
"delete" => KeyCode::Delete,
|
||||||
|
"insert" => KeyCode::Insert,
|
||||||
|
"f1" => KeyCode::F(1),
|
||||||
|
"f2" => KeyCode::F(2),
|
||||||
|
"f3" => KeyCode::F(3),
|
||||||
|
"f4" => KeyCode::F(4),
|
||||||
|
"f5" => KeyCode::F(5),
|
||||||
|
"f6" => KeyCode::F(6),
|
||||||
|
"f7" => KeyCode::F(7),
|
||||||
|
"f8" => KeyCode::F(8),
|
||||||
|
"f9" => KeyCode::F(9),
|
||||||
|
"f10" => KeyCode::F(10),
|
||||||
|
"f11" => KeyCode::F(11),
|
||||||
|
"f12" => KeyCode::F(12),
|
||||||
|
"space" => KeyCode::Char(' '),
|
||||||
|
"hyphen" => KeyCode::Char('-'),
|
||||||
|
"minus" => KeyCode::Char('-'),
|
||||||
|
"tab" => KeyCode::Tab,
|
||||||
|
c if c.len() == 1 => {
|
||||||
|
let mut c = c.chars().next().unwrap();
|
||||||
|
if modifiers.contains(KeyModifiers::SHIFT) {
|
||||||
|
c = c.to_ascii_uppercase();
|
||||||
|
}
|
||||||
|
KeyCode::Char(c)
|
||||||
|
}
|
||||||
|
_ => return Err(format!("Unable to parse {raw}")),
|
||||||
|
};
|
||||||
|
Ok(KeyEvent::new(c, modifiers))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) {
|
||||||
|
let mut modifiers = KeyModifiers::empty();
|
||||||
|
let mut current = raw;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match current {
|
||||||
|
rest if rest.starts_with("ctrl-") => {
|
||||||
|
modifiers.insert(KeyModifiers::CONTROL);
|
||||||
|
current = &rest[5..];
|
||||||
|
}
|
||||||
|
rest if rest.starts_with("alt-") => {
|
||||||
|
modifiers.insert(KeyModifiers::ALT);
|
||||||
|
current = &rest[4..];
|
||||||
|
}
|
||||||
|
rest if rest.starts_with("shift-") => {
|
||||||
|
modifiers.insert(KeyModifiers::SHIFT);
|
||||||
|
current = &rest[6..];
|
||||||
|
}
|
||||||
|
_ => break, // break out of the loop if no known prefix is detected
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
(current, modifiers)
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
// based on https://github.com/ratatui-org/ratatui-async-template template
|
||||||
|
|
||||||
|
use crate::action::Action;
|
||||||
|
use crate::app::App;
|
||||||
|
use crate::cli::Args;
|
||||||
|
|
||||||
|
use crate::utils::{initialise_logger, initialize_panic_handler};
|
||||||
|
use clap::Parser;
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use tokio::sync::mpsc::UnboundedReceiver;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
pub mod action;
|
||||||
|
pub mod app;
|
||||||
|
pub mod cli;
|
||||||
|
pub mod components;
|
||||||
|
pub mod keybindings;
|
||||||
|
mod nyxd;
|
||||||
|
pub mod tui;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
initialize_panic_handler()?;
|
||||||
|
initialise_logger();
|
||||||
|
|
||||||
|
let args = Args::parse();
|
||||||
|
nym_network_defaults::setup_env(args.config_env_file.as_ref());
|
||||||
|
|
||||||
|
let (app, action_rx) = App::new(args).await?;
|
||||||
|
// app.run().await?;
|
||||||
|
run_app(app, action_rx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_app(mut app: App, mut action_rx: UnboundedReceiver<Action>) -> anyhow::Result<()> {
|
||||||
|
let mut tui = tui::Tui::new()?;
|
||||||
|
tui.enter()?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// convert tui event to an application action
|
||||||
|
if let Some(event) = tui.next().await {
|
||||||
|
match event {
|
||||||
|
tui::Event::Tick => app.tick(),
|
||||||
|
tui::Event::Render => {
|
||||||
|
tui.draw(|f| {
|
||||||
|
if let Err(err) = app.draw(f) {
|
||||||
|
error!("failed to draw: {err:?}")
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
tui::Event::Resize(x, y) => {
|
||||||
|
tui.resize(Rect::new(0, 0, x, y))?;
|
||||||
|
tui.draw(|_f| {})?;
|
||||||
|
}
|
||||||
|
tui::Event::Key(key) => app.key_event(key)?,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// consume all emitted actions
|
||||||
|
while let Ok(action) = action_rx.try_recv() {
|
||||||
|
app.update(action)?
|
||||||
|
}
|
||||||
|
|
||||||
|
if app.should_quit {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tui.exit()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::action::{BandwidthInfo, ContractsInfo, DkgInfo, GroupInfo, MultisigInfo};
|
||||||
|
use crate::cli::Args;
|
||||||
|
use crate::components::basic_contract_info::BasicContractInfo;
|
||||||
|
use crate::utils::zero_coin;
|
||||||
|
use futures::future::{join, join5};
|
||||||
|
use nym_coconut_dkg_common::types::Addr;
|
||||||
|
use nym_network_defaults::NymNetworkDetails;
|
||||||
|
use nym_validator_client::nyxd::contract_traits::{
|
||||||
|
DkgQueryClient, DkgSigningClient, GroupQueryClient, GroupSigningClient, NymContractsProvider,
|
||||||
|
PagedDkgQueryClient, PagedGroupQueryClient,
|
||||||
|
};
|
||||||
|
use nym_validator_client::nyxd::cw4::Cw4Contract;
|
||||||
|
use nym_validator_client::nyxd::error::NyxdError;
|
||||||
|
use nym_validator_client::nyxd::{cw2, cw4, AccountId, CosmWasmClient};
|
||||||
|
use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct NyxdClient(Arc<RwLock<Inner>>);
|
||||||
|
|
||||||
|
struct Inner(DirectSigningHttpRpcNyxdClient);
|
||||||
|
|
||||||
|
impl Deref for Inner {
|
||||||
|
type Target = DirectSigningHttpRpcNyxdClient;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
|
||||||
|
pub struct DkgState {
|
||||||
|
pub mix_denom: String,
|
||||||
|
pub multisig_addr: Addr,
|
||||||
|
pub group_addr: Cw4Contract,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Inner {
|
||||||
|
// that's nasty, but it works
|
||||||
|
pub async fn get_dkg_state(&self) -> anyhow::Result<DkgState> {
|
||||||
|
let dkg_contract_address = &self
|
||||||
|
.dkg_contract_address()
|
||||||
|
.ok_or_else(|| NyxdError::unavailable_contract_address("dkg contract"))?;
|
||||||
|
|
||||||
|
let res = self
|
||||||
|
.query_contract_raw(dkg_contract_address, b"state".to_vec())
|
||||||
|
.await?;
|
||||||
|
Ok(serde_json::from_slice(&res).map_err(NyxdError::from)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_raw_cw2_version(
|
||||||
|
&self,
|
||||||
|
contract: &AccountId,
|
||||||
|
) -> anyhow::Result<cw2::ContractVersion> {
|
||||||
|
let res = self
|
||||||
|
.query_contract_raw(contract, b"contract_info".to_vec())
|
||||||
|
.await?;
|
||||||
|
Ok(serde_json::from_slice(&res).map_err(NyxdError::from)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NyxdClient {
|
||||||
|
pub async fn dkg_contract(&self) -> anyhow::Result<AccountId> {
|
||||||
|
Ok(self
|
||||||
|
.0
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.dkg_contract_address()
|
||||||
|
.ok_or_else(|| NyxdError::unavailable_contract_address("dkg contract"))?
|
||||||
|
.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn group_contract(&self) -> anyhow::Result<AccountId> {
|
||||||
|
Ok(self
|
||||||
|
.0
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.group_contract_address()
|
||||||
|
.ok_or_else(|| NyxdError::unavailable_contract_address("group contract"))?
|
||||||
|
.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn bandwidth_contract(&self) -> anyhow::Result<AccountId> {
|
||||||
|
Ok(self
|
||||||
|
.0
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.coconut_bandwidth_contract_address()
|
||||||
|
.ok_or_else(|| NyxdError::unavailable_contract_address("bandwidth contract"))?
|
||||||
|
.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn multisig_contract(&self) -> anyhow::Result<AccountId> {
|
||||||
|
Ok(self
|
||||||
|
.0
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.multisig_contract_address()
|
||||||
|
.ok_or_else(|| NyxdError::unavailable_contract_address("multisig contract"))?
|
||||||
|
.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn address(&self) -> AccountId {
|
||||||
|
self.0.read().await.address()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn dkg_update(&self) -> anyhow::Result<DkgInfo> {
|
||||||
|
let address = self.dkg_contract().await?;
|
||||||
|
let guard = self.0.read().await;
|
||||||
|
|
||||||
|
let balance_fut = guard.get_balance(&address, "unym".to_string());
|
||||||
|
|
||||||
|
let epoch_fut = guard.get_current_epoch();
|
||||||
|
let threshold_fut = guard.get_current_epoch_threshold();
|
||||||
|
let dealers_fut = guard.get_all_current_dealers();
|
||||||
|
let past_dealers_fut = guard.get_all_past_dealers();
|
||||||
|
let state_fut = guard.get_dkg_state();
|
||||||
|
|
||||||
|
let info_res = join5(
|
||||||
|
epoch_fut,
|
||||||
|
threshold_fut,
|
||||||
|
dealers_fut,
|
||||||
|
past_dealers_fut,
|
||||||
|
state_fut,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let dkg_epoch = info_res.0?;
|
||||||
|
|
||||||
|
let epoch_dealings_fut = guard.get_all_epoch_dealings(dkg_epoch.epoch_id);
|
||||||
|
let epoch_vk_shares_fut = guard.get_all_verification_key_shares(dkg_epoch.epoch_id);
|
||||||
|
|
||||||
|
let epoch_res = join(epoch_dealings_fut, epoch_vk_shares_fut).await;
|
||||||
|
|
||||||
|
Ok(DkgInfo {
|
||||||
|
base: BasicContractInfo {
|
||||||
|
name: "DKG Contract".to_string(),
|
||||||
|
address: address.to_string(),
|
||||||
|
balance: balance_fut.await?.unwrap_or(zero_coin()),
|
||||||
|
cw2_version: None,
|
||||||
|
build_info: None,
|
||||||
|
},
|
||||||
|
epoch: dkg_epoch,
|
||||||
|
threshold: info_res.1?,
|
||||||
|
dealers: info_res.2?,
|
||||||
|
past_dealers: info_res.3?,
|
||||||
|
debug_state: info_res.4?,
|
||||||
|
epoch_dealings: epoch_res.0?,
|
||||||
|
vk_shares: epoch_res.1?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn group_update(&self) -> anyhow::Result<GroupInfo> {
|
||||||
|
let address = self.group_contract().await?;
|
||||||
|
let guard = self.0.read().await;
|
||||||
|
|
||||||
|
let balance_fut = guard.get_balance(&address, "unym".to_string());
|
||||||
|
let cw_version = guard.get_raw_cw2_version(&address);
|
||||||
|
|
||||||
|
let admin_fut = guard.admin();
|
||||||
|
let member_fut = guard.get_all_members();
|
||||||
|
let total_fut = guard.total_weight(None);
|
||||||
|
|
||||||
|
let res = join5(balance_fut, cw_version, admin_fut, member_fut, total_fut).await;
|
||||||
|
|
||||||
|
Ok(GroupInfo {
|
||||||
|
base: BasicContractInfo {
|
||||||
|
name: "CW4 Group Contract".to_string(),
|
||||||
|
address: address.to_string(),
|
||||||
|
balance: res.0?.unwrap_or(zero_coin()),
|
||||||
|
cw2_version: Some(res.1?),
|
||||||
|
build_info: None,
|
||||||
|
},
|
||||||
|
admin: res.2?,
|
||||||
|
members: res.3?,
|
||||||
|
total_weight: res.4?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn bandwidth_update(&self) -> anyhow::Result<BandwidthInfo> {
|
||||||
|
let address = self.bandwidth_contract().await?;
|
||||||
|
let guard = self.0.read().await;
|
||||||
|
|
||||||
|
let balance_fut = guard.get_balance(&address, "unym".to_string());
|
||||||
|
|
||||||
|
Ok(BandwidthInfo {
|
||||||
|
base: BasicContractInfo {
|
||||||
|
name: "Coconut Bandwidth Contract".to_string(),
|
||||||
|
address: address.to_string(),
|
||||||
|
balance: balance_fut.await?.unwrap_or(zero_coin()),
|
||||||
|
cw2_version: None,
|
||||||
|
build_info: None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn multisig_update(&self) -> anyhow::Result<MultisigInfo> {
|
||||||
|
let address = self.multisig_contract().await?;
|
||||||
|
let guard = self.0.read().await;
|
||||||
|
|
||||||
|
let balance_fut = guard.get_balance(&address, "unym".to_string());
|
||||||
|
let cw_version = guard.get_raw_cw2_version(&address);
|
||||||
|
|
||||||
|
let res = join(balance_fut, cw_version).await;
|
||||||
|
|
||||||
|
Ok(MultisigInfo {
|
||||||
|
base: BasicContractInfo {
|
||||||
|
name: "CW3 Flex Multisig".to_string(),
|
||||||
|
address: address.to_string(),
|
||||||
|
balance: res.0?.unwrap_or(zero_coin()),
|
||||||
|
cw2_version: Some(res.1?),
|
||||||
|
build_info: None,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_contract_update(&self) -> anyhow::Result<ContractsInfo> {
|
||||||
|
Ok(ContractsInfo {
|
||||||
|
dkg: self.dkg_update().await?,
|
||||||
|
group: self.group_update().await?,
|
||||||
|
bandwidth: self.bandwidth_update().await?,
|
||||||
|
multisig: self.multisig_update().await?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_group_member(&self, addr: String, weight: u64) -> anyhow::Result<()> {
|
||||||
|
let member = cw4::Member { addr, weight };
|
||||||
|
|
||||||
|
// we need to have a write lock here so that we wouldn't accidentally send multiple transactions
|
||||||
|
// into the same block (and thus have invalid seq numbers)
|
||||||
|
self.0
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.update_members(vec![member], vec![], None)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn remove_group_member(&self, address: String) -> anyhow::Result<()> {
|
||||||
|
// we need to have a write lock here so that we wouldn't accidentally send multiple transactions
|
||||||
|
// into the same block (and thus have invalid seq numbers)
|
||||||
|
self.0
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.update_members(vec![], vec![address], None)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn try_advance_epoch_state(&self) -> anyhow::Result<()> {
|
||||||
|
// we need to have a write lock here so that we wouldn't accidentally send multiple transactions
|
||||||
|
// into the same block (and thus have invalid seq numbers)
|
||||||
|
self.0.write().await.advance_dkg_epoch_state(None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn try_surpass_threshold(&self) -> anyhow::Result<()> {
|
||||||
|
// we need to have a write lock here so that we wouldn't accidentally send multiple transactions
|
||||||
|
// into the same block (and thus have invalid seq numbers)
|
||||||
|
self.0.write().await.surpass_threshold(None).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setup_nyxd_client(args: Args) -> anyhow::Result<(NyxdClient, Url)> {
|
||||||
|
let mut network_details = NymNetworkDetails::new_from_env();
|
||||||
|
|
||||||
|
if let Some(dkg_contract) = args.dkg_contract_address {
|
||||||
|
network_details.contracts.coconut_dkg_contract_address = Some(dkg_contract.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?;
|
||||||
|
|
||||||
|
let validator_endpoint = match args.nyxd_validator {
|
||||||
|
Some(endpoint) => endpoint,
|
||||||
|
None => network_details.endpoints[0].nyxd_url(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
NyxdClient(Arc::new(RwLock::new(Inner(
|
||||||
|
DirectSigningHttpRpcNyxdClient::connect_with_mnemonic(
|
||||||
|
client_config,
|
||||||
|
validator_endpoint.as_ref(),
|
||||||
|
args.admin_mnemonic,
|
||||||
|
)?,
|
||||||
|
)))),
|
||||||
|
validator_endpoint,
|
||||||
|
))
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
cursor,
|
||||||
|
event::{Event as CrosstermEvent, KeyEvent, KeyEventKind, MouseEvent},
|
||||||
|
terminal::{EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
use futures::{FutureExt, StreamExt};
|
||||||
|
use ratatui::backend::CrosstermBackend as Backend;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
use tokio::{
|
||||||
|
sync::mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||||
|
task::JoinHandle,
|
||||||
|
};
|
||||||
|
use tokio_util::sync::CancellationToken;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum Event {
|
||||||
|
Init,
|
||||||
|
Error,
|
||||||
|
Tick,
|
||||||
|
Render,
|
||||||
|
FocusGained,
|
||||||
|
FocusLost,
|
||||||
|
Paste(String),
|
||||||
|
Key(KeyEvent),
|
||||||
|
Mouse(MouseEvent),
|
||||||
|
Resize(u16, u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Tui {
|
||||||
|
pub terminal: ratatui::Terminal<Backend<std::io::Stderr>>,
|
||||||
|
pub task: JoinHandle<()>,
|
||||||
|
pub cancellation_token: CancellationToken,
|
||||||
|
pub event_rx: UnboundedReceiver<Event>,
|
||||||
|
pub event_tx: UnboundedSender<Event>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tui {
|
||||||
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
|
let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?;
|
||||||
|
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
||||||
|
let cancellation_token = CancellationToken::new();
|
||||||
|
let task = tokio::spawn(async {});
|
||||||
|
Ok(Self {
|
||||||
|
terminal,
|
||||||
|
task,
|
||||||
|
cancellation_token,
|
||||||
|
event_rx,
|
||||||
|
event_tx,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self) {
|
||||||
|
let tick_delay = std::time::Duration::from_secs_f64(1.0 / 4.0);
|
||||||
|
let render_delay = std::time::Duration::from_secs_f64(1.0 / 30.0);
|
||||||
|
self.cancel();
|
||||||
|
self.cancellation_token = CancellationToken::new();
|
||||||
|
let _cancellation_token = self.cancellation_token.clone();
|
||||||
|
let _event_tx = self.event_tx.clone();
|
||||||
|
self.task = tokio::spawn(async move {
|
||||||
|
let mut reader = crossterm::event::EventStream::new();
|
||||||
|
let mut tick_interval = tokio::time::interval(tick_delay);
|
||||||
|
let mut render_interval = tokio::time::interval(render_delay);
|
||||||
|
_event_tx.send(Event::Init).unwrap();
|
||||||
|
loop {
|
||||||
|
let tick_delay = tick_interval.tick();
|
||||||
|
let render_delay = render_interval.tick();
|
||||||
|
let crossterm_event = reader.next().fuse();
|
||||||
|
tokio::select! {
|
||||||
|
_ = _cancellation_token.cancelled() => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
maybe_event = crossterm_event => {
|
||||||
|
match maybe_event {
|
||||||
|
Some(Ok(evt)) => {
|
||||||
|
match evt {
|
||||||
|
CrosstermEvent::Key(key) => {
|
||||||
|
if key.kind == KeyEventKind::Press {
|
||||||
|
_event_tx.send(Event::Key(key)).unwrap();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
CrosstermEvent::Mouse(mouse) => {
|
||||||
|
_event_tx.send(Event::Mouse(mouse)).unwrap();
|
||||||
|
},
|
||||||
|
CrosstermEvent::Resize(x, y) => {
|
||||||
|
_event_tx.send(Event::Resize(x, y)).unwrap();
|
||||||
|
},
|
||||||
|
CrosstermEvent::FocusLost => {
|
||||||
|
_event_tx.send(Event::FocusLost).unwrap();
|
||||||
|
},
|
||||||
|
CrosstermEvent::FocusGained => {
|
||||||
|
_event_tx.send(Event::FocusGained).unwrap();
|
||||||
|
},
|
||||||
|
CrosstermEvent::Paste(s) => {
|
||||||
|
_event_tx.send(Event::Paste(s)).unwrap();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Err(_)) => {
|
||||||
|
_event_tx.send(Event::Error).unwrap();
|
||||||
|
}
|
||||||
|
None => {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ = tick_delay => {
|
||||||
|
_event_tx.send(Event::Tick).unwrap();
|
||||||
|
},
|
||||||
|
_ = render_delay => {
|
||||||
|
_event_tx.send(Event::Render).unwrap();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(&self) -> anyhow::Result<()> {
|
||||||
|
self.cancel();
|
||||||
|
let mut counter = 0;
|
||||||
|
while !self.task.is_finished() {
|
||||||
|
std::thread::sleep(Duration::from_millis(1));
|
||||||
|
counter += 1;
|
||||||
|
if counter > 50 {
|
||||||
|
self.task.abort();
|
||||||
|
}
|
||||||
|
if counter > 100 {
|
||||||
|
error!("Failed to abort task in 100 milliseconds for unknown reason");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enter(&mut self) -> anyhow::Result<()> {
|
||||||
|
crossterm::terminal::enable_raw_mode()?;
|
||||||
|
crossterm::execute!(std::io::stderr(), EnterAlternateScreen, cursor::Hide)?;
|
||||||
|
self.start();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exit(&mut self) -> anyhow::Result<()> {
|
||||||
|
self.stop()?;
|
||||||
|
if crossterm::terminal::is_raw_mode_enabled()? {
|
||||||
|
self.flush()?;
|
||||||
|
crossterm::execute!(std::io::stderr(), LeaveAlternateScreen, cursor::Show)?;
|
||||||
|
crossterm::terminal::disable_raw_mode()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel(&self) {
|
||||||
|
self.cancellation_token.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resume(&mut self) -> anyhow::Result<()> {
|
||||||
|
self.enter()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn next(&mut self) -> Option<Event> {
|
||||||
|
self.event_rx.recv().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Tui {
|
||||||
|
type Target = ratatui::Terminal<Backend<std::io::Stderr>>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.terminal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for Tui {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.terminal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Tui {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.exit().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
use nym_validator_client::nyxd::Coin;
|
||||||
|
use tracing::error;
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
use tracing_subscriber::{EnvFilter, Layer};
|
||||||
|
|
||||||
|
pub fn key_event_to_string(key_event: &KeyEvent) -> String {
|
||||||
|
let char;
|
||||||
|
let key_code = match key_event.code {
|
||||||
|
KeyCode::Backspace => "backspace",
|
||||||
|
KeyCode::Enter => "enter",
|
||||||
|
KeyCode::Left => "left",
|
||||||
|
KeyCode::Right => "right",
|
||||||
|
KeyCode::Up => "up",
|
||||||
|
KeyCode::Down => "down",
|
||||||
|
KeyCode::Home => "home",
|
||||||
|
KeyCode::End => "end",
|
||||||
|
KeyCode::PageUp => "pageup",
|
||||||
|
KeyCode::PageDown => "pagedown",
|
||||||
|
KeyCode::Tab => "tab",
|
||||||
|
KeyCode::BackTab => "backtab",
|
||||||
|
KeyCode::Delete => "delete",
|
||||||
|
KeyCode::Insert => "insert",
|
||||||
|
KeyCode::F(c) => {
|
||||||
|
char = format!("f({c})");
|
||||||
|
&char
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) if c == ' ' => "space",
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
char = c.to_string();
|
||||||
|
&char
|
||||||
|
}
|
||||||
|
KeyCode::Esc => "esc",
|
||||||
|
KeyCode::Null => "",
|
||||||
|
KeyCode::CapsLock => "",
|
||||||
|
KeyCode::Menu => "",
|
||||||
|
KeyCode::ScrollLock => "",
|
||||||
|
KeyCode::Media(_) => "",
|
||||||
|
KeyCode::NumLock => "",
|
||||||
|
KeyCode::PrintScreen => "",
|
||||||
|
KeyCode::Pause => "",
|
||||||
|
KeyCode::KeypadBegin => "",
|
||||||
|
KeyCode::Modifier(_) => "",
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut modifiers = Vec::with_capacity(3);
|
||||||
|
|
||||||
|
if key_event.modifiers.intersects(KeyModifiers::CONTROL) {
|
||||||
|
modifiers.push("ctrl");
|
||||||
|
}
|
||||||
|
|
||||||
|
if key_event.modifiers.intersects(KeyModifiers::SHIFT) {
|
||||||
|
modifiers.push("shift");
|
||||||
|
}
|
||||||
|
|
||||||
|
if key_event.modifiers.intersects(KeyModifiers::ALT) {
|
||||||
|
modifiers.push("alt");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut key = modifiers.join("-");
|
||||||
|
|
||||||
|
if !key.is_empty() {
|
||||||
|
key.push('-');
|
||||||
|
}
|
||||||
|
key.push_str(key_code);
|
||||||
|
|
||||||
|
key
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initialize_panic_handler() -> anyhow::Result<()> {
|
||||||
|
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default()
|
||||||
|
.panic_section(format!(
|
||||||
|
"This is a bug. Consider reporting it at {}",
|
||||||
|
env!("CARGO_PKG_REPOSITORY")
|
||||||
|
))
|
||||||
|
.display_location_section(true)
|
||||||
|
.display_env_section(true)
|
||||||
|
.into_hooks();
|
||||||
|
eyre_hook.install()?;
|
||||||
|
std::panic::set_hook(Box::new(move |panic_info| {
|
||||||
|
if let Ok(mut t) = crate::tui::Tui::new() {
|
||||||
|
if let Err(r) = t.exit() {
|
||||||
|
error!("Unable to exit Terminal: {:?}", r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = format!("{}", panic_hook.panic_report(panic_info));
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
eprintln!("{}", msg); // prints color-eyre stack trace to stderr
|
||||||
|
}
|
||||||
|
error!("Error: {}", strip_ansi_escapes::strip_str(msg));
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
// Better Panic stacktrace that is only enabled when debugging.
|
||||||
|
better_panic::Settings::auto()
|
||||||
|
.most_recent_first(false)
|
||||||
|
.lineno_suffix(true)
|
||||||
|
.verbosity(better_panic::Verbosity::Full)
|
||||||
|
.create_panic_handler()(panic_info);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::process::exit(1);
|
||||||
|
}));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initialise_logger() {
|
||||||
|
// tui_logger::set_default_level(log::LevelFilter::Trace);
|
||||||
|
|
||||||
|
let filter: EnvFilter = "trace,mio=warn,hyper=warn,tendermint_rpc=warn"
|
||||||
|
.parse()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(tui_logger::tracing_subscriber_layer().with_filter(filter))
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn zero_coin() -> Coin {
|
||||||
|
Coin::new(0, "unym")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user