Compare commits

...

1 Commits

Author SHA1 Message Date
Jędrzej Stuczyński f77e350211 [squashed] Created a DKG-manager tui interface 2023-11-30 17:38:19 +00:00
33 changed files with 2637 additions and 20 deletions
+1
View File
@@ -9,6 +9,7 @@
target
.env
.env.dev
envs/devnet.env
/.vscode/settings.json
validator/.vscode
sample-configs/validator-config.toml
+3 -1
View File
@@ -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 }
+2 -2
View File
@@ -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" }
@@ -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,
@@ -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"),
}
+1 -1
View File
@@ -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 }
+1 -1
View File
@@ -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"] }
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
+1 -1
View File
@@ -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"
+45
View File
@@ -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" }
+189
View File
@@ -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)
// }
// }
+242
View File
@@ -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(())
}
}
+33
View File
@@ -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)
}
+75
View File
@@ -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(())
}
+296
View File
@@ -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,
))
}
+189
View File
@@ -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();
}
}
+127
View File
@@ -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")
}