// Copyright 2023 The Grim Developers // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::sync::Arc; use std::thread; use egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea}; use egui::os::OperatingSystem; use egui::scroll_area::ScrollBarVisibility; use parking_lot::RwLock; use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; use grin_wallet_libwallet::SlatepackAddress; use crate::gui::Colors; use crate::gui::icons::{CHECK_CIRCLE, COMPUTER_TOWER, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, WARNING_CIRCLE, X_CIRCLE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, QrCodeContent, Root, View}; use crate::gui::views::types::{ModalPosition, TextEditOptions}; use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::wallet::WalletContent; use crate::tor::{Tor, TorBridge, TorConfig}; use crate::wallet::types::WalletData; use crate::wallet::Wallet; /// Wallet transport tab content. pub struct WalletTransport { /// Flag to check if transaction is sending over Tor to show progress at [`Modal`]. tor_sending: Arc>, /// Flag to check if error occurred during sending of transaction over Tor at [`Modal`]. tor_send_error: Arc>, /// Flag to check if transaction sent successfully over Tor [`Modal`]. tor_success: Arc>, /// Entered amount value for [`Modal`]. amount_edit: String, /// Entered address value for [`Modal`]. address_edit: String, /// Flag to check if entered address is incorrect at [`Modal`]. address_error: bool, /// Flag to check if [`Modal`] was just opened to focus on first field. modal_just_opened: bool, /// QR code address image [`Modal`] content. qr_code_content: QrCodeContent, /// Tor bridge binary path edit text. bridge_bin_path_edit: String, } impl WalletTab for WalletTransport { fn get_type(&self) -> WalletTabType { WalletTabType::Transport } fn ui(&mut self, ui: &mut egui::Ui, _: &mut eframe::Frame, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { if WalletContent::sync_ui(ui, wallet) { return; } // Show modal content for this ui container. self.modal_content_ui(ui, wallet, cb); // Show transport content panel. egui::CentralPanel::default() .frame(egui::Frame { stroke: View::ITEM_STROKE, fill: Colors::WHITE, inner_margin: Margin { left: View::far_left_inset_margin(ui) + 4.0, right: View::get_right_inset() + 4.0, top: 3.0, bottom: 4.0, }, ..Default::default() }) .show_inside(ui, |ui| { ScrollArea::vertical() .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) .id_source(Id::from("wallet_transport").with(wallet.get_config().id)) .auto_shrink([false; 2]) .show(ui, |ui| { ui.vertical_centered(|ui| { View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| { self.ui(ui, wallet, cb); }); }); }); }); } } /// Identifier for [`Modal`] to send amount over Tor. const SEND_TOR_MODAL: &'static str = "send_tor_modal"; /// Identifier for [`Modal`] to setup Tor service. const TOR_SETTINGS_MODAL: &'static str = "tor_settings_modal"; /// Identifier for [`Modal`] to show QR code address image. const QR_ADDRESS_MODAL: &'static str = "qr_address_modal"; impl WalletTransport { /// Create new content instance from provided Slatepack address text. pub fn new(addr: String) -> Self { // Setup Tor bridge binary path edit text. let bridge = TorConfig::get_bridge(); let bridge_bin_path_edit = if let Some(b) = bridge { b.binary_path() } else { "".to_string() }; Self { tor_sending: Arc::new(RwLock::new(false)), tor_send_error: Arc::new(RwLock::new(false)), tor_success: Arc::new(RwLock::new(false)), amount_edit: "".to_string(), address_edit: "".to_string(), address_error: false, modal_just_opened: false, qr_code_content: QrCodeContent::new(addr), bridge_bin_path_edit } } /// Draw wallet transport content. pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { ui.add_space(3.0); ui.label(RichText::new(t!("transport.desc")) .size(16.0) .color(Colors::INACTIVE_TEXT)); ui.add_space(7.0); // Draw Tor content. self.tor_ui(ui, wallet, cb); } /// Draw [`Modal`] content for this ui container. fn modal_content_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { match Modal::opened() { None => {} Some(id) => { match id { SEND_TOR_MODAL => { Modal::ui(ui.ctx(), |ui, modal| { self.send_tor_modal_ui(ui, wallet, modal, cb); }); } TOR_SETTINGS_MODAL => { Modal::ui(ui.ctx(), |ui, modal| { self.tor_settings_modal_ui(ui, wallet, modal, cb); }); } QR_ADDRESS_MODAL => { Modal::ui(ui.ctx(), |ui, modal| { self.qr_address_modal_ui(ui, modal); }); } _ => {} } } } } /// Draw Tor transport content. fn tor_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { let data = wallet.get_data().unwrap(); // Draw header content. self.tor_header_ui(ui, wallet); // Draw receive info content. if wallet.slatepack_address().is_some() { self.tor_receive_ui(ui, wallet, &data, cb); } // Draw send content. if data.info.amount_currently_spendable > 0 { self.tor_send_ui(ui, cb); } } /// Draw Tor transport header content. fn tor_header_ui(&self, ui: &mut egui::Ui, wallet: &Wallet) { // Setup layout size. let mut rect = ui.available_rect_before_wrap(); rect.set_height(78.0); // Draw round background. let bg_rect = rect.clone(); let item_rounding = View::item_rounding(0, 2, false); ui.painter().rect(bg_rect, item_rounding, Colors::BUTTON, View::ITEM_STROKE); ui.vertical(|ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Draw button to setup Tor transport. let button_rounding = View::item_rounding(0, 2, true); View::item_button(ui, button_rounding, GEAR_SIX, None, || { // Show Tor settings modal. Modal::new(TOR_SETTINGS_MODAL) .position(ModalPosition::CenterTop) .title(t!("transport.tor_settings")) .show(); }); // Draw button to enable/disable Tor listener for current wallet. let service_id = &wallet.identifier(); if !Tor::is_service_starting(service_id) { if !Tor::is_service_running(service_id) && wallet.foreign_api_port().is_some() { View::item_button(ui, Rounding::default(), POWER, Some(Colors::GREEN), || { if let Ok(key) = wallet.secret_key() { let api_port = wallet.foreign_api_port().unwrap(); Tor::start_service(api_port, key, service_id); } }); } else { View::item_button(ui, Rounding::default(), POWER, Some(Colors::RED), || { Tor::stop_service(service_id); }); } } let layout_size = ui.available_size(); ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { ui.add_space(6.0); ui.vertical(|ui| { ui.add_space(3.0); ui.label(RichText::new(t!("transport.tor_network")) .size(18.0) .color(Colors::TITLE)); // Setup wallet API address text. let port = wallet.foreign_api_port().unwrap(); let address_text = format!("{} http://127.0.0.1:{}", COMPUTER_TOWER, port); ui.label(RichText::new(address_text).size(15.0).color(Colors::TEXT)); ui.add_space(1.0); // Setup Tor status text. let is_running = Tor::is_service_running(service_id); let is_starting = Tor::is_service_starting(service_id); let has_error = Tor::is_service_failed(service_id); let (icon, text) = if is_starting { (DOTS_THREE_CIRCLE, t!("transport.connecting")) } else if has_error { (WARNING_CIRCLE, t!("transport.conn_error")) } else if is_running { (CHECK_CIRCLE, t!("transport.connected")) } else { (X_CIRCLE, t!("transport.disconnected")) }; let status_text = format!("{} {}", icon, text); ui.label(RichText::new(status_text).size(15.0).color(Colors::GRAY)); }); }); }); }); } /// Draw tor transport settings [`Modal`] content. fn tor_settings_modal_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) { // Do not show bridges setup on Android. let os = OperatingSystem::from_target_os(); let show_bridges = os != OperatingSystem::Android; ui.add_space(6.0); if show_bridges { let bridge = TorConfig::get_bridge(); ui.vertical_centered(|ui| { ui.label(RichText::new(t!("transport.bridges_desc")) .size(17.0) .color(Colors::INACTIVE_TEXT)); // Draw checkbox to enable/disable bridges. View::checkbox(ui, bridge.is_some(), t!("transport.bridges"), || { let value = if bridge.is_some() { None } else { let b = TorConfig::get_snowflake(); self.bridge_bin_path_edit = b.binary_path(); Some(b) }; TorConfig::save_bridge(value); }); }); // Draw bridges selection and path. if bridge.is_some() { let current_bridge = bridge.unwrap(); let mut bridge = current_bridge.clone(); ui.add_space(6.0); ui.columns(2, |columns| { columns[0].vertical_centered(|ui| { // Draw Snowflake bridge selector. let snowflake = TorConfig::get_snowflake(); let name = snowflake.protocol_name().to_uppercase(); View::radio_value(ui, &mut bridge, snowflake, name); }); columns[1].vertical_centered(|ui| { // Draw Obfs4 bridge selector. let obfs4 = TorConfig::get_obfs4(); let name = obfs4.protocol_name().to_uppercase(); View::radio_value(ui, &mut bridge, obfs4, name); }); }); ui.add_space(12.0); // Check if bridge type was changed to save. if current_bridge != bridge { TorConfig::save_bridge(Some(bridge.clone())); self.bridge_bin_path_edit = bridge.binary_path(); } // Draw binary path text edit. let bin_edit_id = Id::from(modal.id).with(wallet.get_config().id).with("_bridge_bin"); let bin_edit_opts = TextEditOptions::new(bin_edit_id).paste(); let bin_edit_before = self.bridge_bin_path_edit.clone(); ui.vertical_centered(|ui| { View::text_edit(ui, cb, &mut self.bridge_bin_path_edit, bin_edit_opts); }); // Check if text was changed to save bin path. if bin_edit_before != self.bridge_bin_path_edit { let b = match bridge { TorBridge::Snowflake(_) => { TorBridge::Snowflake(self.bridge_bin_path_edit.clone()) }, TorBridge::Obfs4(_) => { TorBridge::Obfs4(self.bridge_bin_path_edit.clone()) } }; TorConfig::save_bridge(Some(b)); } ui.add_space(2.0); } ui.add_space(6.0); View::horizontal_line(ui, Colors::ITEM_STROKE); ui.add_space(6.0); } ui.vertical_centered(|ui| { ui.label(RichText::new(t!("transport.tor_autorun_desc")) .size(17.0) .color(Colors::INACTIVE_TEXT)); // Show Tor service autorun checkbox. let autorun = wallet.auto_start_tor_listener(); View::checkbox(ui, autorun, t!("network.autorun"), || { wallet.update_auto_start_tor_listener(!autorun); }); }); ui.add_space(6.0); ui.vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::WHITE, || { modal.close(); }); }); ui.add_space(6.0); } /// Draw Tor send content. fn tor_receive_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, data: &WalletData, cb: &dyn PlatformCallbacks) { let slatepack_addr = wallet.slatepack_address().unwrap(); let service_id = &wallet.identifier(); let can_send = data.info.amount_currently_spendable > 0; // Setup layout size. let mut rect = ui.available_rect_before_wrap(); rect.set_height(52.0); // Draw round background. let bg_rect = rect.clone(); let item_rounding = if can_send { View::item_rounding(1, 3, false) } else { View::item_rounding(1, 2, false) }; ui.painter().rect(bg_rect, item_rounding, Colors::BUTTON, View::ITEM_STROKE); ui.vertical(|ui| { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Draw button to setup Tor transport. let button_rounding = if can_send { View::item_rounding(1, 3, true) } else { View::item_rounding(1, 2, true) }; View::item_button(ui, button_rounding, QR_CODE, None, || { // Show QR code image address modal. self.qr_code_content.clear_state(); Modal::new(QR_ADDRESS_MODAL) .position(ModalPosition::CenterTop) .title(t!("network_mining.address")) .show(); }); // Show button to enable/disable Tor listener for current wallet. View::item_button(ui, Rounding::default(), COPY, None, || { cb.copy_string_to_buffer(slatepack_addr.clone()); }); let layout_size = ui.available_size(); ui.allocate_ui_with_layout(layout_size, Layout::left_to_right(Align::Center), |ui| { ui.add_space(6.0); ui.vertical(|ui| { ui.add_space(3.0); // Show wallet Slatepack address. let address_color = if Tor::is_service_starting(service_id) { Colors::INACTIVE_TEXT } else if Tor::is_service_running(service_id) { Colors::GREEN } else { Colors::RED }; View::ellipsize_text(ui, slatepack_addr, 15.0, address_color); let address_label = format!("{} {}", GLOBE_SIMPLE, t!("network_mining.address")); ui.label(RichText::new(address_label).size(15.0).color(Colors::GRAY)); }); }); }); }); } /// Draw QR code image address [`Modal`] content. fn qr_address_modal_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) { ui.add_space(6.0); // Draw QR code content. let text = self.qr_code_content.text.clone(); self.qr_code_content.ui(ui, text.clone()); ui.add_space(6.0); // Show address. View::ellipsize_text(ui, text, 16.0, Colors::GRAY); ui.add_space(6.0); ui.vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::WHITE, || { self.qr_code_content.clear_state(); modal.close(); }); ui.add_space(6.0); }); } /// Draw Tor receive content. fn tor_send_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { // Setup layout size. let mut rect = ui.available_rect_before_wrap(); rect.set_height(55.0); // Draw round background. let bg_rect = rect.clone(); let item_rounding = View::item_rounding(1, 2, false); ui.painter().rect(bg_rect, item_rounding, Colors::FILL, View::ITEM_STROKE); ui.vertical(|ui| { ui.allocate_ui_with_layout(rect.size(), Layout::top_down(Align::Center), |ui| { ui.add_space(7.0); // Draw button to open sending modal. let send_text = format!("{} {}", EXPORT, t!("wallets.send")); View::button(ui, send_text, Colors::WHITE, || { self.show_send_tor_modal(cb, None); }); }); }); } /// Show [`Modal`] to send over Tor. pub fn show_send_tor_modal(&mut self, cb: &dyn PlatformCallbacks, address: Option) { // Setup modal values. { let mut w_send_err = self.tor_send_error.write(); *w_send_err = false; let mut w_sending = self.tor_sending.write(); *w_sending = false; let mut w_success = self.tor_success.write(); *w_success = false; } self.modal_just_opened = true; self.amount_edit = "".to_string(); self.address_edit = address.unwrap_or("".to_string()); self.address_error = false; // Show modal. Modal::new(SEND_TOR_MODAL) .position(ModalPosition::CenterTop) .title(t!("wallets.send")) .show(); cb.show_keyboard(); } /// Check if error occurred during sending over Tor at [`Modal`]. fn has_tor_send_error(&self) -> bool { let r_send_err = self.tor_send_error.read(); r_send_err.clone() } /// Check if transaction is sending over Tor to show progress at [`Modal`]. fn tor_sending(&self) -> bool { let r_sending = self.tor_sending.read(); r_sending.clone() } /// Check if transaction sent over Tor with success at [`Modal`]. fn tor_success(&self) -> bool { let r_success = self.tor_success.read(); r_success.clone() } /// Draw amount input [`Modal`] content to send over Tor. fn send_tor_modal_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) { ui.add_space(6.0); let has_send_err = self.has_tor_send_error(); let sending = self.tor_sending(); if !has_send_err && !sending { ui.vertical_centered(|ui| { let data = wallet.get_data().unwrap(); let amount = amount_to_hr_string(data.info.amount_currently_spendable, true); let enter_text = t!("wallets.enter_amount_send","amount" => amount); ui.label(RichText::new(enter_text) .size(17.0) .color(Colors::GRAY)); }); ui.add_space(8.0); // Draw amount text edit. let amount_edit_id = Id::from(modal.id).with("amount").with(wallet.get_config().id); let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center().no_focus(); let amount_edit_before = self.amount_edit.clone(); if self.modal_just_opened { self.modal_just_opened = false; amount_edit_opts.focus = true; } View::text_edit(ui, cb, &mut self.amount_edit, amount_edit_opts); ui.add_space(8.0); // Check value if input was changed. if amount_edit_before != self.amount_edit { if !self.amount_edit.is_empty() { match amount_from_hr_string(self.amount_edit.as_str()) { Ok(a) => { if !self.amount_edit.contains(".") { // To avoid input of several "0". if a == 0 { self.amount_edit = "0".to_string(); return; } } else { // Check input after ".". let parts = self.amount_edit.split(".").collect::>(); if parts.len() == 2 && parts[1].len() > 9 { self.amount_edit = amount_edit_before; return; } } // Do not input amount more than balance in sending. let b = wallet.get_data().unwrap().info.amount_currently_spendable; if b < a { self.amount_edit = amount_edit_before; } } Err(_) => { self.amount_edit = amount_edit_before; } } } } // Show address error or input description. ui.vertical_centered(|ui| { if self.address_error { ui.label(RichText::new(t!("transport.incorrect_addr_err")) .size(17.0) .color(Colors::RED)); } else { ui.label(RichText::new(t!("transport.receiver_address")) .size(17.0) .color(Colors::GRAY)); } }); ui.add_space(8.0); // Draw address text edit. let addr_edit_before = self.address_edit.clone(); let address_edit_id = Id::from(modal.id).with("address").with(wallet.get_config().id); let address_edit_opts = TextEditOptions::new(address_edit_id) .paste() .scan_qr() .no_focus(); View::text_edit(ui, cb, &mut self.address_edit, address_edit_opts); ui.add_space(12.0); // Check value if input was changed. if addr_edit_before != self.address_edit { self.address_error = false; } // Setup spacing between buttons. ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); ui.columns(2, |columns| { columns[0].vertical_centered_justified(|ui| { View::button(ui, t!("modal.cancel"), Colors::WHITE, || { self.amount_edit = "".to_string(); self.address_edit = "".to_string(); cb.hide_keyboard(); modal.close(); }); }); columns[1].vertical_centered_justified(|ui| { View::button(ui, t!("continue"), Colors::WHITE, || { if self.amount_edit.is_empty() { return; } // Check entered address. let addr_str = self.address_edit.as_str(); if let Ok(addr) = SlatepackAddress::try_from(addr_str) { // Parse amount and send over Tor. if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { cb.hide_keyboard(); modal.disable_closing(); let mut w_sending = self.tor_sending.write(); *w_sending = true; { let send_error = self.tor_send_error.clone(); let send_success = self.tor_success.clone(); let mut wallet = wallet.clone(); thread::spawn(move || { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap() .block_on(async { if wallet.send_tor(a, &addr).await.is_some() { let mut w_send_success = send_success.write(); *w_send_success = true; } else { let mut w_send_error = send_error.write(); *w_send_error = true; } }); }); } } } else { self.address_error = true; } }); }); }); ui.add_space(6.0); } else if has_send_err { ui.add_space(6.0); ui.vertical_centered(|ui| { ui.label(RichText::new(t!("transport.tor_send_error")) .size(17.0) .color(Colors::RED)); }); ui.add_space(12.0); // Setup spacing between buttons. ui.spacing_mut().item_spacing = egui::Vec2::new(6.0, 0.0); ui.columns(2, |columns| { columns[0].vertical_centered_justified(|ui| { View::button(ui, t!("modal.cancel"), Colors::WHITE, || { self.amount_edit = "".to_string(); self.address_edit = "".to_string(); cb.hide_keyboard(); modal.close(); }); }); columns[1].vertical_centered_justified(|ui| { View::button(ui, t!("repeat"), Colors::WHITE, || { // Parse amount and send over Tor. if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { let mut w_send_error = self.tor_send_error.write(); *w_send_error = false; let mut w_sending = self.tor_sending.write(); *w_sending = true; { let addr_text = self.address_edit.clone(); let send_error = self.tor_send_error.clone(); let send_success = self.tor_success.clone(); let mut wallet = wallet.clone(); thread::spawn(move || { tokio::runtime::Builder::new_multi_thread() .enable_all() .build() .unwrap() .block_on(async { let addr_str = addr_text.as_str(); let addr = &SlatepackAddress::try_from(addr_str) .unwrap(); if wallet.send_tor(a, &addr).await.is_some() { let mut w_send_success = send_success.write(); *w_send_success = true; } else { let mut w_send_error = send_error.write(); *w_send_error = true; } }); }); } } }); }); }); ui.add_space(6.0); } else { ui.add_space(16.0); ui.vertical_centered(|ui| { View::small_loading_spinner(ui); ui.add_space(12.0); ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit)) .size(17.0) .color(Colors::GRAY)); }); ui.add_space(10.0); // Close modal on success sending. if self.tor_success() { modal.close(); } } } }