Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f77e350211 |
@@ -9,6 +9,7 @@
|
||||
target
|
||||
.env
|
||||
.env.dev
|
||||
envs/devnet.env
|
||||
/.vscode/settings.json
|
||||
validator/.vscode
|
||||
sample-configs/validator-config.toml
|
||||
|
||||
+3
-1
@@ -103,6 +103,7 @@ members = [
|
||||
"nym-outfox",
|
||||
"tools/internal/ssl-inject",
|
||||
"tools/internal/sdk-version-bump",
|
||||
"tools/internal/dkg-manager",
|
||||
"tools/nym-cli",
|
||||
"tools/nym-nr-query",
|
||||
"tools/ts-rs-cli",
|
||||
@@ -160,7 +161,8 @@ serde = "1.0.152"
|
||||
serde_json = "1.0.91"
|
||||
tap = "1.0.1"
|
||||
thiserror = "1.0.48"
|
||||
tokio = "1.24.1"
|
||||
tokio = "1.33.0"
|
||||
tokio-util = "0.7.10"
|
||||
tokio-tungstenite = "0.20.1"
|
||||
tracing = "0.1.37"
|
||||
tungstenite = { version = "0.20.1", default-features = false }
|
||||
|
||||
@@ -9,8 +9,8 @@ edition = "2021"
|
||||
[dependencies]
|
||||
futures = { workspace = true }
|
||||
log = { workspace = true }
|
||||
tokio = { version = "1.24.1", features = ["time", "net", "rt"] }
|
||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
||||
tokio = { workspace = true, features = ["time", "net", "rt"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
|
||||
# internal
|
||||
nym-sphinx = { path = "../../nymsphinx" }
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ pub trait CoconutBandwidthSigningClient {
|
||||
fee: Option<Fee>,
|
||||
) -> Result<ExecuteResult, NyxdError> {
|
||||
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(
|
||||
fee,
|
||||
|
||||
+22
-2
@@ -6,8 +6,8 @@ use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use cw3::{
|
||||
ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterListResponse,
|
||||
VoterResponse,
|
||||
ProposalListResponse, ProposalResponse, VoteListResponse, VoteResponse, VoterDetail,
|
||||
VoterListResponse, VoterResponse,
|
||||
};
|
||||
use cw_utils::ThresholdResponse;
|
||||
use nym_multisig_contract_common::msg::QueryMsg as MultisigQueryMsg;
|
||||
@@ -114,6 +114,26 @@ pub trait PagedMultisigQueryClient: MultisigQueryClient {
|
||||
|
||||
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]
|
||||
|
||||
@@ -47,6 +47,10 @@ pub use cosmrs::Coin as CosmosCoin;
|
||||
pub use cosmrs::Gas;
|
||||
pub use cosmrs::{bip32, AccountId, Denom};
|
||||
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 tendermint_rpc::{
|
||||
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 {
|
||||
match self {
|
||||
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 } => {
|
||||
write!(f, "VerificationKeySubmission with resharing {resharing}")
|
||||
write!(f, "VerificationKeySubmission (resharing: {resharing})")
|
||||
}
|
||||
EpochState::VerificationKeyValidation { resharing } => {
|
||||
write!(f, "VerificationKeyValidation with resharing {resharing}")
|
||||
write!(f, "VerificationKeyValidation (resharing: {resharing})")
|
||||
}
|
||||
EpochState::VerificationKeyFinalization { resharing } => {
|
||||
write!(f, "VerificationKeyFinalization with resharing {resharing}")
|
||||
write!(f, "VerificationKeyFinalization (resharing: {resharing})")
|
||||
}
|
||||
EpochState::InProgress => write!(f, "InProgress"),
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ tokio = { version = "1.24.1", features = [
|
||||
"net",
|
||||
"io-util",
|
||||
] }
|
||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
url = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ repository = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.0"
|
||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] }
|
||||
|
||||
@@ -8,8 +8,8 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytes = "1.0"
|
||||
tokio = { version = "1.24.1", 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 = { workspace = true, features = [ "net", "io-util", "sync", "macros", "time", "rt-multi-thread" ] }
|
||||
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.
|
||||
# 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 }
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ serde_json = "1.0.91"
|
||||
thiserror = { workspace = true }
|
||||
tokio = { version = "1", features = ["macros", "net","rt-multi-thread"] }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
tokio-util = { version = "0.7.4", features = ["full"] }
|
||||
tokio-util = { workspace = true, features = ["full"] }
|
||||
toml = "0.7.0"
|
||||
unsigned-varint = "0.7.1"
|
||||
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-tungstenite = { version = "0.20.1" }
|
||||
tokio-util = { version = "0.7.4", features = ["codec"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
zeroize = { workspace = true }
|
||||
|
||||
|
||||
+2
-2
@@ -33,8 +33,8 @@ rand = "0.7.3"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sysinfo = "0.27.7"
|
||||
tokio = { version = "1.21.2", features = ["rt-multi-thread", "net", "signal"] }
|
||||
tokio-util = { version = "0.7.3", features = ["codec"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
toml = "0.5.8"
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
cfg-if = "1.0.0"
|
||||
|
||||
@@ -49,7 +49,7 @@ nym-bin-common = { path = "../../../common/bin-common" }
|
||||
# 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" ]}
|
||||
tokio-stream = "0.1.12"
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
parking_lot = "0.12"
|
||||
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