diff --git a/src/gui/views/qr.rs b/src/gui/views/qr.rs index e0690ff..86599cf 100644 --- a/src/gui/views/qr.rs +++ b/src/gui/views/qr.rs @@ -30,8 +30,8 @@ use crate::gui::views::View; /// QR code image from text. pub struct QrCodeContent { - /// Text to create QR code. - pub(crate) text: String, + /// QR code text. + text: String, /// Flag to draw animated QR with Uniform Resources /// https://github.com/BlockchainCommons/Research/blob/master/papers/bcr-2020-005-ur.md @@ -62,18 +62,18 @@ impl QrCodeContent { } /// Draw QR code. - pub fn ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { + pub fn ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if self.animated { // Show animated QR code. - self.animated_ui(ui, text, cb); + self.animated_ui(ui, cb); } else { // Show static QR code. - self.static_ui(ui, text, cb); + self.static_ui(ui, cb); } } /// Draw animated QR code content. - fn animated_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { + fn animated_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if !self.has_image() { let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; ui.vertical_centered(|ui| { @@ -84,7 +84,7 @@ impl QrCodeContent { // Create multiple vector images from text if not creating. if !self.loading() { - self.create_svg_list(text); + self.create_svg_list(); } } else { let svg_list = { @@ -111,7 +111,7 @@ impl QrCodeContent { // Show QR code text. ui.add_space(6.0); - View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text()); + View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text()); ui.add_space(6.0); ui.vertical_centered(|ui| { @@ -131,7 +131,7 @@ impl QrCodeContent { w_state.exporting = true; } // Create GIF to export. - self.create_qr_gif(text, DEFAULT_QR_SIZE as usize); + self.create_qr_gif(); }); } else { ui.vertical_centered(|ui| { @@ -171,7 +171,7 @@ impl QrCodeContent { } /// Draw static QR code content. - fn static_ui(&mut self, ui: &mut egui::Ui, text: String, cb: &dyn PlatformCallbacks) { + fn static_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { if !self.has_image() { let space = (ui.available_width() - View::BIG_SPINNER_SIZE) / 2.0; ui.vertical_centered(|ui| { @@ -182,7 +182,7 @@ impl QrCodeContent { // Create vector image from text if not creating. if !self.loading() { - self.create_svg(text); + self.create_svg(); } } else { // Create image from SVG data. @@ -194,7 +194,7 @@ impl QrCodeContent { // Show QR code text. ui.add_space(6.0); - View::ellipsize_text(ui, text.clone(), 16.0, Colors::inactive_text()); + View::ellipsize_text(ui, self.text.clone(), 16.0, Colors::inactive_text()); ui.add_space(6.0); // Show button to share QR. @@ -204,21 +204,22 @@ impl QrCodeContent { share_text, Colors::blue(), Colors::white_or_black(false), || { - if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { - if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) { - let mut png = vec![]; - let png_enc = PngEncoder::new_with_quality(&mut png, - CompressionType::Best, - FilterType::NoFilter); - if let Ok(()) = png_enc.write_image(data.as_slice(), - DEFAULT_QR_SIZE, - DEFAULT_QR_SIZE, - ExtendedColorType::L8) { - let name = format!("{}.png", chrono::Utc::now().timestamp()); - cb.share_data(name, png).unwrap_or_default(); + let text = self.text.as_str(); + if let Ok(qr) = QrCode::encode_text(text, qrcodegen::QrCodeEcc::Low) { + if let Some(data) = Self::qr_to_image_data(qr, DEFAULT_QR_SIZE as usize) { + let mut png = vec![]; + let png_enc = PngEncoder::new_with_quality(&mut png, + CompressionType::Best, + FilterType::NoFilter); + if let Ok(()) = png_enc.write_image(data.as_slice(), + DEFAULT_QR_SIZE, + DEFAULT_QR_SIZE, + ExtendedColorType::L8) { + let name = format!("{}.png", chrono::Utc::now().timestamp()); + cb.share_data(name, png).unwrap_or_default(); + } } } - } }); }); ui.add_space(8.0); @@ -267,8 +268,9 @@ impl QrCodeContent { } /// Create multiple vector QR code images at separate thread. - fn create_svg_list(&self, text: String) { + fn create_svg_list(&self) { let qr_state = self.qr_image_state.clone(); + let text = self.text.clone(); thread::spawn(move || { let mut encoder = ur::Encoder::bytes(text.as_bytes(), 100).unwrap(); let mut data = Vec::with_capacity(encoder.fragment_count()); @@ -294,8 +296,9 @@ impl QrCodeContent { } /// Create vector QR code image at separate thread. - fn create_svg(&self, text: String) { + fn create_svg(&self) { let qr_state = self.qr_image_state.clone(); + let text = self.text.clone(); thread::spawn(move || { if let Ok(qr) = QrCode::encode_text(text.as_str(), qrcodegen::QrCodeEcc::Low) { let svg = Self::qr_to_svg(qr, 0); @@ -332,13 +335,14 @@ impl QrCodeContent { } /// Create GIF image at separate thread. - fn create_qr_gif(&self, text: String, size: usize) { + fn create_qr_gif(&self) { { let mut w_state = self.qr_image_state.write(); w_state.gif_creating = true; } let qr_state = self.qr_image_state.clone(); + let text = self.text.clone(); thread::spawn(move || { // Setup GIF image encoder. let mut gif = vec![]; @@ -354,7 +358,7 @@ impl QrCodeContent { ) { // Create an image from QR data. let image = qr.render() - .max_dimensions(size as u32, size as u32) + .max_dimensions(DEFAULT_QR_SIZE, DEFAULT_QR_SIZE) .dark_color(image::Rgb([0, 0, 0])) .light_color(image::Rgb([255, 255, 255])) .build(); diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index edc8134..3cd5061 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -148,7 +148,7 @@ impl WalletContent { ui.vertical_centered(|ui| { // Draw wallet tabs. View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.tabs_ui(ui, wallet); + self.tabs_ui(ui); }); }); }); @@ -468,7 +468,7 @@ impl WalletContent { QrScanResult::Slatepack(message) => { // Redirect to messages to handle parsed message. let mut messages = - WalletMessages::new(wallet.can_use_dandelion(), Some(message.to_string())); + WalletMessages::new(Some(message.to_string())); messages.parse_message(wallet); modal.close(); self.current_tab = Box::new(messages); @@ -477,8 +477,7 @@ impl WalletContent { QrScanResult::Address(receiver) => { if wallet.get_data().unwrap().info.amount_currently_spendable > 0 { // Redirect to send amount with Tor. - let addr = wallet.slatepack_address().unwrap(); - let mut transport = WalletTransport::new(addr.clone()); + let mut transport = WalletTransport::default(); modal.close(); transport.show_send_tor_modal(cb, Some(receiver.to_string())); self.current_tab = Box::new(transport); @@ -528,7 +527,7 @@ impl WalletContent { } /// Draw tab buttons in the bottom of the screen. - fn tabs_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + fn tabs_ui(&mut self, ui: &mut egui::Ui) { ui.scope(|ui| { // Setup spacing between tabs. ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); @@ -547,14 +546,13 @@ impl WalletContent { let is_messages = current_type == WalletTabType::Messages; View::tab_button(ui, CHAT_CIRCLE_TEXT, is_messages, || { self.current_tab = Box::new( - WalletMessages::new(wallet.can_use_dandelion(), None) + WalletMessages::new(None) ); }); }); columns[2].vertical_centered_justified(|ui| { View::tab_button(ui, BRIDGE, current_type == WalletTabType::Transport, || { - let addr = wallet.slatepack_address().unwrap(); - self.current_tab = Box::new(WalletTransport::new(addr)); + self.current_tab = Box::new(WalletTransport::default()); }); }); columns[3].vertical_centered_justified(|ui| { diff --git a/src/gui/views/wallets/wallet/messages.rs b/src/gui/views/wallets/wallet/messages.rs deleted file mode 100644 index 1306b1e..0000000 --- a/src/gui/views/wallets/wallet/messages.rs +++ /dev/null @@ -1,1166 +0,0 @@ -// 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::{Id, Margin, RichText, ScrollArea}; -use egui::scroll_area::ScrollBarVisibility; -use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; -use grin_wallet_libwallet::{Error, Slate, SlateState}; -use log::error; -use parking_lot::RwLock; - -use crate::gui::Colors; -use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, DOWNLOAD_SIMPLE, PROHIBIT, QR_CODE, SCAN, UPLOAD_SIMPLE}; -use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, Content, View}; -use crate::gui::views::types::{ModalPosition, QrScanResult, TextEditOptions}; -use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType}; -use crate::gui::views::wallets::wallet::WalletContent; -use crate::wallet::types::WalletTransaction; -use crate::wallet::Wallet; - -#[derive(Clone, Eq, PartialEq, Debug, thiserror::Error)] -enum MessageError { - #[error("{0}")] - Response(String), - #[error("{0}")] - Parse(String), - #[error("{0}")] - Finalize(String), - #[error("{0}")] - Other(String), -} - -impl MessageError { - pub fn text(&self) -> &String { - match self { - MessageError::Response(text) => text, - MessageError::Parse(text) => text, - MessageError::Finalize(text) => text, - MessageError::Other(text) => text - } - } -} - -/// Slatepacks messages interaction tab content. -pub struct WalletMessages { - /// Slatepack message to create response message. - message_edit: String, - /// Parsed Slatepack message. - message_slate: Option, - /// Flag to check if message request is loading. - message_loading: bool, - /// Message request result. - receive_pay_result: Arc)>>>, - /// Message finalize or post result. - final_post_result: Arc>>>, - /// Slatepack error on finalization, parse and response creation. - message_error: Option, - /// Generated Slatepack response message. - response_edit: String, - /// Flag to check if Dandelion is needed to finalize transaction. - dandelion: bool, - /// Button to parse picked file content. - file_pick_button: FilePickButton, - - /// Flag to check if invoice or sending request was opened for [`Modal`]. - request_invoice: bool, - /// Amount to send or receive at [`Modal`]. - request_amount_edit: String, - /// Generated Slatepack message as request to send or receive funds at [`Modal`]. - request_edit: String, - /// Flag to check if there is an error happened on request creation at [`Modal`]. - request_error: Option, - /// Flag to check if response Slatepack message is showing as QR code image at [`Modal`]. - request_qr: bool, - /// Request Slatepack message QR code image [`Modal`] content. - request_qr_content: QrCodeContent, - /// Flag to check if request is loading at [`Modal`]. - request_loading: bool, - /// Request result if there is no error at [`Modal`]. - request_result: Arc>>>, - - /// Camera content for Slatepack message QR code scanning [`Modal`]. - message_camera_content: CameraContent, - /// Flag to check if there is an error on scanning Slatepack message QR code at [`Modal`]. - message_scan_error: bool, - - /// QR code Slatepacks message text to show at [`Modal`]. - qr_message_text: Option, - /// QR code Slatepack message image [`Modal`] content. - qr_message_content: QrCodeContent, -} - -/// Identifier for amount input [`Modal`] to create invoice or sending request. -const REQUEST_MODAL: &'static str = "messages_request_modal"; - -/// Identifier for QR code Slatepack message scan [`Modal`]. -const QR_SLATEPACK_MESSAGE_SCAN_MODAL: &'static str = "qr_slatepack_message_scan_modal"; - -/// Identifier for [`Modal`] to show QR code Slatepack message image. -const QR_SLATEPACK_MESSAGE_MODAL: &'static str = "qr_slatepack_message_modal"; - -impl WalletTab for WalletMessages { - fn get_type(&self) -> WalletTabType { - WalletTabType::Messages - } - - fn ui(&mut self, ui: &mut egui::Ui, 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); - - egui::CentralPanel::default() - .frame(egui::Frame { - stroke: View::item_stroke(), - fill: Colors::white_or_black(false), - 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::AlwaysHidden) - .id_source(Id::from("wallet_messages").with(wallet.get_config().id)) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.vertical_centered(|ui| { - View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.ui(ui, wallet, cb); - }); - }); - }); - }); - } -} - -impl WalletMessages { - /// Create new content instance, put message into input if provided. - pub fn new(dandelion: bool, message: Option) -> Self { - Self { - request_invoice: false, - message_edit: message.unwrap_or("".to_string()), - message_slate: None, - message_loading: false, - receive_pay_result: Arc::new(RwLock::new(None)), - final_post_result: Arc::new(RwLock::new(None)), - message_error: None, - response_edit: "".to_string(), - dandelion, - file_pick_button: FilePickButton::default(), - request_amount_edit: "".to_string(), - request_edit: "".to_string(), - request_error: None, - request_qr: false, - request_qr_content: QrCodeContent::new("".to_string(), true), - request_loading: false, - request_result: Arc::new(RwLock::new(None)), - message_camera_content: CameraContent::default(), - message_scan_error: false, - qr_message_text: None, - qr_message_content: QrCodeContent::new("".to_string(), true), - } - } - - /// Draw manual wallet transaction interaction content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { - ui.add_space(3.0); - - // Show creation of request to send or receive funds. - self.request_ui(ui, wallet, cb); - - ui.add_space(12.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(6.0); - - // Show Slatepack message input field. - self.input_slatepack_ui(ui, wallet, cb); - - ui.add_space(6.0); - } - - /// 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 { - REQUEST_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.request_modal_ui(ui, wallet, modal, cb); - }); - } - QR_SLATEPACK_MESSAGE_SCAN_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.qr_message_scan_modal_ui(ui, modal, wallet, cb); - }); - } - QR_SLATEPACK_MESSAGE_MODAL => { - Modal::ui(ui.ctx(), |ui, modal| { - self.qr_message_modal_ui(ui, modal, cb); - }); - } - _ => {} - } - } - } - } - - /// Draw creation of request to send or receive funds. - fn request_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - cb: &dyn PlatformCallbacks) { - ui.label(RichText::new(t!("wallets.create_request_desc")) - .size(16.0) - .color(Colors::inactive_text())); - ui.add_space(7.0); - - // Show send button only if balance is not empty. - let data = wallet.get_data().unwrap(); - if data.info.amount_currently_spendable > 0 { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw sending request creation button. - let send_text = format!("{} {}", UPLOAD_SIMPLE, t!("wallets.send")); - View::colored_text_button(ui, send_text, Colors::red(), Colors::button(), || { - self.show_request_modal(false, cb); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Draw invoice request creation button. - self.receive_button_ui(ui, cb); - }); - }); - } else { - // Draw invoice creation button. - self.receive_button_ui(ui, cb); - } - } - - /// Draw invoice request creation button. - fn receive_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - let receive_text = format!("{} {}", DOWNLOAD_SIMPLE, t!("wallets.receive")); - View::colored_text_button(ui, receive_text, Colors::green(), Colors::button(), || { - self.show_request_modal(true, cb); - }); - } - - /// Show [`Modal`] to create invoice or sending request. - fn show_request_modal(&mut self, invoice: bool, cb: &dyn PlatformCallbacks) { - // Setup modal values. - self.request_invoice = invoice; - self.request_qr = false; - self.request_edit = "".to_string(); - self.request_amount_edit = "".to_string(); - self.request_error = None; - { - let mut w_result = self.request_result.write(); - *w_result = None; - } - // Show receive amount modal. - let title = if self.request_invoice { - t!("wallets.receive") - } else { - t!("wallets.send") - }; - Modal::new(REQUEST_MODAL).position(ModalPosition::CenterTop).title(title).show(); - cb.show_keyboard(); - } - - /// Draw invoice or sending request creation [`Modal`] content. - fn request_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - - if self.request_loading { - ui.add_space(34.0); - ui.vertical_centered(|ui| { - View::big_loading_spinner(ui); - }); - ui.add_space(50.0); - - // Check if there is request result error. - if self.request_error.is_some() { - modal.enable_closing(); - self.request_loading = false; - return; - } - - // Update data on request result. - let r_request = self.request_result.read(); - if r_request.is_some() { - let message = r_request.as_ref().unwrap(); - match message { - Ok((_, message)) => { - self.request_edit = message.clone(); - } - Err(err) => { - match err { - Error::NotEnoughFunds { .. } => { - let m = t!( - "wallets.pay_balance_error", - "amount" => self.request_amount_edit - ); - self.request_error = Some(MessageError::Other(m)); - } - _ => { - let m = t!("wallets.invoice_slatepack_err"); - self.request_error = Some(MessageError::Other(m)); - } - } - } - } - modal.enable_closing(); - self.request_loading = false; - } - } else if self.request_edit.is_empty() { - ui.vertical_centered(|ui| { - let enter_text = if self.request_invoice { - t!("wallets.enter_amount_receive") - } else { - let data = wallet.get_data().unwrap(); - let amount = amount_to_hr_string(data.info.amount_currently_spendable, true); - 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 request amount text input. - let amount_edit_id = Id::from(modal.id).with(wallet.get_config().id); - let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center(); - let amount_edit_before = self.request_amount_edit.clone(); - View::text_edit(ui, cb, &mut self.request_amount_edit, &mut amount_edit_opts); - - // Check value if input was changed. - if amount_edit_before != self.request_amount_edit { - self.request_error = None; - if !self.request_amount_edit.is_empty() { - self.request_amount_edit = self.request_amount_edit.trim().replace(",", "."); - match amount_from_hr_string(self.request_amount_edit.as_str()) { - Ok(a) => { - if !self.request_amount_edit.contains(".") { - // To avoid input of several "0". - if a == 0 { - self.request_amount_edit = "0".to_string(); - return; - } - } else { - // Check input after ".". - let parts = self.request_amount_edit - .split(".") - .collect::>(); - if parts.len() == 2 && parts[1].len() > 9 { - self.request_amount_edit = amount_edit_before; - return; - } - } - - // Do not input amount more than balance in sending. - if !self.request_invoice { - let b = wallet.get_data().unwrap().info.amount_currently_spendable; - if b < a { - self.request_amount_edit = amount_edit_before; - } - } - } - Err(_) => { - self.request_amount_edit = amount_edit_before; - } - } - } - } - - // Show request creation error. - if self.request_error.is_some() { - ui.add_space(12.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(self.request_error.clone().unwrap().text()) - .size(17.0) - .color(Colors::red())); - }); - } - - ui.add_space(12.0); - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - self.request_amount_edit = "".to_string(); - self.request_error = None; - cb.hide_keyboard(); - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Button to create Slatepack message request. - View::button(ui, t!("continue"), Colors::white_or_black(false), || { - if self.request_amount_edit.is_empty() { - return; - } - if let Ok(a) = amount_from_hr_string(self.request_amount_edit.as_str()) { - cb.hide_keyboard(); - // Setup data for request. - let wallet = wallet.clone(); - let invoice = self.request_invoice.clone(); - let result = self.request_result.clone(); - // Send request at another thread. - self.request_loading = true; - modal.disable_closing(); - thread::spawn(move || { - let message = if invoice { - wallet.issue_invoice(a) - } else { - wallet.send(a) - }; - let mut w_result = result.write(); - *w_result = Some(message); - }); - } else { - self.request_error = Some( - MessageError::Other(t!("wallets.invoice_slatepack_err")) - ); - } - }); - }); - }); - ui.add_space(6.0); - } else { - ui.vertical_centered(|ui| { - let amount = amount_from_hr_string(self.request_amount_edit.as_str()).unwrap(); - let amount_format = amount_to_hr_string(amount, true); - let desc_text = if self.request_invoice { - t!("wallets.invoice_desc","amount" => amount_format) - } else { - t!("wallets.send_request_desc","amount" => amount_format) - }; - ui.label(RichText::new(desc_text).size(16.0).color(Colors::gray())); - }); - ui.add_space(6.0); - - // Draw QR code content if requested. - if self.request_qr { - // Draw QR code content. - let text = self.request_edit.clone(); - if text.is_empty() { - self.request_qr = false; - } - self.request_qr_content.ui(ui, text.clone(), cb); - - // Show button to close modal. - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.request_qr_content.clear_state(); - self.request_qr = false; - modal.close(); - }); - }); - ui.add_space(6.0); - return; - } - - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - - // Draw request Slatepack message text. - let scroll_id = if self.request_invoice { - Id::from("receive_request").with(wallet.get_config().id) - } else { - Id::from("send_request").with(wallet.get_config().id) - }; - ScrollArea::vertical() - .id_source(scroll_id) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(128.0) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(7.0); - let input_id = Id::from(scroll_id).with("_input"); - egui::TextEdit::multiline(&mut self.request_edit) - .id(input_id) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(false) - .hint_text(SLATEPACK_MESSAGE_HINT) - .desired_width(f32::INFINITY) - .show(ui); - ui.add_space(6.0); - }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - - ui.add_space(10.0); - - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw button to show request as QR code. - let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); - View::button(ui, qr_text, Colors::button(), || { - self.request_qr = true; - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Draw button to copy request to clipboard. - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::button(), || { - cb.copy_string_to_buffer(self.request_edit.clone()); - self.request_amount_edit = "".to_string(); - self.request_edit = "".to_string(); - modal.close(); - }); - }); - }); - - ui.add_space(10.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw button to cancel transaction. - let cancel = t!("modal.cancel"); - View::colored_text_button(ui, cancel, Colors::red(), Colors::button(), || { - if let Ok(slate) = wallet.parse_slatepack(&self.request_edit) { - if let Some(tx) = wallet.tx_by_slate(&slate) { - wallet.cancel(tx.data.id); - } - } - self.request_amount_edit = "".to_string(); - self.request_edit = "".to_string(); - cb.hide_keyboard(); - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - // Draw button to close modal. - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.request_amount_edit = "".to_string(); - self.request_edit = "".to_string(); - modal.close(); - }); - }); - }); - }); - ui.add_space(6.0); - } - } - - /// Draw Slatepack message input content. - fn input_slatepack_ui(&mut self, - ui: &mut egui::Ui, - wallet: &mut Wallet, - cb: &dyn PlatformCallbacks) { - // Setup description text. - let empty_fields = self.message_edit.is_empty() && self.request_edit.is_empty(); - let response_empty = self.response_edit.is_empty(); - if let Some(err) = &self.message_error { - ui.label(RichText::new(err.text()).size(16.0).color(Colors::red())); - } else { - let desc_text = if self.message_slate.is_none() || empty_fields { - t!("wallets.input_slatepack_desc") - } else { - let slate = self.message_slate.clone().unwrap(); - let amount = amount_to_hr_string(slate.amount, true); - match slate.state { - SlateState::Standard1 => { - t!("wallets.parse_s1_slatepack_desc","amount" => amount) - } - SlateState::Standard2 => { - t!("wallets.parse_s2_slatepack_desc","amount" => amount) - } - SlateState::Standard3 => { - t!("wallets.parse_s3_slatepack_desc","amount" => amount) - } - SlateState::Invoice1 => { - t!("wallets.parse_i1_slatepack_desc","amount" => amount) - } - SlateState::Invoice2 => { - t!("wallets.parse_i2_slatepack_desc","amount" => amount) - } - SlateState::Invoice3 => { - t!("wallets.parse_i3_slatepack_desc","amount" => amount) - } - _ => { - t!("wallets.input_slatepack_desc") - } - } - }; - ui.label(RichText::new(desc_text).size(16.0).color(Colors::inactive_text())); - } - ui.add_space(6.0); - - // Setup Slatepack message text input. - let message = if response_empty { - &mut self.message_edit - } else { - &mut self.response_edit - }; - - // Save message to check for changes. - let message_before = message.clone(); - - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(3.0); - let scroll_id = Id::from( - if response_empty { - "message_input" - } else { - "response_input" - }).with(wallet.get_config().id); - ScrollArea::vertical() - .id_source(scroll_id) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .max_height(128.0) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.add_space(7.0); - let input_id = scroll_id.with("_input"); - let resp = egui::TextEdit::multiline(message) - .id(input_id) - .font(egui::TextStyle::Small) - .desired_rows(5) - .interactive(response_empty && !self.message_loading) - .hint_text(SLATEPACK_MESSAGE_HINT) - .desired_width(f32::INFINITY) - .show(ui) - .response; - // Show soft keyboard on click. - if response_empty && resp.clicked() { - resp.request_focus(); - cb.show_keyboard(); - } - if response_empty && resp.has_focus() { - // Apply text from input on Android as temporary fix for egui. - View::on_soft_input(ui, input_id, message); - } - ui.add_space(6.0); - }); - ui.add_space(2.0); - View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(10.0); - - // Parse Slatepack message if input field was changed, resetting message error. - if &message_before != message { - self.parse_message(wallet); - } - - // Draw buttons to clear/copy/paste. - let columns_num = if self.message_loading { 1 } else { 2 }; - let mut show_dandelion = false; - ui.scope(|ui| { - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(columns_num, |columns| { - let first_column_content = |ui: &mut egui::Ui| { - if self.message_slate.is_some() && !empty_fields { - if self.response_edit.is_empty() { - // Draw button to clear message input. - let clear_text = format!("{} {}", BROOM, t!("clear")); - View::button(ui, clear_text, Colors::button(), || { - self.message_edit.clear(); - self.response_edit.clear(); - self.message_error = None; - self.message_slate = None; - }); - } else { - // Draw button to show Slatepack message as QR code. - let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); - View::button(ui, qr_text, Colors::button(), || { - let text = self.response_edit.clone(); - self.message_edit.clear(); - self.response_edit.clear(); - self.show_qr_message_modal(text); - }); - } - } else { - if self.message_loading { - View::small_loading_spinner(ui); - // Check loading result. - self.check_message_loading_result(wallet); - } else { - // Draw button to scan Slatepack message QR code. - let scan_text = format!("{} {}", SCAN, t!("scan")); - View::button(ui, scan_text, Colors::button(), || { - self.message_edit.clear(); - self.message_error = None; - self.show_qr_message_scan_modal(cb); - }); - } - } - }; - if columns_num == 1 { - columns[0].vertical_centered(first_column_content); - } else { - columns[0].vertical_centered_justified(first_column_content); - columns[1].vertical_centered_justified(|ui| { - if self.message_slate.is_some() && !empty_fields { - if !self.response_edit.is_empty() { - // Draw button to copy response to clipboard. - let copy_text = format!("{} {}", COPY, t!("copy")); - View::button(ui, copy_text, Colors::button(), || { - cb.copy_string_to_buffer(self.response_edit.clone()); - self.message_edit.clear(); - self.response_edit.clear(); - self.message_slate = None; - }); - } else { - show_dandelion = true; - // Draw button to finalize or repost transaction. - View::action_button(ui, t!("wallets.finalize"), || { - let slate = self.message_slate.clone().unwrap(); - self.message_slate = None; - let dandelion = self.dandelion; - let message_edit = self.message_edit.clone(); - let wallet = wallet.clone(); - let result = self.final_post_result.clone(); - - // Finalize or post transaction at separate thread. - self.message_loading = true; - thread::spawn(move || { - let res = if slate.state == SlateState::Invoice3 || - slate.state == SlateState::Standard3 { - wallet.post(&slate, dandelion) - } else { - match wallet.finalize(&message_edit, dandelion) { - Ok(_) => { - Ok(()) - } - Err(e) => { - Err(e) - } - } - }; - let mut w_res = result.write(); - *w_res = Some(res); - }); - }); - } - } else { - // Draw button to paste text from clipboard. - let paste = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); - View::button(ui, paste, Colors::button(), || { - let buf = cb.get_string_from_buffer(); - let previous = self.message_edit.clone(); - self.message_edit = buf.clone().trim().to_string(); - // Parse Slatepack message resetting message error. - if buf != previous { - self.parse_message(wallet); - } - }); - } - }); - } - }); - - ui.add_space(10.0); - - // Draw clear button on message input, - // cancel and clear buttons on response - // or button to choose text or image file. - if !self.message_loading { - if self.message_slate.is_none() && !self.message_edit.is_empty() { - // Draw button to clear message input. - let clear_text = format!("{} {}", BROOM, t!("clear")); - View::button(ui, clear_text, Colors::button(), || { - self.message_edit.clear(); - self.response_edit.clear(); - self.message_error = None; - self.message_slate = None; - }); - } else if !self.response_edit.is_empty() && self.message_slate.is_some() { - // Draw cancel button. - let cancel_text = format!("{} {}", PROHIBIT, t!("modal.cancel")); - View::colored_text_button(ui, cancel_text, Colors::red(), Colors::button(), || { - let slate = self.message_slate.clone().unwrap(); - if let Some(tx) = wallet.tx_by_slate(&slate) { - wallet.cancel(tx.data.id); - self.message_edit.clear(); - self.response_edit.clear(); - self.message_slate = None; - } - }); - } else if self.message_slate.is_none() { - // Draw button to choose file. - let mut parsed_text = "".to_string(); - self.file_pick_button.ui(ui, cb, |text| { - parsed_text = text; - }); - if !parsed_text.is_empty() { - // Parse Slatepack message from file content. - self.message_edit = parsed_text; - self.parse_message(wallet); - } - } - } - }); - - // Draw setup of ability to post transaction with Dandelion. - if show_dandelion { - let dandelion_before = self.dandelion; - View::checkbox(ui, dandelion_before, t!("wallets.use_dandelion"), || { - self.dandelion = !dandelion_before; - wallet.update_use_dandelion(self.dandelion); - }); - } - } - - /// Show QR code Slatepack message [`Modal`]. - pub fn show_qr_message_modal(&mut self, text: String) { - self.qr_message_text = Some(text); - self.qr_message_content.clear_state(); - let slate = self.message_slate.clone().unwrap(); - let title = if slate.state == SlateState::Standard1 { - t!("wallets.receive") - } else { - t!("wallets.send") - }; - Modal::new(QR_SLATEPACK_MESSAGE_MODAL) - .position(ModalPosition::CenterTop) - .title(title) - .show(); - } - - /// Draw QR code Slatepack message image [`Modal`] content. - fn qr_message_modal_ui(&mut self, ui: &mut egui::Ui, m: &Modal, cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - - // Setup title for Slatepack message. - ui.vertical_centered(|ui| { - let slate = self.message_slate.clone().unwrap(); - let amount = amount_to_hr_string(slate.amount, true); - let title = if slate.state == SlateState::Standard1 { - t!("wallets.parse_s1_slatepack_desc","amount" => amount) - } else { - t!("wallets.parse_i1_slatepack_desc","amount" => amount) - }; - ui.label(RichText::new(title).size(16.0).color(Colors::inactive_text())); - }); - ui.add_space(6.0); - - // Draw QR code content. - let text = self.qr_message_text.clone().unwrap(); - self.qr_message_content.ui(ui, text.clone(), cb); - - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.qr_message_text = None; - self.qr_message_content.clear_state(); - self.response_edit.clear(); - self.message_slate = None; - m.close(); - }); - }); - ui.add_space(6.0); - } - - /// Show QR code Slatepack message scanner [`Modal`]. - pub fn show_qr_message_scan_modal(&mut self, cb: &dyn PlatformCallbacks) { - self.message_scan_error = false; - // Show QR code scan modal. - Modal::new(QR_SLATEPACK_MESSAGE_SCAN_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("scan_qr")) - .closeable(false) - .show(); - cb.start_camera(); - } - - /// Draw QR code scanner [`Modal`] content. - fn qr_message_scan_modal_ui(&mut self, - ui: &mut egui::Ui, - modal: &Modal, - wallet: &Wallet, - cb: &dyn PlatformCallbacks) { - if self.message_scan_error { - ui.add_space(6.0); - ui.vertical_centered(|ui| { - let err_text = format!("{}", t!("wallets.parse_slatepack_err")).replace(":", "."); - ui.label(RichText::new(err_text) - .size(17.0) - .color(Colors::red())); - }); - ui.add_space(12.0); - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.message_scan_error = false; - modal.close(); - }); - }); - columns[1].vertical_centered_justified(|ui| { - View::button(ui, t!("repeat"), Colors::white_or_black(false), || { - Modal::set_title(t!("scan_qr")); - self.message_scan_error = false; - cb.start_camera(); - }); - }); - }); - ui.add_space(6.0); - return; - } else if let Some(result) = self.message_camera_content.qr_scan_result() { - cb.stop_camera(); - self.message_camera_content.clear_state(); - match &result { - QrScanResult::Slatepack(text) => { - self.message_edit = text.to_string(); - self.parse_message(wallet); - modal.close(); - } - _ => { - self.message_scan_error = true; - } - } - } else { - ui.add_space(6.0); - self.message_camera_content.ui(ui, cb); - ui.add_space(8.0); - } - - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - cb.stop_camera(); - modal.close(); - }); - }); - ui.add_space(6.0); - } - - /// Check Slatepack message request loading result. - fn check_message_loading_result(&mut self, wallet: &Wallet) { - // Check finalize post pay result. - let has_finalize_post_result = { - let r_res = self.final_post_result.read(); - r_res.is_some() - }; - if has_finalize_post_result { - let resp = { - let r_res = self.final_post_result.read(); - r_res.as_ref().unwrap().clone() - }; - if resp.is_ok() { - self.message_edit.clear(); - self.message_slate = None; - } else { - self.message_error = Some( - MessageError::Finalize( - t!("wallets.finalize_slatepack_err") - ) - ); - } - self.message_loading = false; - } - - // Check receive pay result. - let has_receive_pay_result = { - let r_res = self.receive_pay_result.read(); - r_res.is_some() - }; - if has_receive_pay_result { - let (slate, resp) = { - let r_res = self.receive_pay_result.read(); - r_res.as_ref().unwrap().clone() - }; - if resp.is_ok() { - self.response_edit = resp.as_ref().unwrap().clone(); - } else { - let err = resp.as_ref().err().unwrap(); - match err { - // Set already canceled transaction error message. - Error::TransactionWasCancelled {..} - => { - self.message_error = Some( - MessageError::Response( - t!("wallets.resp_canceled_err") - ) - ); - } - // Set an error when there is not enough funds to pay. - Error::NotEnoughFunds {..} => { - let m = t!( - "wallets.pay_balance_error", - "amount" => amount_to_hr_string(slate.amount, true) - ); - self.message_error = Some(MessageError::Response(m)); - } - // Set default error message. - _ => { - self.message_error = Some( - MessageError::Response( - t!("wallets.resp_slatepack_err") - ) - ); - } - } - // Check if tx with same slate id already exists. - if self.message_error.is_none() { - let exists_tx = wallet.tx_by_slate(&slate).is_some(); - if exists_tx { - let mut sl = slate.clone(); - sl.state = if sl.state == SlateState::Standard1 { - SlateState::Standard2 - } else { - SlateState::Invoice2 - }; - match wallet.read_slatepack(&sl) { - None => { - self.message_error = Some( - MessageError::Response( - t!("wallets.resp_slatepack_err") - ) - ); - } - Some(sp) => { - self.response_edit = sp; - } - } - } - } - } - // Setup message slate. - if self.message_error.is_none() { - self.message_slate = Some(slate); - } - // Clear message loading result and status. - { - let mut w_res = self.receive_pay_result.write(); - *w_res = None; - } - self.message_loading = false; - } - } - - /// Parse message input into [`Slate`] updating slate and response input. - pub fn parse_message(&mut self, wallet: &Wallet) { - self.message_slate = None; - self.message_error = None; - if self.message_edit.is_empty() { - return; - } - // Trim message. - self.message_edit = self.message_edit.trim().to_string(); - - // Parse message. - if let Ok(mut slate) = wallet.parse_slatepack(&self.message_edit) { - // Try to setup empty amount from transaction by id. - if slate.amount == 0 { - let _ = wallet.get_data().unwrap().txs.as_ref().unwrap().iter().map(|tx| { - if tx.data.tx_slate_id == Some(slate.id) { - if slate.amount == 0 { - slate.amount = tx.amount; - } - } - tx - }).collect::>(); - } - - if slate.amount == 0 { - self.message_error = Some( - MessageError::Response(t!("wallets.resp_slatepack_err")) - ); - return; - } - - // Make operation based on incoming state status. - match slate.state { - SlateState::Standard1 | SlateState::Invoice1 => { - let slate = slate.clone(); - let message = self.message_edit.clone(); - let message_result = self.receive_pay_result.clone(); - let wallet = wallet.clone(); - // Create response to sender or receiver at separate thread. - self.message_loading = true; - thread::spawn(move || { - let resp = if slate.state == SlateState::Standard1 { - wallet.receive(&message) - } else { - wallet.pay(&message) - }; - let mut w_res = message_result.write(); - *w_res = Some((slate, resp)); - }); - return; - } - SlateState::Standard2 | SlateState::Invoice2 => { - // Check if slatepack with same id and state already exists. - let mut sl = slate.clone(); - sl.state = if sl.state == SlateState::Standard2 { - SlateState::Standard1 - } else { - SlateState::Invoice1 - }; - match wallet.read_slatepack(&sl) { - None => { - match wallet.read_slatepack(&slate) { - None => { - self.message_error = Some( - MessageError::Response(t!("wallets.resp_slatepack_err")) - ); - } - Some(sp) => { - self.message_slate = Some(sl); - self.response_edit = sp; - return; - } - } - } - Some(_) => { - self.message_slate = Some(slate.clone()); - return; - } - } - } - _ => { - self.response_edit = "".to_string(); - } - } - self.message_slate = Some(slate); - } else { - self.message_slate = None; - self.message_error = Some(MessageError::Parse(t!("wallets.resp_slatepack_err"))); - } - } -} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/messages/content.rs b/src/gui/views/wallets/wallet/messages/content.rs new file mode 100644 index 0000000..b0214e5 --- /dev/null +++ b/src/gui/views/wallets/wallet/messages/content.rs @@ -0,0 +1,541 @@ +// Copyright 2024 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::{Id, Margin, RichText, ScrollArea}; +use egui::scroll_area::ScrollBarVisibility; +use grin_core::core::amount_to_hr_string; +use grin_wallet_libwallet::{Error, Slate, SlateState}; +use parking_lot::RwLock; + +use crate::gui::Colors; +use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, DOWNLOAD_SIMPLE, SCAN, UPLOAD_SIMPLE}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{FilePickButton, Modal, Content, View, CameraContent}; +use crate::gui::views::types::{ModalPosition, QrScanResult}; +use crate::gui::views::wallets::wallet::messages::request::MessageRequestModal; +use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType}; +use crate::gui::views::wallets::wallet::{WalletContent, WalletTransactionModal}; +use crate::wallet::types::WalletTransaction; +use crate::wallet::Wallet; + +/// Slatepack messages interaction tab content. +pub struct WalletMessages { + /// Slatepacks message input text. + message_edit: String, + /// Flag to check if message request is loading. + message_loading: bool, + /// Error on finalization, parse or response creation. + message_error: String, + /// Parsed message result with finalization flag and transaction. + message_result: Arc)>>>, + + /// Wallet transaction [`Modal`] content. + tx_info_content: Option, + + /// Invoice or sending request creation [`Modal`] content. + request_modal_content: Option, + + /// Camera content for Slatepack message QR code scanning [`Modal`]. + message_camera_content: CameraContent, + /// Flag to check if there is an error on scanning Slatepack message QR code at [`Modal`]. + message_scan_error: bool, + + /// Button to parse picked file content. + file_pick_button: FilePickButton, +} + +/// Identifier for amount input [`Modal`] to create invoice or sending request. +const REQUEST_MODAL: &'static str = "messages_request"; + +/// Identifier for [`Modal`] modal to show transaction information. +const TX_INFO_MODAL: &'static str = "messages_tx_info"; + +/// Identifier for [`Modal`] to scan Slatepack message from QR code. +const SCAN_QR_MESSAGE_MODAL: &'static str = "qr_slatepack_message_scan_modal"; + +impl WalletTab for WalletMessages { + fn get_type(&self) -> WalletTabType { + WalletTabType::Messages + } + + fn ui(&mut self, ui: &mut egui::Ui, 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); + + egui::CentralPanel::default() + .frame(egui::Frame { + stroke: View::item_stroke(), + fill: Colors::white_or_black(false), + 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::AlwaysHidden) + .id_source(Id::from("wallet_messages").with(wallet.get_config().id)) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.vertical_centered(|ui| { + View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { + self.ui(ui, wallet, cb); + }); + }); + }); + }); + } +} + +impl WalletMessages { + /// Create new content instance, put message into input if provided. + pub fn new(message: Option) -> Self { + Self { + message_edit: message.unwrap_or("".to_string()), + message_loading: false, + message_error: "".to_string(), + message_result: Arc::new(Default::default()), + tx_info_content: None, + request_modal_content: None, + message_camera_content: Default::default(), + message_scan_error: false, + file_pick_button: FilePickButton::default(), + } + } + + /// Draw manual wallet transaction interaction content. + pub fn ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + ui.add_space(3.0); + + // Show creation of request to send or receive funds. + self.request_ui(ui, wallet, cb); + + ui.add_space(12.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); + + // Show Slatepack message input field. + self.input_slatepack_ui(ui, wallet, cb); + + ui.add_space(6.0); + } + + /// 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 { + REQUEST_MODAL => { + if let Some(content) = self.request_modal_content.as_mut() { + Modal::ui(ui.ctx(), |ui, modal| { + content.ui(ui, wallet, modal, cb); + }); + } + } + TX_INFO_MODAL => { + if let Some(content) = self.tx_info_content.as_mut() { + Modal::ui(ui.ctx(), |ui, modal| { + content.ui(ui, wallet, modal, cb); + }); + } + } + SCAN_QR_MESSAGE_MODAL => { + Modal::ui(ui.ctx(), |ui, modal| { + self.qr_message_scan_modal_ui(ui, modal, wallet, cb); + }); + } + _ => {} + } + } + } + } + + /// Draw creation of request to send or receive funds. + fn request_ui(&mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + cb: &dyn PlatformCallbacks) { + ui.label(RichText::new(t!("wallets.create_request_desc")) + .size(16.0) + .color(Colors::inactive_text())); + ui.add_space(7.0); + + // Show send button only if balance is not empty. + let data = wallet.get_data().unwrap(); + if data.info.amount_currently_spendable > 0 { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + let send_text = format!("{} {}", UPLOAD_SIMPLE, t!("wallets.send")); + View::colored_text_button(ui, send_text, Colors::red(), Colors::button(), || { + self.show_request_modal(false, cb); + }); + }); + columns[1].vertical_centered_justified(|ui| { + self.receive_button_ui(ui, cb); + }); + }); + } else { + self.receive_button_ui(ui, cb); + } + } + + /// Draw invoice request creation button. + fn receive_button_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + let receive_text = format!("{} {}", DOWNLOAD_SIMPLE, t!("wallets.receive")); + View::colored_text_button(ui, receive_text, Colors::green(), Colors::button(), || { + self.show_request_modal(true, cb); + }); + } + + /// Show [`Modal`] to create invoice or sending request. + fn show_request_modal(&mut self, invoice: bool, cb: &dyn PlatformCallbacks) { + self.request_modal_content = Some(MessageRequestModal::new(invoice)); + let title = if invoice { + t!("wallets.receive") + } else { + t!("wallets.send") + }; + Modal::new(REQUEST_MODAL).position(ModalPosition::CenterTop).title(title).show(); + cb.show_keyboard(); + } + + /// Draw Slatepack message input content. + fn input_slatepack_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + cb: &dyn PlatformCallbacks) { + // Setup description text. + if !self.message_error.is_empty() { + ui.label(RichText::new(&self.message_error).size(16.0).color(Colors::red())); + } else { + ui.label(RichText::new(t!("wallets.input_slatepack_desc")) + .size(16.0) + .color(Colors::inactive_text())); + } + ui.add_space(6.0); + + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + + // Save message to check for changes. + let message_before = self.message_edit.clone(); + + let scroll_id = Id::from("message_input_scroll").with(wallet.get_config().id); + ScrollArea::vertical() + .id_source(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("_input"); + let resp = egui::TextEdit::multiline(&mut self.message_edit) + .id(input_id) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(!self.message_loading) + .hint_text(SLATEPACK_MESSAGE_HINT) + .desired_width(f32::INFINITY) + .show(ui) + .response; + // Show soft keyboard on click. + if resp.clicked() { + resp.request_focus(); + cb.show_keyboard(); + } + if resp.has_focus() { + // Apply text from input on Android as temporary fix for egui. + View::on_soft_input(ui, input_id, &mut self.message_edit); + } + ui.add_space(6.0); + }); + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(10.0); + + // Parse message if input field was changed. + if message_before != self.message_edit { + self.parse_message(wallet); + } + + if self.message_loading { + View::small_loading_spinner(ui); + // Check loading result. + let has_tx = { + let r_res = self.message_result.read(); + r_res.is_some() + }; + if has_tx { + let mut w_res = self.message_result.write(); + let tx_res = w_res.as_ref().unwrap(); + let slate = &tx_res.0; + match &tx_res.1 { + Ok(tx) => { + self.message_edit.clear(); + // Show transaction modal on success. + self.tx_info_content = Some(WalletTransactionModal::new(wallet, tx, false)); + Modal::new(TX_INFO_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.tx")) + .show(); + *w_res = None; + } + Err(err) => { + match err { + // Set already canceled transaction error message. + Error::TransactionWasCancelled {..} => { + self.message_error = t!("wallets.resp_canceled_err"); + } + // Set an error when there is not enough funds to pay. + Error::NotEnoughFunds {..} => { + let m = t!( + "wallets.pay_balance_error", + "amount" => amount_to_hr_string(slate.amount, true) + ); + self.message_error = m; + } + // Set default error message. + _ => { + let finalize = slate.state == SlateState::Standard2 || + slate.state == SlateState::Invoice2; + self.message_error = if finalize { + t!("wallets.finalize_slatepack_err") + } else { + t!("wallets.resp_slatepack_err") + }; + } + } + } + } + self.message_loading = false; + } + return; + } + + ui.scope(|ui| { + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + // Draw button to scan Slatepack message QR code. + let scan_text = format!("{} {}", SCAN, t!("scan")); + View::button(ui, scan_text, Colors::button(), || { + self.message_edit.clear(); + self.message_error.clear(); + self.show_qr_message_scan_modal(cb); + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Draw button to paste text from clipboard. + let paste = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, paste, Colors::button(), || { + let buf = cb.get_string_from_buffer(); + let previous = self.message_edit.clone(); + self.message_edit = buf.clone().trim().to_string(); + // Parse Slatepack message resetting message error. + if buf != previous { + self.parse_message(wallet); + self.parse_message(wallet); + } + }); + }); + }); + ui.add_space(10.0); + }); + + if self.message_edit.is_empty() { + // Draw button to choose file. + let mut parsed_text = "".to_string(); + self.file_pick_button.ui(ui, cb, |text| { + parsed_text = text; + }); + self.message_edit = parsed_text; + self.parse_message(wallet); + } else { + // Draw button to clear message input. + let clear_text = format!("{} {}", BROOM, t!("clear")); + View::button(ui, clear_text, Colors::button(), || { + self.message_edit.clear(); + self.message_error.clear(); + }); + } + } + + /// Parse message input making operation based on incoming status. + pub fn parse_message(&mut self, wallet: &Wallet) { + self.message_error.clear(); + self.message_edit = self.message_edit.trim().to_string(); + if self.message_edit.is_empty() { + return; + } + if let Ok(mut slate) = wallet.parse_slatepack(&self.message_edit) { + // Try to setup empty amount from transaction by id. + if slate.amount == 0 { + let _ = wallet.get_data().unwrap().txs.as_ref().unwrap().iter().map(|tx| { + if tx.data.tx_slate_id == Some(slate.id) { + if slate.amount == 0 { + slate.amount = tx.amount; + } + } + tx + }).collect::>(); + } + + // Check if message with same id and state already exists to show tx modal. + let exists = wallet.read_slatepack(&slate).is_some(); + if exists { + if let Some(tx) = wallet.tx_by_slate(&slate).as_ref() { + self.message_edit.clear(); + self.tx_info_content = Some(WalletTransactionModal::new(wallet, tx, false)); + Modal::new(TX_INFO_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.tx")) + .show(); + } else { + self.message_error = t!("wallets.parse_slatepack_err"); + } + return; + } + + // Create response or finalize at separate thread. + let sl = slate.clone(); + let message = self.message_edit.clone(); + let message_result = self.message_result.clone(); + let wallet = wallet.clone(); + + self.message_loading = true; + thread::spawn(move || { + let result = match slate.state { + SlateState::Standard1 | SlateState::Invoice1 => { + if sl.state != SlateState::Standard1 { + wallet.pay(&message) + } else { + wallet.receive(&message) + } + } + SlateState::Standard2 | SlateState::Invoice2 => { + wallet.finalize(&message) + } + _ => { + if let Some(tx) = wallet.tx_by_slate(&slate) { + Ok(tx) + } else { + Err(Error::GenericError(t!("wallets.parse_slatepack_err"))) + } + } + }; + let mut w_res = message_result.write(); + *w_res = Some((slate, result)); + }); + } else { + self.message_error = t!("wallets.parse_slatepack_err"); + } + } + + /// Show QR code Slatepack message scanner [`Modal`]. + pub fn show_qr_message_scan_modal(&mut self, cb: &dyn PlatformCallbacks) { + self.message_scan_error = false; + // Show QR code scan modal. + Modal::new(SCAN_QR_MESSAGE_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("scan_qr")) + .closeable(false) + .show(); + cb.start_camera(); + } + + /// Draw QR code scanner [`Modal`] content. + fn qr_message_scan_modal_ui(&mut self, + ui: &mut egui::Ui, + modal: &Modal, + wallet: &Wallet, + cb: &dyn PlatformCallbacks) { + if self.message_scan_error { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let err_text = format!("{}", t!("wallets.parse_slatepack_err")).replace(":", "."); + ui.label(RichText::new(err_text) + .size(17.0) + .color(Colors::red())); + }); + ui.add_space(12.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.message_scan_error = false; + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("repeat"), Colors::white_or_black(false), || { + Modal::set_title(t!("scan_qr")); + self.message_scan_error = false; + cb.start_camera(); + }); + }); + }); + ui.add_space(6.0); + return; + } else if let Some(result) = self.message_camera_content.qr_scan_result() { + cb.stop_camera(); + self.message_camera_content.clear_state(); + match &result { + QrScanResult::Slatepack(text) => { + self.message_edit = text.to_string(); + self.parse_message(wallet); + modal.close(); + } + _ => { + self.message_scan_error = true; + } + } + } else { + ui.add_space(6.0); + self.message_camera_content.ui(ui, cb); + ui.add_space(8.0); + } + + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + cb.stop_camera(); + modal.close(); + }); + }); + ui.add_space(6.0); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/messages/mod.rs b/src/gui/views/wallets/wallet/messages/mod.rs new file mode 100644 index 0000000..5199266 --- /dev/null +++ b/src/gui/views/wallets/wallet/messages/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2024 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. + +mod content; +pub use content::*; + +mod request; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/messages/request.rs b/src/gui/views/wallets/wallet/messages/request.rs new file mode 100644 index 0000000..6517f7b --- /dev/null +++ b/src/gui/views/wallets/wallet/messages/request.rs @@ -0,0 +1,260 @@ +// Copyright 2024 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 parking_lot::RwLock; +use egui::{Id, RichText}; +use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; +use grin_wallet_libwallet::Error; + +use crate::gui::Colors; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, View}; +use crate::gui::views::types::TextEditOptions; +use crate::gui::views::wallets::wallet::WalletTransactionModal; +use crate::wallet::types::WalletTransaction; +use crate::wallet::Wallet; + +/// Invoice or sending request creation [`Modal`] content. +pub struct MessageRequestModal { + /// Flag to check if invoice or sending request was opened. + invoice: bool, + + /// Amount to send or receive. + amount_edit: String, + + /// Flag to check if request is loading. + request_loading: bool, + /// Request result if there is no error. + request_result: Arc>>>, + /// Flag to check if there is an error happened on request creation. + request_error: Option, + + /// Request result transaction content. + result_tx_content: Option, +} + +impl MessageRequestModal { + /// Create new content instance. + pub fn new(invoice: bool) -> Self { + Self { + invoice, + amount_edit: "".to_string(), + request_loading: false, + request_result: Arc::new(RwLock::new(None)), + request_error: None, + result_tx_content: None, + } + } + + /// Draw [`Modal`] content. + pub fn ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + // Draw transaction information on request result. + if let Some(tx) = self.result_tx_content.as_mut() { + tx.ui(ui, wallet, modal, cb); + return; + } + + ui.add_space(6.0); + + // Draw content on request loading. + if self.request_loading { + self.loading_request_ui(ui, wallet, modal); + return; + } + + // Draw amount input content. + self.amount_input_ui(ui, wallet, modal, cb); + + // Show request creation error. + if let Some(err) = &self.request_error { + ui.add_space(12.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(err) + .size(17.0) + .color(Colors::red())); + }); + } + + ui.add_space(12.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + self.amount_edit = "".to_string(); + self.request_error = None; + cb.hide_keyboard(); + modal.close(); + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Button to create Slatepack message request. + View::button(ui, t!("continue"), Colors::white_or_black(false), || { + if self.amount_edit.is_empty() { + return; + } + if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { + cb.hide_keyboard(); + modal.disable_closing(); + // Setup data for request. + let wallet = wallet.clone(); + let invoice = self.invoice.clone(); + let result = self.request_result.clone(); + // Send request at another thread. + self.request_loading = true; + thread::spawn(move || { + let res = if invoice { + wallet.issue_invoice(a) + } else { + wallet.send(a) + }; + let mut w_result = result.write(); + *w_result = Some(res); + }); + } else { + let err = if self.invoice { + t!("wallets.invoice_slatepack_err") + } else { + t!("wallets.send_slatepack_err") + }; + self.request_error = Some(err); + } + }); + }); + }); + ui.add_space(6.0); + } + + /// Draw amount input content. + fn amount_input_ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + ui.vertical_centered(|ui| { + let enter_text = if self.invoice { + t!("wallets.enter_amount_receive") + } else { + let data = wallet.get_data().unwrap(); + let amount = amount_to_hr_string(data.info.amount_currently_spendable, true); + 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 request amount text input. + let amount_edit_id = Id::from(modal.id).with(wallet.get_config().id); + let mut amount_edit_opts = TextEditOptions::new(amount_edit_id).h_center(); + let amount_edit_before = self.amount_edit.clone(); + View::text_edit(ui, cb, &mut self.amount_edit, &mut amount_edit_opts); + + // Check value if input was changed. + if amount_edit_before != self.amount_edit { + self.request_error = None; + if !self.amount_edit.is_empty() { + self.amount_edit = self.amount_edit.trim().replace(",", "."); + 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. + if !self.invoice { + 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; + } + } + } + } + } + + /// Draw loading request content. + fn loading_request_ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, modal: &Modal) { + ui.add_space(34.0); + ui.vertical_centered(|ui| { + View::big_loading_spinner(ui); + }); + ui.add_space(50.0); + + // Check if there is request result error. + if self.request_error.is_some() { + modal.enable_closing(); + self.request_loading = false; + return; + } + + // Update data on request result. + let r_request = self.request_result.read(); + if r_request.is_some() { + modal.enable_closing(); + let result = r_request.as_ref().unwrap(); + match result { + Ok(tx) => { + self.result_tx_content = Some(WalletTransactionModal::new(wallet, tx, false)); + } + Err(err) => { + match err { + Error::NotEnoughFunds { .. } => { + let m = t!( + "wallets.pay_balance_error", + "amount" => self.amount_edit + ); + self.request_error = Some(m); + } + _ => { + let m = if self.invoice { + t!("wallets.invoice_slatepack_err") + } else { + t!("wallets.send_slatepack_err") + }; + self.request_error = Some(m); + } + } + self.request_loading = false; + } + } + } + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/settings/common.rs b/src/gui/views/wallets/wallet/settings/common.rs index e312e70..f60d26e 100644 --- a/src/gui/views/wallets/wallet/settings/common.rs +++ b/src/gui/views/wallets/wallet/settings/common.rs @@ -36,7 +36,7 @@ pub struct CommonSettings { new_pass_edit: String, /// Minimum confirmations number value. - min_confirmations_edit: String + min_confirmations_edit: String, } /// Identifier for wallet name [`Modal`]. @@ -54,25 +54,26 @@ impl Default for CommonSettings { wrong_pass: false, old_pass_edit: "".to_string(), new_pass_edit: "".to_string(), - min_confirmations_edit: "".to_string() + min_confirmations_edit: "".to_string(), } } } impl CommonSettings { + /// Draw common wallet settings content. pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &mut Wallet, cb: &dyn PlatformCallbacks) { // Show modal content for this ui container. self.modal_content_ui(ui, wallet, cb); ui.vertical_centered(|ui| { - let wallet_name = wallet.get_config().name; + let config = wallet.get_config(); // Show wallet name. ui.add_space(2.0); ui.label(RichText::new(t!("wallets.name")) .size(16.0) .color(Colors::gray())); ui.add_space(2.0); - ui.label(RichText::new(wallet_name.clone()) + ui.label(RichText::new(&config.name) .size(16.0) .color(Colors::white_or_black(true))); ui.add_space(8.0); @@ -80,7 +81,7 @@ impl CommonSettings { // Show wallet name setup. let name_text = format!("{} {}", PENCIL, t!("change")); View::button(ui, name_text, Colors::button(), || { - self.name_edit = wallet_name; + self.name_edit = config.name; // Show wallet name modal. Modal::new(NAME_EDIT_MODAL) .position(ModalPosition::CenterTop) @@ -118,10 +119,9 @@ impl CommonSettings { ui.add_space(6.0); // Show minimum amount of confirmations value setup. - let min_confirmations = wallet.get_config().min_confirmations; - let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, min_confirmations); + let min_conf_text = format!("{} {}", CLOCK_COUNTDOWN, config.min_confirmations); View::button(ui, min_conf_text, Colors::button(), || { - self.min_confirmations_edit = min_confirmations.to_string(); + self.min_confirmations_edit = config.min_confirmations.to_string(); // Show minimum amount of confirmations value modal. Modal::new(MIN_CONFIRMATIONS_EDIT_MODAL) .position(ModalPosition::CenterTop) @@ -131,8 +131,15 @@ impl CommonSettings { }); ui.add_space(12.0); + + // Setup ability to post wallet transactions with Dandelion. + View::checkbox(ui, wallet.can_use_dandelion(), t!("wallets.use_dandelion"), || { + wallet.update_use_dandelion(!wallet.can_use_dandelion()); + }); + + ui.add_space(6.0); View::horizontal_line(ui, Colors::stroke()); - ui.add_space(4.0); + ui.add_space(6.0); }); } diff --git a/src/gui/views/wallets/wallet/settings/recovery.rs b/src/gui/views/wallets/wallet/settings/recovery.rs index 969e2cd..686d059 100644 --- a/src/gui/views/wallets/wallet/settings/recovery.rs +++ b/src/gui/views/wallets/wallet/settings/recovery.rs @@ -232,7 +232,7 @@ impl RecoverySettings { }); }); columns[1].vertical_centered_justified(|ui| { - View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || { + let mut on_next = || { match wallet.get_recovery(self.pass_edit.clone()) { Ok(phrase) => { self.wrong_pass = false; @@ -243,6 +243,12 @@ impl RecoverySettings { self.wrong_pass = true; } } + }; + View::on_enter_key(ui, || { + (on_next)(); + }); + View::button(ui, "OK".to_owned(), Colors::white_or_black(false), || { + on_next(); }); }); }); diff --git a/src/gui/views/wallets/wallet/transport.rs b/src/gui/views/wallets/wallet/transport.rs deleted file mode 100644 index e3192cd..0000000 --- a/src/gui/views/wallets/wallet/transport.rs +++ /dev/null @@ -1,944 +0,0 @@ -// 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 tor_rtcompat::BlockOn; -use tor_rtcompat::tokio::TokioNativeTlsRuntime; -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, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE}; -use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, Modal, QrCodeContent, Content, 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 QR code scanner is opened at address [`Modal`]. - show_address_scan: bool, - /// Address QR code scanner [`Modal`] content. - address_scan_content: CameraContent, - /// Flag to check if [`Modal`] was just opened to focus on first field. - modal_just_opened: bool, - - /// QR code address image [`Modal`] content. - qr_address_content: QrCodeContent, - - /// Flag to check if Tor settings were changed. - tor_settings_changed: bool, - /// Tor bridge binary path edit text. - bridge_bin_path_edit: String, - /// Tor bridge connection line edit text. - bridge_conn_line_edit: String, - /// Flag to check if QR code scanner is opened at bridge [`Modal`]. - show_bridge_scan: bool, - /// Address QR code scanner [`Modal`] content. - bridge_qr_scan_content: CameraContent, -} - -impl WalletTab for WalletTransport { - fn get_type(&self) -> WalletTabType { - WalletTabType::Transport - } - - fn ui(&mut self, - ui: &mut egui::Ui, - 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_or_black(false), - 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() - .id_source(Id::from("wallet_transport").with(wallet.get_config().id)) - .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) - .auto_shrink([false; 2]) - .show(ui, |ui| { - ui.vertical_centered(|ui| { - View::max_width_ui(ui, Content::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 (bin_path, conn_line) = if let Some(b) = bridge { - (b.binary_path(), b.connection_line()) - } else { - ("".to_string(), "".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, - show_address_scan: false, - address_scan_content: CameraContent::default(), - modal_just_opened: false, - qr_address_content: QrCodeContent::new(addr, false), - tor_settings_changed: false, - bridge_bin_path_edit: bin_path, - bridge_conn_line_edit: conn_line, - show_bridge_scan: false, - bridge_qr_scan_content: CameraContent::default(), - } - } - - /// 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, cb); - }); - } - _ => {} - } - } - } - } - - /// 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 && wallet.foreign_api_port().is_some() { - self.tor_send_ui(ui, cb); - } - } - - /// Draw Tor transport header content. - fn tor_header_ui(&mut 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, || { - self.show_tor_settings_modal(); - }); - - // Draw button to enable/disable Tor listener for current wallet. - let service_id = &wallet.identifier(); - if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() { - if !Tor::is_service_running(service_id) { - 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.with_layout(Layout::left_to_right(Align::Min), |ui| { - ui.add_space(1.0); - ui.label(RichText::new(t!("transport.tor_network")) - .size(18.0) - .color(Colors::title(false))); - }); - - // 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 wallet.foreign_api_port().is_none() { - (DOTS_THREE_CIRCLE, t!("wallets.loading")) - } else 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::text(false))); - ui.add_space(1.0); - - // Setup bridges status text. - let bridge = TorConfig::get_bridge(); - let bridges_text = match &bridge { - None => { - format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled")) - } - Some(b) => { - let name = b.protocol_name().to_uppercase(); - format!("{} {}", - SHIELD_CHECKERED, - t!("transport.bridge_name", "b" = name)) - } - }; - - ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray())); - }); - }); - }); - }); - } - - /// Show Tor transport settings [`Modal`]. - fn show_tor_settings_modal(&mut self) { - self.tor_settings_changed = false; - // Show Tor settings modal. - Modal::new(TOR_SETTINGS_MODAL) - .position(ModalPosition::CenterTop) - .title(t!("transport.tor_settings")) - .closeable(false) - .show(); - } - - /// Draw Tor transport settings [`Modal`] content. - fn tor_settings_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - - // Draw QR code scanner content if requested. - if self.show_bridge_scan { - let mut on_stop = |content: &mut CameraContent| { - cb.stop_camera(); - content.clear_state(); - modal.enable_closing(); - self.show_bridge_scan = false; - }; - - if let Some(result) = self.bridge_qr_scan_content.qr_scan_result() { - self.bridge_conn_line_edit = result.text(); - on_stop(&mut self.bridge_qr_scan_content); - cb.show_keyboard(); - } else { - self.bridge_qr_scan_content.ui(ui, cb); - ui.add_space(12.0); - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - // Show buttons to close modal or come back to sending input. - ui.columns(2, |cols| { - cols[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - on_stop(&mut self.bridge_qr_scan_content); - modal.close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - on_stop(&mut self.bridge_qr_scan_content); - }); - }); - }); - ui.add_space(6.0); - } - return; - } - - // Do not show bridges setup on Android. - let os = OperatingSystem::from_target_os(); - let show_bridges = os != OperatingSystem::Android; - 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"), || { - // Save value. - let value = if bridge.is_some() { - None - } else { - let default_bridge = TorConfig::get_obfs4(); - self.bridge_bin_path_edit = default_bridge.binary_path(); - self.bridge_conn_line_edit = default_bridge.connection_line(); - Some(default_bridge) - }; - TorConfig::save_bridge(value); - self.tor_settings_changed = true; - }); - }); - - // 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 Obfs4 bridge selector. - let obfs4 = TorConfig::get_obfs4(); - let name = obfs4.protocol_name().to_uppercase(); - View::radio_value(ui, &mut bridge, obfs4, name); - }); - columns[1].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); - }); - }); - ui.add_space(12.0); - - // Check if bridge type was changed to save. - if current_bridge != bridge { - self.tor_settings_changed = true; - TorConfig::save_bridge(Some(bridge.clone())); - self.bridge_bin_path_edit = bridge.binary_path(); - self.bridge_conn_line_edit = bridge.connection_line(); - } - - // Draw binary path text edit. - let bin_edit_id = Id::from(modal.id) - .with(wallet.get_config().id) - .with("_bin_edit"); - let mut bin_edit_opts = TextEditOptions::new(bin_edit_id) - .paste() - .no_focus(); - let bin_edit_before = self.bridge_bin_path_edit.clone(); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.bin_file")) - .size(17.0) - .color(Colors::inactive_text())); - ui.add_space(6.0); - View::text_edit(ui, cb, &mut self.bridge_bin_path_edit, &mut bin_edit_opts); - ui.add_space(6.0); - }); - - // Draw connection line text edit. - let conn_edit_before = self.bridge_conn_line_edit.clone(); - let conn_edit_id = Id::from(modal.id) - .with(wallet.get_config().id) - .with("_conn_edit"); - let mut conn_edit_opts = TextEditOptions::new(conn_edit_id) - .paste() - .no_focus() - .scan_qr(); - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.conn_line")) - .size(17.0) - .color(Colors::inactive_text())); - ui.add_space(6.0); - View::text_edit(ui, cb, &mut self.bridge_conn_line_edit, &mut conn_edit_opts); - // Check if scan button was pressed. - if conn_edit_opts.scan_pressed { - cb.hide_keyboard(); - modal.disable_closing(); - conn_edit_opts.scan_pressed = false; - self.show_bridge_scan = true; - } - }); - - // Check if bin path or connection line text was changed to save bridge. - if conn_edit_before != self.bridge_conn_line_edit || - bin_edit_before != self.bridge_bin_path_edit { - let bin_path = self.bridge_bin_path_edit.trim().to_string(); - let conn_line = self.bridge_conn_line_edit.trim().to_string(); - let b = match bridge { - TorBridge::Snowflake(_, _) => { - TorBridge::Snowflake(bin_path, conn_line) - }, - TorBridge::Obfs4(_, _) => { - TorBridge::Obfs4(bin_path, conn_line) - } - }; - TorConfig::save_bridge(Some(b)); - self.tor_settings_changed = true; - } - - 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_or_black(false), || { - if self.tor_settings_changed { - self.tor_settings_changed = false; - // Restart running service or rebuild client. - let service_id = &wallet.identifier(); - if Tor::is_service_running(service_id) { - if let Ok(key) = wallet.secret_key() { - let api_port = wallet.foreign_api_port().unwrap(); - Tor::restart_service(api_port, key, service_id); - } - } else { - Tor::rebuild_client(); - } - } - modal.close(); - }); - }); - ui.add_space(6.0); - } - - /// Draw Tor receive 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_address_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) || - wallet.foreign_api_port().is_none() { - 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, m: &Modal, cb: &dyn PlatformCallbacks) { - ui.add_space(6.0); - - // Draw QR code content. - let text = self.qr_address_content.text.clone(); - self.qr_address_content.ui(ui, text.clone(), cb); - - ui.vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.qr_address_content.clear_state(); - m.close(); - }); - }); - ui.add_space(6.0); - } - - /// Draw Tor send 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_or_black(false), || { - 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) { - { - 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 { - // Draw QR code scanner content if requested. - if self.show_address_scan { - let mut on_stop = |content: &mut CameraContent| { - cb.stop_camera(); - content.clear_state(); - modal.enable_closing(); - self.show_address_scan = false; - }; - - if let Some(result) = self.address_scan_content.qr_scan_result() { - self.address_edit = result.text(); - self.modal_just_opened = true; - on_stop(&mut self.address_scan_content); - cb.show_keyboard(); - } else { - self.address_scan_content.ui(ui, cb); - ui.add_space(6.0); - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - // Show buttons to close modal or come back to sending input. - ui.columns(2, |cols| { - cols[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - on_stop(&mut self.address_scan_content); - modal.close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - self.modal_just_opened = true; - on_stop(&mut self.address_scan_content); - cb.show_keyboard(); - }); - }); - }); - ui.add_space(6.0); - } - return; - } - - 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, &mut 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() { - // Trim text, replace "," by "." and parse amount. - self.amount_edit = self.amount_edit.trim().replace(",", "."); - 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(6.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 mut address_edit_opts = TextEditOptions::new(address_edit_id) - .paste() - .no_focus() - .scan_qr(); - View::text_edit(ui, cb, &mut self.address_edit, &mut address_edit_opts); - // Check if scan button was pressed. - if address_edit_opts.scan_pressed { - cb.hide_keyboard(); - modal.disable_closing(); - address_edit_opts.scan_pressed = false; - self.show_address_scan = true; - } - 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(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - 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_or_black(false), || { - 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 || { - let runtime = TokioNativeTlsRuntime::create().unwrap(); - runtime - .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(8.0, 0.0); - - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { - 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_or_black(false), || { - // 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 || { - let runtime = TokioNativeTlsRuntime::create().unwrap(); - runtime - .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(); - } - } - } -} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/content.rs b/src/gui/views/wallets/wallet/transport/content.rs new file mode 100644 index 0000000..4876d1d --- /dev/null +++ b/src/gui/views/wallets/wallet/transport/content.rs @@ -0,0 +1,397 @@ +// 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 egui::{Align, Id, Layout, Margin, RichText, Rounding, ScrollArea}; +use egui::scroll_area::ScrollBarVisibility; + +use crate::gui::Colors; +use crate::gui::icons::{CHECK_CIRCLE, COPY, DOTS_THREE_CIRCLE, EXPORT, GEAR_SIX, GLOBE_SIMPLE, POWER, QR_CODE, SHIELD_CHECKERED, SHIELD_SLASH, WARNING_CIRCLE, X_CIRCLE}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{Modal, QrCodeContent, Content, View}; +use crate::gui::views::types::ModalPosition; +use crate::gui::views::wallets::wallet::transport::send::TransportSendModal; +use crate::gui::views::wallets::wallet::transport::settings::TransportSettingsModal; +use crate::gui::views::wallets::wallet::types::{WalletTab, WalletTabType}; +use crate::gui::views::wallets::wallet::WalletContent; +use crate::tor::{Tor, TorConfig}; +use crate::wallet::types::WalletData; +use crate::wallet::Wallet; + +/// Wallet transport tab content. +pub struct WalletTransport { + /// Sending [`Modal`] content. + send_modal_content: Option, + + /// QR code address image [`Modal`] content. + qr_address_content: Option, + + /// Tor settings [`Modal`] content. + settings_modal_content: Option, +} + +impl WalletTab for WalletTransport { + fn get_type(&self) -> WalletTabType { + WalletTabType::Transport + } + + fn ui(&mut self, + ui: &mut egui::Ui, + 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_or_black(false), + 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() + .id_source(Id::from("wallet_transport").with(wallet.get_config().id)) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.vertical_centered(|ui| { + View::max_width_ui(ui, Content::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 Default for WalletTransport { + fn default() -> Self { + Self { + send_modal_content: None, + qr_address_content: None, + settings_modal_content: None, + } + } +} + +impl WalletTransport { + /// 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 transport 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 => { + if let Some(content) = self.send_modal_content.as_mut() { + Modal::ui(ui.ctx(), |ui, modal| { + content.ui(ui, wallet, modal, cb); + }); + } + } + TOR_SETTINGS_MODAL => { + if let Some(content) = self.settings_modal_content.as_mut() { + Modal::ui(ui.ctx(), |ui, modal| { + content.ui(ui, wallet, modal, cb); + }); + } + } + QR_ADDRESS_MODAL => { + Modal::ui(ui.ctx(), |ui, modal| { + self.qr_address_modal_ui(ui, modal, cb); + }); + } + _ => {} + } + } + } + } + + /// 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. + let service_id = &wallet.identifier(); + if data.info.amount_currently_spendable > 0 && wallet.foreign_api_port().is_some() && + !Tor::is_service_starting(service_id) { + self.tor_send_ui(ui, cb); + } + } + + /// Draw Tor transport header content. + fn tor_header_ui(&mut 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, || { + self.settings_modal_content = Some(TransportSettingsModal::default()); + // Show Tor settings modal. + Modal::new(TOR_SETTINGS_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("transport.tor_settings")) + .closeable(false) + .show(); + }); + + // Draw button to enable/disable Tor listener for current wallet. + let service_id = &wallet.identifier(); + if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() { + if !Tor::is_service_running(service_id) { + 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.with_layout(Layout::left_to_right(Align::Min), |ui| { + ui.add_space(1.0); + ui.label(RichText::new(t!("transport.tor_network")) + .size(18.0) + .color(Colors::title(false))); + }); + + // 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 wallet.foreign_api_port().is_none() { + (DOTS_THREE_CIRCLE, t!("wallets.loading")) + } else 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::text(false))); + ui.add_space(1.0); + + // Setup bridges status text. + let bridge = TorConfig::get_bridge(); + let bridges_text = match &bridge { + None => { + format!("{} {}", SHIELD_SLASH, t!("transport.bridges_disabled")) + } + Some(b) => { + let name = b.protocol_name().to_uppercase(); + format!("{} {}", + SHIELD_CHECKERED, + t!("transport.bridge_name", "b" = name)) + } + }; + + ui.label(RichText::new(bridges_text).size(15.0).color(Colors::gray())); + }); + }); + }); + }); + } + + /// Draw Tor receive content. + fn tor_receive_ui(&mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + data: &WalletData, + cb: &dyn PlatformCallbacks) { + let 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_address_content = Some(QrCodeContent::new(addr.clone(), false)); + 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(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) || + wallet.foreign_api_port().is_none() { + Colors::inactive_text() + } else if Tor::is_service_running(service_id) { + Colors::green() + } else { + Colors::red() + }; + View::ellipsize_text(ui, 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, + cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + + // Draw QR code content. + if let Some(content) = self.qr_address_content.as_mut() { + content.ui(ui, cb); + } else { + modal.close(); + return; + } + + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.qr_address_content = None; + modal.close(); + }); + }); + ui.add_space(6.0); + } + + /// Draw Tor send 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_or_black(false), || { + 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) { + self.send_modal_content = Some(TransportSendModal::new(address)); + // Show modal. + Modal::new(SEND_TOR_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("wallets.send")) + .show(); + cb.show_keyboard(); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/mod.rs b/src/gui/views/wallets/wallet/transport/mod.rs new file mode 100644 index 0000000..845225a --- /dev/null +++ b/src/gui/views/wallets/wallet/transport/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2024 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. + +mod content; +pub use content::*; + +mod send; +mod settings; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/send.rs b/src/gui/views/wallets/wallet/transport/send.rs new file mode 100644 index 0000000..76e08b3 --- /dev/null +++ b/src/gui/views/wallets/wallet/transport/send.rs @@ -0,0 +1,357 @@ +// Copyright 2024 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::{Id, RichText}; +use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; +use grin_wallet_libwallet::{Error, SlatepackAddress}; +use parking_lot::RwLock; +use tor_rtcompat::BlockOn; +use tor_rtcompat::tokio::TokioNativeTlsRuntime; +use crate::gui::Colors; +use crate::gui::platform::PlatformCallbacks; + +use crate::gui::views::{CameraContent, Modal, View}; +use crate::gui::views::types::TextEditOptions; +use crate::gui::views::wallets::wallet::WalletTransactionModal; +use crate::wallet::types::WalletTransaction; +use crate::wallet::Wallet; + +/// Transport sending [`Modal`] content. +pub struct TransportSendModal { + /// Flag to focus on first input field after opening. + first_draw: bool, + + /// Flag to check if transaction is sending to show progress. + sending: bool, + /// Flag to check if there is an error to repeat. + error: bool, + /// Transaction result. + send_result: Arc>>>, + + /// Entered amount value. + amount_edit: String, + /// Entered address value. + address_edit: String, + /// Flag to check if entered address is incorrect. + address_error: bool, + + /// Address QR code scanner content. + address_scan_content: Option, + + /// Transaction information content. + tx_info_content: Option, +} + +impl TransportSendModal { + /// Create new instance from provided address. + pub fn new(addr: Option) -> Self { + Self { + first_draw: true, + sending: false, + error: false, + send_result: Arc::new(RwLock::new(None)), + amount_edit: "".to_string(), + address_edit: addr.unwrap_or("".to_string()), + address_error: false, + address_scan_content: None, + tx_info_content: None, + } + } + + /// Draw [`Modal`] content. + pub fn ui(&mut self, + ui: &mut egui::Ui, + wallet: &mut Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + // Draw transaction information on request result. + if let Some(tx) = self.tx_info_content.as_mut() { + tx.ui(ui, wallet, modal, cb); + return; + } + + // Draw sending content, progress or an error. + if self.sending { + self.progress_ui(ui, wallet); + } else if self.error { + self.error_ui(ui, wallet, modal, cb); + } else { + self.content_ui(ui, wallet, modal, cb); + } + } + + /// Draw content to send. + fn content_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, + cb: &dyn PlatformCallbacks) { + // Draw QR code scanner content if requested. + if let Some(scanner) = self.address_scan_content.as_mut() { + let mut on_stop = || { + self.first_draw = true; + cb.stop_camera(); + modal.enable_closing(); + }; + + if let Some(result) = scanner.qr_scan_result() { + self.address_edit = result.text(); + on_stop(); + self.address_scan_content = None; + cb.show_keyboard(); + } else { + scanner.ui(ui, cb); + ui.add_space(6.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + // Show buttons to close modal or come back to sending input. + ui.columns(2, |cols| { + cols[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + on_stop(); + self.address_scan_content = None; + modal.close(); + }); + }); + cols[1].vertical_centered_justified(|ui| { + View::button(ui, t!("back"), Colors::white_or_black(false), || { + on_stop(); + self.address_scan_content = None; + cb.show_keyboard(); + }); + }); + }); + ui.add_space(6.0); + } + return; + } + + 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.first_draw { + self.first_draw = false; + amount_edit_opts.focus = true; + } + View::text_edit(ui, cb, &mut self.amount_edit, &mut 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() { + // Trim text, replace "," by "." and parse amount. + self.amount_edit = self.amount_edit.trim().replace(",", "."); + 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(6.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 mut address_edit_opts = TextEditOptions::new(address_edit_id) + .paste() + .no_focus() + .scan_qr(); + View::text_edit(ui, cb, &mut self.address_edit, &mut address_edit_opts); + // Check if scan button was pressed. + if address_edit_opts.scan_pressed { + cb.hide_keyboard(); + modal.disable_closing(); + address_edit_opts.scan_pressed = false; + self.address_scan_content = Some(CameraContent::default()); + } + 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(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + self.close(modal, cb); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("continue"), Colors::white_or_black(false), || { + self.send(wallet, modal, cb); + }); + }); + }); + ui.add_space(6.0); + } + + /// Draw error content. + fn error_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) { + 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(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.cancel"), Colors::white_or_black(false), || { + self.close(modal, cb); + }); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("repeat"), Colors::white_or_black(false), || { + self.send(wallet, modal, cb); + }); + }); + }); + ui.add_space(6.0); + } + + /// Close modal and clear data. + fn close(&mut self, modal: &Modal, cb: &dyn PlatformCallbacks) { + self.amount_edit = "".to_string(); + self.address_edit = "".to_string(); + + let mut w_res = self.send_result.write(); + *w_res = None; + + self.tx_info_content = None; + self.address_scan_content = None; + + cb.hide_keyboard(); + modal.close(); + } + + /// Send entered amount to address. + fn send(&mut self, wallet: &Wallet, modal: &Modal, cb: &dyn PlatformCallbacks) { + if self.amount_edit.is_empty() { + return; + } + let addr_str = self.address_edit.as_str(); + if let Ok(addr) = SlatepackAddress::try_from(addr_str) { + if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { + cb.hide_keyboard(); + modal.disable_closing(); + // Send amount over Tor. + let mut wallet = wallet.clone(); + let res = self.send_result.clone(); + self.sending = true; + thread::spawn(move || { + let runtime = TokioNativeTlsRuntime::create().unwrap(); + runtime + .block_on(async { + let result = wallet.send_tor(a, &addr).await; + let mut w_res = res.write(); + *w_res = Some(result); + }); + }); + } + } else { + self.address_error = true; + } + } + + /// Draw sending progress content. + fn progress_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + 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); + + // Check sending result. + let has_result = { + let r_result = self.send_result.read(); + r_result.is_some() + }; + if has_result { + { + let res = self.send_result.read().clone().unwrap(); + match res { + Ok(tx) => { + self.tx_info_content = Some(WalletTransactionModal::new(wallet, &tx, false)); + } + Err(_) => { + self.error = true; + } + } + } + let mut w_res = self.send_result.write(); + *w_res = None; + self.sending = false; + } + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/transport/settings.rs b/src/gui/views/wallets/wallet/transport/settings.rs new file mode 100644 index 0000000..377d2f0 --- /dev/null +++ b/src/gui/views/wallets/wallet/transport/settings.rs @@ -0,0 +1,258 @@ +// Copyright 2024 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 egui::os::OperatingSystem; +use egui::{Id, RichText}; + +use crate::gui::Colors; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{CameraContent, Modal, View}; +use crate::gui::views::types::TextEditOptions; +use crate::tor::{Tor, TorBridge, TorConfig}; +use crate::wallet::Wallet; + +/// Transport settings [`Modal`] content. +pub struct TransportSettingsModal { + /// Flag to check if Tor settings were changed. + settings_changed: bool, + + /// Tor bridge binary path edit text. + bridge_bin_path_edit: String, + /// Tor bridge connection line edit text. + bridge_conn_line_edit: String, + /// Address QR code scanner [`Modal`] content. + bridge_qr_scan_content: Option, +} + +impl Default for TransportSettingsModal { + fn default() -> Self { + // Setup Tor bridge binary path edit text. + let bridge = TorConfig::get_bridge(); + let (bin_path, conn_line) = if let Some(b) = bridge { + (b.binary_path(), b.connection_line()) + } else { + ("".to_string(), "".to_string()) + }; + Self { + settings_changed: false, + bridge_bin_path_edit: bin_path, + bridge_conn_line_edit: conn_line, + bridge_qr_scan_content: None, + } + } +} + +impl TransportSettingsModal { + pub fn ui(&mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + ui.add_space(6.0); + + // Draw QR code scanner content if requested. + if let Some(scanner) = self.bridge_qr_scan_content.as_mut() { + let on_stop = || { + cb.stop_camera(); + modal.enable_closing(); + }; + + if let Some(result) = scanner.qr_scan_result() { + self.bridge_conn_line_edit = result.text(); + on_stop(); + self.bridge_qr_scan_content = None; + cb.show_keyboard(); + } else { + scanner.ui(ui, cb); + ui.add_space(12.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + // Show buttons to close modal or come back to sending input. + ui.columns(2, |cols| { + cols[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + on_stop(); + self.bridge_qr_scan_content = None; + modal.close(); + }); + }); + cols[1].vertical_centered_justified(|ui| { + View::button(ui, t!("back"), Colors::white_or_black(false), || { + on_stop(); + self.bridge_qr_scan_content = None; + }); + }); + }); + ui.add_space(6.0); + } + return; + } + + // Do not show bridges setup on Android. + let os = OperatingSystem::from_target_os(); + let show_bridges = os != OperatingSystem::Android; + 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"), || { + // Save value. + let value = if bridge.is_some() { + None + } else { + let default_bridge = TorConfig::get_obfs4(); + self.bridge_bin_path_edit = default_bridge.binary_path(); + self.bridge_conn_line_edit = default_bridge.connection_line(); + Some(default_bridge) + }; + TorConfig::save_bridge(value); + self.settings_changed = true; + }); + }); + + // 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 Obfs4 bridge selector. + let obfs4 = TorConfig::get_obfs4(); + let name = obfs4.protocol_name().to_uppercase(); + View::radio_value(ui, &mut bridge, obfs4, name); + }); + columns[1].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); + }); + }); + ui.add_space(12.0); + + // Check if bridge type was changed to save. + if current_bridge != bridge { + self.settings_changed = true; + TorConfig::save_bridge(Some(bridge.clone())); + self.bridge_bin_path_edit = bridge.binary_path(); + self.bridge_conn_line_edit = bridge.connection_line(); + } + + // Draw binary path text edit. + let bin_edit_id = Id::from(modal.id) + .with(wallet.get_config().id) + .with("_bin_edit"); + let mut bin_edit_opts = TextEditOptions::new(bin_edit_id) + .paste() + .no_focus(); + let bin_edit_before = self.bridge_bin_path_edit.clone(); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("transport.bin_file")) + .size(17.0) + .color(Colors::inactive_text())); + ui.add_space(6.0); + View::text_edit(ui, cb, &mut self.bridge_bin_path_edit, &mut bin_edit_opts); + ui.add_space(6.0); + }); + + // Draw connection line text edit. + let conn_edit_before = self.bridge_conn_line_edit.clone(); + let conn_edit_id = Id::from(modal.id) + .with(wallet.get_config().id) + .with("_conn_edit"); + let mut conn_edit_opts = TextEditOptions::new(conn_edit_id) + .paste() + .no_focus() + .scan_qr(); + ui.vertical_centered(|ui| { + ui.label(RichText::new(t!("transport.conn_line")) + .size(17.0) + .color(Colors::inactive_text())); + ui.add_space(6.0); + View::text_edit(ui, cb, &mut self.bridge_conn_line_edit, &mut conn_edit_opts); + // Check if scan button was pressed. + if conn_edit_opts.scan_pressed { + cb.hide_keyboard(); + modal.disable_closing(); + conn_edit_opts.scan_pressed = false; + self.bridge_qr_scan_content = Some(CameraContent::default()); + } + }); + + // Check if bin path or connection line text was changed to save bridge. + if conn_edit_before != self.bridge_conn_line_edit || + bin_edit_before != self.bridge_bin_path_edit { + let bin_path = self.bridge_bin_path_edit.trim().to_string(); + let conn_line = self.bridge_conn_line_edit.trim().to_string(); + let b = match bridge { + TorBridge::Snowflake(_, _) => { + TorBridge::Snowflake(bin_path, conn_line) + }, + TorBridge::Obfs4(_, _) => { + TorBridge::Obfs4(bin_path, conn_line) + } + }; + TorConfig::save_bridge(Some(b)); + self.settings_changed = true; + } + + 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_or_black(false), || { + if self.settings_changed { + self.settings_changed = false; + // Restart running service or rebuild client. + let service_id = &wallet.identifier(); + if Tor::is_service_running(service_id) { + if let Ok(key) = wallet.secret_key() { + let api_port = wallet.foreign_api_port().unwrap(); + Tor::restart_service(api_port, key, service_id); + } + } else { + Tor::rebuild_client(); + } + } + modal.close(); + }); + }); + ui.add_space(6.0); + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/txs/content.rs b/src/gui/views/wallets/wallet/txs/content.rs index 1977f9d..b1d98d0 100644 --- a/src/gui/views/wallets/wallet/txs/content.rs +++ b/src/gui/views/wallets/wallet/txs/content.rs @@ -19,7 +19,7 @@ use grin_core::core::amount_to_hr_string; use grin_wallet_libwallet::TxLogEntryType; use crate::gui::Colors; -use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, ARROW_CLOCKWISE, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE}; +use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, DOTS_THREE_CIRCLE, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, PullToRefresh, Content, View}; use crate::gui::views::types::ModalPosition; @@ -226,19 +226,6 @@ impl WalletTransactions { .show(); }); } - - // Draw button to repost transaction. - if tx.can_repost(data) { - let r = Rounding::default(); - let (icon, color) = (ARROW_CLOCKWISE, Colors::green()); - View::item_button(ui, r, icon, Some(color), || { - cb.hide_keyboard(); - // Post tx after getting slate from slatepack file. - if let Some((s, _)) = wallet.read_slate_by_tx(tx) { - let _ = wallet.post(&s, wallet.can_use_dandelion()); - } - }); - } }); } }); @@ -249,7 +236,7 @@ impl WalletTransactions { if refresh_resp.should_refresh() { self.manual_sync = Some(now); if !wallet.syncing() { - wallet.sync(true); + wallet.sync(); } } } @@ -339,7 +326,7 @@ impl WalletTransactions { || tx.data.tx_type == TxLogEntryType::TxReceivedCancelled; if is_canceled { format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled")) - } else if tx.posting { + } else if tx.finalizing { format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_finalizing")) } else { if tx.cancelling { @@ -431,8 +418,7 @@ impl WalletTransactions { /// Show transaction information [`Modal`]. fn show_tx_info_modal(&mut self, wallet: &Wallet, tx: &WalletTransaction, finalize: bool) { - let mut modal = WalletTransactionModal::new(wallet, tx); - modal.show_finalization = finalize; + let modal = WalletTransactionModal::new(wallet, tx, finalize); self.tx_info_content = Some(modal); Modal::new(TX_INFO_MODAL) .position(ModalPosition::CenterTop) diff --git a/src/gui/views/wallets/wallet/txs/tx.rs b/src/gui/views/wallets/wallet/txs/tx.rs index f6ec4e6..8bcc89a 100644 --- a/src/gui/views/wallets/wallet/txs/tx.rs +++ b/src/gui/views/wallets/wallet/txs/tx.rs @@ -21,7 +21,7 @@ use grin_util::ToHex; use grin_wallet_libwallet::{Error, Slate, SlateState, TxLogEntryType}; use parking_lot::RwLock; use crate::gui::Colors; -use crate::gui::icons::{ARROW_CLOCKWISE, BROOM, CHECK, CLIPBOARD_TEXT, COPY, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN}; +use crate::gui::icons::{BROOM, CHECK, CLIPBOARD_TEXT, COPY, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, View}; @@ -41,7 +41,7 @@ pub struct WalletTransactionModal { response_edit: String, /// Flag to show transaction finalization input. - pub show_finalization: bool, + show_finalization: bool, /// Finalization Slatepack message input value. finalize_edit: String, /// Flag to check if error happened during transaction finalization. @@ -49,17 +49,13 @@ pub struct WalletTransactionModal { /// Flag to check if transaction is finalizing. finalizing: bool, /// Transaction finalization result. - final_result: Arc>>>, + final_result: Arc>>>, - /// Flag to check if QR code is showing. - show_qr: bool, /// QR code Slatepack message image content. - qr_code_content: QrCodeContent, + qr_code_content: Option, - /// Flag to check if QR code scanner is showing. - show_scanner: bool, /// QR code scanner content. - scanner_content: CameraContent, + qr_scan_content: Option, /// Button to parse picked file content. file_pick_button: FilePickButton, @@ -67,7 +63,7 @@ pub struct WalletTransactionModal { impl WalletTransactionModal { /// Create new content instance with [`Wallet`] from provided [`WalletTransaction`]. - pub fn new(wallet: &Wallet, tx: &WalletTransaction) -> Self { + pub fn new(wallet: &Wallet, tx: &WalletTransaction, show_finalization: bool) -> Self { Self { tx_id: tx.data.id, slate_id: match tx.data.tx_slate_id { @@ -98,13 +94,11 @@ impl WalletTransactionModal { }, finalize_edit: "".to_string(), finalize_error: false, - show_finalization: false, + show_finalization, finalizing: false, final_result: Arc::new(RwLock::new(None)), - show_qr: false, - qr_code_content: QrCodeContent::new("".to_string(), true), - show_scanner: false, - scanner_content: CameraContent::default(), + qr_code_content: None, + qr_scan_content: None, file_pick_button: FilePickButton::default(), } } @@ -133,7 +127,7 @@ impl WalletTransactionModal { } let tx = txs.get(0).unwrap(); - if !self.show_qr && !self.show_scanner { + if self.qr_code_content.is_none() && self.qr_scan_content.is_none() { ui.add_space(6.0); // Show transaction amount status and time. @@ -174,25 +168,6 @@ impl WalletTransactionModal { wallet.cancel(tx.data.id); }); } - - // Draw button to repost transaction. - if wallet_loaded && tx.can_repost(&data) { - let r = if self.show_finalization { - Rounding::default() - } else { - let mut r = r.clone(); - r.nw = 0.0; - r.sw = 0.0; - r - }; - View::item_button(ui, r, ARROW_CLOCKWISE, Some(Colors::green()), || { - cb.hide_keyboard(); - // Post tx after getting slate from slatepack file. - if let Some((s, _)) = wallet.read_slate_by_tx(tx) { - let _ = wallet.post(&s, wallet.can_use_dandelion()); - } - }); - } }); // Show transaction ID info. @@ -207,54 +182,49 @@ impl WalletTransactionModal { } } - // Show Slatepack message or reset flag to show QR if not available. - if !tx.posting && !tx.data.confirmed && !tx.cancelling && + // Show Slatepack message or reset QR code state if not available. + if !tx.finalizing && !tx.data.confirmed && !tx.cancelling && (tx.data.tx_type == TxLogEntryType::TxSent || - tx.data.tx_type == TxLogEntryType::TxReceived) { + tx.data.tx_type == TxLogEntryType::TxReceived) && !self.response_edit.is_empty() { self.message_ui(ui, tx, wallet, modal, cb); - } else if self.show_qr { - self.qr_code_content.clear_state(); - self.show_qr = false; + } else if let Some(qr_content) = self.qr_code_content.as_mut() { + qr_content.clear_state(); } if !self.finalizing { // Setup spacing between buttons. ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - if self.show_qr { + if self.qr_code_content.is_some() { // Show buttons to close modal or come back to text request content. ui.columns(2, |cols| { cols[0].vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::white_or_black(false), || { - self.qr_code_content.clear_state(); - self.show_qr = false; + self.qr_code_content = None; modal.close(); }); }); cols[1].vertical_centered_justified(|ui| { View::button(ui, t!("back"), Colors::white_or_black(false), || { - self.qr_code_content.clear_state(); - self.show_qr = false; + self.qr_code_content = None; }); }); }); - } else if self.show_scanner { + } else if self.qr_scan_content.is_some() { ui.add_space(8.0); // Show buttons to close modal or scanner. ui.columns(2, |cols| { cols[0].vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::white_or_black(false), || { cb.stop_camera(); - self.scanner_content.clear_state(); - self.show_scanner = false; + self.qr_scan_content = None; modal.close(); }); }); cols[1].vertical_centered_justified(|ui| { View::button(ui, t!("back"), Colors::white_or_black(false), || { cb.stop_camera(); - self.scanner_content.clear_state(); - self.show_scanner = false; + self.qr_scan_content = None; modal.enable_closing(); }); }); @@ -361,20 +331,19 @@ impl WalletTransactionModal { ui.add_space(6.0); // Draw QR code scanner content if requested. - if self.show_scanner { - if let Some(result) = self.scanner_content.qr_scan_result() { + if let Some(qr_scan_content) = self.qr_scan_content.as_mut() { + if let Some(result) = qr_scan_content.qr_scan_result() { cb.stop_camera(); - self.scanner_content.clear_state(); + qr_scan_content.clear_state(); // Setup value to finalization input field. self.finalize_edit = result.text(); self.on_finalization_input_change(tx, wallet, modal, cb); modal.enable_closing(); - self.scanner_content.clear_state(); - self.show_scanner = false; + self.qr_scan_content = None; } else { - self.scanner_content.ui(ui, cb); + qr_scan_content.ui(ui, cb); } return; } @@ -427,16 +396,9 @@ impl WalletTransactionModal { let message_before = message_edit.clone(); // Draw QR code content if requested. - if self.show_qr { - let text = message_edit.clone(); - if text.is_empty() { - self.qr_code_content.clear_state(); - self.show_qr = false; - } else { - // Draw QR code content. - self.qr_code_content.ui(ui, text.clone(), cb); - return; - } + if let Some(qr_content) = self.qr_code_content.as_mut() { + qr_content.ui(ui, cb); + return; } // Draw Slatepack message finalization input or request text. @@ -498,7 +460,7 @@ impl WalletTransactionModal { cb.hide_keyboard(); modal.disable_closing(); cb.start_camera(); - self.show_scanner = true; + self.qr_scan_content = Some(CameraContent::default()); }); }); columns[1].vertical_centered_justified(|ui| { @@ -535,9 +497,10 @@ impl WalletTransactionModal { columns[0].vertical_centered_justified(|ui| { // Draw button to show Slatepack message as QR code. let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); - View::button(ui, qr_text, Colors::button(), || { + View::button(ui, qr_text.clone(), Colors::button(), || { cb.hide_keyboard(); - self.show_qr = true; + let text = self.response_edit.clone(); + self.qr_code_content = Some(QrCodeContent::new(text, true)); }); }); columns[1].vertical_centered_justified(|ui| { @@ -599,7 +562,7 @@ impl WalletTransactionModal { self.finalizing = true; modal.disable_closing(); thread::spawn(move || { - let res = wallet.finalize(&message, wallet.can_use_dandelion()); + let res = wallet.finalize(&message); let mut w_res = final_res.write(); *w_res = Some(res); }); diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 49885c0..067f1a6 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -158,14 +158,12 @@ pub struct WalletTransaction { pub amount: u64, /// Flag to check if transaction is cancelling. pub cancelling: bool, - /// Flag to check if transaction is posting after finalization. - pub posting: bool, /// Flag to check if transaction can be finalized based on Slatepack message state. pub can_finalize: bool, + /// Flag to check if transaction is finalizing. + pub finalizing: bool, /// Block height when tx was confirmed. pub conf_height: Option, - /// Block height when tx was reposted. - pub repost_height: Option, /// Flag to check if tx was received after sync from node. pub from_node: bool, } @@ -173,16 +171,8 @@ pub struct WalletTransaction { impl WalletTransaction { /// Check if transaction can be cancelled. pub fn can_cancel(&self) -> bool { - self.from_node && !self.cancelling && !self.posting && !self.data.confirmed && + self.from_node && !self.cancelling && !self.data.confirmed && self.data.tx_type != TxLogEntryType::TxReceivedCancelled && self.data.tx_type != TxLogEntryType::TxSentCancelled } - - /// Check if transaction can be reposted. - pub fn can_repost(&self, data: &WalletData) -> bool { - let last_height = data.info.last_confirmed_height; - let min_conf = data.info.minimum_confirmations; - self.from_node && self.posting && self.repost_height.is_some() && - last_height - self.repost_height.unwrap() > min_conf - } } \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 99d313e..771e013 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -437,8 +437,8 @@ impl Wallet { // Mark wallet as not opened. wallet_close.closing.store(false, Ordering::Relaxed); wallet_close.is_open.store(false, Ordering::Relaxed); - // Wake up thread to exit. - wallet_close.sync(true); + // Start sync to exit from thread. + wallet_close.sync(); }); } @@ -464,8 +464,8 @@ impl Wallet { }); } - // Sync wallet data. - self.sync(false); + // Refresh wallet info. + sync_wallet_data(&self, false); Ok(()) }) } @@ -498,7 +498,7 @@ impl Wallet { self.info_sync_progress.store(0, Ordering::Relaxed); // Sync wallet data. - self.sync(false); + self.sync(); Ok(()) } @@ -555,18 +555,11 @@ impl Wallet { r_data.clone() } - /// Sync wallet data from node or locally. - pub fn sync(&self, from_node: bool) { - if from_node { - let thread_r = self.sync_thread.read(); - if let Some(thread) = thread_r.as_ref() { - thread.unpark(); - } - } else { - let wallet = self.clone(); - thread::spawn(move || { - sync_wallet_data(&wallet, false); - }); + /// Sync wallet data from node at sync thread or locally synchronously. + pub fn sync(&self) { + let thread_r = self.sync_thread.read(); + if let Some(thread) = thread_r.as_ref() { + thread.unpark(); } } @@ -625,13 +618,7 @@ impl Wallet { let mut slate = None; if let Some(slate_id) = tx.data.tx_slate_id { // Get slate state based on tx state and status. - let state = if tx.posting { - if tx.data.tx_type == TxLogEntryType::TxSent { - Some(SlateState::Standard3) - } else { - Some(SlateState::Invoice3) - } - } else if !tx.data.confirmed && (tx.data.tx_type == TxLogEntryType::TxSent || + let state = if !tx.data.confirmed && (tx.data.tx_type == TxLogEntryType::TxSent || tx.data.tx_type == TxLogEntryType::TxReceived) { if tx.can_finalize { if tx.data.tx_type == TxLogEntryType::TxSent { @@ -681,7 +668,7 @@ impl Wallet { } /// Initialize a transaction to send amount, return request for funds receiver. - pub fn send(&self, amount: u64) -> Result<(Slate, String), Error> { + pub fn send(&self, amount: u64) -> Result { let config = self.get_config(); let args = InitTxArgs { src_acct_name: Some(config.account), @@ -698,51 +685,35 @@ impl Wallet { api.tx_lock_outputs(None, &slate)?; // Create Slatepack message response. - let message_resp = self.create_slatepack_message(&slate)?; + let _ = self.create_slatepack_message(&slate)?; - // Sync wallet info. - self.sync(false); + // Refresh wallet info. + sync_wallet_data(&self, false); - Ok((slate, message_resp)) + let tx = self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?; + Ok(tx) } /// Send amount to provided address with Tor transport. - pub async fn send_tor(&mut self, amount: u64, addr: &SlatepackAddress) -> Option { + pub async fn send_tor(&mut self, + amount: u64, + addr: &SlatepackAddress) -> Result { // Initialize transaction. - let send_res = self.send(amount); - - if send_res.is_err() { - return None; + let tx = self.send(amount)?; + let slate_res = self.read_slate_by_tx(&tx); + if slate_res.is_none() { + return Err(Error::GenericError("Slate not found".to_string())); } - let slate = send_res.unwrap().0; + let (slate, _) = slate_res.unwrap(); // Function to cancel initialized tx in case of error. let cancel_tx = || { let instance = self.instance.clone().unwrap(); let id = slate.clone().id; cancel_tx(instance, None, &None, None, Some(id.clone())).unwrap(); - // Setup posting flag, and ability to finalize. - { - let mut w_data = self.data.write(); - let mut data = w_data.clone().unwrap(); - let txs = data.txs.clone().unwrap().iter_mut().map(|tx| { - if tx.data.tx_slate_id == Some(id) { - tx.cancelling = false; - tx.posting = false; - tx.can_finalize = false; - tx.data.tx_type = if tx.data.tx_type == TxLogEntryType::TxReceived { - TxLogEntryType::TxReceivedCancelled - } else { - TxLogEntryType::TxSentCancelled - }; - } - tx.clone() - }).collect::>(); - data.txs = Some(txs); - *w_data = Some(data); - } + // Refresh wallet info to update statuses. - self.sync(false); + sync_wallet_data(&self, false); }; // Initialize parameters. @@ -764,17 +735,15 @@ impl Wallet { let req_res = Tor::post(body, url).await; if req_res.is_none() { cancel_tx(); - return None; + return Err(Error::GenericError("Tor post error".to_string())); } - // Parse response and finalize transaction. + // Parse response. let res: Value = serde_json::from_str(&req_res.unwrap()).unwrap(); if res["error"] != json!(null) { cancel_tx(); - return None; + return Err(Error::GenericError("Tx error".to_string())); } - - // Slatepack message json value. let slate_value = res["result"]["Ok"].clone(); let mut ret_slate = None; @@ -788,7 +757,7 @@ impl Wallet { // Save Slatepack message to file. let _ = self.create_slatepack_message(&slate).unwrap_or("".to_string()); // Post transaction to blockchain. - let result = self.post(&slate, self.can_use_dandelion()); + let result = self.post(&slate); match result { Ok(_) => { Ok(()) @@ -798,21 +767,25 @@ impl Wallet { } } } else { - Err(Error::GenericError("TX finalization error".to_string())) + Err(Error::GenericError("Tx finalization error".to_string())) }; - }).unwrap(); + })?; } Err(_) => {} }; + // Cancel transaction on error. if ret_slate.is_none() { cancel_tx(); + return Err(Error::GenericError("Tx error".to_string())); } - ret_slate + let tx = self.tx_by_slate(ret_slate.as_ref().unwrap()) + .ok_or(Error::GenericError("No tx found".to_string()))?; + Ok(tx) } /// Initialize an invoice transaction to receive amount, return request for funds sender. - pub fn issue_invoice(&self, amount: u64) -> Result<(Slate, String), Error> { + pub fn issue_invoice(&self, amount: u64) -> Result { let args = IssueInvoiceTxArgs { dest_acct_name: None, amount, @@ -822,16 +795,17 @@ impl Wallet { let slate = api.issue_invoice_tx(None, args)?; // Create Slatepack message response. - let response = self.create_slatepack_message(&slate.clone())?; + let _ = self.create_slatepack_message(&slate)?; - // Sync wallet info. - self.sync(false); + // Refresh wallet info. + sync_wallet_data(&self, false); - Ok((slate, response)) + let tx = self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?; + Ok(tx) } /// Handle message from the invoice issuer to send founds, return response for funds receiver. - pub fn pay(&self, message: &String) -> Result { + pub fn pay(&self, message: &String) -> Result { if let Ok(slate) = self.parse_slatepack(message) { let config = self.get_config(); let args = InitTxArgs { @@ -846,19 +820,19 @@ impl Wallet { api.tx_lock_outputs(None, &slate)?; // Create Slatepack message response. - let response = self.create_slatepack_message(&slate)?; + let _ = self.create_slatepack_message(&slate)?; - // Sync wallet info. - self.sync(false); + // Refresh wallet info. + sync_wallet_data(&self, false); - Ok(response) + Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?) } else { Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) } } /// Handle message to receive funds, return response to sender. - pub fn receive(&self, message: &String) -> Result { + pub fn receive(&self, message: &String) -> Result { if let Ok(mut slate) = self.parse_slatepack(message) { let api = Owner::new(self.instance.clone().unwrap(), None); controller::foreign_single_use(api.wallet_inst.clone(), None, |api| { @@ -866,61 +840,47 @@ impl Wallet { Ok(()) })?; // Create Slatepack message response. - let response = self.create_slatepack_message(&slate)?; + let _ = self.create_slatepack_message(&slate)?; - // Sync wallet info. - self.sync(false); + // Refresh wallet info. + sync_wallet_data(&self, false); - Ok(response) + Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?) } else { Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) } } /// Finalize transaction from provided message as sender or invoice issuer with Dandelion. - pub fn finalize(&self, message: &String, dandelion: bool) -> Result { + pub fn finalize(&self, message: &String) -> Result { if let Ok(mut slate) = self.parse_slatepack(message) { let api = Owner::new(self.instance.clone().unwrap(), None); slate = api.finalize_tx(None, &slate)?; // Save Slatepack message to file. let _ = self.create_slatepack_message(&slate)?; + // Post transaction to blockchain. - let _ = self.post(&slate, dandelion); - Ok(slate) + let tx = self.post(&slate)?; + + // Refresh wallet info. + sync_wallet_data(&self, false); + + Ok(tx) } else { Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) } } /// Post transaction to blockchain. - pub fn post(&self, slate: &Slate, dandelion: bool) -> Result<(), Error> { + fn post(&self, slate: &Slate) -> Result { // Post transaction to blockchain. let api = Owner::new(self.instance.clone().unwrap(), None); - api.post_tx(None, slate, dandelion)?; - // Setup transaction repost height, posting flag and ability to finalize. - let mut slate = slate.clone(); - if slate.state == SlateState::Invoice2 { - slate.state = SlateState::Invoice3 - } else if slate.state == SlateState::Standard2 { - slate.state = SlateState::Standard3 - }; - if let Some(tx) = self.tx_by_slate(&slate) { - let mut w_data = self.data.write(); - let mut data = w_data.clone().unwrap(); - let mut data_txs = data.txs.unwrap(); - for t in &mut data_txs { - if t.data.id == tx.data.id { - t.repost_height = Some(data.info.last_confirmed_height); - t.posting = true; - t.can_finalize = false; - } - } - data.txs = Some(data_txs); - *w_data = Some(data); - } - // Sync local wallet info. - self.sync(false); - Ok(()) + api.post_tx(None, slate, self.can_use_dandelion())?; + + // Refresh wallet info. + sync_wallet_data(&self, false); + + Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?) } /// Cancel transaction. @@ -948,27 +908,7 @@ impl Wallet { } let instance = wallet.instance.clone().unwrap(); let _ = cancel_tx(instance, None, &None, Some(id), None); - // Setup tx status, cancelling, posting flag, and ability to finalize. - { - let mut w_data = wallet.data.write(); - let mut data = w_data.clone().unwrap(); - let mut data_txs = data.txs.unwrap(); - let txs = data_txs.iter_mut().map(|tx| { - if tx.data.id == id { - tx.cancelling = false; - tx.posting = false; - tx.can_finalize = false; - tx.data.tx_type = if tx.data.tx_type == TxLogEntryType::TxReceived { - TxLogEntryType::TxReceivedCancelled - } else { - TxLogEntryType::TxSentCancelled - }; - } - tx.clone() - }).collect::>(); - data.txs = Some(txs); - *w_data = Some(data); - } + // Refresh wallet info to update statuses. sync_wallet_data(&wallet, false); }); @@ -985,7 +925,7 @@ impl Wallet { /// Initiate wallet repair by scanning its outputs. pub fn repair(&self) { self.repair_needed.store(true, Ordering::Relaxed); - self.sync(true); + self.sync(); } /// Check if wallet is repairing. @@ -1013,7 +953,7 @@ impl Wallet { // Remove wallet db files. let _ = fs::remove_dir_all(wallet_delete.get_config().get_db_path()); // Start sync to close thread. - wallet_delete.sync(true); + wallet_delete.sync(); // Mark wallet to reopen. wallet_delete.set_reopen(reopen); }); @@ -1046,7 +986,7 @@ impl Wallet { // Mark wallet as deleted. wallet_delete.deleted.store(true, Ordering::Relaxed); // Start sync to close thread. - wallet_delete.sync(true); + wallet_delete.sync(); }); } @@ -1268,7 +1208,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { wallet.reset_sync_attempts(); // Filter transactions for current account. - let filter_txs = txs.1.iter().map(|v| v.clone()).filter(|tx| { + let account_txs = txs.1.iter().map(|v| v.clone()).filter(|tx| { match wallet.get_parent_key_id() { Ok(key) => { tx.parent_key_id == key @@ -1286,7 +1226,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { // Create wallet txs. let mut new_txs: Vec = vec![]; - for tx in &filter_txs { + for tx in &account_txs { // Setup transaction amount. let amount = if tx.amount_debited > tx.amount_credited { tx.amount_debited - tx.amount_credited @@ -1294,54 +1234,36 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { tx.amount_credited - tx.amount_debited }; + // Setup flag for ability to finalize transaction. let unconfirmed_sent_or_received = tx.tx_slate_id.is_some() && !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent || tx.tx_type == TxLogEntryType::TxReceived); - - // Setup transaction posting status based on slate state. - let posting = if unconfirmed_sent_or_received { - // Create slate to check existing file. - let is_invoice = tx.tx_type == TxLogEntryType::TxReceived; - let mut slate = Slate::blank(0, is_invoice); - slate.id = tx.tx_slate_id.unwrap(); - slate.state = match is_invoice { - true => SlateState::Invoice3, - _ => SlateState::Standard3 + let mut finalizing = false; + let can_finalize = if unconfirmed_sent_or_received { + let initial_state = { + let mut slate = Slate::blank(1, false); + slate.id = tx.tx_slate_id.unwrap(); + slate.state = match tx.tx_type { + TxLogEntryType::TxReceived => SlateState::Invoice1, + _ => SlateState::Standard1 + }; + wallet.read_slatepack(&slate).is_some() }; - - // Setup posting status if we have other tx with same slate id. - let mut same_tx_posting = false; - for t in &mut new_txs { - if t.data.tx_slate_id == tx.tx_slate_id && - tx.tx_type != t.data.tx_type { - same_tx_posting = t.posting || - wallet.read_slatepack(&slate).is_some(); - if same_tx_posting && !t.posting { - t.posting = true; - } - break; - } - } - same_tx_posting || wallet.read_slatepack(&slate).is_some() + finalizing = { + let mut slate = Slate::blank(1, false); + slate.id = tx.tx_slate_id.unwrap(); + slate.state = match tx.tx_type { + TxLogEntryType::TxReceived => SlateState::Invoice3, + _ => SlateState::Standard3 + }; + wallet.read_slatepack(&slate).is_some() + }; + initial_state && !finalizing } else { false }; - // Setup flag for ability to finalize transaction. - let can_finalize = if !posting && unconfirmed_sent_or_received { - // Check existing file. - let mut slate = Slate::blank(1, false); - slate.id = tx.tx_slate_id.unwrap(); - slate.state = match tx.tx_type { - TxLogEntryType::TxReceived => SlateState::Invoice1, - _ => SlateState::Standard1 - }; - wallet.read_slatepack(&slate).is_some() - } else { - false - }; - - // Setup confirmation, reposting height and cancelling status. + // Setup confirmation and cancelling status. let mut conf_height = None; let mut setup_conf_height = |t: &TxLogEntry, current_empty: bool| -> bool { if current_empty && t.kernel_lookup_min_height.is_some() && @@ -1376,7 +1298,6 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { false }; - let mut repost_height = None; let mut cancelling = false; if data_txs.is_empty() { setup_conf_height(tx, true); @@ -1387,7 +1308,6 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { t.conf_height.unwrap() == 0) { conf_height = t.conf_height; } - repost_height = t.repost_height; if t.cancelling && tx.tx_type != TxLogEntryType::TxReceivedCancelled && tx.tx_type != TxLogEntryType::TxSentCancelled { @@ -1403,10 +1323,9 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { data: tx.clone(), amount, cancelling, - posting, can_finalize, + finalizing, conf_height, - repost_height, from_node: !fresh_sync || from_node }); }