Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f77e350211 | |||
| 4a8c09f476 | |||
| ca2ad13579 | |||
| 44395f8466 | |||
| 39d714f2c0 | |||
| 336f220c83 | |||
| 6b2c13b3fd | |||
| 17357da97e | |||
| d92c8c4149 | |||
| 4c8fa74dfe | |||
| b1ac5b4e86 | |||
| ae22a132a3 | |||
| d0b9583b79 | |||
| 1f5568b4a7 | |||
| 16f51a0014 | |||
| 5e8767c5d6 | |||
| 15abfb390f | |||
| 45e5f12198 | |||
| 24746dc47b | |||
| ef8e452f30 |
@@ -9,6 +9,7 @@
|
||||
target
|
||||
.env
|
||||
.env.dev
|
||||
envs/devnet.env
|
||||
/.vscode/settings.json
|
||||
validator/.vscode
|
||||
sample-configs/validator-config.toml
|
||||
|
||||
Generated
+3
@@ -6608,7 +6608,9 @@ dependencies = [
|
||||
"bincode",
|
||||
"bytes",
|
||||
"nym-sphinx",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6633,6 +6635,7 @@ dependencies = [
|
||||
"nym-tun",
|
||||
"nym-wireguard",
|
||||
"nym-wireguard-types",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
||||
+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"),
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::Decimal;
|
||||
use cosmwasm_std::OverflowError;
|
||||
use cosmwasm_std::Uint128;
|
||||
use serde::de::Error;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
@@ -71,6 +72,10 @@ impl Percent {
|
||||
// we know the cast from u128 to u8 is a safe one since the internal value must be within 0 - 1 range
|
||||
truncate_decimal(hundred * self.0).u128() as u8
|
||||
}
|
||||
|
||||
pub fn checked_pow(&self, exp: u32) -> Result<Self, OverflowError> {
|
||||
self.0.checked_pow(exp).map(Percent)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Percent {
|
||||
|
||||
@@ -366,11 +366,7 @@ pub fn creating_proof_of_chunking_for_100_parties(c: &mut Criterion) {
|
||||
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let remote_share_key_pairs = shares
|
||||
.iter()
|
||||
.zip(receivers.values())
|
||||
.map(|(share, key)| (share, key))
|
||||
.collect::<Vec<_>>();
|
||||
let remote_share_key_pairs = shares.iter().zip(receivers.values()).collect::<Vec<_>>();
|
||||
let ordered_public_keys = receivers.values().copied().collect::<Vec<_>>();
|
||||
|
||||
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng);
|
||||
@@ -400,11 +396,7 @@ pub fn verifying_proof_of_chunking_for_100_parties(c: &mut Criterion) {
|
||||
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let remote_share_key_pairs = shares
|
||||
.iter()
|
||||
.zip(receivers.values())
|
||||
.map(|(share, key)| (share, key))
|
||||
.collect::<Vec<_>>();
|
||||
let remote_share_key_pairs = shares.iter().zip(receivers.values()).collect::<Vec<_>>();
|
||||
let ordered_public_keys = receivers.values().copied().collect::<Vec<_>>();
|
||||
|
||||
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng);
|
||||
@@ -436,11 +428,7 @@ pub fn creating_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) {
|
||||
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let remote_share_key_pairs = shares
|
||||
.iter()
|
||||
.zip(receivers.values())
|
||||
.map(|(share, key)| (share, key))
|
||||
.collect::<Vec<_>>();
|
||||
let remote_share_key_pairs = shares.iter().zip(receivers.values()).collect::<Vec<_>>();
|
||||
|
||||
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng);
|
||||
|
||||
@@ -478,11 +466,7 @@ pub fn verifying_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) {
|
||||
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let remote_share_key_pairs = shares
|
||||
.iter()
|
||||
.zip(receivers.values())
|
||||
.map(|(share, key)| (share, key))
|
||||
.collect::<Vec<_>>();
|
||||
let remote_share_key_pairs = shares.iter().zip(receivers.values()).collect::<Vec<_>>();
|
||||
|
||||
let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng);
|
||||
|
||||
@@ -545,11 +529,7 @@ pub fn share_encryption_100(c: &mut Criterion) {
|
||||
.map(|&node_index| polynomial.evaluate_at(&Scalar::from(node_index)).into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let remote_share_key_pairs = shares
|
||||
.iter()
|
||||
.zip(receivers.values())
|
||||
.map(|(share, key)| (share, key))
|
||||
.collect::<Vec<_>>();
|
||||
let remote_share_key_pairs = shares.iter().zip(receivers.values()).collect::<Vec<_>>();
|
||||
|
||||
c.bench_function("100 shares encryption", |b| {
|
||||
b.iter(|| black_box(encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng)))
|
||||
|
||||
@@ -14,4 +14,6 @@ license.workspace = true
|
||||
bincode = "1.3.3"
|
||||
bytes = "1.5.0"
|
||||
nym-sphinx = { path = "../nymsphinx" }
|
||||
rand = "0.8.5"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
@@ -1,35 +1,308 @@
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct TaggedIpPacket {
|
||||
pub packet: bytes::Bytes,
|
||||
pub return_address: nym_sphinx::addressing::clients::Recipient,
|
||||
pub return_mix_hops: Option<u8>,
|
||||
// pub return_mix_delays: Option<f64>,
|
||||
use std::net::IpAddr;
|
||||
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub const CURRENT_VERSION: u8 = 1;
|
||||
|
||||
fn generate_random() -> u64 {
|
||||
use rand::RngCore;
|
||||
let mut rng = rand::rngs::OsRng;
|
||||
rng.next_u64()
|
||||
}
|
||||
|
||||
impl TaggedIpPacket {
|
||||
pub fn new(
|
||||
packet: bytes::Bytes,
|
||||
return_address: nym_sphinx::addressing::clients::Recipient,
|
||||
return_mix_hops: Option<u8>,
|
||||
) -> Self {
|
||||
TaggedIpPacket {
|
||||
packet,
|
||||
return_address,
|
||||
return_mix_hops,
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct IpPacketRequest {
|
||||
pub version: u8,
|
||||
pub data: IpPacketRequestData,
|
||||
}
|
||||
|
||||
impl IpPacketRequest {
|
||||
pub fn new_static_connect_request(
|
||||
ip: IpAddr,
|
||||
reply_to: Recipient,
|
||||
reply_to_hops: Option<u8>,
|
||||
reply_to_avg_mix_delays: Option<f64>,
|
||||
) -> (Self, u64) {
|
||||
let request_id = generate_random();
|
||||
(
|
||||
Self {
|
||||
version: CURRENT_VERSION,
|
||||
data: IpPacketRequestData::StaticConnect(StaticConnectRequest {
|
||||
request_id,
|
||||
ip,
|
||||
reply_to,
|
||||
reply_to_hops,
|
||||
reply_to_avg_mix_delays,
|
||||
}),
|
||||
},
|
||||
request_id,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_dynamic_connect_request(
|
||||
reply_to: Recipient,
|
||||
reply_to_hops: Option<u8>,
|
||||
reply_to_avg_mix_delays: Option<f64>,
|
||||
) -> (Self, u64) {
|
||||
let request_id = generate_random();
|
||||
(
|
||||
Self {
|
||||
version: CURRENT_VERSION,
|
||||
data: IpPacketRequestData::DynamicConnect(DynamicConnectRequest {
|
||||
request_id,
|
||||
reply_to,
|
||||
reply_to_hops,
|
||||
reply_to_avg_mix_delays,
|
||||
}),
|
||||
},
|
||||
request_id,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn new_ip_packet(ip_packet: bytes::Bytes) -> Self {
|
||||
Self {
|
||||
version: CURRENT_VERSION,
|
||||
data: IpPacketRequestData::Data(DataRequest { ip_packet }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Option<u64> {
|
||||
match &self.data {
|
||||
IpPacketRequestData::StaticConnect(request) => Some(request.request_id),
|
||||
IpPacketRequestData::DynamicConnect(request) => Some(request.request_id),
|
||||
IpPacketRequestData::Data(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recipient(&self) -> Option<&Recipient> {
|
||||
match &self.data {
|
||||
IpPacketRequestData::StaticConnect(request) => Some(&request.reply_to),
|
||||
IpPacketRequestData::DynamicConnect(request) => Some(&request.reply_to),
|
||||
IpPacketRequestData::Data(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().serialize(self)
|
||||
}
|
||||
|
||||
pub fn from_reconstructed_message(
|
||||
message: &nym_sphinx::receiver::ReconstructedMessage,
|
||||
) -> Result<Self, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().deserialize(&message.message)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum IpPacketRequestData {
|
||||
StaticConnect(StaticConnectRequest),
|
||||
DynamicConnect(DynamicConnectRequest),
|
||||
Data(DataRequest),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct StaticConnectRequest {
|
||||
pub request_id: u64,
|
||||
pub ip: IpAddr,
|
||||
pub reply_to: Recipient,
|
||||
pub reply_to_hops: Option<u8>,
|
||||
pub reply_to_avg_mix_delays: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DynamicConnectRequest {
|
||||
pub request_id: u64,
|
||||
pub reply_to: Recipient,
|
||||
pub reply_to_hops: Option<u8>,
|
||||
pub reply_to_avg_mix_delays: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DataRequest {
|
||||
pub ip_packet: bytes::Bytes,
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct IpPacketResponse {
|
||||
pub version: u8,
|
||||
pub data: IpPacketResponseData,
|
||||
}
|
||||
|
||||
impl IpPacketResponse {
|
||||
pub fn new_static_connect_success(request_id: u64, reply_to: Recipient) -> Self {
|
||||
Self {
|
||||
version: CURRENT_VERSION,
|
||||
data: IpPacketResponseData::StaticConnect(StaticConnectResponse {
|
||||
request_id,
|
||||
reply_to,
|
||||
reply: StaticConnectResponseReply::Success,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_static_connect_failure(
|
||||
request_id: u64,
|
||||
reply_to: Recipient,
|
||||
reason: StaticConnectFailureReason,
|
||||
) -> Self {
|
||||
Self {
|
||||
version: CURRENT_VERSION,
|
||||
data: IpPacketResponseData::StaticConnect(StaticConnectResponse {
|
||||
request_id,
|
||||
reply_to,
|
||||
reply: StaticConnectResponseReply::Failure(reason),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_dynamic_connect_success(request_id: u64, reply_to: Recipient, ip: IpAddr) -> Self {
|
||||
Self {
|
||||
version: CURRENT_VERSION,
|
||||
data: IpPacketResponseData::DynamicConnect(DynamicConnectResponse {
|
||||
request_id,
|
||||
reply_to,
|
||||
reply: DynamicConnectResponseReply::Success(DynamicConnectSuccess { ip }),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_dynamic_connect_failure(
|
||||
request_id: u64,
|
||||
reply_to: Recipient,
|
||||
reason: DynamicConnectFailureReason,
|
||||
) -> Self {
|
||||
Self {
|
||||
version: CURRENT_VERSION,
|
||||
data: IpPacketResponseData::DynamicConnect(DynamicConnectResponse {
|
||||
request_id,
|
||||
reply_to,
|
||||
reply: DynamicConnectResponseReply::Failure(reason),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_ip_packet(ip_packet: bytes::Bytes) -> Self {
|
||||
Self {
|
||||
version: CURRENT_VERSION,
|
||||
data: IpPacketResponseData::Data(DataResponse { ip_packet }),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Option<u64> {
|
||||
match &self.data {
|
||||
IpPacketResponseData::StaticConnect(response) => Some(response.request_id),
|
||||
IpPacketResponseData::DynamicConnect(response) => Some(response.request_id),
|
||||
IpPacketResponseData::Data(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recipient(&self) -> Option<&Recipient> {
|
||||
match &self.data {
|
||||
IpPacketResponseData::StaticConnect(response) => Some(&response.reply_to),
|
||||
IpPacketResponseData::DynamicConnect(response) => Some(&response.reply_to),
|
||||
IpPacketResponseData::Data(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().serialize(self)
|
||||
}
|
||||
|
||||
pub fn from_reconstructed_message(
|
||||
message: &nym_sphinx::receiver::ReconstructedMessage,
|
||||
) -> Result<Self, bincode::Error> {
|
||||
use bincode::Options;
|
||||
make_bincode_serializer().deserialize(&message.message)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum IpPacketResponseData {
|
||||
StaticConnect(StaticConnectResponse),
|
||||
DynamicConnect(DynamicConnectResponse),
|
||||
Data(DataResponse),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct StaticConnectResponse {
|
||||
pub request_id: u64,
|
||||
pub reply_to: Recipient,
|
||||
pub reply: StaticConnectResponseReply,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum StaticConnectResponseReply {
|
||||
Success,
|
||||
Failure(StaticConnectFailureReason),
|
||||
}
|
||||
|
||||
impl StaticConnectResponseReply {
|
||||
pub fn is_success(&self) -> bool {
|
||||
match self {
|
||||
StaticConnectResponseReply::Success => true,
|
||||
StaticConnectResponseReply::Failure(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum StaticConnectFailureReason {
|
||||
#[error("requested ip address is already in use")]
|
||||
RequestedIpAlreadyInUse,
|
||||
#[error("requested nym-address is already in use")]
|
||||
RequestedNymAddressAlreadyInUse,
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct DynamicConnectResponse {
|
||||
pub request_id: u64,
|
||||
pub reply_to: Recipient,
|
||||
pub reply: DynamicConnectResponseReply,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum DynamicConnectResponseReply {
|
||||
Success(DynamicConnectSuccess),
|
||||
Failure(DynamicConnectFailureReason),
|
||||
}
|
||||
|
||||
impl DynamicConnectResponseReply {
|
||||
pub fn is_success(&self) -> bool {
|
||||
match self {
|
||||
DynamicConnectResponseReply::Success(_) => true,
|
||||
DynamicConnectResponseReply::Failure(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct DynamicConnectSuccess {
|
||||
pub ip: IpAddr,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, thiserror::Error)]
|
||||
pub enum DynamicConnectFailureReason {
|
||||
#[error("requested nym-address is already in use")]
|
||||
RequestedNymAddressAlreadyInUse,
|
||||
#[error("no available ip address")]
|
||||
NoAvailableIp,
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct DataResponse {
|
||||
pub ip_packet: bytes::Bytes,
|
||||
}
|
||||
|
||||
fn make_bincode_serializer() -> impl bincode::Options {
|
||||
@@ -38,3 +311,63 @@ fn make_bincode_serializer() -> impl bincode::Options {
|
||||
.with_big_endian()
|
||||
.with_varint_encoding()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn check_size_of_request() {
|
||||
let connect = IpPacketRequest {
|
||||
version: 4,
|
||||
data: IpPacketRequestData::StaticConnect(
|
||||
StaticConnectRequest {
|
||||
request_id: 123,
|
||||
ip: IpAddr::from([10, 0, 0, 1]),
|
||||
reply_to: Recipient::try_from_base58_string("D1rrpsysCGCYXy9saP8y3kmNpGtJZUXN9SvFoUcqAsM9.9Ssso1ea5NfkbMASdiseDSjTN1fSWda5SgEVjdSN4CvV@GJqd3ZxpXWSNxTfx7B1pPtswpetH4LnJdFeLeuY5KUuN").unwrap(),
|
||||
reply_to_hops: None,
|
||||
reply_to_avg_mix_delays: None,
|
||||
},
|
||||
)
|
||||
};
|
||||
assert_eq!(connect.to_bytes().unwrap().len(), 107);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_size_of_data() {
|
||||
let data = IpPacketRequest {
|
||||
version: 4,
|
||||
data: IpPacketRequestData::Data(DataRequest {
|
||||
ip_packet: bytes::Bytes::from(vec![1u8; 32]),
|
||||
}),
|
||||
};
|
||||
assert_eq!(data.to_bytes().unwrap().len(), 35);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_and_deserialize_data_request() {
|
||||
let data = IpPacketRequest {
|
||||
version: 4,
|
||||
data: IpPacketRequestData::Data(DataRequest {
|
||||
ip_packet: bytes::Bytes::from(vec![1, 2, 4, 2, 5]),
|
||||
}),
|
||||
};
|
||||
|
||||
let serialized = data.to_bytes().unwrap();
|
||||
let deserialized = IpPacketRequest::from_reconstructed_message(
|
||||
&nym_sphinx::receiver::ReconstructedMessage {
|
||||
message: serialized,
|
||||
sender_tag: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(deserialized.version, 4);
|
||||
assert_eq!(
|
||||
deserialized.data,
|
||||
IpPacketRequestData::Data(DataRequest {
|
||||
ip_packet: bytes::Bytes::from(vec![1, 2, 4, 2, 5]),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@ use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use ff::Field;
|
||||
use group::{Curve, Group};
|
||||
use nym_coconut::{
|
||||
aggregate_signature_shares, aggregate_verification_keys, blind_sign, elgamal_keygen,
|
||||
prepare_blind_sign, prove_bandwidth_credential, setup, ttp_keygen, verify_credential,
|
||||
Attribute, BlindedSignature, Parameters, Signature, SignatureShare, VerificationKey,
|
||||
aggregate_signature_shares, aggregate_verification_keys, blind_sign, prepare_blind_sign,
|
||||
prove_bandwidth_credential, setup, ttp_keygen, verify_credential,
|
||||
verify_partial_blind_signature, Attribute, BlindedSignature, Parameters, Signature,
|
||||
SignatureShare, VerificationKey,
|
||||
};
|
||||
use rand::seq::SliceRandom;
|
||||
use std::ops::Neg;
|
||||
@@ -175,8 +176,6 @@ fn bench_coconut(c: &mut Criterion) {
|
||||
let binding_number = params.random_scalar();
|
||||
let private_attributes = vec![serial_number, binding_number];
|
||||
|
||||
let _elgamal_keypair = elgamal_keygen(¶ms);
|
||||
|
||||
// The prepare blind sign is performed by the user
|
||||
let (pedersen_commitments_openings, blind_sign_request) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
@@ -242,6 +241,29 @@ fn bench_coconut(c: &mut Criterion) {
|
||||
.map(|keypair| keypair.verification_key())
|
||||
.collect();
|
||||
|
||||
// verify a random partial blind signature
|
||||
let rand_idx = 1;
|
||||
let random_blind_signature = blinded_signatures.get(rand_idx).unwrap();
|
||||
let partial_verification_key = verification_keys.get(rand_idx).unwrap();
|
||||
|
||||
group.bench_function(
|
||||
&format!(
|
||||
"verify_partial_blind_signature_{}_private_attributes_{}_public_attributes",
|
||||
case.num_private_attrs, case.num_public_attrs
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
verify_partial_blind_signature(
|
||||
¶ms,
|
||||
&blind_sign_request,
|
||||
&public_attributes,
|
||||
random_blind_signature,
|
||||
partial_verification_key,
|
||||
)
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
// Lets bench worse case, ie aggregating all
|
||||
let indices: Vec<u64> = (1..=case.num_authorities).collect();
|
||||
// aggregate verification keys
|
||||
|
||||
@@ -14,6 +14,7 @@ pub use scheme::aggregation::aggregate_signature_shares;
|
||||
pub use scheme::aggregation::aggregate_verification_keys;
|
||||
pub use scheme::issuance::blind_sign;
|
||||
pub use scheme::issuance::prepare_blind_sign;
|
||||
pub use scheme::issuance::verify_partial_blind_signature;
|
||||
pub use scheme::issuance::BlindSignRequest;
|
||||
pub use scheme::keygen::ttp_keygen;
|
||||
pub use scheme::keygen::KeyPair;
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
use std::ops::Neg;
|
||||
|
||||
use bls12_381::{G1Affine, G1Projective, Scalar};
|
||||
use group::{Curve, GroupEncoding};
|
||||
use bls12_381::{multi_miller_loop, G1Affine, G1Projective, G2Prepared, Scalar};
|
||||
use group::{Curve, Group, GroupEncoding};
|
||||
|
||||
use crate::error::{CoconutError, Result};
|
||||
use crate::proofs::ProofCmCs;
|
||||
use crate::scheme::keygen::VerificationKey;
|
||||
use crate::scheme::setup::Parameters;
|
||||
use crate::scheme::BlindedSignature;
|
||||
use crate::scheme::SecretKey;
|
||||
@@ -318,6 +320,96 @@ pub fn blind_sign(
|
||||
Ok(BlindedSignature(h, sig))
|
||||
}
|
||||
|
||||
/// Verifies a partial blind signature using the provided parameters and validator's verification key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `params` - A reference to the cryptographic parameters.
|
||||
/// * `blind_sign_request` - A reference to the blind signature request signed by the client.
|
||||
/// * `public_attributes` - A reference to the public attributes included in the client's request.
|
||||
/// * `blind_sig` - A reference to the issued partial blinded signature to be verified.
|
||||
/// * `partial_verification_key` - A reference to the validator's partial verification key.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A boolean indicating whether the partial blind signature is valid (`true`) or not (`false`).
|
||||
///
|
||||
/// # Remarks
|
||||
///
|
||||
/// This function verifies the correctness and validity of a partial blind signature using
|
||||
/// the provided cryptographic parameters, blind signature request, blinded signature,
|
||||
/// and partial verification key.
|
||||
/// It calculates pairings based on the provided values and checks whether the partial blind signature
|
||||
/// is consistent with the verification key and commitments in the blind signature request.
|
||||
/// The function returns `true` if the partial blind signature is valid, and `false` otherwise.
|
||||
pub fn verify_partial_blind_signature(
|
||||
params: &Parameters,
|
||||
blind_sign_request: &BlindSignRequest,
|
||||
public_attributes: &[Attribute],
|
||||
blind_sig: &BlindedSignature,
|
||||
partial_verification_key: &VerificationKey,
|
||||
) -> bool {
|
||||
let num_private_attributes = blind_sign_request.private_attributes_commitments.len();
|
||||
if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: we're losing some memory here due to extra allocation,
|
||||
// but worst-case scenario (given SANE amount of attributes), it's just few kb at most
|
||||
let c_neg = blind_sig.1.to_affine().neg();
|
||||
let g2_prep = params.prepared_miller_g2();
|
||||
|
||||
let mut terms = vec![
|
||||
// (c^{-1}, g2)
|
||||
(c_neg, g2_prep.clone()),
|
||||
// (s, alpha)
|
||||
(
|
||||
blind_sig.0.to_affine(),
|
||||
G2Prepared::from(partial_verification_key.alpha.to_affine()),
|
||||
),
|
||||
];
|
||||
|
||||
// for each private attribute, add (cm_i, beta_i) to the miller terms
|
||||
for (private_attr_commit, beta_g2) in blind_sign_request
|
||||
.private_attributes_commitments
|
||||
.iter()
|
||||
.zip(&partial_verification_key.beta_g2)
|
||||
{
|
||||
// (cm_i, beta_i)
|
||||
terms.push((
|
||||
private_attr_commit.to_affine(),
|
||||
G2Prepared::from(beta_g2.to_affine()),
|
||||
))
|
||||
}
|
||||
|
||||
// for each public attribute, add (s^pub_j, beta_{priv + j}) to the miller terms
|
||||
for (pub_attr, beta_g2) in public_attributes.iter().zip(
|
||||
partial_verification_key
|
||||
.beta_g2
|
||||
.iter()
|
||||
.skip(num_private_attributes),
|
||||
) {
|
||||
// (s^pub_j, beta_j)
|
||||
terms.push((
|
||||
(blind_sig.0 * pub_attr).to_affine(),
|
||||
G2Prepared::from(beta_g2.to_affine()),
|
||||
))
|
||||
}
|
||||
|
||||
// get the references to all the terms to get the arguments the miller loop expects
|
||||
let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::<Vec<_>>();
|
||||
|
||||
// since checking whether e(a, b) == e(c, d)
|
||||
// is equivalent to checking e(a, b) • e(c, d)^{-1} == id
|
||||
// and thus to e(a, b) • e(c^{-1}, d) == id
|
||||
//
|
||||
// compute e(c^1, g2) • e(s, alpha) • e(cm_0, beta_0) • e(cm_i, beta_i) • (s^pub_0, beta_{i+1}) (s^pub_j, beta_{i + j})
|
||||
multi_miller_loop(&terms_refs)
|
||||
.final_exponentiation()
|
||||
.is_identity()
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn sign(
|
||||
params: &mut Parameters,
|
||||
@@ -354,6 +446,7 @@ pub fn sign(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::scheme::keygen::keygen;
|
||||
|
||||
#[test]
|
||||
fn blind_sign_request_bytes_roundtrip() {
|
||||
@@ -385,4 +478,81 @@ mod tests {
|
||||
lambda
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_verify_partial_blind_signature() {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
let private_attributes = params.n_random_scalars(2);
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
|
||||
let (_commitments_openings, request) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
|
||||
let validator_keypair = keygen(¶ms);
|
||||
let blind_sig = blind_sign(
|
||||
¶ms,
|
||||
&validator_keypair.secret_key(),
|
||||
&request,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(verify_partial_blind_signature(
|
||||
¶ms,
|
||||
&request,
|
||||
&public_attributes,
|
||||
&blind_sig,
|
||||
&validator_keypair.verification_key()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn successful_verify_partial_blind_signature_no_public_attributes() {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
let private_attributes = params.n_random_scalars(2);
|
||||
|
||||
let (_commitments_openings, request) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &[]).unwrap();
|
||||
|
||||
let validator_keypair = keygen(¶ms);
|
||||
let blind_sig =
|
||||
blind_sign(¶ms, &validator_keypair.secret_key(), &request, &[]).unwrap();
|
||||
|
||||
assert!(verify_partial_blind_signature(
|
||||
¶ms,
|
||||
&request,
|
||||
&[],
|
||||
&blind_sig,
|
||||
&validator_keypair.verification_key()
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fail_verify_partial_blind_signature_with_wrong_key() {
|
||||
let params = Parameters::new(4).unwrap();
|
||||
let private_attributes = params.n_random_scalars(2);
|
||||
let public_attributes = params.n_random_scalars(2);
|
||||
|
||||
let (_commitments_openings, request) =
|
||||
prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap();
|
||||
|
||||
let validator_keypair = keygen(¶ms);
|
||||
let validator2_keypair = keygen(¶ms);
|
||||
let blind_sig = blind_sign(
|
||||
¶ms,
|
||||
&validator_keypair.secret_key(),
|
||||
&request,
|
||||
&public_attributes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// this assertion should fail, as we try to verify with a wrong validator key
|
||||
assert!(!verify_partial_blind_signature(
|
||||
¶ms,
|
||||
&request,
|
||||
&public_attributes,
|
||||
&blind_sig,
|
||||
&validator2_keypair.verification_key()
|
||||
),);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -198,7 +198,7 @@ impl TunDevice {
|
||||
async fn handle_tun_read(&self, packet: &[u8]) -> Result<(), TunDeviceError> {
|
||||
let ParsedAddresses { src_addr, dst_addr } = parse_src_dst_address(packet)?;
|
||||
log::debug!(
|
||||
"iface: read Packet({src_addr} -> {dst_addr}, {} bytes)",
|
||||
"iface: read Packet({dst_addr} <- {src_addr}, {} bytes)",
|
||||
packet.len(),
|
||||
);
|
||||
|
||||
|
||||
+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 }
|
||||
|
||||
|
||||
@@ -476,6 +476,13 @@ impl<St> Gateway<St> {
|
||||
});
|
||||
}
|
||||
|
||||
self.start_client_websocket_listener(
|
||||
mix_forwarding_channel.clone(),
|
||||
active_clients_store.clone(),
|
||||
shutdown.subscribe().named("websocket::Listener"),
|
||||
Arc::new(coconut_verifier),
|
||||
);
|
||||
|
||||
let nr_request_filter = if self.config.network_requester.enabled {
|
||||
let embedded_nr = self
|
||||
.start_network_requester(
|
||||
@@ -496,7 +503,7 @@ impl<St> Gateway<St> {
|
||||
if self.config.ip_packet_router.enabled {
|
||||
let embedded_ip_sp = self
|
||||
.start_ip_packet_router(
|
||||
mix_forwarding_channel.clone(),
|
||||
mix_forwarding_channel,
|
||||
shutdown.subscribe().named("ip_service_provider"),
|
||||
)
|
||||
.await?;
|
||||
@@ -513,13 +520,6 @@ impl<St> Gateway<St> {
|
||||
.with_maybe_network_request_filter(nr_request_filter)
|
||||
.start(shutdown.subscribe().named("http-api"))?;
|
||||
|
||||
self.start_client_websocket_listener(
|
||||
mix_forwarding_channel,
|
||||
active_clients_store,
|
||||
shutdown.subscribe().named("websocket::Listener"),
|
||||
Arc::new(coconut_verifier),
|
||||
);
|
||||
|
||||
// Once this is a bit more mature, make this a commandline flag instead of a compile time
|
||||
// flag
|
||||
#[cfg(feature = "wireguard")]
|
||||
|
||||
+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"
|
||||
|
||||
@@ -24,7 +24,15 @@ struct MixnodeWithStakeAndPerformance {
|
||||
|
||||
impl MixnodeWithStakeAndPerformance {
|
||||
fn to_selection_weight(&self) -> f64 {
|
||||
let scaled_stake = self.total_stake * self.performance;
|
||||
let scaled_performance = match self.performance.checked_pow(20) {
|
||||
Ok(perf) => perf,
|
||||
Err(overflow) => {
|
||||
warn!("the node's performance ({}) has overflow while scaling it by the factor of 20: {overflow}. Setting it to 0 instead.", self.performance);
|
||||
return 0.;
|
||||
}
|
||||
};
|
||||
|
||||
let scaled_stake = self.total_stake * scaled_performance;
|
||||
stake_to_f64(scaled_stake)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ nym-task = { path = "../../common/task" }
|
||||
nym-tun = { path = "../../common/tun" }
|
||||
nym-wireguard = { path = "../../common/wireguard" }
|
||||
nym-wireguard-types = { path = "../../common/wireguard-types" }
|
||||
rand = "0.8.5"
|
||||
reqwest.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -30,6 +30,9 @@ pub enum IpPacketRouterError {
|
||||
#[error("the entity wrapping the network requester has disconnected")]
|
||||
DisconnectedParent,
|
||||
|
||||
#[error("received packet has an invalid version: {0}")]
|
||||
InvalidPacketVersion(u8),
|
||||
|
||||
#[error("failed to deserialize tagged packet: {source}")]
|
||||
FailedToDeserializeTaggedPacket { source: bincode::Error },
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
};
|
||||
|
||||
use crate::{ConnectedClient, TUN_DEVICE_ADDRESS};
|
||||
|
||||
// Find an available IP address in self.connected_clients
|
||||
// TODO: make this nicer
|
||||
fn generate_random_ip_within_subnet() -> Ipv4Addr {
|
||||
let mut rng = rand::thread_rng();
|
||||
// Generate a random number in the range 1-254
|
||||
let last_octet = rand::Rng::gen_range(&mut rng, 1..=254);
|
||||
Ipv4Addr::new(10, 0, 0, last_octet)
|
||||
}
|
||||
|
||||
fn is_ip_taken(
|
||||
connected_clients: &HashMap<IpAddr, ConnectedClient>,
|
||||
tun_ip: Ipv4Addr,
|
||||
ip: Ipv4Addr,
|
||||
) -> bool {
|
||||
connected_clients.contains_key(&ip.into()) || ip == tun_ip
|
||||
}
|
||||
|
||||
// TODO: brute force approach. We could consider using a more efficient algorithm
|
||||
pub(crate) fn find_new_ip(connected_clients: &HashMap<IpAddr, ConnectedClient>) -> Option<IpAddr> {
|
||||
let mut new_ip = generate_random_ip_within_subnet();
|
||||
let mut tries = 0;
|
||||
let tun_ip = TUN_DEVICE_ADDRESS.parse::<Ipv4Addr>().unwrap();
|
||||
while is_ip_taken(connected_clients, tun_ip, new_ip) {
|
||||
new_ip = generate_random_ip_within_subnet();
|
||||
tries += 1;
|
||||
if tries > 100 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(new_ip.into())
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
#![cfg_attr(not(target_os = "linux"), allow(dead_code))]
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
net::{IpAddr, SocketAddr},
|
||||
path::Path,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use error::IpPacketRouterError;
|
||||
@@ -11,6 +13,10 @@ use nym_client_core::{
|
||||
client::mix_traffic::transceiver::GatewayTransceiver,
|
||||
config::disk_persistence::CommonClientPaths, HardcodedTopologyProvider, TopologyProvider,
|
||||
};
|
||||
use nym_ip_packet_requests::{
|
||||
DynamicConnectFailureReason, IpPacketRequest, IpPacketRequestData, IpPacketResponse,
|
||||
StaticConnectFailureReason,
|
||||
};
|
||||
use nym_sdk::{
|
||||
mixnet::{InputMessage, MixnetMessageSender, Recipient},
|
||||
NymNetworkDetails,
|
||||
@@ -25,6 +31,7 @@ pub use crate::config::Config;
|
||||
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
mod generate_new_ip;
|
||||
mod request_filter;
|
||||
|
||||
// The interface used to route traffic
|
||||
@@ -32,6 +39,9 @@ pub const TUN_BASE_NAME: &str = "nymtun";
|
||||
pub const TUN_DEVICE_ADDRESS: &str = "10.0.0.1";
|
||||
pub const TUN_DEVICE_NETMASK: &str = "255.255.255.0";
|
||||
|
||||
const DISCONNECT_TIMER_INTERVAL: Duration = Duration::from_secs(10);
|
||||
const CLIENT_INACTIVITY_TIMEOUT: Duration = Duration::from_secs(5 * 60);
|
||||
|
||||
pub struct OnStartData {
|
||||
// to add more fields as required
|
||||
pub address: Recipient,
|
||||
@@ -160,6 +170,7 @@ impl IpPacketRouterBuilder {
|
||||
tun_task_response_rx,
|
||||
mixnet_client,
|
||||
task_handle,
|
||||
connected_clients: Default::default(),
|
||||
};
|
||||
|
||||
log::info!("The address of this client is: {self_address}");
|
||||
@@ -179,7 +190,7 @@ impl IpPacketRouterBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[cfg_attr(not(target_os = "linux"), allow(unused))]
|
||||
struct IpPacketRouter {
|
||||
_config: Config,
|
||||
request_filter: request_filter::RequestFilter,
|
||||
@@ -187,77 +198,150 @@ struct IpPacketRouter {
|
||||
tun_task_response_rx: nym_tun::tun_task_channel::TunTaskResponseRx,
|
||||
mixnet_client: nym_sdk::mixnet::MixnetClient,
|
||||
task_handle: TaskHandle,
|
||||
|
||||
connected_clients: HashMap<IpAddr, ConnectedClient>,
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
struct ConnectedClient {
|
||||
nym_address: Recipient,
|
||||
last_activity: std::time::Instant,
|
||||
}
|
||||
|
||||
#[cfg_attr(not(target_os = "linux"), allow(unused))]
|
||||
impl IpPacketRouter {
|
||||
async fn run(mut self) -> Result<(), IpPacketRouterError> {
|
||||
let mut task_client = self.task_handle.fork("main_loop");
|
||||
|
||||
while !task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = task_client.recv() => {
|
||||
log::debug!("IpPacketRouter [main loop]: received shutdown");
|
||||
},
|
||||
msg = self.mixnet_client.next() => {
|
||||
if let Some(msg) = msg {
|
||||
if let Err(err) = self.on_message(msg).await {
|
||||
log::error!("Error handling mixnet message: {err}");
|
||||
};
|
||||
} else {
|
||||
log::trace!("IpPacketRouter [main loop]: stopping since channel closed");
|
||||
break;
|
||||
};
|
||||
},
|
||||
packet = self.tun_task_response_rx.recv() => {
|
||||
if let Some((_tag, packet)) = packet {
|
||||
// Read recipient from env variable NYM_CLIENT_ADDR which is a base58
|
||||
// string of the nym-address of the client that the packet should be
|
||||
// sent back to.
|
||||
//
|
||||
// In the near future we will let the client expose it's nym-address
|
||||
// directly, and after that, provide SURBS
|
||||
let recipient = std::env::var("NYM_CLIENT_ADDR").ok().and_then(|addr| {
|
||||
Recipient::try_from_base58_string(addr).ok()
|
||||
});
|
||||
|
||||
if let Some(recipient) = recipient {
|
||||
let lane = TransmissionLane::General;
|
||||
let packet_type = None;
|
||||
let input_message = InputMessage::new_regular(recipient, packet, lane, packet_type);
|
||||
|
||||
if let Err(err) = self.mixnet_client.send(input_message).await {
|
||||
log::error!("IpPacketRouter [main loop]: failed to send packet to mixnet: {err}");
|
||||
};
|
||||
} else {
|
||||
log::error!("NYM_CLIENT_ADDR not set or invalid");
|
||||
}
|
||||
} else {
|
||||
log::trace!("IpPacketRouter [main loop]: stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
log::info!("IpPacketRouter: stopping");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on_message(
|
||||
async fn on_static_connect_request(
|
||||
&mut self,
|
||||
reconstructed: ReconstructedMessage,
|
||||
) -> Result<(), IpPacketRouterError> {
|
||||
log::debug!(
|
||||
"Received message with sender_tag: {:?}",
|
||||
reconstructed.sender_tag
|
||||
connect_request: nym_ip_packet_requests::StaticConnectRequest,
|
||||
) -> Result<Option<IpPacketResponse>, IpPacketRouterError> {
|
||||
log::info!(
|
||||
"Received static connect request from {sender_address}",
|
||||
sender_address = connect_request.reply_to
|
||||
);
|
||||
|
||||
let tagged_packet =
|
||||
nym_ip_packet_requests::TaggedIpPacket::from_reconstructed_message(&reconstructed)
|
||||
.map_err(|err| IpPacketRouterError::FailedToDeserializeTaggedPacket {
|
||||
source: err,
|
||||
})?;
|
||||
let request_id = connect_request.request_id;
|
||||
let requested_ip = connect_request.ip;
|
||||
let reply_to = connect_request.reply_to;
|
||||
// TODO: ignoring reply_to_hops and reply_to_avg_mix_delays for now
|
||||
|
||||
// Check that the IP is available in the set of connected clients
|
||||
let is_ip_taken = self.connected_clients.contains_key(&requested_ip);
|
||||
|
||||
// Check that the nym address isn't already registered
|
||||
let is_nym_address_taken = self
|
||||
.connected_clients
|
||||
.values()
|
||||
.any(|client| client.nym_address == reply_to);
|
||||
|
||||
match (is_ip_taken, is_nym_address_taken) {
|
||||
(true, true) => {
|
||||
log::info!("Connecting an already connected client");
|
||||
// Update the last activity time for the client
|
||||
if let Some(client) = self.connected_clients.get_mut(&requested_ip) {
|
||||
client.last_activity = std::time::Instant::now();
|
||||
} else {
|
||||
log::error!("Failed to update last activity time for client");
|
||||
}
|
||||
Ok(Some(IpPacketResponse::new_static_connect_success(
|
||||
request_id, reply_to,
|
||||
)))
|
||||
}
|
||||
(false, false) => {
|
||||
log::info!("Connecting a new client");
|
||||
self.connected_clients.insert(
|
||||
requested_ip,
|
||||
ConnectedClient {
|
||||
nym_address: reply_to,
|
||||
last_activity: std::time::Instant::now(),
|
||||
},
|
||||
);
|
||||
Ok(Some(IpPacketResponse::new_static_connect_success(
|
||||
request_id, reply_to,
|
||||
)))
|
||||
}
|
||||
(true, false) => {
|
||||
log::info!("Requested IP is not available");
|
||||
Ok(Some(IpPacketResponse::new_static_connect_failure(
|
||||
request_id,
|
||||
reply_to,
|
||||
StaticConnectFailureReason::RequestedIpAlreadyInUse,
|
||||
)))
|
||||
}
|
||||
(false, true) => {
|
||||
log::info!("Nym address is already registered");
|
||||
Ok(Some(IpPacketResponse::new_static_connect_failure(
|
||||
request_id,
|
||||
reply_to,
|
||||
StaticConnectFailureReason::RequestedNymAddressAlreadyInUse,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_dynamic_connect_request(
|
||||
&mut self,
|
||||
connect_request: nym_ip_packet_requests::DynamicConnectRequest,
|
||||
) -> Result<Option<IpPacketResponse>, IpPacketRouterError> {
|
||||
log::info!(
|
||||
"Received dynamic connect request from {sender_address}",
|
||||
sender_address = connect_request.reply_to
|
||||
);
|
||||
|
||||
let request_id = connect_request.request_id;
|
||||
let reply_to = connect_request.reply_to;
|
||||
// TODO: ignoring reply_to_hops and reply_to_avg_mix_delays for now
|
||||
|
||||
// Check if it's the same client connecting again, then we just reuse the same IP
|
||||
// TODO: this is problematic. Until we sign connect requests this means you can spam people
|
||||
// with return traffic
|
||||
let existing_ip = self.connected_clients.iter().find_map(|(ip, client)| {
|
||||
if client.nym_address == reply_to {
|
||||
Some(*ip)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(existing_ip) = existing_ip {
|
||||
log::info!("Found existing client for nym address");
|
||||
// Update the last activity time for the client
|
||||
if let Some(client) = self.connected_clients.get_mut(&existing_ip) {
|
||||
client.last_activity = std::time::Instant::now();
|
||||
} else {
|
||||
log::error!("Failed to update last activity time for client");
|
||||
}
|
||||
return Ok(Some(IpPacketResponse::new_dynamic_connect_success(
|
||||
request_id,
|
||||
reply_to,
|
||||
existing_ip,
|
||||
)));
|
||||
}
|
||||
|
||||
let Some(new_ip) = generate_new_ip::find_new_ip(&self.connected_clients) else {
|
||||
log::info!("No available IP address");
|
||||
return Ok(Some(IpPacketResponse::new_dynamic_connect_failure(
|
||||
request_id,
|
||||
reply_to,
|
||||
DynamicConnectFailureReason::NoAvailableIp,
|
||||
)));
|
||||
};
|
||||
|
||||
self.connected_clients.insert(
|
||||
new_ip,
|
||||
ConnectedClient {
|
||||
nym_address: reply_to,
|
||||
last_activity: std::time::Instant::now(),
|
||||
},
|
||||
);
|
||||
Ok(Some(IpPacketResponse::new_dynamic_connect_success(
|
||||
request_id, reply_to, new_ip,
|
||||
)))
|
||||
}
|
||||
|
||||
async fn on_data_request(
|
||||
&mut self,
|
||||
data_request: nym_ip_packet_requests::DataRequest,
|
||||
) -> Result<Option<IpPacketResponse>, IpPacketRouterError> {
|
||||
log::info!("Received data request");
|
||||
|
||||
// We don't forward packets that we are not able to parse. BUT, there might be a good
|
||||
// reason to still forward them.
|
||||
@@ -271,11 +355,20 @@ impl IpPacketRouter {
|
||||
src_addr,
|
||||
dst_addr,
|
||||
dst,
|
||||
} = parse_packet(&tagged_packet.packet)?;
|
||||
} = parse_packet(&data_request.ip_packet)?;
|
||||
|
||||
let dst_str = dst.map_or(dst_addr.to_string(), |dst| dst.to_string());
|
||||
log::info!("Received packet: {packet_type}: {src_addr} -> {dst_str}");
|
||||
|
||||
// Check if there is a connected client for this src_addr. If there is, update the last activity time
|
||||
// for the client. If there isn't, drop the packet.
|
||||
if let Some(client) = self.connected_clients.get_mut(&src_addr) {
|
||||
client.last_activity = std::time::Instant::now();
|
||||
} else {
|
||||
log::info!("Dropping packet: no connected client for {src_addr}");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Filter check
|
||||
if let Some(dst) = dst {
|
||||
if !self.request_filter.check_address(&dst).await {
|
||||
@@ -288,12 +381,146 @@ impl IpPacketRouter {
|
||||
log::warn!("Ignoring filter check for packet without port number! TODO!");
|
||||
}
|
||||
|
||||
// TODO: set the tag correctly. Can we just reuse sender_tag?
|
||||
let peer_tag = 0;
|
||||
// TODO: consider changing from Vec<u8> to bytes::Bytes?
|
||||
// TODO: consider just removing the tag
|
||||
let tag = 0;
|
||||
self.tun_task_tx
|
||||
.try_send((peer_tag, tagged_packet.packet.into()))
|
||||
.try_send((tag, data_request.ip_packet.into()))
|
||||
.map_err(|err| IpPacketRouterError::FailedToSendPacketToTun { source: err })?;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
async fn on_reconstructed_message(
|
||||
&mut self,
|
||||
reconstructed: ReconstructedMessage,
|
||||
) -> Result<Option<IpPacketResponse>, IpPacketRouterError> {
|
||||
log::debug!(
|
||||
"Received message with sender_tag: {:?}",
|
||||
reconstructed.sender_tag
|
||||
);
|
||||
|
||||
// Check version of request
|
||||
if let Some(version) = reconstructed.message.first() {
|
||||
// The idea is that in the future we can add logic here to parse older versions to stay
|
||||
// backwards compatible.
|
||||
if *version != nym_ip_packet_requests::CURRENT_VERSION {
|
||||
log::warn!("Received packet with invalid version");
|
||||
return Err(IpPacketRouterError::InvalidPacketVersion(*version));
|
||||
}
|
||||
}
|
||||
|
||||
let request = IpPacketRequest::from_reconstructed_message(&reconstructed)
|
||||
.map_err(|err| IpPacketRouterError::FailedToDeserializeTaggedPacket { source: err })?;
|
||||
|
||||
match request.data {
|
||||
IpPacketRequestData::StaticConnect(connect_request) => {
|
||||
self.on_static_connect_request(connect_request).await
|
||||
}
|
||||
IpPacketRequestData::DynamicConnect(connect_request) => {
|
||||
self.on_dynamic_connect_request(connect_request).await
|
||||
}
|
||||
IpPacketRequestData::Data(data_request) => self.on_data_request(data_request).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(mut self) -> Result<(), IpPacketRouterError> {
|
||||
let mut task_client = self.task_handle.fork("main_loop");
|
||||
let mut disconnect_timer = tokio::time::interval(DISCONNECT_TIMER_INTERVAL);
|
||||
|
||||
while !task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = task_client.recv() => {
|
||||
log::debug!("IpPacketRouter [main loop]: received shutdown");
|
||||
},
|
||||
_ = disconnect_timer.tick() => {
|
||||
let now = std::time::Instant::now();
|
||||
let inactive_clients: Vec<IpAddr> = self.connected_clients.iter()
|
||||
.filter_map(|(ip, client)| {
|
||||
if now.duration_since(client.last_activity) > CLIENT_INACTIVITY_TIMEOUT {
|
||||
Some(*ip)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
for ip in inactive_clients {
|
||||
log::info!("Disconnect inactive client: {ip}");
|
||||
self.connected_clients.remove(&ip);
|
||||
}
|
||||
},
|
||||
msg = self.mixnet_client.next() => {
|
||||
if let Some(msg) = msg {
|
||||
match self.on_reconstructed_message(msg).await {
|
||||
Ok(Some(response)) => {
|
||||
let Some(recipient) = response.recipient() else {
|
||||
log::error!("IpPacketRouter [main loop]: failed to get recipient from response");
|
||||
continue;
|
||||
};
|
||||
let response_packet = response.to_bytes();
|
||||
let Ok(response_packet) = response_packet else {
|
||||
log::error!("Failed to serialize response packet");
|
||||
continue;
|
||||
};
|
||||
let lane = TransmissionLane::General;
|
||||
let packet_type = None;
|
||||
let input_message = InputMessage::new_regular(*recipient, response_packet, lane, packet_type);
|
||||
if let Err(err) = self.mixnet_client.send(input_message).await {
|
||||
log::error!("IpPacketRouter [main loop]: failed to send packet to mixnet: {err}");
|
||||
};
|
||||
},
|
||||
Ok(None) => {
|
||||
continue;
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!("Error handling mixnet message: {err}");
|
||||
}
|
||||
|
||||
};
|
||||
} else {
|
||||
log::trace!("IpPacketRouter [main loop]: stopping since channel closed");
|
||||
break;
|
||||
};
|
||||
},
|
||||
packet = self.tun_task_response_rx.recv() => {
|
||||
if let Some((_tag, packet)) = packet {
|
||||
// TODO: skip full parsing since we only need dst_addr
|
||||
let Ok(ParsedPacket {
|
||||
packet_type: _,
|
||||
src_addr: _,
|
||||
dst_addr,
|
||||
dst: _,
|
||||
}) = parse_packet(&packet) else {
|
||||
log::warn!("Failed to parse packet");
|
||||
continue;
|
||||
};
|
||||
|
||||
let recipient = self.connected_clients.get(&dst_addr).map(|c| c.nym_address);
|
||||
|
||||
if let Some(recipient) = recipient {
|
||||
let lane = TransmissionLane::General;
|
||||
let packet_type = None;
|
||||
let response_packet = IpPacketResponse::new_ip_packet(packet.into()).to_bytes();
|
||||
let Ok(response_packet) = response_packet else {
|
||||
log::error!("Failed to serialize response packet");
|
||||
continue;
|
||||
};
|
||||
let input_message = InputMessage::new_regular(recipient, response_packet, lane, packet_type);
|
||||
|
||||
if let Err(err) = self.mixnet_client.send(input_message).await {
|
||||
log::error!("IpPacketRouter [main loop]: failed to send packet to mixnet: {err}");
|
||||
};
|
||||
} else {
|
||||
log::error!("IpPacketRouter [main loop]: no nym-address recipient for packet");
|
||||
}
|
||||
} else {
|
||||
log::trace!("IpPacketRouter [main loop]: stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
log::info!("IpPacketRouter: stopping");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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