ghost-eye/src/components/wallet/address_book.rs
Uncle Stretch f8b4215546
reload keys and push own wallet to address book
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
2025-02-22 15:58:48 +03:00

480 lines
18 KiB
Rust

use std::fs::File;
use std::path::PathBuf;
use std::io::{Write, BufRead, BufReader};
use color_eyre::Result;
use crossterm::event::{KeyCode, KeyEvent};
use ratatui::layout::{Constraint, Margin};
use ratatui::text::Text;
use ratatui::widgets::{Cell, Padding, Scrollbar, ScrollbarOrientation};
use ratatui::{
layout::{Alignment, Rect},
widgets::{Block, ScrollbarState, Row, Table, TableState},
Frame
};
use subxt::ext::sp_core::crypto::{
ByteArray, Ss58Codec, Ss58AddressFormat, AccountId32,
};
use tokio::sync::mpsc::UnboundedSender;
use std::sync::mpsc::Sender;
use super::{Component, PartialComponent, CurrentTab};
use crate::types::{ActionLevel, ActionTarget, SystemAccount};
use crate::widgets::DotSpinner;
use crate::{
action::Action,
config::Config,
palette::StylePalette,
};
struct BookRecord {
name: String,
address: String,
account_id: [u8; 32],
seed: String,
}
pub struct AddressBook {
is_active: bool,
action_tx: Option<UnboundedSender<Action>>,
network_tx: Option<Sender<Action>>,
address_book_file: PathBuf,
address_book: Vec<BookRecord>,
balances: std::collections::HashMap<[u8; 32], SystemAccount>,
scroll_state: ScrollbarState,
table_state: TableState,
palette: StylePalette,
}
impl Default for AddressBook {
fn default() -> Self {
Self::new()
}
}
impl AddressBook {
const TICKER: &str = " CSPR";
const DECIMALS: usize = 3;
pub fn new() -> Self {
Self {
is_active: false,
action_tx: None,
network_tx: None,
address_book_file: Default::default(),
address_book: Default::default(),
balances: Default::default(),
scroll_state: ScrollbarState::new(0),
table_state: TableState::new(),
palette: StylePalette::default(),
}
}
fn save_to_file(&mut self) {
let mut file = File::create(&self.address_book_file).unwrap();
for wallet in self.address_book.iter() {
writeln!(file, "{}:0x{}", wallet.name, &wallet.seed).unwrap();
}
}
fn send_transfer_to(&mut self) {
if let Some(action_tx) = &self.action_tx {
if let Some(index) = self.table_state.selected() {
let _ = action_tx.send(Action::TransferTo(
self.address_book[index].address.clone()));
}
}
}
fn send_balance_request(&mut self, account_id: [u8; 32], remove: bool) {
if let Some(network_tx) = &self.network_tx {
let _ = network_tx.send(Action::BalanceRequest(account_id, remove));
}
}
fn log_event(&mut self, message: String, level: ActionLevel) {
if let Some(action_tx) = &self.action_tx {
let _ = action_tx.send(
Action::EventLog(message, level, ActionTarget::WalletLog));
}
}
fn read_or_create(&mut self, file_path: &PathBuf) -> Result<()> {
assert!(self.address_book.len() == 0, "address_book already exists");
match File::open(file_path) {
Ok(file) => {
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?.replace("\n", "");
let line_split_at = line.find(":").unwrap_or(line.len());
let (name, seed) = line.split_at(line_split_at);
let seed_str = &seed[3..];
let account_id: [u8; 32] = hex::decode(seed_str)
.expect("stored seed is valid hex string; qed")
.as_slice()
.try_into()
.expect("stored seed is valid length; qed");
let address = AccountId32::from(account_id)
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
self.address_book.push(BookRecord {
name: name.to_string(),
address,
account_id,
seed: seed_str.to_string(),
});
self.send_balance_request(account_id, false);
}
self.log_event(
format!("read {} records from address book", self.address_book.len()),
ActionLevel::Info)
},
Err(_) => {
let chad_boyz = vec![
("Pierre", "328d3b7c3046ef7700937d99fb2e98ce2591682c2b5dcf3f562e4da157650237"),
("Ghost_7", "3666e4e19f87bb8680495f31864ce1f1c69d4178002cc01911aef2cc7313f203"),
("Neptune1", "ac871e8bab00dd56ba3a1c0bd289357203dcaf10010b0b04ad7472870cd22a3c"),
("Neptune2", "425ccd7bda4f5c76788ba23bc0381d7a2e496179c93301208c57501c80a4232a"),
("Doctor K", "927a98dcf8f721103005f168476c24b91d7d10d580f457006a908e10e62c7729"),
("Starman", "ac9e227e30a63ce6eeb55cfbb1fb832aa7e1d3fad2bcb3f663de4a91d744fd50"),
("Kitsune1", "46c78fcacffd80abc9cca4917ef8369a37e21a1691ca11e7a3b53f80be745313"),
("Scientio", "fa5e5a295ec74c3dda81118d9240db1552b28f831838465ae0712e97e78a6728"),
("Kitsune2", "4078ddb1ba1388f768fe6aa40ba9124a72692ecbcc83dc088fa86c735e4dc128"),
("Proxmio", "5e1456904c40192cd3a18183df7dffea90d97739830a902cabb702ecdae4f649"),
];
let mut new_file = File::create(file_path)?;
chad_boyz
.iter()
.for_each(|chad_info| {
writeln!(new_file, "{}:0x{}", chad_info.0, chad_info.1)
.expect("should write to address book; qed");
let chad_account_id: [u8; 32] = hex::decode(&chad_info.1[..])
.expect("stored seed is valid hex string; qed")
.as_slice()
.try_into()
.expect("stored seed is valid length; qed");
let address = AccountId32::from(chad_account_id)
.to_ss58check_with_version(Ss58AddressFormat::custom(1996));
self.address_book.push(BookRecord {
name: chad_info.0.to_string(),
address,
account_id: chad_account_id,
seed: chad_info.1.to_string(),
});
self.send_balance_request(chad_account_id, false);
});
self.log_event(
format!("address book is empty, filling it with giga chad boyz as default"),
ActionLevel::Warn)
}
};
self.scroll_state = self.scroll_state.content_length(self.address_book.len());
Ok(())
}
fn rename_record(&mut self, new_name: String) {
if let Some(index) = self.table_state.selected() {
let old_name = self.address_book[index].name.clone();
self.log_event(
format!("record renamed from {} to {}", &old_name, &new_name),
ActionLevel::Info);
self.address_book[index].name = new_name;
self.save_to_file();
}
}
fn add_new_record(&mut self, name: String, address: String) {
match AccountId32::from_ss58check_with_version(&address) {
Ok((account_id, format)) => {
if format != Ss58AddressFormat::custom(1996) {
self.log_event(
format!("provided public address for {} is not part of Casper/Ghost ecosystem", &address),
ActionLevel::Error);
}
match self.address_book.iter().position(|record| record.address == address) {
Some(index) => {
let record = self.address_book
.get(index)
.expect("record should be in range of address book; qed");
self.log_event(
format!("record with name `{}` already stores address {}", &record.name, &record.address),
ActionLevel::Warn);
self.table_state.select(Some(index));
self.scroll_state = self.scroll_state.position(index);
},
None => {
let seed_vec = account_id.to_raw_vec();
let mut account_id = [0u8; 32];
account_id.copy_from_slice(&seed_vec);
let seed_str = hex::encode(seed_vec);
self.log_event(
format!("account {} with address {} added to address book", &name, &address),
ActionLevel::Info);
self.address_book.push(BookRecord {
name,
address,
account_id,
seed: seed_str,
});
self.scroll_state = self.scroll_state.content_length(self.address_book.len());
self.save_to_file();
self.send_balance_request(account_id, false);
self.last_row();
},
}
},
Err(_) => self.log_event(
format!("provided account address {} is invalid", &address),
ActionLevel::Error),
}
}
fn update_address_book_record(&mut self) {
if let Some(index) = self.table_state.selected() {
if let Some(action_tx) = &self.action_tx {
let _ = action_tx.send(Action::RenameAddressBookRecord(
self.address_book[index].name.clone()
));
}
}
}
fn show_account_details(&mut self) {
if let Some(index) = self.table_state.selected() {
if let Some(action_tx) = &self.action_tx {
let _ = action_tx.send(Action::AccountDetailsOf(
self.address_book[index].name.clone(),
self.balances
.get(&self.address_book[index].account_id)
.map(|data| data.clone()),
));
}
}
}
fn swap_up(&mut self) {
if let Some(src_index) = self.table_state.selected() {
let dst_index = src_index.saturating_sub(1);
if src_index > dst_index {
self.address_book.swap(src_index, dst_index);
self.previous_row();
self.save_to_file();
}
}
}
fn swap_down(&mut self) {
if let Some(src_index) = self.table_state.selected() {
let dst_index = src_index + 1;
if dst_index < self.address_book.len() {
self.address_book.swap(src_index, dst_index);
self.next_row();
self.save_to_file();
}
}
}
fn delete_row(&mut self) {
if let Some(index) = self.table_state.selected() {
let record = self.address_book.remove(index);
self.previous_row();
self.save_to_file();
self.log_event(format!("record `{}` with public address {} removed",
&record.name, &record.address),
ActionLevel::Warn);
}
}
fn first_row(&mut self) {
if self.address_book.len() > 0 {
self.table_state.select(Some(0));
self.scroll_state = self.scroll_state.position(0);
}
}
fn next_row(&mut self) {
let i = match self.table_state.selected() {
Some(i) => {
if i >= self.address_book.len() - 1 {
i
} else {
i + 1
}
},
None => 0,
};
self.table_state.select(Some(i));
self.scroll_state = self.scroll_state.position(i);
}
fn last_row(&mut self) {
if self.address_book.len() > 0 {
let last = self.address_book.len() - 1;
self.table_state.select(Some(last));
self.scroll_state = self.scroll_state.position(last);
}
}
fn previous_row(&mut self) {
let i = match self.table_state.selected() {
Some(i) => {
if i == 0 {
0
} else {
i - 1
}
},
None => 0
};
self.table_state.select(Some(i));
self.scroll_state = self.scroll_state.position(i);
}
fn prepare_u128(&self, maybe_value: Option<u128>) -> String {
match maybe_value {
Some(value) => {
let value = value as f64 / 10f64.powi(18);
let after = Self::DECIMALS;
format!("{:.after$}{}", value, Self::TICKER)
},
None => format!("{}{}", DotSpinner::default().to_string(), Self::TICKER)
}
}
}
impl PartialComponent for AddressBook {
fn set_active(&mut self, current_tab: CurrentTab) {
match current_tab {
CurrentTab::AddressBook => self.is_active = true,
_ => self.is_active = false,
}
}
}
impl Component for AddressBook {
fn register_network_handler(&mut self, tx: Sender<Action>) -> Result<()> {
self.network_tx = Some(tx);
Ok(())
}
fn register_action_handler(&mut self, tx: UnboundedSender<Action>) -> 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_hover_style(style.get("hover_style").copied());
self.palette.with_normal_border_style(style.get("normal_border_style").copied());
self.palette.with_hover_border_style(style.get("hover_border_style").copied());
self.palette.with_normal_title_style(style.get("normal_title_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());
}
let mut address_book_file = config.config.data_dir;
address_book_file.push("address-book");
self.read_or_create(&address_book_file)?;
self.address_book_file = address_book_file;
Ok(())
}
fn update(&mut self, action: Action) -> Result<Option<Action>> {
match action {
Action::UpdateAddressBookRecord(new_name) =>
self.rename_record(new_name),
Action::NewAddressBookRecord(name, address) =>
self.add_new_record(name, address),
Action::BalanceResponse(account_id, maybe_balance) => {
if self.address_book.iter().any(|record| record.account_id == account_id) {
let _ = match maybe_balance {
Some(balance) => self.balances.insert(account_id, balance),
None => self.balances.remove(&account_id),
};
}
},
_ => {}
};
Ok(None)
}
fn handle_key_event(&mut self, key: KeyEvent) -> Result<Option<Action>> {
if self.is_active {
match key.code {
KeyCode::Up | KeyCode::Char('k') => self.previous_row(),
KeyCode::Down | KeyCode::Char('j') => self.next_row(),
KeyCode::Char('g') => self.first_row(),
KeyCode::Char('G') => self.last_row(),
KeyCode::Char('K') => self.swap_up(),
KeyCode::Char('J') => self.swap_down(),
KeyCode::Char('D') => self.delete_row(),
KeyCode::Char('R') => self.update_address_book_record(),
KeyCode::Char('I') => self.show_account_details(),
KeyCode::Enter => self.send_transfer_to(),
_ => {},
};
}
Ok(None)
}
fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> {
let [_, place] = super::bars_layout(area);
let (border_style, border_type) = self.palette.create_border_style(self.is_active);
let table = Table::new(
self.address_book
.iter()
.map(|info| {
let balance = self.balances
.get(&info.account_id)
.map(|inner_balance| inner_balance.free);
Row::new(vec![
Cell::from(Text::from(info.name.clone()).alignment(Alignment::Left)),
Cell::from(Text::from(info.address.clone()).alignment(Alignment::Center)),
Cell::from(Text::from(self.prepare_u128(balance)).alignment(Alignment::Right)),
])
}),
[
Constraint::Min(5),
Constraint::Max(51),
Constraint::Min(11),
],
)
.style(self.palette.create_basic_style(false))
.highlight_style(self.palette.create_basic_style(true))
.column_spacing(1)
.block(Block::bordered()
.border_style(border_style)
.border_type(border_type)
.padding(Padding::right(2))
.title_alignment(Alignment::Right)
.title_style(self.palette.create_title_style(false))
.title("Address Book"));
let scrollbar = Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
.begin_symbol(None)
.end_symbol(None)
.style(self.palette.create_scrollbar_style());
frame.render_stateful_widget(table, place, &mut self.table_state);
frame.render_stateful_widget(
scrollbar,
place.inner(Margin { vertical: 1, horizontal: 1 }),
&mut self.scroll_state,
);
Ok(())
}
}