From 21ecf200b8b3e0668509960779b2bd1ddf664c60 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Sat, 7 Sep 2024 00:11:17 +0300 Subject: [PATCH] wallet + ui: optimize sync after tx actions, remove tx repost, share message as file from tx modal, show tx info after tor sending and message creation or finalization, messages and transport modules refactoring, qr code text optimization, wallet dandelion setting, recovery phrase modal next step on enter --- src/gui/views/qr.rs | 62 +- src/gui/views/wallets/wallet/content.rs | 14 +- src/gui/views/wallets/wallet/messages.rs | 1166 ----------------- .../views/wallets/wallet/messages/content.rs | 541 ++++++++ src/gui/views/wallets/wallet/messages/mod.rs | 18 + .../views/wallets/wallet/messages/request.rs | 260 ++++ .../views/wallets/wallet/settings/common.rs | 25 +- .../views/wallets/wallet/settings/recovery.rs | 8 +- src/gui/views/wallets/wallet/transport.rs | 944 ------------- .../views/wallets/wallet/transport/content.rs | 397 ++++++ src/gui/views/wallets/wallet/transport/mod.rs | 19 + .../views/wallets/wallet/transport/send.rs | 357 +++++ .../wallets/wallet/transport/settings.rs | 258 ++++ src/gui/views/wallets/wallet/txs/content.rs | 22 +- src/gui/views/wallets/wallet/txs/tx.rs | 105 +- src/wallet/types.rs | 16 +- src/wallet/wallet.rs | 277 ++-- 17 files changed, 2051 insertions(+), 2438 deletions(-) delete mode 100644 src/gui/views/wallets/wallet/messages.rs create mode 100644 src/gui/views/wallets/wallet/messages/content.rs create mode 100644 src/gui/views/wallets/wallet/messages/mod.rs create mode 100644 src/gui/views/wallets/wallet/messages/request.rs delete mode 100644 src/gui/views/wallets/wallet/transport.rs create mode 100644 src/gui/views/wallets/wallet/transport/content.rs create mode 100644 src/gui/views/wallets/wallet/transport/mod.rs create mode 100644 src/gui/views/wallets/wallet/transport/send.rs create mode 100644 src/gui/views/wallets/wallet/transport/settings.rs 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 }); }