diff --git a/Cargo.lock b/Cargo.lock index 9e49985..afaf654 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -70,7 +70,7 @@ checksum = "301e55b39cfc15d9c48943ce5f572204a551646700d0e8efa424585f94fec528" dependencies = [ "accesskit", "accesskit_atspi_common", - "async-channel", + "async-channel 2.5.0", "async-executor", "async-task", "atspi", @@ -596,12 +596,23 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ - "event-listener", + "event-listener 5.4.1", "event-listener-strategy", "futures-core", "pin-project-lite 0.2.16", ] +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + [[package]] name = "async-channel" version = "2.5.0" @@ -652,6 +663,21 @@ dependencies = [ "futures-lite", ] +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.5.0", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + [[package]] name = "async-io" version = "2.6.0" @@ -676,7 +702,7 @@ version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ - "event-listener", + "event-listener 5.4.1", "event-listener-strategy", "pin-project-lite 0.2.16", ] @@ -710,14 +736,14 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ - "async-channel", + "async-channel 2.5.0", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if 1.0.4", - "event-listener", + "event-listener 5.4.1", "futures-lite", "rustix 1.1.2", ] @@ -761,6 +787,32 @@ dependencies = [ "tokio 1.48.0", ] +[[package]] +name = "async-std" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" +dependencies = [ + "async-channel 1.9.0", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite 0.2.16", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -1232,7 +1284,7 @@ version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel", + "async-channel 2.5.0", "async-task", "futures-io", "futures-lite", @@ -2974,6 +3026,12 @@ dependencies = [ "num-traits 0.2.19", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "event-listener" version = "5.4.1" @@ -2991,7 +3049,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "event-listener", + "event-listener 5.4.1", "pin-project-lite 0.2.16", ] @@ -3652,6 +3710,18 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "glow" version = "0.16.0" @@ -3790,6 +3860,7 @@ dependencies = [ "anyhow", "arboard", "arti-client", + "async-std", "backtrace", "bytes 1.10.1", "chrono", @@ -5413,6 +5484,15 @@ dependencies = [ "smallvec", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -5620,6 +5700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" dependencies = [ "serde", + "value-bag", ] [[package]] @@ -10291,7 +10372,7 @@ dependencies = [ "derive_more", "digest 0.10.7", "educe", - "event-listener", + "event-listener 5.4.1", "fs-mistrust", "fslock", "futures 0.3.31", @@ -11507,6 +11588,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + [[package]] name = "vcpkg" version = "0.2.15" @@ -12836,7 +12923,7 @@ dependencies = [ "async-trait", "blocking", "enumflags2", - "event-listener", + "event-listener 5.4.1", "futures-core", "futures-lite", "hex", diff --git a/Cargo.toml b/Cargo.toml index 21ef3d1..67c8b5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,6 +118,7 @@ bytes = "1.10.1" hyper-socks2 = "0.9.1" hyper-proxy2 = "0.1.0" hyper-tls = "0.6.0" +async-std = "1.13.2" ## tor arti-client = { version = "0.36.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] } diff --git a/locales/de.yml b/locales/de.yml index 82931d6..3328479 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -32,6 +32,7 @@ crash_report: Absturzbericht crash_report_warning: Anwendung wurde beim letzten Mal unerwartet geschlossen, Sie können den Absturzbericht mit Entwicklern teilen. confirmation: Bestätigung enter_url: URL eingeben +max_short: MAX wallets: await_conf_amount: Erwarte Bestätigung await_fin_amount: Warten auf die Fertigstellung @@ -130,6 +131,7 @@ wallets: tx_receive_cancel_conf: 'Sind Sie sicher, dass Sie das Empfangen von %{amount} ツ abbrechen wollen?' rec_phrase_not_found: Wiederhestellungsphrase nicht gefunden. restore_wallet_desc: Stellen Sie das Wallet wieder her, indem Sie alle Dateien löschen. Wenn die normale Reparatur nicht geholfen hat, müssen Sie Ihr Wallet erneut öffnen. + fee_base_desc: 'Gebühr (basiswert%{value}):' transport: desc: 'Transport verwenden, um Nachrichten synchron zu empfangen oder zu senden:' tor_network: Tor Netzwek diff --git a/locales/en.yml b/locales/en.yml index d7910d5..6aad6cb 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -32,6 +32,7 @@ crash_report: Crash report crash_report_warning: Application closed unexpectedly last time, you can share crash report with developers. confirmation: Confirmation enter_url: Enter URL +max_short: MAX wallets: await_conf_amount: Awaiting confirmation await_fin_amount: Awaiting finalization @@ -130,6 +131,7 @@ wallets: tx_receive_cancel_conf: 'Are you sure you want to cancel receiving of %{amount} ツ?' rec_phrase_not_found: Recovery phrase not found. restore_wallet_desc: Restore wallet by deleting all files if usual repair not helped, you will need to re-open your wallet. + fee_base_desc: 'Fee (base value%{value}):' transport: desc: 'Use transport to receive or send messages synchronously:' tor_network: Tor network diff --git a/locales/fr.yml b/locales/fr.yml index bcada95..b7e07e5 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -32,6 +32,7 @@ crash_report: Rapport d'échec crash_report_warning: L'application s'est fermée de manière inattendue la dernière fois, vous pouvez partager un rapport d'incident avec les développeurs. confirmation: Confirmation enter_url: Entrez l'URL +max_short: MAX wallets: await_conf_amount: En attente de confirmation await_fin_amount: En attente de finalisation @@ -130,6 +131,7 @@ wallets: tx_receive_cancel_conf: 'Êtes-vous sûr de vouloir annuler la réception de %{amount} ツ?' rec_phrase_not_found: Phrase de récupération non trouvée. restore_wallet_desc: "Restaurer le portefeuille en supprimant tous les fichiers si la réparation habituelle n'a pas aidé. Vous devrez rouvrir votre portefeuille." + fee_base_desc: 'Frais (valeur de base%{value}):' transport: desc: 'Utilisez le transport pour recevoir ou envoyer des messages de manière synchronisée:' tor_network: Réseau Tor diff --git a/locales/ru.yml b/locales/ru.yml index fc02bb0..71f14e2 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -32,6 +32,7 @@ crash_report: Отчёт о сбое crash_report_warning: В прошлый раз приложение неожиданно закрылось, вы можете поделиться отчетом о сбое с разработчиками. confirmation: Подтверждение enter_url: Введите URL-адрес +max_short: МАКС wallets: await_conf_amount: Ожидает подтверждения await_fin_amount: Ожидает завершения @@ -130,6 +131,7 @@ wallets: tx_receive_cancel_conf: 'Вы действительно хотите отменить получение %{amount} ツ?' rec_phrase_not_found: Фраза восстановления не найдена. restore_wallet_desc: Восстановить кошелёк, удалив все файлы, если обычное исправление не помогло. Необходимо переоткрыть кошелёк. + fee_base_desc: 'Комиссия (базовое значение%{value}):' transport: desc: 'Используйте транспорт для синхронных получения или отправки сообщений:' tor_network: Сеть Tor diff --git a/locales/tr.yml b/locales/tr.yml index 08a33b7..83c8cd1 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -32,6 +32,7 @@ crash_report: Ariza Raporu crash_report_warning: Uygulama beklenmedik bir sekilde kapandi son kez, kilitlenme raporunu gelistiricilerle paylasabilirsiniz. confirmation: Onay enter_url: URL'yi girin +max_short: MAKS wallets: await_conf_amount: Onay bekleniyor await_fin_amount: Tamamlanma bekleniyor @@ -130,6 +131,7 @@ wallets: tx_receive_cancel_conf: Gelen tx iptal rec_phrase_not_found: Sifre kelime bulunmuyor restore_wallet_desc: Cuzdani restore et + fee_base_desc: 'Ücret (taban değeri%{value}):' transport: desc: 'Adresten senkronize GONDER veya AL:' tor_network: Tor network diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 2cf13f5..ef4a50c 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -32,6 +32,7 @@ crash_report: 崩溃报告 crash_report_warning: 上次应用程序意外关闭,您可以报告开发人员崩溃事件. confirmation: 确认 enter_url: 输入 URL +max_short: 最大數量 wallets: await_conf_amount: 等待确认中 await_fin_amount: 等待确定中 @@ -131,6 +132,7 @@ wallets: tx_receive_cancel_conf: '您确定要取消 %{amount} ツ的接收吗?' rec_phrase_not_found: 找不到恢复助记词. restore_wallet_desc: 如果常规修复没有帮助,通过删除所有文件来恢复钱包.您将需要重新打开您的钱包. + fee_base_desc: '费用 (基值%{value}):' transport: desc: '使用传输同步接收或发送消息:' tor_network: Tor 网络 diff --git a/src/gui/views/input/edit.rs b/src/gui/views/input/edit.rs index 94639dd..3ffb1fa 100644 --- a/src/gui/views/input/edit.rs +++ b/src/gui/views/input/edit.rs @@ -12,22 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; -use egui::{Layout, TextBuffer, TextStyle, Widget, Align, ViewportCommand}; use egui::text_edit::TextEditState; +use egui::{Align, Layout, TextBuffer, TextStyle, ViewportCommand, Widget}; use lazy_static::lazy_static; use parking_lot::RwLock; +use std::sync::Arc; -use crate::gui::Colors; use crate::gui::icons::{CLIPBOARD_TEXT, COPY, EYE, EYE_SLASH, SCAN}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::input::keyboard::KeyboardContent; use crate::gui::views::{KeyboardEvent, View}; +use crate::gui::Colors; /// Text input content. pub struct TextEdit { /// View identifier. id: egui::Id, + /// Check if input is enabled or disabled. + enabled: bool, /// Horizontal text centering is needed. h_center: bool, /// Focus is needed. @@ -59,6 +61,7 @@ impl TextEdit { pub fn new(id: egui::Id) -> Self { Self { id, + enabled: true, h_center: false, focus: true, focus_request: false, @@ -73,8 +76,26 @@ impl TextEdit { } } - /// Draw text input content. + /// Draw text input. pub fn ui(&mut self, ui: &mut egui::Ui, input: &mut String, cb: &dyn PlatformCallbacks) { + self.input_ui(ui, input, |_| {}, cb); + } + + /// Draw text input with additional buttons (right to left order). + pub fn custom_buttons_ui(&mut self, + ui: &mut egui::Ui, + input: &mut String, + cb: &dyn PlatformCallbacks, + buttons_content: impl FnOnce(&mut egui::Ui)) { + self.input_ui(ui, input, buttons_content, cb); + } + + /// Draw text input content. + fn input_ui(&mut self, + ui: &mut egui::Ui, + input: &mut String, + buttons_content: impl FnOnce(&mut egui::Ui), + cb: &dyn PlatformCallbacks) { let mut layout_rect = ui.available_rect_before_wrap(); layout_rect.set_height(Self::TEXT_EDIT_HEIGHT); ui.allocate_ui_with_layout(layout_rect.size(), Layout::right_to_left(Align::Max), |ui| { @@ -95,6 +116,9 @@ impl TextEdit { ui.add_space(8.0); } + // Extra buttons content. + (buttons_content)(ui); + // Setup copy button. if self.copy { let copy_icon = COPY.to_string(); @@ -137,6 +161,12 @@ impl TextEdit { // Show text edit. let text_edit_resp = egui::TextEdit::singleline(input) + .text_color(if self.enabled { + Colors::text(false) + } else { + Colors::inactive_text() + }) + .interactive(self.enabled) .id(self.id) .font(TextStyle::Heading) .min_size(edit_rect.size()) @@ -181,10 +211,8 @@ impl TextEdit { } }); }); - // Repaint on Android to handle input from Java code without delays. - if is_android() { - ui.ctx().request_repaint(); - } + // Immediate repaint when input is open. + ui.ctx().request_repaint(); } /// Apply soft keyboard input data to provided String, returns `true` if Enter was pressed. @@ -283,6 +311,27 @@ impl TextEdit { false } + /// Set cursor to the end of text. + pub fn cursor_to_end(&self, text_len: usize, ui: &mut egui::Ui) { + let mut state = TextEditState::load(ui.ctx(), self.id).unwrap(); + match state.cursor.char_range() { + None => {} + Some(range) => { + let mut r = range.clone(); + r.primary.index = text_len; + r.secondary.index = text_len; + state.cursor.set_char_range(Some(r)); + TextEditState::store(state, ui.ctx(), self.id); + } + } + } + + /// Disable input. + pub fn disable(mut self) -> Self { + self.enabled = false; + self + } + /// Center text horizontally. pub fn h_center(mut self) -> Self { self.h_center = true; diff --git a/src/gui/views/views.rs b/src/gui/views/views.rs index b6f9156..5256066 100644 --- a/src/gui/views/views.rs +++ b/src/gui/views/views.rs @@ -460,12 +460,17 @@ impl View { }); } + /// Draw loading spinner. + pub fn loading_spinner(ui: &mut egui::Ui, size: f32) { + Spinner::new().size(size).color(Colors::gold()).ui(ui); + } + /// Size of big loading spinner. pub const BIG_SPINNER_SIZE: f32 = 104.0; /// Draw big gold loading spinner. pub fn big_loading_spinner(ui: &mut egui::Ui) { - Spinner::new().size(Self::BIG_SPINNER_SIZE).color(Colors::gold()).ui(ui); + View::loading_spinner(ui, View::BIG_SPINNER_SIZE); } /// Size of big loading spinner. @@ -473,7 +478,7 @@ impl View { /// Draw small gold loading spinner. pub fn small_loading_spinner(ui: &mut egui::Ui) { - Spinner::new().size(30.0).color(Colors::gold()).ui(ui); + View::loading_spinner(ui, View::SMALL_SPINNER_SIZE); } /// Draw the button that looks like checkbox with callback on check. diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index da6949e..fb3a9e2 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -130,11 +130,16 @@ impl ContentContainer for WalletsContent { } fn container_ui(&mut self, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { - ui.ctx().request_repaint_after(if OperatingSystem::from_target_os() == OperatingSystem::Android { - Duration::from_millis(100) + let is_android = OperatingSystem::from_target_os() == OperatingSystem::Android; + let account_list_showing = self.wallet_content.account_content.list_content.is_some(); + // Small repaint delay is needed for Android back navigation and account list opening. + ui.ctx().request_repaint_after(Duration::from_millis(if account_list_showing { + 10 + } else if is_android { + 100 } else { - Duration::from_millis(1000) - }); + 1000 + })); if let Some(data) = crate::consume_incoming_data() { if !data.is_empty() { diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 18e8037..252cb91 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -19,13 +19,11 @@ use grin_chain::SyncStatus; use crate::gui::icons::{ARROWS_CLOCKWISE, FILE_ARROW_DOWN, FILE_ARROW_UP, GEAR_FINE, POWER, STACK}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::{LinePosition, ModalPosition}; -use crate::gui::views::wallets::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::wallet::account::AccountContent; use crate::gui::views::wallets::wallet::request::{InvoiceRequestContent, SendRequestContent}; use crate::gui::views::wallets::wallet::transport::WalletTransportContent; use crate::gui::views::wallets::wallet::types::WalletContentContainer; -use crate::gui::views::wallets::wallet::WalletSettings; -use crate::gui::views::wallets::WalletTransactions; +use crate::gui::views::wallets::wallet::{WalletSettingsContent, WalletTransactionsContent}; use crate::gui::views::{Content, FilePickContent, FilePickContentType, Modal, View}; use crate::gui::Colors; use crate::node::Node; @@ -35,8 +33,11 @@ use crate::AppConfig; /// Wallet content. pub struct WalletContent { - /// Current tab content to show. - current_tab: Box, + /// Transactions content. + pub txs_content: Option, + + /// Settings content. + pub settings_content: Option, /// Account panel content. pub account_content: AccountContent, @@ -44,9 +45,9 @@ pub struct WalletContent { pub transport_content: WalletTransportContent, /// Invoice request creation [`Modal`] content. - invoice_request_content: Option, + invoice_content: Option, /// Send request creation [`Modal`] content. - send_request_content: Option, + send_content: Option, /// Tab button to pick file for parsing. file_pick_tab_button: FilePickContent, @@ -68,12 +69,12 @@ impl WalletContentContainer for WalletContent { fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) { match m.id { INVOICE_MODAL_ID => { - if let Some(c) = self.invoice_request_content.as_mut() { + if let Some(c) = self.invoice_content.as_mut() { c.modal_ui(ui, w, m, cb); } } SEND_MODAL_ID => { - if let Some(c) = self.send_request_content.as_mut() { + if let Some(c) = self.send_content.as_mut() { c.modal_ui(ui, w, m, cb); } } @@ -91,7 +92,7 @@ impl WalletContentContainer for WalletContent { // Show wallet account panel not on settings tab when navigation is not blocked and QR code // scanner is not showing and wallet data is not empty. - let mut show_account = self.current_tab.get_type() != WalletTabType::Settings && !block_nav + let mut show_account = self.settings_content.is_none() && !block_nav && !wallet.sync_error() && data.is_some(); if wallet.get_current_connection() == ConnectionMethod::Integrated && !Node::is_running() { @@ -222,7 +223,7 @@ impl WalletContentContainer for WalletContent { top: 0.0 as i8, bottom: 4.0 as i8, }, - fill: if self.current_tab.get_type() == WalletTabType::Settings { + fill: if self.settings_content.is_some() { Colors::fill_lite() } else { Colors::TRANSPARENT @@ -231,25 +232,69 @@ impl WalletContentContainer for WalletContent { }) .show_inside(ui, |ui| { let rect = ui.available_rect_before_wrap(); - let tab_type = self.current_tab.get_type(); - let show_sync = (tab_type != WalletTabType::Settings || block_nav) && + let show_settings = self.settings_content.is_some(); + let show_txs = self.txs_content.is_some(); + let show_sync = (!show_settings || block_nav) && sync_ui(ui, &wallet); if !show_sync { - if tab_type != WalletTabType::Txs { + if show_settings { ui.add_space(3.0); ScrollArea::vertical() - .id_salt(Id::from("wallet_tab_content_scroll") - .with(tab_type.name()) - .with(wallet_id)) + .id_salt(Id::from("wallet_tab_content_scroll").with(wallet_id)) .auto_shrink([false; 2]) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .show(ui, |ui| { View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.current_tab.ui(ui, &wallet, cb); + self.settings_content + .as_mut() + .unwrap() + .ui(ui, &wallet, cb); }); }); - } else { - self.current_tab.ui(ui, &wallet, cb); + } else if show_txs { + self.txs_content + .as_mut() + .unwrap() + .ui(ui, &wallet, cb); + } + + // Handle wallet task result. + if let Some((id, t)) = wallet.consume_task_result() { + match Modal::opened() { + None => { + // Show transaction modal on wallet task result. + if let Some(id) = id { + let tx = wallet.get_data().unwrap().tx_by_slate_id(id); + if tx.is_some() { + self.txs_content = Some(WalletTransactionsContent::new(tx)); + self.settings_content = None; + } + } + } + Some(modal_id) => { + match modal_id { + SEND_MODAL_ID => { + match t { + WalletTask::CalculateFee(_, f) => { + // Setup calculated tx fee at modal. + if let Some(m) = self.send_content.as_mut() { + if m.max_calculating { + let data = wallet.get_data().unwrap(); + let a = data.info.amount_currently_spendable; + let max = a - f; + m.on_max_amount_calculated(max, f); + } else { + m.on_fee_calculated(f); + } + } + } + _ => {} + } + } + _ => {} + } + } + } } } let rect = { @@ -276,11 +321,12 @@ impl WalletContentContainer for WalletContent { impl Default for WalletContent { fn default() -> Self { Self { - current_tab: Box::new(WalletTransactions::new(None)), + txs_content: Some(WalletTransactionsContent::new(None)), + settings_content: None, account_content: AccountContent::default(), transport_content: WalletTransportContent::default(), - invoice_request_content: None, - send_request_content: None, + invoice_content: None, + send_content: None, file_pick_tab_button: FilePickContent::new(FilePickContentType::Tab), } } @@ -297,8 +343,10 @@ impl WalletContent { t!("wallets.transport") } else if self.transport_content.qr_address_content.is_some() { t!("network_mining.address") + } else if self.settings_content.is_some() { + t!("wallets.settings") } else { - self.current_tab.get_type().name() + t!("wallets.txs") } } @@ -342,12 +390,13 @@ impl WalletContent { let can_send = has_wallet_data && wallet.get_data().unwrap().info.amount_currently_spendable > 0; - let current_type = self.current_tab.get_type(); let tabs_amount = if can_send { 5 } else { 4 }; ui.columns(tabs_amount, |columns| { columns[0].vertical_centered_justified(|ui| { - View::tab_button(ui, STACK, None, Some(current_type == WalletTabType::Txs), |_| { - self.current_tab = Box::new(WalletTransactions::new(None)); + let active = self.settings_content.is_none() && self.txs_content.is_some(); + View::tab_button(ui, STACK, None, Some(active), |_| { + self.txs_content = Some(WalletTransactionsContent::new(None)); + self.settings_content = None; }); }); let active = if has_wallet_data { Some(false) } else { None }; @@ -358,7 +407,7 @@ impl WalletContent { } else { let (icon, color) = (FILE_ARROW_DOWN, Some(Colors::green())); View::tab_button(ui, icon, color, active, |_| { - self.invoice_request_content = Some(InvoiceRequestContent::default()); + self.invoice_content = Some(InvoiceRequestContent::default()); Modal::new(INVOICE_MODAL_ID) .position(ModalPosition::CenterTop) .title(t!("wallets.receive")) @@ -385,7 +434,7 @@ impl WalletContent { } else { let (icon, color) = (FILE_ARROW_UP, Some(Colors::red())); View::tab_button(ui, icon, color, Some(false), |_| { - self.send_request_content = Some(SendRequestContent::new(None)); + self.send_content = Some(SendRequestContent::new(None)); Modal::new(SEND_MODAL_ID) .position(ModalPosition::CenterTop) .title(t!("wallets.send")) @@ -395,10 +444,11 @@ impl WalletContent { }); } columns[tabs_amount - 1].vertical_centered_justified(|ui| { - let active = Some(current_type == WalletTabType::Settings); - View::tab_button(ui, GEAR_FINE, None, active, |ui| { + let active = self.settings_content.is_some(); + View::tab_button(ui, GEAR_FINE, None, Some(active), |ui| { ExternalConnection::check(None, ui.ctx()); - self.current_tab = Box::new(WalletSettings::default()); + self.txs_content = None; + self.settings_content = Some(WalletSettingsContent::default()); }); }); }); diff --git a/src/gui/views/wallets/wallet/request/invoice.rs b/src/gui/views/wallets/wallet/request/invoice.rs index 78b3352..4eddc62 100644 --- a/src/gui/views/wallets/wallet/request/invoice.rs +++ b/src/gui/views/wallets/wallet/request/invoice.rs @@ -13,7 +13,7 @@ // limitations under the License. use egui::{Id, RichText}; -use grin_core::core::amount_from_hr_string; +use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, TextEdit, View}; @@ -79,21 +79,21 @@ impl InvoiceRequestContent { 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) => { + Ok(amount) => { if !self.amount_edit.contains(".") { - // To avoid input of several "0". - if a == 0 { - self.amount_edit = "0".to_string(); - return; + // To avoid input of several `0` before `.` and put `.` after first `0`. + if self.amount_edit.len() != 1 && self.amount_edit.starts_with("0") { + let amount_text = amount_to_hr_string(amount, true); + let amount_parts = amount_text.split(".").collect::>(); + self.amount_edit = format!("0.{}", amount_parts[0]); + amount_edit.cursor_to_end(self.amount_edit.len(), ui); } } else { - // Check input after ".". - let parts = self.amount_edit - .split(".") - .collect::>(); - if parts.len() == 2 && parts[1].len() > 9 { + // Check input after `.`. + let parts = self.amount_edit.split(".").collect::>(); + if parts.len() == 2 && (parts[1].len() > 9 || + (amount == 0 && parts[1].len() > 8)) { self.amount_edit = amount_edit_before; - return; } } } diff --git a/src/gui/views/wallets/wallet/request/send.rs b/src/gui/views/wallets/wallet/request/send.rs index 5b90bfc..9900cd1 100644 --- a/src/gui/views/wallets/wallet/request/send.rs +++ b/src/gui/views/wallets/wallet/request/send.rs @@ -14,8 +14,8 @@ use egui::{Id, RichText}; use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; +use grin_core::global::get_accept_fee_base; use grin_wallet_libwallet::SlatepackAddress; - use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{CameraContent, Modal, TextEdit, View}; use crate::gui::Colors; @@ -24,8 +24,14 @@ use crate::wallet::Wallet; /// Content to create a request to send funds. pub struct SendRequestContent { - /// Amount to send or receive. + /// Amount to send. amount_edit: String, + /// Flag to check if maximum amount is calculating. + pub max_calculating: bool, + + /// Fee amount. + fee_edit: String, + /// Receiver address. address_edit: String, /// Flag to check if entered address is incorrect. @@ -40,12 +46,26 @@ impl SendRequestContent { pub fn new(addr: Option) -> Self { Self { amount_edit: "".to_string(), + max_calculating: false, + fee_edit: "".to_string(), address_edit: addr.unwrap_or("".to_string()), address_error: false, address_scan_content: None, } } + /// Setup fee amount. + pub fn on_fee_calculated(&mut self, fee: u64) { + self.fee_edit = amount_to_hr_string(fee, true); + } + + /// Setup maximum amount to send and fee. + pub fn on_max_amount_calculated(&mut self, amount: u64, fee: u64) { + self.max_calculating = false; + self.amount_edit = amount_to_hr_string(amount, true); + self.fee_edit = amount_to_hr_string(fee, true); + } + /// Draw [`Modal`] content. pub fn modal_ui(&mut self, ui: &mut egui::Ui, @@ -109,45 +129,107 @@ impl SendRequestContent { .h_center() .numeric() .focus(Modal::first_draw()); + if self.max_calculating { + amount_edit = amount_edit.disable(); + } let amount_edit_before = self.amount_edit.clone(); - amount_edit.ui(ui, &mut self.amount_edit, cb); + + // Draw button to calculate maximum amount to send. + let mut calculate_max = false; + amount_edit.custom_buttons_ui(ui, &mut self.amount_edit, cb, |ui| { + if self.max_calculating { + ui.add_space(12.0); + View::loading_spinner(ui, 40.0); + ui.add_space(12.0); + } else { + View::button(ui, t!("max_short"), Colors::white_or_black(false), || { + calculate_max = true; + }); + ui.add_space(8.0); + } + }); + if calculate_max { + self.max_calculating = true; + let max = wallet.get_data().unwrap().info.amount_currently_spendable; + self.amount_edit = amount_to_hr_string(max, true); + } 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. + // 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) => { + Ok(mut amount) => { if !self.amount_edit.contains(".") { - // To avoid input of several "0". - if a == 0 { - self.amount_edit = "0".to_string(); - return; + // To avoid input of several `0` before `.` and put `.` after first `0`. + if self.amount_edit.len() != 1 && self.amount_edit.starts_with("0") { + let amount_text = amount_to_hr_string(amount, true); + let amount_parts = amount_text.split(".").collect::>(); + self.amount_edit = format!("0.{}", amount_parts[0]); + amount = amount_from_hr_string(self.amount_edit.as_str()) + .unwrap_or_else(|_| amount); + amount_edit.cursor_to_end(self.amount_edit.len(), ui); + } + // Reset fee amount on `0`. + if amount == 0 { + self.fee_edit = "".to_string(); } } 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; + if parts.len() == 2 && (parts[1].len() > 9 || + (amount == 0 && parts[1].len() > 8)) { + self.amount_edit = amount_edit_before.clone(); } } - - // 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; + // Do not input amount more than balance. + if amount != 0 && self.amount_edit != amount_edit_before { + let fee = amount_from_hr_string(self.fee_edit.as_str()).unwrap_or(0); + let max = wallet.get_data().unwrap().info.amount_currently_spendable; + if amount > max || amount + fee > max { + self.max_calculating = true; + wallet.task(WalletTask::CalculateFee(max, 0)); + } else { + wallet.task(WalletTask::CalculateFee(amount, 0)); + } } } Err(_) => { self.amount_edit = amount_edit_before; } } + } else { + self.fee_edit = "".to_string(); } } + // Show fee value. + ui.vertical_centered(|ui| { + let fee_label = t!( + "wallets.fee_base_desc", + "value" => format!(": {}", get_accept_fee_base()) + ); + ui.label(RichText::new(fee_label) + .size(17.0) + .color(Colors::gray())); + }); + ui.add_space(6.0); + let fee_edit_id = Id::from(modal.id).with("_fee").with(wallet.get_config().id); + let mut fee_edit = TextEdit::new(fee_edit_id) + .focus(false) + .h_center() + .disable(); + let mut loading_label = format!("{}...", t!("wallets.loading")); + fee_edit.ui(ui, if wallet.fee_calculating() { + &mut loading_label + } else { + &mut self.fee_edit + }, cb); + + ui.add_space(8.0); + // Show address error or input description. ui.vertical_centered(|ui| { if self.address_error { @@ -179,12 +261,10 @@ impl SendRequestContent { 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; } - // Continue on Enter press. if address_edit.enter_pressed { self.on_continue(wallet); @@ -211,7 +291,7 @@ impl SendRequestContent { /// Callback when Continue button was pressed. fn on_continue(&mut self, wallet: &Wallet) { - if self.amount_edit.is_empty() { + if self.amount_edit.is_empty() || self.max_calculating || wallet.fee_calculating() { return; } // Check address to send over Tor if enabled. diff --git a/src/gui/views/wallets/wallet/settings/content.rs b/src/gui/views/wallets/wallet/settings/content.rs index 69bc31d..113aa59 100644 --- a/src/gui/views/wallets/wallet/settings/content.rs +++ b/src/gui/views/wallets/wallet/settings/content.rs @@ -15,12 +15,11 @@ use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::ContentContainer; use crate::gui::views::wallets::{CommonSettings, ConnectionSettings, RecoverySettings}; -use crate::gui::views::wallets::types::{WalletTab, WalletTabType}; use crate::gui::views::wallets::wallet::types::WalletContentContainer; use crate::wallet::Wallet; /// Wallet settings tab content. -pub struct WalletSettings { +pub struct WalletSettingsContent { /// Common setup content. common_setup: CommonSettings, /// Connection setup content. @@ -29,7 +28,7 @@ pub struct WalletSettings { recovery_setup: RecoverySettings } -impl Default for WalletSettings { +impl Default for WalletSettingsContent { fn default() -> Self { Self { common_setup: CommonSettings::default(), @@ -39,12 +38,8 @@ impl Default for WalletSettings { } } -impl WalletTab for WalletSettings { - fn get_type(&self) -> WalletTabType { - WalletTabType::Settings - } - - fn ui(&mut self, +impl WalletSettingsContent { + pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { diff --git a/src/gui/views/wallets/wallet/txs/content.rs b/src/gui/views/wallets/wallet/txs/content.rs index 11cedd7..0b8c744 100644 --- a/src/gui/views/wallets/wallet/txs/content.rs +++ b/src/gui/views/wallets/wallet/txs/content.rs @@ -24,8 +24,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use crate::gui::icons::{ARCHIVE_BOX, ARROWS_CLOCKWISE, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, GEAR_FINE, PROHIBIT, WARNING, X_CIRCLE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::{LinePosition, ModalPosition}; -use crate::gui::views::wallets::types::WalletTab; -use crate::gui::views::wallets::wallet::types::{WalletTabType, GRIN}; +use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN}; use crate::gui::views::wallets::wallet::WalletTransactionContent; use crate::gui::views::{Content, Modal, PullToRefresh, View}; use crate::gui::Colors; @@ -33,7 +32,7 @@ use crate::wallet::types::{WalletData, WalletTask, WalletTransaction, WalletTran use crate::wallet::Wallet; /// Wallet transactions tab content. -pub struct WalletTransactions { +pub struct WalletTransactionsContent { /// Transaction information [`Modal`] content. tx_info_content: Option, @@ -44,22 +43,26 @@ pub struct WalletTransactions { manual_sync: Option } -impl WalletTab for WalletTransactions { - fn get_type(&self) -> WalletTabType { - WalletTabType::Txs +impl WalletContentContainer for WalletTransactionsContent { + fn modal_ids(&self) -> Vec<&'static str> { + vec![TX_INFO_MODAL, CANCEL_TX_CONFIRMATION_MODAL] } - fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { - if Modal::opened().is_none() { - // Show transaction modal on task result. - if let Some(id) = wallet.consume_tx_task_result() { - let tx = wallet.get_data().unwrap().tx_by_slate_id(id); - if let Some(tx) = tx { - self.show_tx_info_modal(tx.data.id); + fn modal_ui(&mut self, ui: &mut egui::Ui, w: &Wallet, m: &Modal, cb: &dyn PlatformCallbacks) { + match m.id { + TX_INFO_MODAL => { + if let Some(content) = self.tx_info_content.as_mut() { + content.ui(ui, w, m, cb); } } + CANCEL_TX_CONFIRMATION_MODAL => { + self.cancel_confirmation_modal(ui, w); + } + _ => {} } - self.modal_content_ui(ui, wallet, cb); + } + + fn container_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, _: &dyn PlatformCallbacks) { self.txs_ui(ui, wallet); } } @@ -69,7 +72,7 @@ const TX_INFO_MODAL: &'static str = "tx_info_modal"; /// Identifier for transaction cancellation confirmation [`Modal`]. const CANCEL_TX_CONFIRMATION_MODAL: &'static str = "cancel_tx_conf_modal"; -impl WalletTransactions { +impl WalletTransactionsContent { /// Height of transaction list item. pub const TX_ITEM_HEIGHT: f32 = 75.0; @@ -122,6 +125,7 @@ impl WalletTransactions { let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis(); let refresh = self.manual_sync.unwrap_or(0) + 1600 > now; let refresh_resp = PullToRefresh::new(refresh) + .id(Id::from("refresh_tx_list").with(config.id)) .can_refresh(!refresh && !wallet.syncing()) .min_refresh_distance(70.0) .scroll_area_ui(ui, |ui| { @@ -241,33 +245,6 @@ impl WalletTransactions { ui.painter().set(bg_idx, bg); } - /// Draw [`Modal`] content for this ui container. - fn modal_content_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - cb: &dyn PlatformCallbacks) { - match Modal::opened() { - None => {} - Some(id) => { - match id { - TX_INFO_MODAL => { - Modal::ui(ui.ctx(), cb, |ui, modal, cb| { - if let Some(content) = self.tx_info_content.as_mut() { - content.ui(ui, wallet, modal, cb); - } - }); - } - CANCEL_TX_CONFIRMATION_MODAL => { - Modal::ui(ui.ctx(), cb, |ui, _, _| { - self.cancel_confirmation_modal(ui, wallet); - }); - } - _ => {} - } - } - } - } - /// Draw transaction item. pub fn tx_item_ui(ui: &mut egui::Ui, tx: &WalletTransaction, @@ -417,7 +394,7 @@ impl WalletTransactions { TxLogEntryType::TxSent | TxLogEntryType::TxReceived => { let min_conf = data.info.minimum_confirmations; if tx.height.is_none() || (tx.height.unwrap() != 0 && - height - tx.height.unwrap() > min_conf - 1) { + height - tx.height.unwrap() >= min_conf - 1) { let (i, t) = if tx.data.tx_type == TxLogEntryType::TxSent { (ARROW_CIRCLE_UP, t!("wallets.tx_sent")) } else { diff --git a/src/gui/views/wallets/wallet/txs/tx.rs b/src/gui/views/wallets/wallet/txs/tx.rs index 1a82c71..ef436e0 100644 --- a/src/gui/views/wallets/wallet/txs/tx.rs +++ b/src/gui/views/wallets/wallet/txs/tx.rs @@ -19,7 +19,7 @@ use grin_wallet_libwallet::TxLogEntryType; use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::wallets::wallet::txs::WalletTransactions; +use crate::gui::views::wallets::wallet::txs::WalletTransactionsContent; use crate::gui::views::{CameraContent, FilePickContent, FilePickContentType, Modal, QrCodeContent, View}; use crate::gui::Colors; use crate::wallet::types::{WalletTask, WalletTransaction}; @@ -220,7 +220,7 @@ impl WalletTransactionContent { ui.add_space(6.0); let mut rect = ui.available_rect_before_wrap(); - rect.set_height(WalletTransactions::TX_ITEM_HEIGHT); + rect.set_height(WalletTransactionsContent::TX_ITEM_HEIGHT); // Draw tx item background. let p = ui.painter(); @@ -229,7 +229,7 @@ impl WalletTransactionContent { // Show transaction amount status and time. let data = wallet.get_data().unwrap(); - WalletTransactions::tx_item_ui(ui, tx, rect, &data, |ui| { + WalletTransactionsContent::tx_item_ui(ui, tx, rect, &data, |ui| { // Show block height or buttons. if let Some(h) = tx.height { if h != 0 { @@ -281,7 +281,7 @@ impl WalletTransactionContent { } else { View::item_rounding(0, 2, true) }; - WalletTransactions::tx_repeat_button_ui(ui, r, tx, wallet, rebroadcast); + WalletTransactionsContent::tx_repeat_button_ui(ui, r, tx, wallet, rebroadcast); } } }); diff --git a/src/gui/views/wallets/wallet/types.rs b/src/gui/views/wallets/wallet/types.rs index 3872d66..d7c50ec 100644 --- a/src/gui/views/wallets/wallet/types.rs +++ b/src/gui/views/wallets/wallet/types.rs @@ -42,32 +42,6 @@ pub trait WalletContentContainer { } } -/// Wallet tab content interface. -pub trait WalletTab { - fn get_type(&self) -> WalletTabType; - fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - cb: &dyn PlatformCallbacks); -} - -/// Type of [`WalletTab`] content. -#[derive(PartialEq)] -pub enum WalletTabType { - Txs, - Settings -} - -impl WalletTabType { - /// Name of wallet tab to show at ui. - pub fn name(&self) -> String { - match *self { - WalletTabType::Txs => t!("wallets.txs"), - WalletTabType::Settings => t!("wallets.settings") - } - } -} - /// Get wallet status text. pub fn wallet_status_text(wallet: &Wallet) -> String { if wallet.is_open() { diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 5cdfbc2..80cd9d8 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -394,6 +394,10 @@ impl WalletTransaction { pub enum WalletTask { /// Open Slatepack message parsing result and making an action. OpenMessage(String), + /// Calculate fee to send amount. + /// * amount + /// * fee (to read at result) + CalculateFee(u64, u64), /// Create request to send. /// * amount /// * receiver diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index ded6749..6f7ce80 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -32,7 +32,7 @@ use grin_wallet_controller::command::parse_slatepack; use grin_wallet_controller::controller; use grin_wallet_controller::controller::ForeignAPIHandlerV2; use grin_wallet_impls::{DefaultLCProvider, DefaultWalletImpl, HTTPNodeClient, LMDBBackend}; -use grin_wallet_libwallet::api_impl::owner::{cancel_tx, retrieve_summary_info, retrieve_txs}; +use grin_wallet_libwallet::api_impl::owner::{cancel_tx, init_send_tx, retrieve_summary_info, retrieve_txs}; use grin_wallet_libwallet::{address, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, RetrieveTxQueryArgs, RetrieveTxQuerySortField, RetrieveTxQuerySortOrder, Slate, SlateState, SlateVersion, SlatepackAddress, StatusMessage, TxLogEntry, TxLogEntryType, VersionedSlate, WalletBackend, WalletInfo, WalletInitStatus, WalletInst, WalletLCProvider}; use grin_wallet_util::OnionV3Address; use parking_lot::RwLock; @@ -100,6 +100,8 @@ pub struct Wallet { /// Flag to check if Slatepack message file is opening. message_opening: Arc, + /// Amount requests to calculate fee. + fee_calculating: Arc, /// Flag to check if sending request is creating. send_creating: Arc, /// Flag to check if invoice is creating. @@ -107,9 +109,8 @@ pub struct Wallet { /// Tasks sender. tasks_sender: Arc>>>, - /// Transaction identifier after successful task completion. - /// To be replaced with https://github.com/lucasmerlin/hello_egui/tree/main/crates/egui_inbox. - task_result_slate_id: Arc>>, + /// Task result with optional transaction identifier. + task_result: Arc, WalletTask)>>>, } impl Wallet { @@ -137,9 +138,10 @@ impl Wallet { repair_progress: Arc::new(AtomicU8::new(0)), message_opening: Arc::new(AtomicBool::from(false)), send_creating: Arc::new(AtomicBool::new(false)), + fee_calculating: Arc::new(AtomicU8::new(0)), invoice_creating: Arc::new(AtomicBool::new(false)), tasks_sender: Arc::new(RwLock::new(None)), - task_result_slate_id: Arc::new(RwLock::new(None)), + task_result: Arc::new(RwLock::new(None)), } } @@ -586,6 +588,13 @@ impl Wallet { pub fn task(&self, task: WalletTask) { let r_tasks = self.tasks_sender.read(); if r_tasks.is_some() { + match task { + WalletTask::CalculateFee(_, _) => { + let calculating = self.fee_calculating.load(Ordering::Relaxed); + self.fee_calculating.store(calculating + 1, Ordering::Relaxed); + } + _ => {} + } let _ = r_tasks.as_ref().unwrap().send(task); } } @@ -706,76 +715,6 @@ impl Wallet { None } - /// Open Slatepack message with the wallet. - fn open_message(&self, message: &String) { - if !self.is_open() || message.is_empty() { - return; - } - let w = self.clone(); - let load = self.message_opening.clone(); - let msg = message.clone(); - thread::spawn(move || { - load.store(true, Ordering::Relaxed); - if let Ok(s) = w.parse_slatepack(&msg) { - // Check if message with same id already exists. - let exists = { - let mut exists = w.slatepack_exists(&s); - if !exists && (s.state == SlateState::Invoice2 || - s.state == SlateState::Standard2) { - let mut slate = s.clone(); - slate.state = if s.state == SlateState::Standard2 { - SlateState::Standard3 - } else { - SlateState::Invoice3 - }; - exists = w.slatepack_exists(&slate); - } - exists - }; - if exists { - w.on_tx_result(&s); - load.store(false, Ordering::Relaxed); - return; - } - // Create response or finalize. - match s.state { - SlateState::Standard1 | SlateState::Invoice1 => { - if s.state != SlateState::Standard1 { - if let Ok(s) = w.pay(&s) { - sync_wallet_data(&w, false); - w.on_tx_result(&s); - } - } else { - if let Ok(s) = w.receive(&s) { - sync_wallet_data(&w, false); - w.on_tx_result(&s); - } - } - } - SlateState::Standard2 | SlateState::Invoice2 => { - match w.finalize(&s) { - Ok(s) => { - match w.post(&s) { - Ok(_) => { - sync_wallet_data(&w, false); - } - Err(e) => { - w.on_tx_error(s.id.to_string(), Some(e)); - } - } - } - Err(e) => { - w.on_tx_error(s.id.to_string(), Some(e)); - } - } - } - _ => {} - }; - } - load.store(false, Ordering::Relaxed); - }); - } - /// Check if Slatepack message is opening. pub fn message_opening(&self) -> bool { self.message_opening.load(Ordering::Relaxed) @@ -826,7 +765,46 @@ impl Wallet { None } - /// Initialize a transaction to send amount, return request for funds receiver. + /// Calculate transaction fee for provided amount. + fn calculate_fee(&self, a: u64) -> Result { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut w_lock = instance.lock(); + let w = w_lock.lc_provider()?.wallet_inst()?; + let config = self.get_config(); + let args = InitTxArgs { + src_acct_name: Some(config.account.clone()), + amount: a, + minimum_confirmations: config.min_confirmations, + num_change_outputs: 1, + selection_strategy_is_use_all: false, + estimate_only: Some(true), + ..Default::default() + }; + let res = init_send_tx(&mut **w, None, args, false); + match res { + Ok(slate) => { + Ok(slate.fee_fields.fee()) + } + Err(e) => { + match e { + Error::NotEnoughFunds { available, needed, .. } => { + Ok(needed - available) + }, + e => { + Err(e) + } + } + } + } + } + + /// Check if transaction fee is calculating. + pub fn fee_calculating(&self) -> bool { + self.fee_calculating.load(Ordering::Relaxed) > 0 + } + + /// Initialize a transaction to send amount. fn send(&self, a: u64, r: Option) -> Result { let config = self.get_config(); let args = InitTxArgs { @@ -1017,27 +995,25 @@ impl Wallet { w_data.as_mut().unwrap().on_tx_error(id, err); } - /// Save identifier of successful transaction after tasks completion to consume later. - fn on_tx_result(&self, slate: &Slate) { - let mut w_res = self.task_result_slate_id.write(); - *w_res = Some(slate.id.to_string()); + /// Save task result to consume later. + fn on_task_result(&self, id: Option, task: &WalletTask) { + let mut w_res = self.task_result.write(); + *w_res = Some((id, task.clone())); } - /// Consume identifier of successful transaction task. - pub fn consume_tx_task_result(&self) -> Option { + /// Consume result of successful task. + pub fn consume_task_result(&self) -> Option<(Option, WalletTask)> { let res = { - let r_res = self.task_result_slate_id.read(); + let r_res = self.task_result.read(); r_res.clone() }; - // Clear result. - if res.is_some() { - let mut w_res = self.task_result_slate_id.write(); - *w_res = None; - } + // Clear result for transaction task. + let mut w_res = self.task_result.write(); + *w_res = None; res } - /// Get possible transaction confirmation height, . + /// Get possible transaction confirmation height. fn tx_height(&self, tx: &WalletTransaction) -> Result, Error> { let mut tx_height = None; if tx.data.confirmed && tx.data.kernel_excess.is_some() { @@ -1176,7 +1152,7 @@ fn start_sync(wallet: Wallet) -> Thread { *w_tasks = Some(tx); } let wallet_thread = wallet.clone(); - thread::spawn(move || loop { + thread::spawn(move || loop { let wallet_task = wallet_thread.clone(); if let Ok(task) = rx.recv() { thread::spawn(move || { @@ -1186,7 +1162,8 @@ fn start_sync(wallet: Wallet) -> Thread { .unwrap() .block_on(async { handle_task(&wallet_task, task).await; - })}); + }); + }); } if wallet_thread.is_closing() || !wallet_thread.is_open() { break; @@ -1233,7 +1210,7 @@ fn start_sync(wallet: Wallet) -> Thread { // Reset loading progress. wallet.info_sync_progress.store(0, Ordering::Relaxed); } - // Set an error when required integrated node is not enabled. + // Set an error when integrated node is not enabled. wallet.set_sync_error(not_enabled); // Skip cycle when node sync is not finished. if !Node::is_running() || Node::get_sync_status() != Some(SyncStatus::NoSync) { @@ -1316,6 +1293,7 @@ fn start_sync(wallet: Wallet) -> Thread { /// Handle wallet task. async fn handle_task(w: &Wallet, t: WalletTask) { let send_tor = async |s: &Slate, r: &SlatepackAddress| { + let id = s.id.to_string(); match w.send_tor(&s, r).await { Ok(s) => { match w.finalize(&s) { @@ -1323,39 +1301,128 @@ async fn handle_task(w: &Wallet, t: WalletTask) { match w.post(&s) { Ok(_) => { sync_wallet_data(&w, false); - w.on_tx_result(&s); + w.on_task_result(Some(id), &t); } Err(e) => { - w.on_tx_error(s.id.to_string(), Some(e)); + w.on_tx_error(id, Some(e)); } } } Err(e) => { - w.on_tx_error(s.id.to_string(), Some(e)); + w.on_tx_error(id, Some(e)); } } } Err(e) => { - w.on_tx_error(s.id.to_string(), Some(e)); - w.on_tx_result(&s); + w.on_tx_error(id.clone(), Some(e)); + w.on_task_result(Some(id), &t); } } }; match &t { WalletTask::OpenMessage(m) => { - w.open_message(m); + if !w.is_open() || m.is_empty() { + return; + } + let w = w.clone(); + let load = w.message_opening.clone(); + let msg = m.clone(); + thread::spawn(move || { + load.store(true, Ordering::Relaxed); + if let Ok(s) = w.parse_slatepack(&msg) { + let id = s.id.to_string(); + // Check if message already exists. + let exists = { + let mut exists = w.slatepack_exists(&s); + if !exists && (s.state == SlateState::Invoice2 || + s.state == SlateState::Standard2) { + let mut slate = s.clone(); + slate.state = if s.state == SlateState::Standard2 { + SlateState::Standard3 + } else { + SlateState::Invoice3 + }; + exists = w.slatepack_exists(&slate); + } + exists + }; + if exists { + w.on_task_result(Some(id), &t); + load.store(false, Ordering::Relaxed); + return; + } + // Create response or finalize. + match s.state { + SlateState::Standard1 | SlateState::Invoice1 => { + if s.state != SlateState::Standard1 { + if let Ok(_) = w.pay(&s) { + sync_wallet_data(&w, false); + w.on_task_result(Some(id), &t); + } + } else { + if let Ok(_) = w.receive(&s) { + sync_wallet_data(&w, false); + w.on_task_result(Some(id), &t); + } + } + } + SlateState::Standard2 | SlateState::Invoice2 => { + match w.finalize(&s) { + Ok(s) => { + match w.post(&s) { + Ok(_) => { + sync_wallet_data(&w, false); + } + Err(e) => { + w.on_tx_error(id, Some(e)); + } + } + } + Err(e) => { + w.on_tx_error(id, Some(e)); + } + } + } + _ => {} + }; + } + load.store(false, Ordering::Relaxed); + }); + } + WalletTask::CalculateFee(a, _) => { + // Wait if there are no more fee tasks or handle next input value. + let calculating = w.fee_calculating.load(Ordering::Relaxed); + if calculating == 1 { + async_std::task::sleep(Duration::from_millis(100)).await; + let calculating = w.fee_calculating.load(Ordering::Relaxed); + if calculating > 1 { + w.fee_calculating.store(calculating - 1, Ordering::Relaxed); + return; + } + } else { + w.fee_calculating.store(calculating - 1, Ordering::Relaxed); + return; + } + // Calculate fee for provided amount. + if let Ok(fee) = w.calculate_fee(*a) { + w.on_task_result(None, &WalletTask::CalculateFee(*a, fee)) + } + let calculating = w.fee_calculating.load(Ordering::Relaxed); + w.fee_calculating.store(calculating - 1, Ordering::Relaxed); } WalletTask::Send(a, r) => { w.send_creating.store(true, Ordering::Relaxed); if let Ok(s) = w.send(*a, r.clone()) { sync_wallet_data(&w, false); - w.send_creating.store(false, Ordering::Relaxed); if let Some(r) = r { + w.send_creating.store(false, Ordering::Relaxed); send_tor(&s, r).await; + return; } else { - w.on_tx_result(&s); + w.on_task_result(Some(s.id.to_string()), &t); } } + w.send_creating.store(false, Ordering::Relaxed); } WalletTask::SendTor(id, r) => { if let Some(s) = w.get_tx(*id) { @@ -1366,7 +1433,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) { w.invoice_creating.store(true, Ordering::Relaxed); if let Ok(s) = w.issue_invoice(*a) { sync_wallet_data(&w, false); - w.on_tx_result(&s); + w.on_task_result(Some(s.id.to_string()), &t); } w.invoice_creating.store(false, Ordering::Relaxed); },