From cd0e3485c5ce316a02c07baef68f2378904af6e1 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Thu, 19 Jun 2025 09:18:20 +0300 Subject: [PATCH] txs: async tasks for wallet --- locales/de.yml | 3 +- locales/en.yml | 3 +- locales/fr.yml | 3 +- locales/ru.yml | 3 +- locales/tr.yml | 3 +- locales/zh-CN.yml | 3 +- src/gui/views/views.rs | 5 +- src/gui/views/wallets/content.rs | 4 +- .../views/wallets/wallet/account/content.rs | 3 +- src/gui/views/wallets/wallet/content.rs | 70 +- .../views/wallets/wallet/request/invoice.rs | 93 +- src/gui/views/wallets/wallet/request/send.rs | 189 +-- .../views/wallets/wallet/transport/content.rs | 2 +- .../wallets/wallet/transport/settings.rs | 2 +- src/gui/views/wallets/wallet/txs/content.rs | 159 ++- src/gui/views/wallets/wallet/txs/tx.rs | 41 +- src/wallet/config.rs | 22 +- src/wallet/types.rs | 157 ++- src/wallet/wallet.rs | 1204 +++++++++-------- 19 files changed, 986 insertions(+), 983 deletions(-) diff --git a/locales/de.yml b/locales/de.yml index 992089f9..82931d66 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -86,6 +86,7 @@ wallets: tx_canceled: Abgebrochen tx_cancelling: Abbrechen tx_finalizing: Finalisierung + tx_posting: Buchungsvorgang tx_confirmed: Bestätigt txs: Transaktionen tx: Transaktion @@ -141,7 +142,7 @@ transport: incorrect_addr_err: 'Eingegebene Addresse ist inkorrekt:' tor_send_error: Beim Senden über Tor ist ein Fehler aufgetreten. Stellen Sie sicher, dass der Empfänger online ist. Die Transaktion wurde abgebrochen. tor_autorun_desc: Gibt an, ob beim Öffnen des Wallets der Tor-Dienst gestartet werden soll, um Transaktionen synchron zu empfangen. - tor_sending: 'Sende %{amount} ツ über Tor' + tor_sending: Sende über Tor tor_settings: Tor Einstellungen bridges: Brücken bridges_desc: Richten Sie Brücken ein, um die Zensur des Tor-Netzwerks zu umgehen, wenn die normale Verbindung nicht funktioniert. diff --git a/locales/en.yml b/locales/en.yml index 0aa8893b..d7910d5f 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -86,6 +86,7 @@ wallets: tx_canceled: Canceled tx_cancelling: Cancelling tx_finalizing: Finalizing + tx_posting: Posting tx_confirmed: Confirmed txs: Transactions tx: Transaction @@ -141,7 +142,7 @@ transport: incorrect_addr_err: 'Entered address is incorrect:' tor_send_error: An error occurred during sending over Tor, make sure receiver is online, transaction was canceled. tor_autorun_desc: Whether to launch Tor service on wallet opening to receive transactions synchronously. - tor_sending: 'Sending %{amount} ツ over Tor' + tor_sending: Sending over Tor tor_settings: Tor Settings bridges: Bridges bridges_desc: Setup bridges to bypass Tor network censorship if usual connection is not working. diff --git a/locales/fr.yml b/locales/fr.yml index ec01af40..bcada956 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -86,6 +86,7 @@ wallets: tx_canceled: Annulé tx_cancelling: Annulation tx_finalizing: Finalisation + tx_posting: Publication tx_confirmed: Confirmé txs: Transactions tx: Transaction @@ -141,7 +142,7 @@ transport: incorrect_addr_err: 'Adresse entrée incorrecte:' tor_send_error: "Une erreur s'est produite lors de l'envoi via Tor. Assurez-vous que le destinataire est en ligne, la transaction a été annulée." tor_autorun_desc: "Lancer automatiquement le service Tor à l'ouverture du portefeuille pour recevoir les transactions de manière synchronisée." - tor_sending: 'Envoi de %{amount} ツ via Tor' + tor_sending: Envoi via Tor tor_settings: Paramètres Tor bridges: Passerelles bridges_desc: Configurez des passerelles pour contourner la censure du réseau Tor si la connexion habituelle ne fonctionne pas. diff --git a/locales/ru.yml b/locales/ru.yml index b57bd9b8..fc02bb00 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -86,6 +86,7 @@ wallets: tx_canceled: Отменено tx_cancelling: Отмена tx_finalizing: Завершение + tx_posting: Публикация tx_confirmed: Подтверждено txs: Транзакции tx: Транзакция @@ -141,7 +142,7 @@ transport: incorrect_addr_err: 'Введённый адрес неверен:' tor_send_error: Во время отправки через Tor произошла ошибка, убедитесь, что получатель находится онлайн, транзакция была отменена. tor_autorun_desc: Запускать ли Tor сервис при открытии кошелька для синхронного получения транзакций. - tor_sending: 'Отправка %{amount} ツ через Tor' + tor_sending: Отправка через Tor tor_settings: Настройки Tor bridges: Мосты bridges_desc: Настройте мосты для обхода цензуры сети Tor, если обычное соединение не работает. diff --git a/locales/tr.yml b/locales/tr.yml index cd5d7b6d..08a33b70 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -86,6 +86,7 @@ wallets: tx_canceled: Iptal edildi tx_cancelling: Iptal ediliyor tx_finalizing: Islem tamamlaniyor + tx_posting: Islem kaydetme tx_confirmed: Onaylandi txs: Islemler tx: Islem @@ -141,7 +142,7 @@ transport: incorrect_addr_err: 'Girilen adres hatali:' tor_send_error: Tor adresi uzerinden gonderimde aksaklik olustu, alici online olmasi gerek, islem iptal edildi. tor_autorun_desc: Islemleri Tor adresi olarak AL,bunun için cuzdan acilisinda Tor hizmetinin baslatilip baslatilmayacagi. - tor_sending: 'Tor adrese %{amount} ツ gonderiliyor.' + tor_sending: Tor adrese gonderiliyor tor_settings: Tor Ayarlar bridges: Bridges bridges_desc: Setup bridges to bypass Tor network censorship if usual connection is not working. diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index e826d567..2cf13f5f 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -81,11 +81,13 @@ wallets: tx_sent: 已发送 tx_received: 已接收 tx_sending: 发送中 + tx_sending_tor: 通过 Tor 发送 tx_receiving: 接收中 tx_confirming: 等待确认 tx_canceled: 已取消 tx_cancelling: 取消 tx_finalizing: 完成 + tx_posting: 过账交易 tx_confirmed: 已确认 txs: 所有交易 tx: 交易 @@ -141,7 +143,6 @@ transport: incorrect_addr_err: '输入的地址不正确:' tor_send_error: 通过 Tor 发送时出错,请确保接收方在线, 交易已取消. tor_autorun_desc: 是否在开钱包时启动 Tor 服务以同步接收交易. - tor_sending: '通过 Tor 发送%{amount} ツ' tor_settings: Tor 设置 bridges: 桥梁 bridges_desc: 如果常规连接不正常,设置网桥,可以绕过 Tor 网络审查. diff --git a/src/gui/views/views.rs b/src/gui/views/views.rs index 6cb45abb..512f6be3 100644 --- a/src/gui/views/views.rs +++ b/src/gui/views/views.rs @@ -468,9 +468,12 @@ impl View { Spinner::new().size(Self::BIG_SPINNER_SIZE).color(Colors::gold()).ui(ui); } + /// Size of big loading spinner. + pub const SMALL_SPINNER_SIZE: f32 = 32.0; + /// Draw small gold loading spinner. pub fn small_loading_spinner(ui: &mut egui::Ui) { - Spinner::new().size(38.0).color(Colors::gold()).ui(ui); + Spinner::new().size(30.0).color(Colors::gold()).ui(ui); } /// 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 13803bf6..3dea6b2d 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -26,7 +26,7 @@ use crate::gui::views::wallets::wallet::types::{wallet_status_text, WalletConten use crate::gui::views::wallets::WalletContent; use crate::gui::views::{Content, Modal, TitlePanel, View}; use crate::gui::Colors; -use crate::wallet::types::ConnectionMethod; +use crate::wallet::types::{ConnectionMethod, WalletTask}; use crate::wallet::{Wallet, WalletList}; use crate::AppConfig; @@ -619,7 +619,7 @@ impl WalletsContent { fn select_wallet(&mut self, wallet: &Wallet, data: Option, cb: &dyn PlatformCallbacks) { self.wallet_content.account_content.close_qr_scan(cb); if let Some(data) = data { - wallet.open_message(data); + wallet.task(WalletTask::OpenMessage(data)); } self.wallets.select(Some(wallet.get_config().id)); } diff --git a/src/gui/views/wallets/wallet/account/content.rs b/src/gui/views/wallets/wallet/account/content.rs index 7e5f8b37..2c644129 100644 --- a/src/gui/views/wallets/wallet/account/content.rs +++ b/src/gui/views/wallets/wallet/account/content.rs @@ -24,6 +24,7 @@ use crate::gui::views::wallets::wallet::types::{WalletContentContainer, GRIN}; use crate::gui::views::{CameraContent, CameraScanContent, Content, Modal, View}; use crate::gui::Colors; use crate::wallet::{Wallet, WalletConfig}; +use crate::wallet::types::WalletTask; /// Wallet account panel content. pub struct AccountContent { @@ -288,7 +289,7 @@ impl AccountContent { //TODO: send with address } QrScanResult::Slatepack(m) => { - wallet.open_message(m.to_string()); + wallet.task(WalletTask::OpenMessage(m)); } _ => { self.qr_scan_result = Some(result); diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 2477798e..ae0ac045 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -16,7 +16,7 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{Id, Margin, RichText, ScrollArea}; use grin_chain::SyncStatus; use std::time::Duration; -use grin_wallet_libwallet::Error; + 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}; @@ -30,7 +30,7 @@ use crate::gui::views::wallets::WalletTransactions; use crate::gui::views::{Content, FilePickContent, FilePickContentType, Modal, View}; use crate::gui::Colors; use crate::node::Node; -use crate::wallet::types::{ConnectionMethod, WalletTransaction}; +use crate::wallet::types::{ConnectionMethod, WalletTask}; use crate::wallet::{ExternalConnection, Wallet}; use crate::AppConfig; @@ -105,11 +105,6 @@ impl WalletContentContainer for WalletContent { show_account = false; } - // Consume inserted message. - if let Some(res) = wallet.consume_message_result() { - self.on_transaction(res); - } - // Show wallet tabs. egui::TopBottomPanel::bottom("wallet_tabs") .frame(egui::Frame { @@ -314,13 +309,6 @@ impl WalletContent { } } - /// Callback on incoming transaction for user to take action. - fn on_transaction(&mut self, tx_result: Result) { - if let Ok(tx) = tx_result { - self.current_tab = Box::new(WalletTransactions::new(Some(tx))); - } - } - /// Check if it's possible to go back at navigation stack. pub fn can_back(&self) -> bool { self.account_content.can_back() || self.transport_content.can_back() @@ -370,41 +358,49 @@ impl WalletContent { }); }); columns[1].vertical_centered_justified(|ui| { - let active = if has_wallet_data { - Some(false) + if wallet.invoice_creating() { + ui.add_space(4.0); + View::small_loading_spinner(ui); } else { - None - }; - View::tab_button(ui, FILE_ARROW_DOWN, Some(Colors::green()), active, |_| { - self.invoice_request_content = Some(InvoiceRequestContent::default()); - Modal::new(INVOICE_MODAL_ID) - .position(ModalPosition::CenterTop) - .title(t!("wallets.receive")) - .show(); - }); + let active = if has_wallet_data { + Some(false) + } else { + None + }; + View::tab_button(ui, FILE_ARROW_DOWN, Some(Colors::green()), active, |_| { + self.invoice_request_content = Some(InvoiceRequestContent::default()); + Modal::new(INVOICE_MODAL_ID) + .position(ModalPosition::CenterTop) + .title(t!("wallets.receive")) + .show(); + }); + } }); columns[2].vertical_centered_justified(|ui| { if wallet.message_opening() { + ui.add_space(4.0); View::small_loading_spinner(ui); } else { - let mut message = "".to_string(); self.file_pick_tab_button.ui(ui, cb, |m| { - message = m; + wallet.task(WalletTask::OpenMessage(m)); }); - if !message.is_empty() { - wallet.open_message(message); - } } }); if can_send { columns[3].vertical_centered_justified(|ui| { - View::tab_button(ui, FILE_ARROW_UP, Some(Colors::red()), Some(false), |_| { - self.send_request_content = Some(SendRequestContent::new(None)); - Modal::new(SEND_MODAL_ID) - .position(ModalPosition::CenterTop) - .title(t!("wallets.send")) - .show(); - }); + if wallet.send_creating() { + ui.add_space(4.0); + View::small_loading_spinner(ui); + } 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)); + Modal::new(SEND_MODAL_ID) + .position(ModalPosition::CenterTop) + .title(t!("wallets.send")) + .show(); + }); + } }); } let settings_index = if tabs_amount == 5 { 4 } else { 3 }; diff --git a/src/gui/views/wallets/wallet/request/invoice.rs b/src/gui/views/wallets/wallet/request/invoice.rs index 7dcabd31..78b33529 100644 --- a/src/gui/views/wallets/wallet/request/invoice.rs +++ b/src/gui/views/wallets/wallet/request/invoice.rs @@ -14,42 +14,23 @@ use egui::{Id, RichText}; use grin_core::core::amount_from_hr_string; -use grin_wallet_libwallet::Error; -use parking_lot::RwLock; -use std::sync::Arc; -use std::thread; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::wallets::wallet::WalletTransactionContent; use crate::gui::views::{Modal, TextEdit, View}; use crate::gui::Colors; -use crate::wallet::types::WalletTransaction; +use crate::wallet::types::WalletTask; use crate::wallet::Wallet; /// Invoice request creation content. pub struct InvoiceRequestContent { /// Amount to 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 Default for InvoiceRequestContent { fn default() -> Self { Self { amount_edit: "".to_string(), - request_loading: false, - request_result: Arc::new(RwLock::new(None)), - request_error: None, - result_tx_content: None, } } } @@ -61,42 +42,20 @@ impl InvoiceRequestContent { wallet: &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; - } - // Setup callback on continue. let on_continue = |m: &mut InvoiceRequestContent| { if m.amount_edit.is_empty() { return; } if let Ok(a) = amount_from_hr_string(m.amount_edit.as_str()) { - modal.disable_closing(); - // Setup data for request. - let wallet = wallet.clone(); - let result = m.request_result.clone(); - // Send request at another thread. - m.request_loading = true; - thread::spawn(move || { - let res = wallet.issue_invoice(a); - let mut w_result = result.write(); - *w_result = Some(res); - }); - } else { - m.request_error = Some(t!("wallets.invoice_slatepack_err")); + m.amount_edit = "".to_string(); + wallet.task(WalletTask::Receive(a)); + Modal::close(); } }; ui.add_space(6.0); - // Draw content on request loading. - if self.request_loading { - self.loading_request_ui(ui, modal); - return; - } - // Draw amount input content. ui.vertical_centered(|ui| { ui.label(RichText::new(t!("wallets.enter_amount_receive")) @@ -117,7 +76,6 @@ impl InvoiceRequestContent { // 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()) { @@ -146,16 +104,6 @@ impl InvoiceRequestContent { } } - // 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. @@ -165,7 +113,6 @@ impl InvoiceRequestContent { 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; Modal::close(); }); }); @@ -178,36 +125,4 @@ impl InvoiceRequestContent { }); ui.add_space(6.0); } - - /// Draw loading request content. - fn loading_request_ui(&mut self, ui: &mut egui::Ui, 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(WalletTransactionContent::new(tx)); - } - Err(_) => { - self.request_error = Some(t!("wallets.invoice_slatepack_err")); - self.request_loading = false; - } - } - } - } } \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/request/send.rs b/src/gui/views/wallets/wallet/request/send.rs index c984b1be..5b90bfc8 100644 --- a/src/gui/views/wallets/wallet/request/send.rs +++ b/src/gui/views/wallets/wallet/request/send.rs @@ -12,19 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; -use grin_wallet_libwallet::{Error, SlatepackAddress}; -use parking_lot::RwLock; -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::SlatepackAddress; -use crate::gui::Colors; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::wallets::wallet::WalletTransactionContent; use crate::gui::views::{CameraContent, Modal, TextEdit, View}; -use crate::gui::views::types::ModalPosition; -use crate::wallet::types::WalletTransaction; +use crate::gui::Colors; +use crate::wallet::types::WalletTask; use crate::wallet::Wallet; /// Content to create a request to send funds. @@ -36,18 +31,8 @@ pub struct SendRequestContent { /// Flag to check if entered address is incorrect. address_error: bool, - /// 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, - /// Address QR code scanner content. address_scan_content: Option, - - /// Request result transaction content. - result_tx_content: Option, } impl SendRequestContent { @@ -57,11 +42,7 @@ impl SendRequestContent { amount_edit: "".to_string(), address_edit: addr.unwrap_or("".to_string()), address_error: false, - request_loading: false, - request_result: Arc::new(RwLock::new(None)), - request_error: None, address_scan_content: None, - result_tx_content: None, } } @@ -71,68 +52,6 @@ impl SendRequestContent { wallet: &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; - } - - // Setup callback on continue. - let on_continue = |m: &mut SendRequestContent| { - if m.amount_edit.is_empty() { - return; - } - // Check address to send over Tor if enabled. - let addr_str = m.address_edit.as_str(); - if let Ok(addr) = SlatepackAddress::try_from(addr_str.trim()) { - if let Ok(a) = amount_from_hr_string(m.amount_edit.as_str()) { - Modal::change_position(ModalPosition::Center); - modal.disable_closing(); - - let mut wallet = wallet.clone(); - let res = m.request_result.clone(); - - // Send request at another thread. - m.request_loading = true; - thread::spawn(move || { - tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .unwrap() - .block_on(async { - let result = wallet.send_tor(a, &addr).await; - let mut w_res = res.write(); - *w_res = Some(result); - }); - }); - } - } else if !addr_str.is_empty() { - m.address_error = true; - } else if let Ok(amount) = amount_from_hr_string(m.amount_edit.as_str()) { - Modal::change_position(ModalPosition::Center); - modal.disable_closing(); - - let wallet = wallet.clone(); - let result = m.request_result.clone(); - - // Send request at another thread. - m.request_loading = true; - thread::spawn(move || { - let res = wallet.send(amount, None); - let mut w_result = result.write(); - *w_result = Some(res); - }); - } else { - m.request_error = Some(t!("wallets.send_slatepack_err")); - } - }; - - // Draw content on request loading. - if self.request_loading { - self.loading_request_ui(ui, modal); - return; - } - ui.add_space(6.0); // Draw QR code scanner content if requested. @@ -158,6 +77,7 @@ impl SendRequestContent { ui.columns(2, |cols| { cols[0].vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::white_or_black(false), || { + on_stop(); self.close(); }); }); @@ -267,17 +187,7 @@ impl SendRequestContent { // Continue on Enter press. if address_edit.enter_pressed { - on_continue(self); - } - - // Show request creation error. - if let Some(err) = &self.request_error { - ui.vertical_centered(|ui| { - ui.label(RichText::new(err) - .size(17.0) - .color(Colors::red())); - }); - ui.add_space(12.0); + self.on_continue(wallet); } // Setup spacing between buttons. @@ -292,83 +202,38 @@ impl SendRequestContent { columns[1].vertical_centered_justified(|ui| { // Button to create Slatepack message request. View::button(ui, t!("continue"), Colors::white_or_black(false), || { - on_continue(self); + self.on_continue(wallet); }); }); }); ui.add_space(6.0); } + /// Callback when Continue button was pressed. + fn on_continue(&mut self, wallet: &Wallet) { + if self.amount_edit.is_empty() { + return; + } + // Check address to send over Tor if enabled. + let addr_str = self.address_edit.as_str(); + if let Ok(r) = SlatepackAddress::try_from(addr_str.trim()) { + if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { + wallet.task(WalletTask::Send(a, Some(r))); + Modal::close(); + } + } else if !addr_str.is_empty() { + self.address_error = true; + } else if let Ok(a) = amount_from_hr_string(self.amount_edit.as_str()) { + wallet.task(WalletTask::Send(a, None)); + Modal::close(); + } + } + /// Close modal and clear data. fn close(&mut self) { self.amount_edit = "".to_string(); self.address_edit = "".to_string(); - - let mut w_res = self.request_result.write(); - *w_res = None; - - self.result_tx_content = None; self.address_scan_content = None; - Modal::close(); } - - /// Draw loading request content. - fn loading_request_ui(&mut self, ui: &mut egui::Ui, modal: &Modal) { - ui.add_space(40.0); - ui.vertical_centered(|ui| { - View::big_loading_spinner(ui); - }); - ui.add_space(40.0); - - if !self.address_edit.is_empty() { - ui.vertical_centered(|ui| { - ui.label(RichText::new(t!("transport.tor_sending", "amount" => self.amount_edit)) - .size(17.0) - .color(Colors::inactive_text())); - }); - ui.add_space(12.0); - } - - // Update data on request result. - let has_res = { - let r_request = self.request_result.read(); - r_request.is_some() - }; - if has_res { - self.request_loading = false; - modal.enable_closing(); - let r_request = self.request_result.read(); - let result = r_request.as_ref().unwrap(); - match result { - Ok(tx) => { - self.result_tx_content = Some(WalletTransactionContent::new(tx)); - } - Err(err) => { - let m = match err { - Error::NotEnoughFunds { .. } => { - t!("wallets.pay_balance_error", "amount" => self.amount_edit) - } - _ => { - if !self.address_edit.is_empty() { - t!("transport.tor_send_error") - } else { - t!("wallets.send_slatepack_err") - } - } - }; - self.request_error = Some(m); - } - } - } - - // Check if there is request result error. - if self.request_error.is_some() { - Modal::change_position(ModalPosition::CenterTop); - modal.enable_closing(); - let mut w_request = self.request_result.write(); - *w_request = None; - self.request_loading = false; - } - } } \ 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 index 6f0a4f3e..06524703 100644 --- a/src/gui/views/wallets/wallet/transport/content.rs +++ b/src/gui/views/wallets/wallet/transport/content.rs @@ -130,7 +130,7 @@ impl WalletTransportContent { if !Tor::is_service_running(service_id) { let r = CornerRadius::default(); View::item_button(ui, r, POWER, Some(Colors::green()), || { - if let Ok(key) = wallet.secret_key() { + if let Ok(key) = wallet.get_secret_key() { let api_port = wallet.foreign_api_port().unwrap(); Tor::start_service(api_port, key, service_id); } diff --git a/src/gui/views/wallets/wallet/transport/settings.rs b/src/gui/views/wallets/wallet/transport/settings.rs index 51553f46..5453abf9 100644 --- a/src/gui/views/wallets/wallet/transport/settings.rs +++ b/src/gui/views/wallets/wallet/transport/settings.rs @@ -80,7 +80,7 @@ impl WalletTransportSettingsContent { // 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() { + if let Ok(key) = wallet.get_secret_key() { let api_port = wallet.foreign_api_port().unwrap(); Tor::restart_service(api_port, key, service_id); } diff --git a/src/gui/views/wallets/wallet/txs/content.rs b/src/gui/views/wallets/wallet/txs/content.rs index 42a1337e..077f13d4 100644 --- a/src/gui/views/wallets/wallet/txs/content.rs +++ b/src/gui/views/wallets/wallet/txs/content.rs @@ -21,7 +21,7 @@ use grin_wallet_libwallet::TxLogEntryType; use std::ops::Range; use std::time::{SystemTime, UNIX_EPOCH}; -use crate::gui::icons::{ARCHIVE_BOX, ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, CALENDAR_CHECK, DOTS_THREE_CIRCLE, FILE_ARROW_DOWN, FILE_TEXT, GEAR_FINE, PROHIBIT, X_CIRCLE}; +use crate::gui::icons::{ARCHIVE_BOX, 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; @@ -29,7 +29,7 @@ use crate::gui::views::wallets::wallet::types::{WalletTabType, GRIN}; use crate::gui::views::wallets::wallet::WalletTransactionContent; use crate::gui::views::{Content, Modal, PullToRefresh, View}; use crate::gui::Colors; -use crate::wallet::types::{WalletData, WalletTransaction}; +use crate::wallet::types::{WalletData, WalletTask, WalletTransaction, WalletTransactionAction}; use crate::wallet::Wallet; /// Wallet transactions tab content. @@ -50,6 +50,15 @@ impl WalletTab for WalletTransactions { } 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); + } + } + } self.modal_content_ui(ui, wallet, cb); self.txs_ui(ui, wallet); } @@ -72,7 +81,7 @@ impl WalletTransactions { manual_sync: None, }; if let Some(tx) = &tx { - content.show_tx_info_modal(tx); + content.show_tx_info_modal(tx.data.id); } content } @@ -104,7 +113,7 @@ impl WalletTransactions { return; } // Draw awaiting amount info if exists. - self.awaiting_info_ui(ui, &data); + self.awaiting_info_ui(ui, &data); }); ui.add_space(4.0); @@ -161,10 +170,9 @@ impl WalletTransactions { r.nw = 0.0 as u8; r.sw = 0.0 as u8; View::item_button(ui, r, FILE_TEXT, None, || { - self.show_tx_info_modal(tx); + self.show_tx_info_modal(tx.data.id); }); } - // Draw button to cancel transaction. if tx.can_cancel() { let (icon, color) = (PROHIBIT, Some(Colors::red())); @@ -177,6 +185,10 @@ impl WalletTransactions { .show(); }); } + //TODO: Draw button to repeat transaction task on error. + if tx.action_error.is_some() { + + } }); } } @@ -303,52 +315,68 @@ impl WalletTransactions { || tx.data.tx_type == TxLogEntryType::TxReceivedCancelled; if is_canceled { format!("{} {}", X_CIRCLE, t!("wallets.tx_canceled")) - } else if tx.finalizing { - format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_finalizing")) - } else { - if tx.cancelling { - format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_cancelling")) + } else if let Some(a) = &tx.action { + let error = if tx.action_error.is_none() { + "".to_string() } else { - match tx.data.tx_type { - TxLogEntryType::TxReceived => { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_receiving")) - }, - TxLogEntryType::TxSent => { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_sending")) - }, - TxLogEntryType::ConfirmedCoinbase => { - let tx_h = tx.height.unwrap_or(1) - 1; - if tx_h != 0 { - let left_conf = height - tx_h; - if height >= tx_h && left_conf < COINBASE_MATURITY { - let conf_info = format!("{}/{}", - left_conf, - COINBASE_MATURITY); - format!("{} {} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming"), - conf_info - ) - } else { - format!("{} {}", - DOTS_THREE_CIRCLE, - t!("wallets.tx_confirming")) - } + format!("{}: ", t!("error")) + }; + let status = match a { + WalletTransactionAction::Cancelling => t!("wallets.tx_cancelling"), + WalletTransactionAction::Finalizing => t!("wallets.tx_finalizing"), + WalletTransactionAction::Posting => t!("wallets.tx_posting"), + WalletTransactionAction::SendingTor => t!("transport.tor_sending") + }; + let icon = if error.is_empty() { + DOTS_THREE_CIRCLE + } else { + WARNING + }; + format!("{} {}{}", icon, error, status) + } else { + match tx.data.tx_type { + TxLogEntryType::TxReceived => { + let text = match tx.finalized() { + true => t!("wallets.await_fin_amount"), + false => t!("wallets.tx_receiving") + }; + format!("{} {}", DOTS_THREE_CIRCLE, text) + }, + TxLogEntryType::TxSent => { + let text = match tx.finalized() { + true => t!("wallets.await_fin_amount"), + false => t!("wallets.tx_sending") + }; + format!("{} {}", DOTS_THREE_CIRCLE, text) + }, + TxLogEntryType::ConfirmedCoinbase => { + let tx_h = tx.height.unwrap_or(1) - 1; + if tx_h != 0 { + let left_conf = height - tx_h; + if height >= tx_h && left_conf < COINBASE_MATURITY { + let conf_info = format!("{}/{}", + left_conf, + COINBASE_MATURITY); + format!("{} {} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming"), + conf_info + ) } else { format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_confirming")) } - }, - _ => { + } else { format!("{} {}", DOTS_THREE_CIRCLE, t!("wallets.tx_confirming")) } + }, + _ => { + format!("{} {}", + DOTS_THREE_CIRCLE, + t!("wallets.tx_confirming")) } } } @@ -439,8 +467,8 @@ impl WalletTransactions { } /// Show transaction information [`Modal`]. - fn show_tx_info_modal(&mut self, tx: &WalletTransaction) { - let modal = WalletTransactionContent::new(tx); + fn show_tx_info_modal(&mut self, id: u32) { + let modal = WalletTransactionContent::new(id); self.tx_info_content = Some(modal); Modal::new(TX_INFO_MODAL) .position(ModalPosition::Center) @@ -450,28 +478,29 @@ impl WalletTransactions { /// Confirmation [`Modal`] to cancel transaction. fn cancel_confirmation_modal(&mut self, ui: &mut egui::Ui, wallet: &Wallet) { + let data = wallet.get_data().unwrap(); + let data_txs = data.txs.unwrap(); + let txs = data_txs.into_iter() + .filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap()) + .collect::>(); + if txs.is_empty() { + Modal::close(); + return; + } + let tx = txs.get(0).unwrap(); + let amount = amount_to_hr_string(tx.amount, true); + let text = match tx.data.tx_type { + TxLogEntryType::TxReceived => { + t!("wallets.tx_receive_cancel_conf", "amount" => amount) + }, + _ => { + t!("wallets.tx_send_cancel_conf", "amount" => amount) + } + }; + + // Show confirmation text. ui.add_space(6.0); ui.vertical_centered(|ui| { - // Setup confirmation text. - let data = wallet.get_data().unwrap(); - let data_txs = data.txs.unwrap(); - let txs = data_txs.into_iter() - .filter(|tx| tx.data.id == self.confirm_cancel_tx_id.unwrap()) - .collect::>(); - if txs.is_empty() { - Modal::close(); - return; - } - let tx = txs.get(0).unwrap(); - let amount = amount_to_hr_string(tx.amount, true); - let text = match tx.data.tx_type { - TxLogEntryType::TxReceived => { - t!("wallets.tx_receive_cancel_conf", "amount" => amount) - }, - _ => { - t!("wallets.tx_send_cancel_conf", "amount" => amount) - } - }; ui.label(RichText::new(text) .size(17.0) .color(Colors::text(false))); @@ -492,7 +521,7 @@ impl WalletTransactions { }); columns[1].vertical_centered_justified(|ui| { View::button(ui, "OK".to_string(), Colors::white_or_black(false), || { - wallet.cancel(self.confirm_cancel_tx_id.unwrap()); + wallet.task(WalletTask::Cancel(tx.clone())); self.confirm_cancel_tx_id = None; Modal::close(); }); diff --git a/src/gui/views/wallets/wallet/txs/tx.rs b/src/gui/views/wallets/wallet/txs/tx.rs index 334dc626..08a960f8 100644 --- a/src/gui/views/wallets/wallet/txs/tx.rs +++ b/src/gui/views/wallets/wallet/txs/tx.rs @@ -17,12 +17,12 @@ use grin_core::core::amount_to_hr_string; use grin_util::ToHex; use grin_wallet_libwallet::TxLogEntryType; -use crate::gui::icons::{COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN}; +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::{CameraContent, FilePickContent, FilePickContentType, Modal, QrCodeContent, View}; use crate::gui::Colors; -use crate::wallet::types::WalletTransaction; +use crate::wallet::types::{WalletTask, WalletTransaction}; use crate::wallet::Wallet; /// Transaction information [`Modal`] content. @@ -42,9 +42,9 @@ pub struct WalletTransactionContent { impl WalletTransactionContent { /// Create new content instance with [`Wallet`] from provided [`WalletTransaction`]. - pub fn new(tx: &WalletTransaction) -> Self { + pub fn new(id: u32) -> Self { Self { - tx_id: tx.data.id, + tx_id: id, qr_code_content: None, scan_qr_content: None, file_pick_button: FilePickContent::new( @@ -103,7 +103,7 @@ impl WalletTransactionContent { modal.enable_closing(); self.scan_qr_content = None; // Provide scan result as Slatepack message. - wallet.open_message(result.text()); + wallet.task(WalletTask::OpenMessage(result.text())); } else { scan_content.ui(ui, cb); } @@ -133,8 +133,8 @@ impl WalletTransactionContent { // Show transaction information. self.info_ui(ui, modal, tx, wallet, cb); - // Show transaction sharing content. - if tx.can_finalize || tx.is_response { + // Show transaction sharing content when can cancel or finalized. + if tx.can_cancel() && !tx.finalized() { self.share_ui(ui, wallet, tx, cb); } @@ -159,7 +159,7 @@ impl WalletTransactionContent { tx: &WalletTransaction, cb: &dyn PlatformCallbacks) { let amount = amount_to_hr_string(tx.amount, true); - let desc_text = if tx.can_finalize { + let desc_text = if tx.can_finalize() { if tx.data.tx_type == TxLogEntryType::TxSent { t!("wallets.send_request_desc", "amount" => amount) } else { @@ -186,8 +186,8 @@ impl WalletTransactionContent { // Draw button to show Slatepack message as QR code. let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); View::button(ui, qr_text.clone(), Colors::white_or_black(false), || { - if let Some((_, d)) = wallet.read_slatepack_by_tx(tx) { - self.qr_code_content = Some(QrCodeContent::new(d, true)); + if let Some(c) = wallet.read_slatepack(tx) { + self.qr_code_content = Some(QrCodeContent::new(c, true)); } }); }); @@ -198,10 +198,12 @@ impl WalletTransactionContent { share_text, Colors::blue(), Colors::white_or_black(false), || { - if let Some((s, d)) = wallet.read_slatepack_by_tx(tx) { - let name = format!("{}.{}.slatepack", s.id, s.state); - let data = d.as_bytes().to_vec(); - cb.share_data(name, data).unwrap_or_default(); + if let Some(slate_id) = tx.data.tx_slate_id { + let name = format!("{}.{}.slatepack", slate_id, tx.state); + if let Some(c) = wallet.read_slatepack(tx) { + let data = c.as_bytes().to_vec(); + cb.share_data(name, data).unwrap_or_default(); + } } }); }); @@ -242,10 +244,10 @@ impl WalletTransactionContent { } return; } - if tx.can_finalize { + if tx.can_finalize() { // Draw button to pick file. self.file_pick_button.ui(ui, cb, |data| { - wallet.open_message(data); + wallet.task(WalletTask::OpenMessage(data)); }); // Draw button to scan QR code. let r = CornerRadius::default(); @@ -257,13 +259,14 @@ impl WalletTransactionContent { } // Draw button to cancel transaction. if tx.can_cancel() { - let r = if tx.can_finalize { + let r = if tx.can_finalize() { CornerRadius::default() } else { View::item_rounding(0, 2, true) }; View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || { - wallet.cancel(tx.data.id); + wallet.task(WalletTask::Cancel(tx.clone())); + Modal::close(); }); } }); @@ -280,7 +283,7 @@ impl WalletTransactionContent { } // Show receiver address. if let Some(rec) = tx.receiver() { - let label = format!("{} {}", CUBE, t!("network_mining.address")); + let label = format!("{} {}", CIRCLE_HALF, t!("network_mining.address")); info_item_ui(ui, rec.to_string(), label, true, cb); } } diff --git a/src/wallet/config.rs b/src/wallet/config.rs index caa00163..ae25c7a0 100644 --- a/src/wallet/config.rs +++ b/src/wallet/config.rs @@ -23,7 +23,7 @@ use serde_derive::{Deserialize, Serialize}; use crate::{AppConfig, Settings}; use crate::wallet::ConnectionsConfig; -use crate::wallet::types::ConnectionMethod; +use crate::wallet::types::{ConnectionMethod, WalletTransaction}; /// Wallet configuration. #[derive(Serialize, Deserialize, Clone)] @@ -192,15 +192,27 @@ impl WalletConfig { path.to_str().unwrap().to_string() } - /// Get Slatepacks data path for current wallet. - pub fn get_slatepack_path(&self, slate: &Slate) -> PathBuf { + /// Get Slatepack file path for transaction. + pub fn get_tx_slate_path(&self, tx: &WalletTransaction) -> PathBuf { let mut path = PathBuf::from(self.get_wallet_path()); path.push(SLATEPACKS_DIR_NAME); if !path.exists() { let _ = fs::create_dir_all(path.clone()); } - let slatepack_file_name = format!("{}.{}.slatepack", slate.id, slate.state); - path.push(slatepack_file_name); + let file = format!("{}.{}.slatepack", tx.data.tx_slate_id.unwrap(), tx.state); + path.push(file); + path + } + + /// Get Slatepack file path for Slate. + pub fn get_slate_path(&self, slate: &Slate) -> PathBuf { + let mut path = PathBuf::from(self.get_wallet_path()); + path.push(SLATEPACKS_DIR_NAME); + if !path.exists() { + let _ = fs::create_dir_all(path.clone()); + } + let file = format!("{}.{}.slatepack", slate.id, slate.state); + path.push(file); path } diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 5a333f40..a6a8e0f1 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -17,7 +17,7 @@ use std::sync::Arc; use grin_keychain::ExtKeychain; use grin_util::Mutex; use grin_wallet_impls::{DefaultLCProvider, HTTPNodeClient}; -use grin_wallet_libwallet::{SlatepackAddress, TxLogEntry, TxLogEntryType, WalletInfo, WalletInst}; +use grin_wallet_libwallet::{Error, Slate, SlateState, SlatepackAddress, TxLogEntry, TxLogEntryType, WalletInfo, WalletInst}; use grin_wallet_util::OnionV3Address; use serde_derive::{Deserialize, Serialize}; @@ -151,31 +151,133 @@ pub struct WalletData { pub txs: Option> } +impl WalletData { + /// Update transaction action status. + pub fn on_tx_action(&mut self, id: String, action: Option) { + if self.txs.is_none() { + return; + } + for tx in self.txs.as_mut().unwrap() { + if let Some(slate_id) = tx.data.tx_slate_id { + if slate_id.to_string() == id { + tx.action = action; + tx.action_error = None; + break; + } + } + } + } + + /// Update transaction action error status. + pub fn on_tx_error(&mut self, id: String, err: Option) { + if self.txs.is_none() { + return; + } + for tx in self.txs.as_mut().unwrap() { + if let Some(slate_id) = tx.data.tx_slate_id { + if slate_id.to_string() == id { + tx.action_error = err; + break; + } + } + } + } + + /// Get transaction by slate identifier. + pub fn tx_by_slate_id(&self, id: String) -> Option { + if self.txs.is_none() { + return None; + } + for tx in self.txs.as_ref().unwrap() { + if let Some(slate_id) = tx.data.tx_slate_id { + if slate_id.to_string() == id { + return Some(tx.clone()); + } + } + } + None + } +} + +/// Wallet transaction action. +#[derive(Clone, PartialEq)] +pub enum WalletTransactionAction { + Cancelling, Finalizing, Posting, SendingTor +} + /// Wallet transaction data. #[derive(Clone)] pub struct WalletTransaction { - /// Transaction information. + /// Information from database. pub data: TxLogEntry, + /// State of transaction Slate. + pub state: SlateState, /// Calculated transaction amount between debited and credited amount. pub amount: u64, - /// Flag to check if transaction is cancelling. - pub cancelling: bool, - /// Flag to check if transaction can be sent back to initiator to finalize. - pub is_response: 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 where tx was included. pub height: Option, + + /// Action on transaction. + pub action: Option, + /// Action result error. + pub action_error: Option } impl WalletTransaction { + /// Check if transactions can be finalized after receiving response. + pub fn can_finalize(&self) -> bool { + !self.cancelling() && !self.data.confirmed && + (!self.sending_tor() || self.action_error.is_some()) && + (self.data.tx_type == TxLogEntryType::TxSent || + self.data.tx_type == TxLogEntryType::TxReceived) && + (self.state == SlateState::Invoice1 || self.state == SlateState::Standard1) + } + + /// Check if transaction was finalized. + pub fn finalized(&self) -> bool { + (self.data.tx_type == TxLogEntryType::TxSent || + self.data.tx_type == TxLogEntryType::TxReceived) && + self.state == SlateState::Invoice3 || self.state == SlateState::Standard3 + } + + /// Check if transaction is sending over Tor. + pub fn sending_tor(&self) -> bool { + if let Some(a) = self.action.as_ref() { + return a == &WalletTransactionAction::SendingTor; + } + false + } + + /// Check if transaction is cancelling. + pub fn cancelling(&self) -> bool { + if let Some(a) = self.action.as_ref() { + return a == &WalletTransactionAction::Cancelling; + } + false + } + + /// Check if transaction is posting. + pub fn posting(&self) -> bool { + if let Some(a) = self.action.as_ref() { + return a == &WalletTransactionAction::Posting; + } + false + } + /// Check if transaction can be cancelled. pub fn can_cancel(&self) -> bool { - !self.cancelling && !self.data.confirmed && - self.data.tx_type != TxLogEntryType::TxReceivedCancelled - && self.data.tx_type != TxLogEntryType::TxSentCancelled + !self.cancelling() && !self.data.confirmed && + (!self.sending_tor() || self.action_error.is_some()) && + self.data.tx_type != TxLogEntryType::TxReceivedCancelled && + self.data.tx_type != TxLogEntryType::TxSentCancelled + } + + /// Check if transaction is finalizing. + pub fn finalizing(&self) -> bool { + if let Some(a) = self.action.as_ref() { + return a == &WalletTransactionAction::Finalizing; + } + false } /// Get receiver address if payment proof was created. @@ -188,4 +290,33 @@ impl WalletTransaction { } None } +} + +/// Task for the wallet. +#[derive(Clone)] +pub enum WalletTask { + /// Open Slatepack message parsing result and making an action. + OpenMessage(String), + /// Create request to send. + /// * amount + /// * receiver + Send(u64, Option), + /// Resend request over Tor. + /// * local tx id + /// * receiver + SendTor(u32, SlatepackAddress), + /// Invoice creation. + /// * amount + Receive(u64), + /// Transaction finalization. + /// * tx + /// * local tx id + Finalize(Option, u32), + /// Post transaction to blockchain. + /// * tx + /// * local tx id + Post(Option, u32), + /// Cancel transaction. + /// * tx + Cancel(WalletTransaction), } \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 8ff166fe..f7622a3d 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -25,7 +25,7 @@ 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::{address, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, RetrieveTxQueryArgs, RetrieveTxQuerySortField, RetrieveTxQuerySortOrder, Slate, SlateState, SlateVersion, SlatepackAddress, StatusMessage, TxLogEntry, TxLogEntryType, VersionedSlate, WalletBackend, WalletInitStatus, WalletInst, WalletLCProvider}; +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; use rand::Rng; @@ -35,6 +35,7 @@ use std::io::Write; use std::net::{SocketAddr, TcpListener}; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::sync::mpsc::Sender; use std::sync::{mpsc, Arc}; use std::thread::Thread; use std::time::Duration; @@ -44,7 +45,7 @@ use crate::node::{Node, NodeConfig}; use crate::tor::Tor; use crate::wallet::seed::WalletSeed; use crate::wallet::store::TxHeightStore; -use crate::wallet::types::{ConnectionMethod, PhraseMode, WalletAccount, WalletData, WalletInstance, WalletTransaction}; +use crate::wallet::types::{ConnectionMethod, PhraseMode, WalletAccount, WalletData, WalletInstance, WalletTask, WalletTransaction, WalletTransactionAction}; use crate::wallet::{ConnectionsConfig, Mnemonic, WalletConfig}; use crate::AppConfig; @@ -61,11 +62,22 @@ pub struct Wallet { /// Wallet Slatepack address to receive txs at transport. slatepack_address: Arc>>, + /// Wallet accounts. + accounts: Arc>>, + /// Wallet sync thread. sync_thread: Arc>>, + /// Flag to check if wallet is syncing. + syncing: Arc, + /// Info loading progress in percents. + info_sync_progress: Arc, + /// Error on wallet loading. + sync_error: Arc, + /// Attempts amount to update wallet data. + sync_attempts: Arc, - /// Running wallet foreign API server and port. - foreign_api_server: Arc>>, + /// Wallet data. + data: Arc>>, /// Flag to check if wallet reopening is needed. reopen: Arc, @@ -73,24 +85,11 @@ pub struct Wallet { is_open: Arc, /// Flag to check if wallet is closing. closing: Arc, - /// Flag to check if wallet was deleted to remove it from the list. deleted: Arc, - /// Error on wallet loading. - sync_error: Arc, - /// Info loading progress in percents. - info_sync_progress: Arc, - - /// Wallet accounts. - accounts: Arc>>, - - /// Wallet info to show at ui. - data: Arc>>, - /// Attempts amount to update wallet data. - sync_attempts: Arc, - /// Flag to check if wallet is syncing. - syncing: Arc, + /// Running wallet foreign API server and port. + foreign_api_server: Arc>>, /// Flag to check if wallet repairing and restoring missing outputs is needed. repair_needed: Arc, @@ -99,8 +98,17 @@ pub struct Wallet { /// Flag to check if Slatepack message file is opening. message_opening: Arc, - /// Result of Slatepack message file opening. - message_result: Arc>>>, + + /// Flag to check if sending request is creating. + send_creating: Arc, + /// Flag to check if invoice is creating. + invoice_creating: Arc, + + /// 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>>, } impl Wallet { @@ -127,7 +135,10 @@ impl Wallet { repair_needed: Arc::new(AtomicBool::new(false)), repair_progress: Arc::new(AtomicU8::new(0)), message_opening: Arc::new(AtomicBool::from(false)), - message_result: Arc::new(RwLock::new(None)), + send_creating: Arc::new(AtomicBool::new(false)), + invoice_creating: Arc::new(AtomicBool::new(false)), + tasks_sender: Arc::new(RwLock::new(None)), + task_result_slate_id: Arc::new(RwLock::new(None)), } } @@ -236,11 +247,11 @@ impl Wallet { config: &mut WalletConfig, node_client: C, ) -> Result>>>, Error> - where - DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, - L: WalletLCProvider<'static, C, K>, - C: NodeClient + 'static, - K: Keychain + 'static, + where + DefaultWalletImpl<'static, C>: WalletInst<'static, L, C, K>, + L: WalletLCProvider<'static, C, K>, + C: NodeClient + 'static, + K: Keychain + 'static, { let mut wallet = Box::new(DefaultWalletImpl::<'static, C>::new(node_client).unwrap()) as Box>; @@ -249,8 +260,83 @@ impl Wallet { Ok(Arc::new(Mutex::new(wallet))) } + /// Open the wallet and start [`WalletData`] sync at separate thread. + pub fn open(&self, password: ZeroingString) -> Result<(), Error> { + if self.is_open() { + return Err(Error::GenericError("Already opened".to_string())); + } + + // Create new wallet instance if sync thread was stopped or instance was not created. + let has_instance = { + let r_inst = self.instance.as_ref().read(); + r_inst.is_some() + }; + if self.sync_thread.read().is_none() || !has_instance { + let mut config = self.get_config(); + // Setup current connection. + { + let mut w_conn = self.connection.write(); + *w_conn = config.connection(); + } + let new_instance = Self::create_wallet_instance(&mut config)?; + let mut w_inst = self.instance.write(); + *w_inst = Some(new_instance); + } + + // Open the wallet. + { + let instance = { + let r_inst = self.instance.as_ref().read(); + r_inst.clone().unwrap() + }; + let mut wallet_lock = instance.lock(); + let lc = wallet_lock.lc_provider()?; + match lc.open_wallet(None, password, false, false) { + Ok(_) => { + // Reset an error on opening. + self.set_sync_error(false); + self.reset_sync_attempts(); + + // Set current account. + let wallet_inst = lc.wallet_inst()?; + let label = self.get_config().account.to_owned(); + wallet_inst.set_parent_key_id_by_name(label.as_str())?; + + // Start new synchronization thread or wake up existing one. + let mut thread_w = self.sync_thread.write(); + if thread_w.is_none() { + let thread = start_sync(self.clone()); + *thread_w = Some(thread); + } else { + thread_w.clone().unwrap().unpark(); + } + self.is_open.store(true, Ordering::Relaxed); + } + Err(e) => { + if !self.syncing() { + let mut w_inst = self.instance.write(); + *w_inst = None; + } + return Err(e) + } + } + } + + // Set slatepack address. + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut api = Owner::new(instance, None); + controller::owner_single_use(None, None, Some(&mut api), |api, m| { + let mut w_address = self.slatepack_address.write(); + *w_address = Some(api.get_slatepack_address(m, 0)?.to_string()); + Ok(()) + })?; + + Ok(()) + } + /// Get parent key identifier for current account. - pub fn get_parent_key_id(&self) -> Result { + fn get_parent_key_id(&self) -> Result { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let mut w_lock = instance.lock(); @@ -260,7 +346,7 @@ impl Wallet { } /// Get wallet [`SecretKey`] for transports. - pub fn secret_key(&self) -> Result { + pub fn get_secret_key(&self) -> Result { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let mut w_lock = instance.lock(); @@ -344,81 +430,6 @@ impl Wallet { w_config.save(); } - /// Open the wallet and start [`WalletData`] sync at separate thread. - pub fn open(&self, password: ZeroingString) -> Result<(), Error> { - if self.is_open() { - return Err(Error::GenericError("Already opened".to_string())); - } - - // Create new wallet instance if sync thread was stopped or instance was not created. - let has_instance = { - let r_inst = self.instance.as_ref().read(); - r_inst.is_some() - }; - if self.sync_thread.read().is_none() || !has_instance { - let mut config = self.get_config(); - // Setup current connection. - { - let mut w_conn = self.connection.write(); - *w_conn = config.connection(); - } - let new_instance = Self::create_wallet_instance(&mut config)?; - let mut w_inst = self.instance.write(); - *w_inst = Some(new_instance); - } - - // Open the wallet. - { - let instance = { - let r_inst = self.instance.as_ref().read(); - r_inst.clone().unwrap() - }; - let mut wallet_lock = instance.lock(); - let lc = wallet_lock.lc_provider()?; - match lc.open_wallet(None, password, false, false) { - Ok(_) => { - // Reset an error on opening. - self.set_sync_error(false); - self.reset_sync_attempts(); - - // Set current account. - let wallet_inst = lc.wallet_inst()?; - let label = self.get_config().account.to_owned(); - wallet_inst.set_parent_key_id_by_name(label.as_str())?; - - // Start new synchronization thread or wake up existing one. - let mut thread_w = self.sync_thread.write(); - if thread_w.is_none() { - let thread = start_sync(self.clone()); - *thread_w = Some(thread); - } else { - thread_w.clone().unwrap().unpark(); - } - self.is_open.store(true, Ordering::Relaxed); - } - Err(e) => { - if !self.syncing() { - let mut w_inst = self.instance.write(); - *w_inst = None; - } - return Err(e) - } - } - } - - // Set slatepack address. - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut api = Owner::new(instance, None); - controller::owner_single_use(None, None, Some(&mut api), |api, m| { - let mut w_address = self.slatepack_address.write(); - *w_address = Some(api.get_slatepack_address(m, 0)?.to_string()); - Ok(()) - })?; - - Ok(()) - } - /// Get external connection URL applied to [`WalletInstance`] /// after wallet opening if sync is running or get it from config. pub fn get_current_connection(&self) -> ConnectionMethod { @@ -456,11 +467,11 @@ impl Wallet { let wallet_close = self.clone(); let service_id = wallet_close.identifier(); let conn = wallet_close.connection.clone(); - let message_opening = self.message_opening.clone(); thread::spawn(move || { - // Wait message opening to finish. - while message_opening.load(Ordering::Relaxed) { - thread::sleep(Duration::from_millis(100)); + // Wait common operations to finish. + while wallet_close.message_opening() || wallet_close.send_creating() || + wallet_close.invoice_creating() { + thread::sleep(Duration::from_millis(300)); } // Stop running API server. let api_server_exists = { @@ -473,7 +484,6 @@ impl Wallet { } // Stop running Tor service. Tor::stop_service(&service_id); - // Close the wallet. let r_inst = wallet_close.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); @@ -497,6 +507,56 @@ impl Wallet { let _ = lc.close_wallet(None); } + /// Set wallet reopen status. + pub fn set_reopen(&self, reopen: bool) { + self.reopen.store(reopen, Ordering::Relaxed); + } + + /// Check if wallet reopen is needed. + pub fn reopen_needed(&self) -> bool { + self.reopen.load(Ordering::Relaxed) + } + + /// Get wallet info synchronization progress. + pub fn info_sync_progress(&self) -> u8 { + self.info_sync_progress.load(Ordering::Relaxed) + } + + /// Check if wallet had an error on synchronization. + pub fn sync_error(&self) -> bool { + self.sync_error.load(Ordering::Relaxed) + } + + /// Set an error for wallet on synchronization. + pub fn set_sync_error(&self, error: bool) { + self.sync_error.store(error, Ordering::Relaxed); + } + + /// Get current wallet synchronization attempts before setting an error. + fn get_sync_attempts(&self) -> u8 { + self.sync_attempts.load(Ordering::Relaxed) + } + + /// Increment wallet synchronization attempts before setting an error. + fn increment_sync_attempts(&self) { + let mut attempts = self.get_sync_attempts(); + attempts += 1; + self.sync_attempts.store(attempts, Ordering::Relaxed); + } + + /// Reset wallet synchronization attempts. + fn reset_sync_attempts(&self) { + self.sync_attempts.store(0, Ordering::Relaxed); + } + + /// Send a task to the wallet. + pub fn task(&self, task: WalletTask) { + let r_tasks = self.tasks_sender.read(); + if r_tasks.is_some() { + let _ = r_tasks.as_ref().unwrap().send(task); + } + } + /// Create account into wallet. pub fn create_account(&self, label: &String) -> Result<(), Error> { let r_inst = self.instance.as_ref().read(); @@ -584,48 +644,6 @@ impl Wallet { self.accounts.read().clone() } - /// Set wallet reopen status. - pub fn set_reopen(&self, reopen: bool) { - self.reopen.store(reopen, Ordering::Relaxed); - } - - /// Check if wallet reopen is needed. - pub fn reopen_needed(&self) -> bool { - self.reopen.load(Ordering::Relaxed) - } - - /// Get wallet info synchronization progress. - pub fn info_sync_progress(&self) -> u8 { - self.info_sync_progress.load(Ordering::Relaxed) - } - - /// Check if wallet had an error on synchronization. - pub fn sync_error(&self) -> bool { - self.sync_error.load(Ordering::Relaxed) - } - - /// Set an error for wallet on synchronization. - pub fn set_sync_error(&self, error: bool) { - self.sync_error.store(error, Ordering::Relaxed); - } - - /// Get current wallet synchronization attempts before setting an error. - fn get_sync_attempts(&self) -> u8 { - self.sync_attempts.load(Ordering::Relaxed) - } - - /// Increment wallet synchronization attempts before setting an error. - fn increment_sync_attempts(&self) { - let mut attempts = self.get_sync_attempts(); - attempts += 1; - self.sync_attempts.store(attempts, Ordering::Relaxed); - } - - /// Reset wallet synchronization attempts. - fn reset_sync_attempts(&self) { - self.sync_attempts.store(0, Ordering::Relaxed); - } - /// Get wallet data. pub fn get_data(&self) -> Option { let r_data = self.data.read(); @@ -656,60 +674,57 @@ impl Wallet { } /// Open Slatepack message with the wallet. - pub fn open_message(&self, message: String) { - if !self.is_open() { + fn open_message(&self, message: &String) { + if !self.is_open() || message.is_empty() { return; } - if message.is_empty() { - let mut res_w = self.message_result.write(); - *res_w = Some(Err(Error::InvalidSlatepackData("".to_string()))); - } let w = self.clone(); let load = self.message_opening.clone(); - let res = self.message_result.clone(); let msg = message.clone(); thread::spawn(move || { load.store(true, Ordering::Relaxed); - if let Ok(slate) = w.parse_slatepack(&msg) { + if let Ok(s) = w.parse_slatepack(&msg) { // Check if message with same id and state already exists. - let exists = w.slatepack_exists(&slate); + let exists = w.slatepack_exists(&s); if exists { - if let Some(tx) = w.tx_by_slate(&slate) { - let mut w_res = res.write(); - *w_res = Some(Ok(tx)); - } + w.on_tx_result(&s); load.store(false, Ordering::Relaxed); return; } // Create response or finalize. - let r = match slate.state { + match s.state { SlateState::Standard1 | SlateState::Invoice1 => { - if slate.state != SlateState::Standard1 { - w.pay(&msg) + if s.state != SlateState::Standard1 { + if let Ok(s) = w.pay(&s) { + sync_wallet_data(&w, false); + w.on_tx_result(&s); + } } else { - w.receive(&msg) + if let Ok(s) = w.receive(&s) { + sync_wallet_data(&w, false); + w.on_tx_result(&s); + } } } SlateState::Standard2 | SlateState::Invoice2 => { - w.finalize(&msg) - } - _ => { - if let Some(tx) = w.tx_by_slate(&slate) { - Ok(tx) - } else { - Err(Error::InvalidSlatepackData(msg)) + 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)); + } } } + _ => {} }; - if w.is_open() { - let mut w_res = res.write(); - *w_res = Some(r); - } - } else { - if w.is_open() { - let mut w_res = res.write(); - *w_res = Some(Err(Error::InvalidSlatepackData(msg))); - } } load.store(false, Ordering::Relaxed); }); @@ -720,22 +735,8 @@ impl Wallet { self.message_opening.load(Ordering::Relaxed) } - /// Consume Slatepack message result. - pub fn consume_message_result(&self) -> Option> { - let res = { - let r_mes = self.message_result.read(); - r_mes.clone() - }; - // Clear message result. - if res.is_some() { - let mut w_mes = self.message_result.write(); - *w_mes = None; - } - res - } - /// Parse Slatepack message into [`Slate`]. - pub fn parse_slatepack(&self, text: &String) -> Result { + fn parse_slatepack(&self, text: &String) -> Result { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let mut api = Owner::new(instance, None); @@ -757,7 +758,7 @@ impl Wallet { })?; // Write Slatepack message to file. - let slatepack_dir = self.get_config().get_slatepack_path(&slate); + let slatepack_dir = self.get_config().get_slate_path(&slate); let mut output = File::create(slatepack_dir)?; output.write_all(message.as_bytes())?; output.sync_all()?; @@ -766,72 +767,26 @@ impl Wallet { /// Check if Slatepack file exists. pub fn slatepack_exists(&self, slate: &Slate) -> bool { - let slatepack_path = self.get_config().get_slatepack_path(slate); + let slatepack_path = self.get_config().get_slate_path(slate); fs::exists(slatepack_path).unwrap() } - /// Get last stored Slatepack message for transaction. - pub fn read_slatepack_by_tx(&self, tx: &WalletTransaction) -> Option<(Slate, String)> { - 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.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 { - Some(SlateState::Standard1) - } else { - Some(SlateState::Invoice1) - } - } else { - if tx.data.tx_type == TxLogEntryType::TxReceived { - Some(SlateState::Standard2) - } else { - Some(SlateState::Invoice2) - } - } - } else { - None - }; - // Get slate from state by reading Slatepack message file. - if let Some(st) = state { - let mut s = Slate::blank(0, false); - s.id = slate_id; - s.state = st; - let slatepack_path = self.get_config().get_slatepack_path(&s); - if let Ok(m) = fs::read_to_string(slatepack_path) { - if let Ok(s) = self.parse_slatepack(&m) { - slate = Some((s, m)); - } - } - } - } - slate - } - - /// Get transaction for [`Slate`] id. - pub fn tx_by_slate(&self, slate: &Slate) -> Option { - if let Some(data) = self.get_data() { - let data_txs = data.txs.unwrap(); - let txs = data_txs.iter().map(|tx| tx.clone()).filter(|tx| { - tx.data.tx_slate_id == Some(slate.id) - }).collect::>(); - return if let Some(tx) = txs.get(0) { - Some(tx.clone()) - } else { - None - } + /// Read Slatepack file content. + pub fn read_slatepack(&self, tx: &WalletTransaction) -> Option { + let slatepack_path = self.get_config().get_tx_slate_path(tx); + if let Ok(m) = fs::read_to_string(slatepack_path) { + return Some(m); } None } /// Initialize a transaction to send amount, return request for funds receiver. - pub fn send(&self, amount: u64, receiver: Option) -> Result { + fn send(&self, a: u64, r: Option) -> Result { let config = self.get_config(); let args = InitTxArgs { - payment_proof_recipient_address: receiver, + payment_proof_recipient_address: r, src_acct_name: Some(config.account), - amount, + amount: a, minimum_confirmations: config.min_confirmations, num_change_outputs: 1, selection_strategy_is_use_all: false, @@ -848,39 +803,16 @@ impl Wallet { // Create Slatepack message response. let _ = self.create_slatepack_message(&slate)?; - // Refresh wallet info. - sync_wallet_data(&self, false); - - let tx = self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?; - Ok(tx) + Ok(slate) } - /// Send amount to provided address with Tor transport. - pub async fn send_tor(&mut self, - amount: u64, - addr: &SlatepackAddress) -> Result { - // Initialize transaction. - let tx = self.send(amount, Some(addr.clone()))?; - let slate_res = self.read_slatepack_by_tx(&tx); - if slate_res.is_none() { - return Err(Error::GenericError("Slate not found".to_string())); - } - let (slate, _) = slate_res.unwrap(); + /// Send slate to Tor address. + async fn send_tor(&self, slate: &Slate, addr: &SlatepackAddress) -> Result { + self.on_tx_action(slate.id.to_string(), Some(WalletTransactionAction::SendingTor)); - // Function to cancel initialized tx in case of error. - let cancel_tx = || { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let id = slate.clone().id; - if cancel_tx(instance, None, &None, None, Some(id.clone())).is_ok() { - sync_wallet_data(&self, false); - } - }; - - // Initialize parameters. let tor_addr = OnionV3Address::try_from(addr).unwrap().to_http_str(); let url = format!("{}/v2/foreign", tor_addr); - let slate_send = VersionedSlate::into_version(slate.clone(), SlateVersion::V4).unwrap(); + let slate_send = VersionedSlate::into_version(slate.clone(), SlateVersion::V4)?; let body = json!({ "jsonrpc": "2.0", "method": "receive_tx", @@ -895,60 +827,30 @@ impl Wallet { // Send request to receiver. let req_res = Tor::post(body, url).await; if req_res.is_none() { - cancel_tx(); - return Err(Error::GenericError("Tor post error".to_string())); + return Err(Error::GenericError("Tor request error".to_string())); } // Parse response. let res: Value = serde_json::from_str(&req_res.unwrap()).unwrap(); if res["error"] != json!(null) { - cancel_tx(); - return Err(Error::GenericError("Tx error".to_string())); + return Err(Error::GenericError("Response error".to_string())); } let slate_value = res["result"]["Ok"].clone(); + let res = Slate::deserialize_upgrade(&serde_json::to_string(&slate_value).unwrap()); - let mut ret_slate = None; - match Slate::deserialize_upgrade(&serde_json::to_string(&slate_value).unwrap()) { - Ok(s) => { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let mut api = Owner::new(instance, None); - controller::owner_single_use(None, None, Some(&mut api), |api, m| { - // Finalize transaction. - return if let Ok(slate) = api.finalize_tx(m, &s) { - ret_slate = Some(slate.clone()); - // Save Slatepack message to file. - let _ = self.create_slatepack_message(&slate).unwrap_or("".to_string()); - // Post transaction to blockchain. - let result = self.post(&slate); - match result { - Ok(_) => { - Ok(()) - } - Err(e) => { - Err(e) - } - } - } else { - Err(Error::GenericError("Tx finalization error".to_string())) - }; - })?; - } - Err(_) => {} - }; + // Clear tx action. + self.on_tx_action(slate.id.to_string(), None); - // Cancel transaction on error. - if ret_slate.is_none() { - cancel_tx(); - return Err(Error::GenericError("Tx error".to_string())); - } - let tx = self.tx_by_slate(ret_slate.as_ref().unwrap()) - .ok_or(Error::GenericError("No tx found".to_string()))?; - Ok(tx) + res + } + + /// Check if request to send funds is creating. + pub fn send_creating(&self) -> bool { + self.send_creating.load(Ordering::Relaxed) } /// Initialize an invoice transaction to receive amount, return request for funds sender. - pub fn issue_invoice(&self, amount: u64) -> Result { + fn issue_invoice(&self, amount: u64) -> Result { let args = IssueInvoiceTxArgs { dest_acct_name: None, amount, @@ -962,129 +864,131 @@ impl Wallet { // Create Slatepack message response. let _ = self.create_slatepack_message(&slate)?; - // Refresh wallet info. - sync_wallet_data(&self, false); + Ok(slate) + } - let tx = self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?; - Ok(tx) + /// Check if request to receive funds is creating. + pub fn invoice_creating(&self) -> bool { + self.invoice_creating.load(Ordering::Relaxed) } /// Handle message from the invoice issuer to send founds, return response for funds receiver. - pub fn pay(&self, message: &String) -> Result { - if let Ok(slate) = self.parse_slatepack(message) { - let config = self.get_config(); - let args = InitTxArgs { - src_acct_name: None, - amount: slate.amount, - minimum_confirmations: config.min_confirmations, - selection_strategy_is_use_all: false, - ..Default::default() - }; - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let api = Owner::new(instance, None); - let slate = api.process_invoice_tx(None, &slate, args)?; - api.tx_lock_outputs(None, &slate)?; + fn pay(&self, slate: &Slate) -> Result { + let config = self.get_config(); + let args = InitTxArgs { + src_acct_name: None, + amount: slate.amount, + minimum_confirmations: config.min_confirmations, + selection_strategy_is_use_all: false, + ..Default::default() + }; + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let api = Owner::new(instance, None); + let slate = api.process_invoice_tx(None, &slate, args)?; + api.tx_lock_outputs(None, &slate)?; - // Create Slatepack message response. - let _ = self.create_slatepack_message(&slate)?; + // Create Slatepack message response. + let _ = self.create_slatepack_message(&slate)?; - // Refresh wallet info. - sync_wallet_data(&self, false); - - Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?) - } else { - Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) - } + Ok(slate) } - /// Handle message to receive funds, return response to sender. - pub fn receive(&self, message: &String) -> Result { - if let Ok(mut slate) = self.parse_slatepack(message) { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let api = Owner::new(instance, None); - controller::foreign_single_use(api.wallet_inst.clone(), None, |api| { - slate = api.receive_tx(&slate, Some(self.get_config().account.as_str()), None)?; - Ok(()) - })?; - // Create Slatepack message response. - let _ = self.create_slatepack_message(&slate)?; + /// Create response to sender to receive funds. + fn receive(&self, slate: &Slate) -> Result { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let api = Owner::new(instance, None); + let mut slate = slate.clone(); + controller::foreign_single_use(api.wallet_inst.clone(), None, |api| { + slate = api.receive_tx(&slate, Some(self.get_config().account.as_str()), None)?; + Ok(()) + })?; - // Refresh wallet info. - sync_wallet_data(&self, false); + // Create Slatepack message response. + let _ = self.create_slatepack_message(&slate)?; - Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?) - } else { - Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) - } + Ok(slate) } - /// Finalize transaction from provided message as sender or invoice issuer with Dandelion. - pub fn finalize(&self, message: &String) -> Result { - if let Ok(mut slate) = self.parse_slatepack(message) { - let r_inst = self.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - let api = Owner::new(instance, None); - slate = api.finalize_tx(None, &slate)?; - // Save Slatepack message to file. - let _ = self.create_slatepack_message(&slate)?; + /// Finalize transaction from provided message as sender or invoice issuer. + fn finalize(&self, slate: &Slate) -> Result { + self.on_tx_action(slate.id.to_string(), Some(WalletTransactionAction::Finalizing)); - // Post transaction to blockchain. - let tx = self.post(&slate)?; + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let api = Owner::new(instance, None); + let slate = api.finalize_tx(None, slate)?; - // Refresh wallet info. - sync_wallet_data(&self, false); + // Save Slatepack message to file. + let _ = self.create_slatepack_message(&slate)?; - Ok(tx) - } else { - Err(Error::SlatepackDeser("Slatepack parsing error".to_string())) - } + // Clear tx action. + self.on_tx_action(slate.id.to_string(), None); + + Ok(slate) } /// Post transaction to blockchain. - fn post(&self, slate: &Slate) -> Result { - // Post transaction to blockchain. + fn post(&self, slate: &Slate) -> Result<(), Error> { + self.on_tx_action(slate.id.to_string(), Some(WalletTransactionAction::Posting)); + let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let api = Owner::new(instance, None); api.post_tx(None, slate, self.can_use_dandelion())?; - // Refresh wallet info. - sync_wallet_data(&self, false); + // Clear tx action. + self.on_tx_action(slate.id.to_string(), None); - Ok(self.tx_by_slate(&slate).ok_or(Error::GenericError("No tx found".to_string()))?) + Ok(()) } /// Cancel transaction. - pub fn cancel(&self, id: u32) { - // Setup cancelling status. - { - 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.id == id { - tx.cancelling = true; - tx.can_finalize = false; - } - tx.clone() - }).collect::>(); - data.txs = Some(txs); - *w_data = Some(data); - } + fn cancel(&self, tx: &WalletTransaction) -> Result<(), Error> { + let id = tx.data.tx_slate_id.unwrap().to_string(); + self.on_tx_action(id.clone(), Some(WalletTransactionAction::Cancelling)); - let wallet = self.clone(); - thread::spawn(move || { - // Wait sync to finish. - if wallet.syncing() { - thread::sleep(Duration::from_millis(1000)); - } - let r_inst = wallet.instance.as_ref().read(); - let instance = r_inst.clone().unwrap(); - if cancel_tx(instance, None, &None, Some(id), None).is_ok() { - sync_wallet_data(&wallet, false); - } - }); + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + cancel_tx(instance, None, &None, Some(tx.data.id), None)?; + + // Clear tx action. + self.on_tx_action(id, None); + + Ok(()) + } + + /// Update transaction action status. + fn on_tx_action(&self, id: String, action: Option) { + let mut w_data = self.data.write(); + w_data.as_mut().unwrap().on_tx_action(id, action); + } + + /// Update transaction action error status. + fn on_tx_error(&self, id: String, err: Option) { + let mut w_data = self.data.write(); + 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()); + } + + /// Consume identifier of successful transaction task. + pub fn consume_tx_task_result(&self) -> Option { + let res = { + let r_res = self.task_result_slate_id.read(); + r_res.clone() + }; + // Clear result. + if res.is_some() { + let mut w_res = self.task_result_slate_id.write(); + *w_res = None; + } + res } /// Get possible transaction confirmation height from db or node. @@ -1115,6 +1019,17 @@ impl Wallet { Ok(tx_height) } + /// Get transaction from database. + fn get_tx(&self, tx_id: u32) -> Option { + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let api = Owner::new(instance, None); + if let Ok(s) = api.get_stored_tx(None, Some(tx_id), None) { + return s; + } + None + } + /// Change wallet password. pub fn change_password(&self, old: String, new: String) -> Result<(), Error> { let r_inst = self.instance.as_ref().read(); @@ -1211,6 +1126,30 @@ const SYNC_ATTEMPTS: u8 = 10; /// Launch thread to sync wallet data from node. fn start_sync(wallet: Wallet) -> Thread { + // Start tasks thread. + let (tx, rx) = mpsc::channel(); + { + let mut w_tasks = wallet.tasks_sender.write(); + *w_tasks = Some(tx); + } + let wallet_thread = wallet.clone(); + thread::spawn(move || loop { + let wallet_task = wallet_thread.clone(); + if let Ok(task) = rx.recv() { + thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + handle_task(&wallet_task, task).await; + })}); + } + if wallet_thread.is_closing() || !wallet_thread.is_open() { + break; + } + }); + // Reset progress values. wallet.info_sync_progress.store(0, Ordering::Relaxed); wallet.repair_progress.store(0, Ordering::Relaxed); @@ -1275,28 +1214,31 @@ fn start_sync(wallet: Wallet) -> Thread { sync_wallet_data(&wallet, false); } - // Start Foreign API listener if API server is not running. - let mut api_server_running = { - wallet.foreign_api_server.read().is_some() - }; - if !api_server_running && wallet.is_open() && !wallet.is_closing() { - match start_api_server(&wallet) { - Ok(api_server) => { - let mut api_server_w = wallet.foreign_api_server.write(); - *api_server_w = Some(api_server); - api_server_running = true; + if wallet.is_open() && !wallet.is_closing() { + // Start Foreign API listener if not running. + let mut api_server_running = { + wallet.foreign_api_server.read().is_some() + }; + if !api_server_running { + match start_api_server(&wallet) { + Ok(api_server) => { + let mut api_server_w = wallet.foreign_api_server.write(); + *api_server_w = Some(api_server); + api_server_running = true; + } + Err(_) => {} } - Err(_) => {} } - } - // Start Tor service if API server is running and wallet is open. - if wallet.auto_start_tor_listener() && wallet.is_open() && !wallet.is_closing() && - api_server_running && !Tor::is_service_running(&wallet.identifier()) { - let r_foreign_api = wallet.foreign_api_server.read(); - let api = r_foreign_api.as_ref().unwrap(); - if let Ok(sec_key) = wallet.secret_key() { - Tor::start_service(api.1, sec_key, &wallet.identifier()); + // Start unfailed Tor service if API server is running. + let service_id = wallet.identifier(); + if wallet.auto_start_tor_listener() && api_server_running && + !Tor::is_service_failed(&service_id) { + let r_foreign_api = wallet.foreign_api_server.read(); + let api = r_foreign_api.as_ref().unwrap(); + if let Ok(sec_key) = wallet.get_secret_key() { + Tor::start_service(api.1, sec_key, &wallet.identifier()); + } } } @@ -1328,7 +1270,116 @@ fn start_sync(wallet: Wallet) -> Thread { }).thread().clone() } -/// Retrieve [`WalletData`] from local base or node. +/// Handle wallet task. +async fn handle_task(w: &Wallet, t: WalletTask) { + let send_tor = async |s: &Slate, r: &SlatepackAddress| { + match w.send_tor(&s, r).await { + Ok(s) => { + match w.finalize(&s) { + Ok(s) => { + match w.post(&s) { + Ok(_) => { + sync_wallet_data(&w, false); + w.on_tx_result(&s); + } + Err(e) => { + w.on_tx_error(s.id.to_string(), Some(e)); + } + } + } + Err(e) => { + w.on_tx_error(s.id.to_string(), Some(e)); + } + } + } + Err(e) => { + w.on_tx_error(s.id.to_string(), Some(e)); + w.on_tx_result(&s); + } + } + }; + match &t { + WalletTask::OpenMessage(m) => { + w.open_message(m); + } + 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); + if let Some(r) = r { + send_tor(&s, r).await; + } else { + w.on_tx_result(&s); + } + } + w.send_creating.store(false, Ordering::Relaxed); + } + WalletTask::SendTor(id, r) => { + if let Some(s) = w.get_tx(*id) { + send_tor(&s, r).await; + } + } + WalletTask::Receive(a) => { + 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.invoice_creating.store(false, Ordering::Relaxed); + }, + WalletTask::Finalize(s, id) => { + let slate = if let Some(s) = s { + s + } else { + &w.get_tx(*id).unwrap() + }; + match w.finalize(slate) { + Ok(s) => { + match w.post(&s) { + Ok(_) => { + sync_wallet_data(&w, false); + } + Err(e) => { + w.on_tx_error(slate.id.to_string(), Some(e)); + } + } + } + Err(e) => { + w.on_tx_error(slate.id.to_string(), Some(e)); + } + } + } + WalletTask::Post(s, id) => { + let slate = if let Some(s) = s { + s + } else { + &w.get_tx(*id).unwrap() + }; + match w.post(slate) { + Ok(_) => { + sync_wallet_data(&w, false); + } + Err(e) => { + w.on_tx_error(slate.id.to_string(), Some(e)); + } + } + + } + WalletTask::Cancel(tx) => { + match w.cancel(tx) { + Ok(_) => { + sync_wallet_data(&w, false); + } + Err(e) => { + let id = tx.data.tx_slate_id.unwrap().to_string(); + w.on_tx_error(id, Some(e)); + } + } + } + }; +} + +/// Refresh [`WalletData`] from local base or node. fn sync_wallet_data(wallet: &Wallet, from_node: bool) { // Update info sync progress at separate thread. let wallet_info = wallet.clone(); @@ -1356,7 +1407,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { let r_inst = wallet.instance.as_ref().read(); if r_inst.is_some() { let instance = r_inst.clone().unwrap(); - if let Ok(info) = retrieve_summary_info( + if let Ok((_, info)) = retrieve_summary_info( instance.clone(), None, &Some(info_tx), @@ -1365,20 +1416,20 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { ) { // Do not retrieve txs if wallet was closed or its first sync. if !wallet.is_open() || wallet.is_closing() || - (!from_node && info.1.last_confirmed_height == 0) { + (!from_node && info.last_confirmed_height == 0) { return; } - if wallet.info_sync_progress() == 100 || !from_node { - // Setup accounts data. - let last_height = info.1.last_confirmed_height; - let spendable = if wallet.get_data().is_none() { - None - } else { - Some(info.1.amount_currently_spendable) - }; - update_accounts(wallet, last_height, spendable); + // Setup accounts data. + let last_height = info.last_confirmed_height; + let spendable = if wallet.get_data().is_none() { + None + } else { + Some(info.amount_currently_spendable) + }; + update_accounts(wallet, last_height, spendable); + if wallet.info_sync_progress() == 100 || !from_node { // Update wallet info. { let mut w_data = wallet.data.write(); @@ -1387,132 +1438,12 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { } else { None }; - *w_data = Some(WalletData { info: info.1.clone(), txs }); + *w_data = Some(WalletData { info: info.clone(), txs }); } - // Retrieve txs from local database. - let txs_args = RetrieveTxQueryArgs { - exclude_cancelled: Some(true), - sort_field: Some(RetrieveTxQuerySortField::CreationTimestamp), - sort_order: Some(RetrieveTxQuerySortOrder::Desc), - ..Default::default() - }; - if let Ok(txs) = retrieve_txs(instance.clone(), - None, - &None, - false, - None, - None, - Some(txs_args)) { - // Exit if wallet was closed. - if !wallet.is_open() { - return; - } - // Reset sync attempts. + // Update wallet transactions. + if update_txs(wallet, instance.clone(), info).is_ok() { wallet.reset_sync_attempts(); - - // Filter transactions for current account. - 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 && tx.tx_slate_id.is_some() - } - Err(_) => { - true - } - } - }).collect::>(); - - // Initialize tx confirmation height storage. - let tx_height_store = TxHeightStore::new(config.get_extra_db_path()); - let data = wallet.get_data().unwrap(); - let data_txs = data.txs.unwrap_or(vec![]); - - let mut new_txs: Vec = vec![]; - for tx in &account_txs { - // Setup transaction amount. - let amount = if tx.amount_debited > tx.amount_credited { - tx.amount_debited - tx.amount_credited - } else { - 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); - let mut is_response = false; - 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.slatepack_exists(&slate) - }; - is_response = { - let mut slate = Slate::blank(1, false); - slate.id = tx.tx_slate_id.unwrap(); - slate.state = match tx.tx_type { - TxLogEntryType::TxReceived => SlateState::Standard2, - _ => SlateState::Invoice2 - }; - wallet.slatepack_exists(&slate) - }; - 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.slatepack_exists(&slate) - }; - initial_state && !is_response && !finalizing - } else { - false - }; - - // Setup confirmation height and cancelling status - let mut conf_height = wallet.tx_height(tx, &tx_height_store).unwrap_or(None); - let mut cancelling = false; - for t in &data_txs { - if t.data.id == tx.id { - if conf_height.is_none() { - conf_height = t.height; - } - if t.cancelling && - tx.tx_type != TxLogEntryType::TxReceivedCancelled && - tx.tx_type != TxLogEntryType::TxSentCancelled { - cancelling = true; - } - break; - } - } - - // Add transaction to the list. - new_txs.push(WalletTransaction { - data: tx.clone(), - amount, - cancelling, - is_response, - can_finalize, - finalizing, - height: conf_height, - }); - } - - // Update wallet txs. - let mut w_data = wallet.data.write(); - let info = if w_data.is_some() { - w_data.clone().unwrap().info - } else { - info.1 - }; - *w_data = Some(WalletData { info, txs: Some(new_txs) }); return; } } @@ -1522,8 +1453,8 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { // Reset progress. wallet.info_sync_progress.store(0, Ordering::Relaxed); - // Exit if wallet was closed. - if !wallet.is_open() { + // Exit if wallet was closed or closing. + if !wallet.is_open() || wallet.is_closing() { return; } @@ -1541,6 +1472,117 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { } } +/// Update wallet transactions. +fn update_txs(wallet: &Wallet, instance: WalletInstance, info: WalletInfo) + -> Result<(), Error> { + // Retrieve txs from local database. + let txs_args = RetrieveTxQueryArgs { + exclude_cancelled: Some(false), + sort_field: Some(RetrieveTxQuerySortField::CreationTimestamp), + sort_order: Some(RetrieveTxQuerySortOrder::Desc), + ..Default::default() + }; + let txs = retrieve_txs(instance, None, &None, false, None, None, Some(txs_args.clone()))?; + + // Exit if wallet was closed. + if !wallet.is_open() || wallet.is_closing() { + return Err(Error::GenericError("Wallet is not open".to_string())); + } + + // Filter transactions for current account. + 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 && tx.tx_slate_id.is_some() + } + Err(_) => { + true + } + } + }).collect::>(); + + // Initialize tx confirmation height storage. + let tx_height_store = TxHeightStore::new(wallet.get_config().get_extra_db_path()); + + let data = wallet.get_data().unwrap(); + let data_txs = data.txs.unwrap_or(vec![]); + let mut new_txs: Vec = vec![]; + for tx in &account_txs { + // Setup transaction amount. + let amount = if tx.amount_debited > tx.amount_credited { + tx.amount_debited - tx.amount_credited + } else { + tx.amount_credited - tx.amount_debited + }; + + // Setup confirmation height, action and state. + let mut height: Option = None; + let mut action: Option = None; + let mut action_error: Option = None; + let mut state: Option = None; + for t in &data_txs { + if t.data.id == tx.id { + height = t.height; + action = t.action.clone(); + action_error = t.action_error.clone(); + state = Some(t.state.clone()); + break; + } + } + if tx.kernel_lookup_min_height.is_some() && + tx.kernel_excess.is_some() && tx.confirmed { + if height.is_none() { + height = wallet.tx_height(tx, &tx_height_store).unwrap_or(None); + } + } + + // Setup transaction state for unconfirmed tx or for initial update. + let unconfirmed = !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent || + tx.tx_type == TxLogEntryType::TxReceived); + if unconfirmed || state.is_none() { + 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 + }; + // Transaction was finalized. + if wallet.slatepack_exists(&slate) { + state = Some(slate.state); + } else { + slate.id = tx.tx_slate_id.unwrap(); + slate.state = match tx.tx_type { + TxLogEntryType::TxReceived => SlateState::Standard2, + _ => SlateState::Invoice2 + }; + // Transaction signed to be finalized. + if wallet.slatepack_exists(&slate) { + state = Some(slate.state); + } else { + // Transaction just was created. + state = Some(match tx.tx_type { + TxLogEntryType::TxReceived => SlateState::Invoice1, + _ => SlateState::Standard1 + }); + } + } + } + // Add transaction to the list. + new_txs.push(WalletTransaction { + data: tx.clone(), + state: state.unwrap(), + amount, + height, + action, + action_error, + }); + } + // Update wallet txs. + let mut w_data = wallet.data.write(); + *w_data = Some(WalletData { info, txs: Some(new_txs) }); + Ok(()) +} + /// Start Foreign API server to receive txs over transport and mining rewards. fn start_api_server(wallet: &Wallet) -> Result<(ApiServer, u16), Error> { let host = "127.0.0.1"; @@ -1588,13 +1630,13 @@ fn start_api_server(wallet: &Wallet) -> Result<(ApiServer, u16), Error> { } /// Update wallet accounts data. -fn update_accounts(wallet: &Wallet, current_height: u64, current_spendable: Option) { +fn update_accounts(wallet: &Wallet, height: u64, spendable: Option) { let current_account = wallet.get_config().account; - if let Some(spendable) = current_spendable { + if let Some(amount) = spendable { let mut accounts = wallet.accounts.read().clone(); for a in accounts.iter_mut() { if a.label == current_account { - a.spendable_amount = spendable; + a.spendable_amount = amount; } } // Save accounts data. @@ -1609,7 +1651,7 @@ fn update_accounts(wallet: &Wallet, current_height: u64, current_spendable: Opti for a in api.accounts(m)? { api.set_active_account(m, a.label.as_str())?; // Calculate account balance. - if let Some(spendable_amount) = wallet.account_balance(current_height, api, m) { + if let Some(spendable_amount) = wallet.account_balance(height, api, m) { accounts.push(WalletAccount { spendable_amount, label: a.label, @@ -1678,4 +1720,4 @@ fn repair_wallet(wallet: &Wallet) { // Reset repair progress. wallet.repair_progress.store(0, Ordering::Relaxed); -} +} \ No newline at end of file