480 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Rust
		
	
	
	
	
	
			
		
		
	
	
			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(())
 | 
						|
    }
 | 
						|
}
 |