From 565758bb841fa39968cb80681e7ec25baefeb8ae Mon Sep 17 00:00:00 2001 From: Uncle Stretch Date: Thu, 13 Nov 2025 17:02:54 +0300 Subject: [PATCH] ability to control RPC endpoints for the gatekeeped network added Signed-off-by: Uncle Stretch --- Cargo.toml | 2 +- src/action.rs | 4 + .../validator/gatekeeper_endpoints_popup.rs | 376 ++++++++++++++++++ src/components/validator/gatekeepers.rs | 7 + src/components/validator/mod.rs | 11 +- src/network/legacy_rpc_calls.rs | 70 ++++ src/network/mod.rs | 9 + src/network/predefined_calls.rs | 9 +- src/types/mod.rs | 1 - src/types/networks.rs | 1 + src/types/staking.rs | 7 - 11 files changed, 484 insertions(+), 13 deletions(-) create mode 100644 src/components/validator/gatekeeper_endpoints_popup.rs diff --git a/Cargo.toml b/Cargo.toml index c0653af..bfd2d38 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "ghost-eye" authors = ["str3tch "] description = "Application for interacting with Casper/Ghost nodes that are exposing RPC only to the localhost" -version = "0.3.74" +version = "0.3.75" edition = "2021" homepage = "https://git.ghostchain.io/ghostchain" repository = "https://git.ghostchain.io/ghostchain/ghost-eye" diff --git a/src/action.rs b/src/action.rs index 9193b58..a6f331b 100644 --- a/src/action.rs +++ b/src/action.rs @@ -39,6 +39,7 @@ pub enum Action { PayoutAllValidatorPopup(Vec), WithdrawValidatorPopup, ChangeBlocksPopup, + GatekeeperEndpoints, BalanceRequest([u8; 32], bool), BalanceResponse([u8; 32], Option), @@ -118,6 +119,7 @@ pub enum Action { GetStakingPayee([u8; 32], bool), GetValidatorIsDisabled([u8; 32], bool), GetCurrentValidatorEraRewards, + GetRpcEndpoints(u64), SetNodeName(Option), SetSystemHealth(Option, bool, bool), @@ -130,6 +132,8 @@ pub enum Action { SetChoosenValidator([u8; 32], u32, u32), SetChoosenGatekeeper(u64), SetSlashingSpansLength(usize, [u8; 32]), + SetStoredRpcEndpoints(Vec), + UpdateStoredRpcEndpoints(u64, Vec), BestBlockInformation(H256, u32), FinalizedBlockInformation(H256, u32), diff --git a/src/components/validator/gatekeeper_endpoints_popup.rs b/src/components/validator/gatekeeper_endpoints_popup.rs new file mode 100644 index 0000000..cc6df37 --- /dev/null +++ b/src/components/validator/gatekeeper_endpoints_popup.rs @@ -0,0 +1,376 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; +use color_eyre::Result; +use ratatui::{ + layout::{Alignment, Constraint, Flex, Layout, Position, Rect}, prelude::Margin, style::{Color, Style}, widgets::{Block, BorderType, Cell, Clear, Paragraph, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, Table, TableState}, Frame +}; +use tokio::sync::mpsc::UnboundedSender; +use std::sync::mpsc::Sender; + +use super::{Component, PartialComponent, CurrentTab}; +use crate::{ + action::Action, config::Config, palette::StylePalette, widgets::{Input, InputRequest} +}; + +#[derive(Debug, Default, Eq, PartialEq)] +enum Selected { + #[default] + Input, + DefaultRpcs, + StoredRpcs, +} + +#[derive(Debug, Default)] +pub struct GatekeeperEndpoints { + is_active: bool, + selected: Selected, + chain_id: u64, + rpc_input: Input, + action_tx: Option>, + network_tx: Option>, + default_endpoints: Vec, + stored_endpoints: Vec, + default_scroll_state: ScrollbarState, + default_table_state: TableState, + stored_scroll_state: ScrollbarState, + stored_table_state: TableState, + palette: StylePalette, +} + +impl GatekeeperEndpoints { + fn close_popup(&mut self) { + self.is_active = false; + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::ClosePopup); + } + } + + fn set_chain_id(&mut self, chain_id: u64) { + self.chain_id = chain_id; + if let Some(network_tx) = &self.network_tx { + let _ = network_tx.send(Action::GetRpcEndpoints(chain_id)); + } + } + + fn submit_message(&mut self) { + if let Some(network_tx) = &self.network_tx { + if self.selected == Selected::Input { + let mut stored_endpoints = self.stored_endpoints.clone(); + stored_endpoints.push(self.rpc_input.value().to_string()); + let _ = network_tx.send(Action::UpdateStoredRpcEndpoints( + self.chain_id, + stored_endpoints)); + self.rpc_input = Input::new(String::new()); + } + } + } + + fn enter_char(&mut self, new_char: char) { + match self.selected { + Selected::Input => { + let _ = self.rpc_input.handle(InputRequest::InsertChar(new_char)); + }, + Selected::StoredRpcs if (new_char == 'd' || new_char == 'D') => { + if let Some(index) = self.stored_table_state.selected() { + let mut stored_endpoints = self.stored_endpoints.clone(); + if let Some(network_tx) = &self.network_tx { + stored_endpoints.remove(index); + let _ = network_tx.send(Action::UpdateStoredRpcEndpoints( + self.chain_id, + stored_endpoints)); + } + } + }, + _ => { + match new_char { + 'j' => self.move_cursor_down(), + 'k' => self.move_cursor_up(), + 'l' => self.move_cursor_right(), + 'h' => self.move_cursor_left(), + _ => {} + } + } + } + } + + fn delete_char(&mut self) { + if self.selected == Selected::Input { + let _ = self.rpc_input.handle(InputRequest::DeletePrevChar); + } + } + + fn move_cursor_up(&mut self) { + match self.selected { + Selected::Input => {}, + Selected::DefaultRpcs => { + let i = match self.default_table_state.selected() { + Some(i) => { + if i == 0 { + 0 + } else { + i - 1 + } + }, + None => 0 + }; + self.default_table_state.select(Some(i)); + self.default_scroll_state = self.default_scroll_state.position(i); + }, + Selected::StoredRpcs => { + let i = match self.stored_table_state.selected() { + Some(i) => { + if i == 0 { + 0 + } else { + i - 1 + } + }, + None => 0 + }; + self.stored_table_state.select(Some(i)); + self.stored_scroll_state = self.stored_scroll_state.position(i); + }, + }; + } + + fn move_cursor_down(&mut self) { + match self.selected { + Selected::Input => {}, + Selected::DefaultRpcs => { + let i = match self.default_table_state.selected() { + Some(i) => { + if i >= self.default_endpoints.len() - 1 { + i + } else { + i + 1 + } + }, + None => 0, + }; + self.default_table_state.select(Some(i)); + self.default_scroll_state = self.default_scroll_state.position(i); + }, + Selected::StoredRpcs => { + let i = match self.stored_table_state.selected() { + Some(i) => { + if i >= self.stored_endpoints.len() - 1 { + i + } else { + i + 1 + } + }, + None => 0, + }; + self.stored_table_state.select(Some(i)); + self.stored_scroll_state = self.stored_scroll_state.position(i); + }, + }; + } + + fn move_cursor_right(&mut self) { + self.selected = match self.selected { + Selected::Input => Selected::StoredRpcs, + Selected::DefaultRpcs => Selected::Input, + Selected::StoredRpcs => Selected::StoredRpcs, + }; + self.update_selected_state(); + } + + fn move_cursor_left(&mut self) { + self.selected = match self.selected { + Selected::Input => Selected::DefaultRpcs, + Selected::StoredRpcs => Selected::Input, + Selected::DefaultRpcs => Selected::DefaultRpcs, + }; + self.update_selected_state(); + } + + fn update_selected_state(&mut self) { + self.stored_table_state.select(None); + self.default_table_state.select(None); + self.stored_scroll_state = self.stored_scroll_state.position(0); + self.default_scroll_state = self.default_scroll_state.position(0); + } + + fn prepare_table<'a>( + &self, + rpcs: &'a Vec, + title_name: &'a str, + border_style: Color, + border_type: BorderType, + scrollbar_style: Style, + ) -> (Table<'a>, Scrollbar<'a>) { + let table = Table::new( + rpcs.iter().map(|endpoint| Row::new(vec![ + Cell::from(endpoint.as_str()) + ])).collect::>(), + [Constraint::Min(1)], + ) + .highlight_style(self.palette.create_highlight_style()) + .block(Block::bordered() + .border_style(border_style) + .border_type(border_type) + .title_alignment(Alignment::Right) + .title_style(self.palette.create_title_style(false)) + .title(title_name)); + + let scrollbar = Scrollbar::default() + .orientation(ScrollbarOrientation::VerticalRight) + .begin_symbol(None) + .end_symbol(None) + .style(scrollbar_style); + + (table, scrollbar) + } +} + +impl PartialComponent for GatekeeperEndpoints { + fn set_active(&mut self, current_tab: CurrentTab) { + match current_tab { + CurrentTab::GatekeeperEndpoints => self.is_active = true, + _ => { + self.is_active = false; + self.rpc_input = Input::new(String::new()); + }, + }; + } +} + +impl Component for GatekeeperEndpoints { + fn register_network_handler(&mut self, tx: Sender) -> Result<()> { + self.network_tx = Some(tx); + Ok(()) + } + + fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { + self.action_tx = Some(tx); + Ok(()) + } + + fn register_config_handler(&mut self, config: Config) -> Result<()> { + if let Some(style) = config.styles.get(&crate::app::Mode::Wallet) { + self.palette.with_normal_style(style.get("normal_style").copied()); + self.palette.with_normal_border_style(style.get("normal_border_style").copied()); + self.palette.with_normal_title_style(style.get("normal_title_style").copied()); + self.palette.with_popup_style(style.get("popup_style").copied()); + self.palette.with_popup_title_style(style.get("popup_title_style").copied()); + self.palette.with_hover_style(style.get("hover_style").copied()); + self.palette.with_hover_border_style(style.get("hover_border_style").copied()); + self.palette.with_hover_title_style(style.get("hover_title_style").copied()); + self.palette.with_highlight_style(style.get("highlight_style").copied()); + self.palette.with_scrollbar_style(style.get("scrollbar_style").copied()); + } + Ok(()) + } + + fn handle_key_event(&mut self, key: KeyEvent) -> Result> { + if self.is_active && key.kind == KeyEventKind::Press { + match key.code { + KeyCode::Enter => self.submit_message(), + KeyCode::Char(to_insert) => self.enter_char(to_insert), + KeyCode::Backspace => self.delete_char(), + KeyCode::Left => self.move_cursor_left(), + KeyCode::Right => self.move_cursor_right(), + KeyCode::Up => self.move_cursor_up(), + KeyCode::Down => self.move_cursor_down(), + KeyCode::Esc => self.close_popup(), + _ => {}, + }; + } + Ok(None) + } + + fn update(&mut self, action: Action) -> Result> { + match action { + Action::SetChoosenGatekeeper(chain_id) => self.set_chain_id(chain_id), + Action::SetGatekeepedNetwork(network) => + self.default_endpoints = network.default_endpoints, + Action::SetStoredRpcEndpoints(stored_endpoints) => + self.stored_endpoints = stored_endpoints, + _ => {} + }; + Ok(None) + } + + fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { + if self.is_active { + let (border_style, border_type) = self.palette.create_popup_style(); + let (selected_border_style, selected_border_type) = self.palette.create_border_style(true); + let scrollbar_style = self.palette.create_scrollbar_style(); + + let (default_border_style, default_border_type) = match self.selected { + Selected::DefaultRpcs => (selected_border_style, selected_border_type), + _ => (border_style, border_type), + }; + + let (stored_border_style, stored_border_type) = match self.selected { + Selected::StoredRpcs => (selected_border_style, selected_border_type), + _ => (border_style, border_type), + }; + + let (input_border_style, input_border_type) = match self.selected { + Selected::Input => (selected_border_style, selected_border_type), + _ => (border_style, border_type), + }; + + let (default_rpcs, default_scrollbar) = self.prepare_table( + &self.default_endpoints, + "Default RPCs", + default_border_style, + default_border_type, + scrollbar_style, + ); + let (stored_rpcs, stored_scrollbar) = self.prepare_table( + &self.stored_endpoints, + "Stored RPCs", + stored_border_style, + stored_border_type, + scrollbar_style, + ); + + let input = Paragraph::new(self.rpc_input.value()) + .block(Block::bordered() + .border_style(input_border_style) + .border_type(input_border_type) + .title_style(self.palette.create_popup_title_style()) + .title_alignment(Alignment::Right) + .title("Input new RPC")); + + let v = Layout::vertical([Constraint::Max(14)]).flex(Flex::Center); + let h = Layout::horizontal([Constraint::Max(80)]).flex(Flex::Center); + let [area] = v.areas(area); + let [area] = h.areas(area); + frame.render_widget(Clear, area); + + let [tables_area, input_area] = Layout::vertical([ + Constraint::Length(11), + Constraint::Length(3), + ]).areas(area); + + let [default_table_area, stored_table_area] = Layout::horizontal([ + Constraint::Max(40), + Constraint::Max(40), + ]).areas(tables_area); + + frame.render_stateful_widget(default_rpcs, default_table_area, &mut self.default_table_state); + frame.render_stateful_widget( + default_scrollbar, + default_table_area.inner(Margin { vertical: 1, horizontal: 1 }), + &mut self.default_scroll_state, + ); + + frame.render_stateful_widget(stored_rpcs, stored_table_area, &mut self.stored_table_state); + frame.render_stateful_widget( + stored_scrollbar, + stored_table_area.inner(Margin { vertical: 1, horizontal: 1 }), + &mut self.stored_scroll_state, + ); + + frame.render_widget(input, input_area); + frame.set_cursor_position(Position::new( + input_area.x + self.rpc_input.cursor() as u16 + 1, + input_area.y + 1 + )); + } + Ok(()) + } +} diff --git a/src/components/validator/gatekeepers.rs b/src/components/validator/gatekeepers.rs index af5890d..762cf9b 100644 --- a/src/components/validator/gatekeepers.rs +++ b/src/components/validator/gatekeepers.rs @@ -61,6 +61,12 @@ impl Gatekeepers { } } + fn gatekeeper_endpoints(&mut self) { + if let Some(action_tx) = &self.action_tx { + let _ = action_tx.send(Action::GatekeeperEndpoints); + } + } + fn change_choosen_gatekeeper(&mut self) { if let Some(action_tx) = &self.action_tx { if let Some(chain_id) = self.list_state @@ -198,6 +204,7 @@ impl Component for Gatekeepers { KeyCode::Char('g') => self.first_row(), KeyCode::Char('G') => self.last_row(), KeyCode::Char('O') => self.nullify_evm_blocks(), + KeyCode::Enter => self.gatekeeper_endpoints(), _ => {}, }; } diff --git a/src/components/validator/mod.rs b/src/components/validator/mod.rs index 064d639..5312db8 100644 --- a/src/components/validator/mod.rs +++ b/src/components/validator/mod.rs @@ -34,6 +34,7 @@ mod rebond_popup; mod withdraw_popup; mod payee_popup; mod change_blocks_popup; +mod gatekeeper_endpoints_popup; use stash_details::StashDetails; use staking_details::StakingDetails; @@ -58,6 +59,7 @@ use rebond_popup::RebondPopup; use withdraw_popup::WithdrawPopup; use payee_popup::PayeePopup; use change_blocks_popup::ChangeBlocksPopup; +use gatekeeper_endpoints_popup::GatekeeperEndpoints; #[derive(Debug, Copy, Clone, PartialEq)] pub enum CurrentTab { @@ -80,6 +82,7 @@ pub enum CurrentTab { WithdrawPopup, PayeePopup, ChangeBlocksPopup, + GatekeeperEndpoints, } pub trait PartialComponent: Component { @@ -123,6 +126,7 @@ impl Default for Validator { Box::new(WithdrawPopup::default()), Box::new(PayeePopup::default()), Box::new(ChangeBlocksPopup::default()), + Box::new(GatekeeperEndpoints::default()), ], } } @@ -190,7 +194,8 @@ impl Component for Validator { CurrentTab::WithdrawPopup | CurrentTab::PayoutPopup | CurrentTab::PayoutAllPopup | - CurrentTab::ChangeBlocksPopup => { + CurrentTab::ChangeBlocksPopup | + CurrentTab::GatekeeperEndpoints => { for component in self.components.iter_mut() { component.handle_key_event(key)?; } @@ -275,6 +280,10 @@ impl Component for Validator { self.previous_tab = self.current_tab; self.current_tab = CurrentTab::ChangeBlocksPopup; }, + Action::GatekeeperEndpoints => { + self.previous_tab = self.current_tab; + self.current_tab = CurrentTab::GatekeeperEndpoints; + }, Action::ClosePopup => self.current_tab = self.previous_tab, _ => {}, } diff --git a/src/network/legacy_rpc_calls.rs b/src/network/legacy_rpc_calls.rs index 657b97a..9204e5f 100644 --- a/src/network/legacy_rpc_calls.rs +++ b/src/network/legacy_rpc_calls.rs @@ -199,3 +199,73 @@ pub async fn nullify_blocks( Ok(()) } + +pub async fn get_stored_rpc_endpoints( + action_tx: &UnboundedSender, + rpc_client: &RpcClient, + chain_id: u64, +) -> Result<()> { + let chain_id_encoded = chain_id.encode(); + let endpoint_key_raw = get_slow_clap_storage_key(b"endpoint-", &chain_id_encoded); + let mut endpoint_key = String::from("0x"); + for byte in endpoint_key_raw { + endpoint_key.push_str(&format!("{:02x}", byte)); + } + + let stored_rpc_endpoints: String = rpc_client + .request("offchain_localStorageGet", rpc_params!["PERSISTENT", endpoint_key]) + .await + .ok() + .unwrap_or_default(); + + let stored_rpc_endpoints = stored_rpc_endpoints.trim_start_matches("0x"); + + let scale_encoded = hex::decode(&stored_rpc_endpoints).unwrap(); + let stored_rpc_endpoints = Vec::::decode(&mut scale_encoded.as_slice()) + .ok() + .unwrap_or_default(); + + action_tx.send(Action::SetStoredRpcEndpoints(stored_rpc_endpoints))?; + + Ok(()) +} + +pub async fn set_stored_rpc_endpoints( + action_tx: &UnboundedSender, + rpc_client: &RpcClient, + chain_id: u64, + stored_endpoints: Vec, +) -> Result<()> { + let chain_id_encoded = chain_id.encode(); + let endpoint_key_raw = get_slow_clap_storage_key(b"endpoint-", &chain_id_encoded); + let mut endpoint_key = String::from("0x"); + for byte in endpoint_key_raw { + endpoint_key.push_str(&format!("{:02x}", byte)); + } + + let stored_endpoints = stored_endpoints.encode(); + let mut encoded_endpoints = String::from("0x"); + for byte in stored_endpoints { + encoded_endpoints.push_str(&format!("{:02x}", byte)); + } + + match rpc_client.request::<()>("offchain_localStorageSet", rpc_params!["PERSISTENT", endpoint_key, encoded_endpoints]) + .await + { + Ok(_) => { + action_tx.send(Action::EventLog( + format!("RPC endpoints updated for network #{:?}", chain_id), + ActionLevel::Info, + ActionTarget::ValidatorLog, + ))?; + get_stored_rpc_endpoints(action_tx, rpc_client, chain_id).await?; + } + Err(err) => action_tx.send(Action::EventLog( + format!("RPC endpoints update failed: {:?}", err), + ActionLevel::Error, + ActionTarget::ValidatorLog, + ))?, + }; + + Ok(()) +} diff --git a/src/network/mod.rs b/src/network/mod.rs index 5e66d97..7a70670 100644 --- a/src/network/mod.rs +++ b/src/network/mod.rs @@ -189,6 +189,7 @@ impl Network { Action::GetConnectedPeers => legacy_rpc_calls::get_connected_peers(&self.action_tx, &self.rpc_client).await, Action::GetListenAddresses => legacy_rpc_calls::get_listen_addresses(&self.action_tx, &self.rpc_client).await, Action::GetLocalIdentity => legacy_rpc_calls::get_local_identity(&self.action_tx, &self.rpc_client).await, + Action::GetRpcEndpoints(chain_id) => legacy_rpc_calls::get_stored_rpc_endpoints(&self.action_tx, &self.rpc_client, chain_id).await, Action::RotateSessionKeys => legacy_rpc_calls::rotate_keys(&self.action_tx, &self.rpc_client).await, Action::GetBlockRange => { for chain_id in GATEKEEPED_CHAIN_IDS { @@ -515,6 +516,14 @@ impl Network { new_block, ).await } + Action::UpdateStoredRpcEndpoints(chain_id, stored_endpoints) => { + legacy_rpc_calls::set_stored_rpc_endpoints( + &self.action_tx, + &self.rpc_client, + chain_id, + stored_endpoints, + ).await + } _ => Ok(()) } } diff --git a/src/network/predefined_calls.rs b/src/network/predefined_calls.rs index 5ef49f4..9d27cf8 100644 --- a/src/network/predefined_calls.rs +++ b/src/network/predefined_calls.rs @@ -4,7 +4,6 @@ use subxt::{ backend::rpc::RpcClient, client::OnlineClient, config::substrate::DigestItem, - ext::sp_runtime::Saturating, ext::sp_core::crypto::{ AccountId32, Ss58AddressFormat, Ss58Codec, }, @@ -15,7 +14,7 @@ use subxt::{ use crate::{ action::Action, casper_network::runtime_types::{ghost_networks::NetworkType, pallet_staking::RewardDestination, sp_consensus_slots}, - types::{EraInfo, EraRewardPoints, Gatekeeper, Nominations, Nominator, SlashingSpan, SessionKeyInfo, SystemAccount, UnlockChunk}, + types::{EraInfo, EraRewardPoints, Gatekeeper, Nominations, Nominator, SessionKeyInfo, SystemAccount, UnlockChunk}, CasperAccountId, CasperConfig }; @@ -593,7 +592,7 @@ pub async fn get_slashing_spans( ) -> Result<()> { let slashing_spans_length = super::raw_calls::staking::slashing_spans(api, None, account_id) .await? - .map(|spans| spans.prior.saturating_add(1)) + .map(|spans| spans.prior.len().saturating_add(1)) .unwrap_or_default(); action_tx.send(Action::SetSlashingSpansLength(slashing_spans_length, *account_id))?; Ok(()) @@ -666,6 +665,10 @@ pub async fn get_gatekeeped_network( NetworkType::Utxo => String::from("UTXO"), NetworkType::Undefined => String::from("???"), }, + default_endpoints: network.default_endpoints + .iter() + .map(|endpoint| String::from_utf8_lossy(&endpoint).to_string()) + .collect(), gatekeeper: String::from_utf8_lossy(&network.gatekeeper) .to_string(), incoming_fee: network.incoming_fee, diff --git a/src/types/mod.rs b/src/types/mod.rs index 98b5728..ad46334 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -19,6 +19,5 @@ pub use nominator::Nominator; pub use nominator::Nominations; pub use staking::UnlockChunk; pub use staking::RewardDestination; -pub use staking::SlashingSpan; pub use networks::Gatekeeper; pub use networks::BlockRange; diff --git a/src/types/networks.rs b/src/types/networks.rs index ae170e0..323df21 100644 --- a/src/types/networks.rs +++ b/src/types/networks.rs @@ -9,6 +9,7 @@ pub struct Gatekeeper { pub gatekeeper: String, pub incoming_fee: u32, pub outgoing_fee: u32, + pub default_endpoints: Vec, } #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Decode)] diff --git a/src/types/staking.rs b/src/types/staking.rs index 9551c23..00b5c4b 100644 --- a/src/types/staking.rs +++ b/src/types/staking.rs @@ -8,13 +8,6 @@ pub struct UnlockChunk { pub era: u32, } -#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, Decode)] -pub struct SlashingSpan { - pub index: u32, - pub start: u32, - pub length: Option -} - #[derive(Default, Debug, Clone, PartialEq, Eq, Display, Serialize, Deserialize)] pub enum RewardDestination { #[default]