extern crate web_view; extern crate tinyfiledialogs as tfd; extern crate serde; extern crate serde_json; use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; use web_view::Content; #[allow(unused_imports)] use log::{debug, error, info, LevelFilter, trace, warn}; use serde::Deserialize; use alfis::{Block, Bytes, Context, Keystore, Transaction, get_domain_zone}; use alfis::miner::Miner; use alfis::{keys, check_domain}; use alfis::event::Event; use alfis::dns::protocol::DnsRecord; use alfis::commons::{ZONE_MAX_LENGTH, ZONE_DIFFICULTY}; use Cmd::*; use alfis::blockchain::transaction::{DomainData, ZoneData}; use self::web_view::{WebView, Handle}; use alfis::blockchain::enums::MineResult; pub fn run_interface(context: Arc>, miner: Arc>) { let file_content = include_str!("webview/index.html"); let mut styles = inline_style(include_str!("webview/bulma.css")); styles.push_str(&inline_style(include_str!("webview/styles.css"))); styles.push_str(&inline_style(include_str!("webview/busy_indicator.css"))); let scripts = inline_script(include_str!("webview/scripts.js")); let html = Content::Html(file_content.to_owned().replace("{styles}", &styles).replace("{scripts}", &scripts)); let title = format!("ALFIS {}", env!("CARGO_PKG_VERSION")); let mut interface = web_view::builder() .title(&title) .content(html) .size(1023, 720) .min_size(773, 350) .resizable(true) .debug(false) .user_data(()) .invoke_handler(|web_view, arg| { debug!("Command {}", arg); match serde_json::from_str(arg).unwrap() { Loaded => { action_loaded(&context, web_view); } LoadKey => { action_load_key(&context, web_view); } CreateKey => { keys::create_key(Arc::clone(&context)); } SaveKey => { action_save_key(&context); } CheckRecord { data } => { action_check_record(web_view, data); } CheckDomain { name } => { action_check_domain(&context, web_view, name); } MineDomain { name, records } => { action_create_domain(Arc::clone(&context), Arc::clone(&miner), web_view, name, &records); } TransferDomain { .. } => {} CheckZone { name } => { action_check_zone(&context, web_view, name); } MineZone { name, data } => { action_create_zone(Arc::clone(&context), Arc::clone(&miner), web_view, name, data); } StopMining => { context.lock().unwrap().bus.post(Event::ActionStopMining); } } Ok(()) }) .build() .expect("Error building GUI"); let mut context = Arc::clone(&context); run_interface_loop(&mut context, &mut interface); interface.exit(); } /// Indefinitely loops through WebView steps fn run_interface_loop(context: &mut Arc>, interface: &mut WebView<()>) { // We use this ugly loop to lower CPU usage a lot. // If we use .run() or only .step() in a loop without sleeps it will try // to support 60FPS and uses more CPU than it should. let pause = Duration::from_millis(25); let mut start = Instant::now(); loop { match interface.step() { None => { info!("Interface closed, exiting"); context.lock().unwrap().bus.post(Event::ActionQuit); thread::sleep(Duration::from_millis(100)); break; } Some(result) => { match result { Ok(_) => {} Err(_) => { error!("Something wrong with webview, exiting"); break; } } } } if start.elapsed().as_millis() > 1 { thread::sleep(pause); start = Instant::now(); } } } fn action_check_zone(context: &Arc>, web_view: &mut WebView<()>, name: String) { let name = name.to_lowercase(); if name.len() > ZONE_MAX_LENGTH || !check_domain(&name, false) || context.lock().unwrap().x_zones.has_zone(&name) { web_view.eval("zoneAvailable(false)").expect("Error evaluating!"); } else { let c = context.lock().unwrap(); if let Some(keystore) = c.get_keystore() { let available = c.get_chain().is_domain_available(&name, &keystore); web_view.eval(&format!("zoneAvailable({})", available)).expect("Error evaluating!"); } } } fn action_check_record(web_view: &mut WebView<()>, data: String) { match serde_json::from_str::(&data) { Ok(_) => { web_view.eval("recordOkay(true)").expect("Error evaluating!"); } Err(e) => { web_view.eval("recordOkay(false)").expect("Error evaluating!"); dbg!(e); } } } fn action_check_domain(context: &Arc>, web_view: &mut WebView<()>, name: String) { let c = context.lock().unwrap(); if let Some(keystore) = c.get_keystore() { let name = name.to_lowercase(); let available = c.get_chain().is_domain_available(&name, &keystore); web_view.eval(&format!("domainAvailable({})", available)).expect("Error evaluating!"); } } fn action_save_key(context: &Arc>) { if context.lock().unwrap().get_keystore().is_none() { return; } let result = tfd::save_file_dialog_with_filter("Save keys file", "", &["*.key"], "Key files (*.key)"); match result { None => {} Some(new_path) => { let mut context = context.lock().unwrap(); let path = new_path.clone(); if let Some(mut keystore) = context.get_keystore() { let public = keystore.get_public().to_string(); let hash = keystore.get_hash().to_string(); keystore.save(&new_path, ""); info!("Key file saved to {}", &path); context.bus.post(Event::KeySaved { path, public, hash }); } } } } fn action_load_key(context: &Arc>, web_view: &mut WebView<()>) { let result = tfd::open_file_dialog("Open keys file", "", Some((&["*.key"], "*.key"))); match result { None => {} Some(file_name) => { match Keystore::from_file(&file_name, "") { None => { error!("Error loading keystore '{}'!", &file_name); show_warning(web_view, "Error loading key!
Key cannot be loaded or its difficulty is not enough."); } Some(keystore) => { info!("Loaded keystore with key: {:?}", &keystore.get_public()); let mut c = context.lock().unwrap(); let path = keystore.get_path().to_owned(); let public = keystore.get_public().to_string(); let hash = keystore.get_hash().to_string(); c.bus.post(Event::KeyLoaded { path, public, hash }); c.set_keystore(Some(keystore)); } } } } } fn action_loaded(context: &Arc>, web_view: &mut WebView<()>) { web_view.eval("showMiningIndicator(false, false);").expect("Error evaluating!"); let handle: Handle<()> = web_view.handle(); let status = Arc::new(Mutex::new(Status::new())); let context_copy = Arc::clone(&context); let mut c = context.lock().unwrap(); c.bus.register(move |_uuid, e| { //debug!("Got event from bus {:?}", &e); let status = Arc::clone(&status); let handle = handle.clone(); let context_copy = Arc::clone(&context_copy); let _ = thread::Builder::new().name(String::from("webui")).spawn(move || { let mut status = status.lock().unwrap(); let context = context_copy.lock().unwrap(); let eval = match e { Event::KeyCreated { path, public, hash } | Event::KeyLoaded { path, public, hash } | Event::KeySaved { path, public, hash } => { format!("keystoreChanged('{}', '{}', '{}');", &path, &public, &hash) } Event::MinerStarted | Event::KeyGeneratorStarted => { status.mining = true; String::from("setLeftStatusBarText('Mining...'); showMiningIndicator(true, false);") } Event::MinerStopped {success, full} => { status.mining = false; let mut s = if status.syncing { String::from("setLeftStatusBarText('Syncing...'); showMiningIndicator(true, true);") } else { String::from("setLeftStatusBarText('Idle'); showMiningIndicator(false, false);") }; if full { match success { true => { s.push_str(" showSuccess('Block successfully mined!')"); } false => { s.push_str(" showSuccess('Mining unsuccessful, sorry.')"); } } } s } Event::KeyGeneratorStopped {success} => { status.mining = false; let mut s = if status.syncing { String::from("setLeftStatusBarText('Syncing...'); showMiningIndicator(true, true);") } else { String::from("setLeftStatusBarText('Idle'); showMiningIndicator(false, false);") }; match success { true => { s.push_str(" showSuccess('Key pair successfully mined!
Don`t forget to save!')"); } false => { s.push_str(" showSuccess('Key mining got nothing, sorry.')"); } } s } Event::Syncing { have, height } => { status.syncing = true; status.synced_blocks = have; status.sync_height = height; if status.mining { String::from("setLeftStatusBarText('Mining...'); showMiningIndicator(true, false);") } else { format!("setLeftStatusBarText('Synchronizing {}/{}'); showMiningIndicator(true, true);", have, height) } } Event::SyncFinished => { status.syncing = false; if status.mining { String::from("setLeftStatusBarText('Mining...'); showMiningIndicator(true, false);") } else { format!("setLeftStatusBarText('Idle'); showMiningIndicator(false, false);") } } Event::NetworkStatus { nodes, blocks } => { if status.mining || status.syncing || nodes < 3 { format!("setRightStatusBarText('Nodes: {}, Blocks: {}')", nodes, blocks) } else { format!("setLeftStatusBarText('Idle'); setRightStatusBarText('Nodes: {}, Blocks: {}')", nodes, blocks) } } Event::BlockchainChanged {index} => { info!("Current blockchain height is {}", index); if let Ok(zones) = serde_json::to_string(&context.chain.get_zones()) { let _ = handle.dispatch(move |web_view|{ web_view.eval(&format!("zonesChanged('{}');", &zones)) }); } String::new() // Nothing } _ => { String::new() } }; if !eval.is_empty() { handle.dispatch(move |web_view| { web_view.eval(&eval.replace("\\", "\\\\")) }).expect("Error dispatching!"); } }); true }); if let Some(keystore) = c.get_keystore() { let path = keystore.get_path().to_owned(); let public = keystore.get_public().to_string(); let hash = keystore.get_hash().to_string(); c.bus.post(Event::KeyLoaded { path, public, hash }); } let index = c.chain.height(); c.bus.post(Event::BlockchainChanged { index }); } fn action_create_domain(context: Arc>, miner: Arc>, web_view: &mut WebView<()>, name: String, records: &String) { debug!("Creating domain with records: {}", records); let c = Arc::clone(&context); let context = context.lock().unwrap(); if context.get_keystore().is_none() { show_warning(web_view, "You don't have keys loaded!
Load or mine the keys and try again."); return; } let keystore = context.get_keystore().unwrap(); let pub_key = keystore.get_public(); match dbg!(context.chain.can_mine_domain(&name, &records, &pub_key)) { MineResult::Fine => { let zone = get_domain_zone(&name); let difficulty = context.chain.get_zone_difficulty(&zone); if let Ok(records) = serde_json::from_str::>(&records) { let data = DomainData::new(zone.clone(), records); let data = serde_json::to_string(&data).unwrap(); std::mem::drop(context); create_domain(c, miner, &name, &data, difficulty, &keystore); let _ = web_view.eval("domainMiningStarted();"); } } MineResult::WrongName => { show_warning(web_view, "You can't mine this domain!"); } MineResult::WrongData => { show_warning(web_view, "You have an error in records!"); } MineResult::WrongKey => { show_warning(web_view, "You can't mine with current key!"); } MineResult::WrongZone => { show_warning(web_view, "You can't mine domain in this zone!"); } MineResult::NotOwned => { show_warning(web_view, "This domain is already taken, and it is not yours!"); } MineResult::Cooldown { time } => { show_warning(web_view, &format!("You have cooldown, just {} more minutes!", time / 60)); } } } fn action_create_zone(context: Arc>, miner: Arc>, web_view: &mut WebView<()>, name: String, data: String) { let name = name.to_lowercase(); if name.len() > ZONE_MAX_LENGTH || !check_domain(&name, false) || context.lock().unwrap().x_zones.has_zone(&name) { warn!("This zone is unavailable for mining!"); show_warning(web_view, "This zone is unavailable for mining!"); return; } let data = data.to_lowercase(); if serde_json::from_str::(&data).is_err() { warn!("Something wrong with zone data!"); show_warning(web_view, "Something wrong with zone data!"); return; } let (keystore, transaction) = { let context = context.lock().unwrap(); (context.get_keystore(), context.chain.get_domain_transaction(&name)) }; if let Some(keystore) = keystore { match transaction { None => { create_domain(Arc::clone(&context), miner.clone(), &name, &data, ZONE_DIFFICULTY, &keystore); } Some(transaction) => { if transaction.pub_key == keystore.get_public() { create_domain(Arc::clone(&context), miner.clone(), &name, &data, ZONE_DIFFICULTY, &keystore); } else { warn!("Tried to mine not owned domain!"); show_warning(web_view, "You cannot change domain that you don't own!"); } } } } else { warn!("Can not mine without keys!"); show_warning(web_view, "You don't have keys loaded!
Load or mine the keys and try again."); } } fn show_warning(web_view: &mut WebView<()>, text: &str) { let str = text.replace('\'', "\\'"); match web_view.eval(&format!("showWarning('{}');", &str)) { Ok(_) => {} Err(_) => { warn!("Error showing warning!"); } } } #[allow(dead_code)] fn show_success(web_view: &mut WebView<()>, text: &str) { let str = text.replace('\'', "\\'"); match web_view.eval(&format!("showSuccess('{}');", &str)) { Ok(_) => {} Err(_) => { warn!("Error showing success!"); } } } fn create_domain(context: Arc>, miner: Arc>, name: &str, data: &str, difficulty: u32, keystore: &Keystore) { let name = name.to_owned(); info!("Generating domain or zone {}", &name); if context.lock().unwrap().x_zones.has_zone(&name) { error!("Unable to mine IANA/OpenNIC/etc zone {}!", &name); return; } let transaction = Transaction::from_str(name, "dns".to_owned(), data.to_owned(), keystore.get_public().clone()); let block = Block::new(Some(transaction), keystore.get_public(), Bytes::default(), difficulty); miner.lock().unwrap().add_block(block, keystore.clone()); } #[derive(Deserialize)] #[serde(tag = "cmd", rename_all = "camelCase")] pub enum Cmd { Loaded, LoadKey, CreateKey, SaveKey, CheckZone { name: String }, MineZone { name: String, data: String }, CheckRecord { data: String }, CheckDomain { name: String }, MineDomain { name: String, records: String }, TransferDomain { name: String, owner: String }, StopMining, } struct Status { pub mining: bool, pub syncing: bool, pub synced_blocks: u64, pub sync_height: u64, pub nodes_connected: usize, pub chain_height: u64 } impl Status { fn new() -> Self { Status { mining: false, syncing: false, synced_blocks: 0, sync_height: 0, nodes_connected: 0, chain_height: 0 } } } fn inline_style(s: &str) -> String { format!(r#""#, s) } fn inline_script(s: &str) -> String { format!(r#""#, s) }