diff --git a/Cargo.lock b/Cargo.lock index c0dc8811..6d6c685a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4043,6 +4043,7 @@ dependencies = [ "local-ip-address", "log", "nokhwa", + "num-bigint 0.4.6", "parking_lot 0.12.5", "pin-project", "qrcode", diff --git a/Cargo.toml b/Cargo.toml index 7491720d..f9e68abc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -88,6 +88,7 @@ hyper-proxy2 = "0.1.0" hyper-tls = "0.6.0" async-std = "1.13.2" uuid = { version = "0.8.2", features = ["v4"] } +num-bigint = "0.4.6" ## tor arti-client = { version = "0.38.0", features = ["pt-client", "static", "onion-service-service", "onion-service-client"] } diff --git a/src/gui/views/network/setup/stratum.rs b/src/gui/views/network/setup/stratum.rs index ed980f0e..b63f2115 100644 --- a/src/gui/views/network/setup/stratum.rs +++ b/src/gui/views/network/setup/stratum.rs @@ -21,7 +21,7 @@ use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, TextEdit, View}; use crate::gui::views::network::settings::NetworkSettings; use crate::gui::views::types::{ContentContainer, ModalPosition}; -use crate::gui::views::wallets::modals::WalletsModal; +use crate::gui::views::wallets::modals::WalletListModal; use crate::node::{Node, NodeConfig}; use crate::wallet::{WalletConfig, WalletList}; @@ -30,7 +30,7 @@ pub struct StratumSetup { /// Wallet list to select for mining rewards. wallets: WalletList, /// Wallets [`Modal`] content. - wallets_modal: WalletsModal, + wallets_modal: WalletListModal, /// IP Addresses available at system. available_ips: Vec, @@ -80,7 +80,7 @@ impl Default for StratumSetup { Self { wallets: WalletList::default(), - wallets_modal: WalletsModal::new(wallet_id, None, false), + wallets_modal: WalletListModal::new(wallet_id, None, false), available_ips: NodeConfig::get_ip_addrs(), stratum_port_edit: port, stratum_port_available_edit: is_port_available, @@ -238,7 +238,7 @@ impl ContentContainer for StratumSetup { impl StratumSetup { /// Show wallet selection [`Modal`]. fn show_wallets_modal(&mut self) { - self.wallets_modal = WalletsModal::new(NodeConfig::get_stratum_wallet_id(), None, false); + self.wallets_modal = WalletListModal::new(NodeConfig::get_stratum_wallet_id(), None, false); // Show modal. Modal::new(WALLET_SELECTION_MODAL) .position(ModalPosition::Center) diff --git a/src/gui/views/wallets/content.rs b/src/gui/views/wallets/content.rs index 7c9bbdf2..f02c62f6 100644 --- a/src/gui/views/wallets/content.rs +++ b/src/gui/views/wallets/content.rs @@ -16,12 +16,13 @@ use std::time::Duration; use egui::scroll_area::ScrollBarVisibility; use egui::{Align, CornerRadius, Id, Layout, Margin, RichText, ScrollArea, StrokeKind}; use egui::os::OperatingSystem; -use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE}; + +use crate::gui::icons::{ARROW_LEFT, CARET_RIGHT, COMPUTER_TOWER, FOLDER_OPEN, FOLDER_PLUS, GEAR, GEAR_FINE, GLOBE, GLOBE_SIMPLE, LOCK_KEY, PLUS, SIDEBAR_SIMPLE, SUITCASE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::settings::SettingsContent; use crate::gui::views::types::{ContentContainer, LinePosition, ModalPosition, TitleContentType, TitleType}; use crate::gui::views::wallets::creation::WalletCreationContent; -use crate::gui::views::wallets::modals::{AddWalletModal, OpenWalletModal, WalletConnectionModal, WalletsModal}; +use crate::gui::views::wallets::modals::{AddWalletModal, OpenWalletModal, WalletSettingsModal, WalletListModal}; use crate::gui::views::wallets::wallet::types::{wallet_status_text, WalletContentContainer}; use crate::gui::views::wallets::WalletContent; use crate::gui::views::{Content, Modal, TitlePanel, View}; @@ -29,6 +30,7 @@ use crate::gui::Colors; use crate::wallet::types::{ConnectionMethod, WalletTask}; use crate::wallet::{Wallet, WalletList}; use crate::AppConfig; +use crate::gui::views::wallets::wallet::RecoverySettings; /// Wallets content. pub struct WalletsContent { @@ -39,10 +41,10 @@ pub struct WalletsContent { add_wallet_modal_content: AddWalletModal, /// Wallet opening [`Modal`] content. open_wallet_content: OpenWalletModal, - /// Wallet connection selection [`Modal`] content. - conn_selection_content: WalletConnectionModal, + /// Wallet settings [`Modal`] content. + wallet_settings_content: WalletSettingsModal, /// Wallet selection [`Modal`] content. - wallet_selection_content: WalletsModal, + wallet_selection_content: WalletListModal, /// Selected [`Wallet`] content. wallet_content: WalletContent, @@ -53,19 +55,23 @@ pub struct WalletsContent { settings_content: Option, } +/// Identifier for [`Modal`] to add the wallet. const ADD_WALLET_MODAL: &'static str = "wallets_add_modal"; +/// Identifier for [`Modal`] to open the wallet. const OPEN_WALLET_MODAL: &'static str = "wallets_open_wallet"; -const SELECT_CONNECTION_MODAL: &'static str = "wallets_select_conn_modal"; +/// Identifier for wallet settings [`Modal`]. +const WALLET_SETTINGS_MODAL: &'static str = "wallets_settings_modal"; +/// Identifier for wallet selection [`Modal`]. const SELECT_WALLET_MODAL: &'static str = "wallets_select_modal"; impl Default for WalletsContent { fn default() -> Self { Self { wallets: WalletList::default(), - wallet_selection_content: WalletsModal::new(None, None, true), + wallet_selection_content: WalletListModal::new(None, None, true), open_wallet_content: OpenWalletModal::new(), add_wallet_modal_content: AddWalletModal::default(), - conn_selection_content: WalletConnectionModal::new(ConnectionMethod::Integrated), + wallet_settings_content: WalletSettingsModal::new(ConnectionMethod::Integrated), wallet_content: WalletContent::default(), creation_content: None, settings_content: None, @@ -78,8 +84,9 @@ impl ContentContainer for WalletsContent { vec![ ADD_WALLET_MODAL, OPEN_WALLET_MODAL, - SELECT_CONNECTION_MODAL, + WALLET_SETTINGS_MODAL, SELECT_WALLET_MODAL, + Self::DELETE_CONFIRMATION_MODAL ] } @@ -103,8 +110,8 @@ impl ContentContainer for WalletsContent { true }); }, - SELECT_CONNECTION_MODAL => { - self.conn_selection_content.ui(ui, modal, cb, |conn| { + WALLET_SETTINGS_MODAL => { + self.wallet_settings_content.ui(ui, modal, cb, |conn| { if let Some(w) = self.wallets.selected().as_ref() { w.update_connection(&conn); } @@ -125,6 +132,11 @@ impl ContentContainer for WalletsContent { } } } + Self::DELETE_CONFIRMATION_MODAL => { + if let Some(w) = self.wallets.selected().as_ref() { + RecoverySettings::deletion_modal_ui(ui, w); + } + } _ => {} } } @@ -314,6 +326,9 @@ impl ContentContainer for WalletsContent { } impl WalletsContent { + /// Identifier for wallet deletion confirmation [`Modal`]. + pub const DELETE_CONFIRMATION_MODAL: &'static str = "wallets_delete_confirmation_modal"; + /// Called to navigate back, return `true` if action was not consumed. pub fn on_back(&mut self, cb: &dyn PlatformCallbacks) -> bool { if self.showing_settings() { @@ -377,7 +392,7 @@ impl WalletsContent { self.show_opening_modal(&w, data, cb); } } else { - self.wallet_selection_content = WalletsModal::new(None, data, true); + self.wallet_selection_content = WalletListModal::new(None, data, true); Modal::new(SELECT_WALLET_MODAL) .position(ModalPosition::Center) .title(t!("network_settings.choose_wallet")) @@ -548,14 +563,14 @@ impl WalletsContent { self.show_opening_modal(wallet, None, cb); }); if !wallet.is_repairing() { - View::item_button(ui, CornerRadius::default(), GLOBE, None, || { + View::item_button(ui, CornerRadius::default(), GEAR_FINE, None, || { self.select_wallet(wallet, None, cb); - self.conn_selection_content = - WalletConnectionModal::new(wallet.get_current_connection()); + let conn = wallet.get_current_connection(); + self.wallet_settings_content = WalletSettingsModal::new(conn); // Show connection selection modal. - Modal::new(SELECT_CONNECTION_MODAL) + Modal::new(WALLET_SETTINGS_MODAL) .position(ModalPosition::CenterTop) - .title(t!("wallets.conn_method")) + .title(t!("wallets.settings")) .show(); }); } diff --git a/src/gui/views/wallets/modals/wallets.rs b/src/gui/views/wallets/modals/list.rs similarity index 99% rename from src/gui/views/wallets/modals/wallets.rs rename to src/gui/views/wallets/modals/list.rs index 5d105e2f..a302f0bf 100644 --- a/src/gui/views/wallets/modals/wallets.rs +++ b/src/gui/views/wallets/modals/list.rs @@ -23,7 +23,7 @@ use crate::wallet::types::ConnectionMethod; use crate::wallet::{Wallet, WalletList}; /// Wallet list [`Modal`] content -pub struct WalletsModal { +pub struct WalletListModal { /// Selected wallet id. selected_id: Option, @@ -34,7 +34,7 @@ pub struct WalletsModal { can_open: bool, } -impl WalletsModal { +impl WalletListModal { /// Create new content instance. pub fn new(selected_id: Option, data: Option, can_open: bool) -> Self { Self { selected_id, data, can_open } diff --git a/src/gui/views/wallets/modals/mod.rs b/src/gui/views/wallets/modals/mod.rs index 615497bd..c3a585f2 100644 --- a/src/gui/views/wallets/modals/mod.rs +++ b/src/gui/views/wallets/modals/mod.rs @@ -12,11 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -mod conn; -pub use conn::*; +mod settings; +pub use settings::*; -mod wallets; -pub use wallets::*; +mod list; +pub use list::*; mod open; pub use open::*; diff --git a/src/gui/views/wallets/modals/conn.rs b/src/gui/views/wallets/modals/settings.rs similarity index 86% rename from src/gui/views/wallets/modals/conn.rs rename to src/gui/views/wallets/modals/settings.rs index 52461062..946abfd1 100644 --- a/src/gui/views/wallets/modals/conn.rs +++ b/src/gui/views/wallets/modals/settings.rs @@ -16,16 +16,18 @@ use egui::{RichText, ScrollArea}; use egui::scroll_area::ScrollBarVisibility; use crate::gui::Colors; -use crate::gui::icons::{CHECK, CHECK_FAT, PLUS_CIRCLE}; +use crate::gui::icons::{CHECK, CHECK_FAT, PLUS_CIRCLE, TRASH}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{Modal, View}; use crate::gui::views::network::ConnectionsContent; use crate::gui::views::network::modals::ExternalConnectionModal; +use crate::gui::views::types::ModalPosition; +use crate::gui::views::wallets::WalletsContent; use crate::wallet::{ConnectionsConfig, ExternalConnection}; use crate::wallet::types::ConnectionMethod; /// Wallet connection selection [`Modal`] content. -pub struct WalletConnectionModal { +pub struct WalletSettingsModal { /// Current connection method. pub conn: ConnectionMethod, @@ -33,7 +35,7 @@ pub struct WalletConnectionModal { new_ext_conn_content: Option } -impl WalletConnectionModal { +impl WalletSettingsModal { /// Create from provided wallet connection. pub fn new(conn: ConnectionMethod) -> Self { Self { @@ -141,6 +143,21 @@ impl WalletConnectionModal { ui.add_space(2.0); View::horizontal_line(ui, Colors::item_stroke()); ui.add_space(6.0); + ui.vertical_centered(|ui| { + // Draw button to delete the wallet. + View::colored_text_button(ui, + format!("{} {}", TRASH, t!("wallets.delete")), + Colors::red(), + Colors::white_or_black(false), || { + Modal::new(WalletsContent::DELETE_CONFIRMATION_MODAL) + .position(ModalPosition::Center) + .title(t!("confirmation")) + .show(); + }); + }); + ui.add_space(6.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(6.0); // Show button to close modal. ui.vertical_centered_justified(|ui| { diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 259f7dad..6dc80fa1 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -360,7 +360,11 @@ impl WalletContent { ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0); let has_wallet_data = wallet.get_data().is_some(); - let can_send = wallet.get_data().unwrap().info.amount_currently_spendable > 0; + let can_send = if has_wallet_data { + wallet.get_data().unwrap().info.amount_currently_spendable > 0 + } else { + false + }; let tabs_amount = if can_send { 5 } else { 4 }; ui.columns(tabs_amount, |columns| { @@ -435,7 +439,7 @@ impl WalletContent { /// Handle wallet task result. fn handle_task_result(&mut self, wallet: &Wallet) { let res = wallet.consume_task_result(); - if res.is_none() { + if res.is_none() || wallet.get_data().is_none() { return; } let (id, t) = res.unwrap(); diff --git a/src/gui/views/wallets/wallet/proof.rs b/src/gui/views/wallets/wallet/proof.rs index acb2bccb..59187a1b 100644 --- a/src/gui/views/wallets/wallet/proof.rs +++ b/src/gui/views/wallets/wallet/proof.rs @@ -1,12 +1,13 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{Id, RichText, ScrollArea}; -use grin_wallet_libwallet::{Error, PaymentProof}; +use grin_util::ToHex; +use grin_wallet_libwallet::{Error, PaymentProof, TxLogEntryType}; use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, FILE_TEXT, SEAL_CHECK}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{FilePickContent, FilePickContentType, Modal, View}; use crate::gui::Colors; -use crate::wallet::types::WalletTask; +use crate::wallet::types::{WalletTask, WalletTransaction}; use crate::wallet::Wallet; pub struct PaymentProofContent { @@ -151,11 +152,19 @@ impl PaymentProofContent { } /// Draw transaction payment proof content to share. - pub fn share_ui(&mut self, ui: &mut egui::Ui, file_name: String, cb: &dyn PlatformCallbacks) { + pub fn share_ui(&mut self, + ui: &mut egui::Ui, + tx: &WalletTransaction, + cb: &dyn PlatformCallbacks) { ui.add_space(6.0); ui.vertical_centered(|ui| { - let desc = format!("{} {}:", SEAL_CHECK, t!("wallets.payment_proof")); - ui.label(RichText::new(desc).size(16.0).color(Colors::inactive_text())); + let (desc_text, color) = if tx.data.tx_type == TxLogEntryType::TxReceived { + (t!("wallets.payment_proof_valid"), Colors::green()) + } else { + (format!("{}:", t!("wallets.payment_proof")), Colors::inactive_text()) + }; + let desc = format!("{} {}", SEAL_CHECK, desc_text); + ui.label(RichText::new(desc).size(16.0).color(color)); }); ui.add_space(6.0); ui.vertical_centered(|ui| { @@ -203,7 +212,7 @@ impl PaymentProofContent { share_text, Colors::blue(), Colors::white_or_black(false), || { - let file_name = format!("{}.txt", file_name); + let file_name = format!("{}.txt", tx.data.kernel_excess.unwrap().to_hex()); let data = self.input_edit.as_bytes().to_vec(); cb.share_data(file_name, data).unwrap_or_default(); Modal::close(); diff --git a/src/gui/views/wallets/wallet/settings/recovery.rs b/src/gui/views/wallets/wallet/settings/recovery.rs index a35a8508..5ec1d7af 100644 --- a/src/gui/views/wallets/wallet/settings/recovery.rs +++ b/src/gui/views/wallets/wallet/settings/recovery.rs @@ -59,7 +59,7 @@ impl WalletContentContainer for RecoverySettings { self.recovery_phrase_modal_ui(ui, wallet, modal, cb); } DELETE_CONFIRMATION_MODAL => { - self.deletion_modal_ui(ui, wallet); + Self::deletion_modal_ui(ui, wallet); } _ => {} } @@ -260,9 +260,7 @@ impl RecoverySettings { } /// Draw wallet deletion [`Modal`] content. - fn deletion_modal_ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet) { + pub fn deletion_modal_ui(ui: &mut egui::Ui, wallet: &Wallet) { ui.add_space(8.0); ui.vertical_centered(|ui| { ui.label(RichText::new(t!("wallets.delete_conf")) diff --git a/src/gui/views/wallets/wallet/transport/content.rs b/src/gui/views/wallets/wallet/transport/content.rs index 40d19ba5..88b4e4e8 100644 --- a/src/gui/views/wallets/wallet/transport/content.rs +++ b/src/gui/views/wallets/wallet/transport/content.rs @@ -88,6 +88,7 @@ impl WalletTransportContent { /// Navigate back on navigation stack. pub fn back(&mut self) { if self.settings_content.is_some() { + Tor::restart_services(); self.settings_content = None; } else if self.qr_address_content.is_some() { self.qr_address_content = None; @@ -134,25 +135,25 @@ impl WalletTransportContent { .with_max_size(320.0)); }); - // Draw button to enable/disable Tor listener for current wallet. let service_id = &wallet.identifier(); - if !Tor::is_service_starting(service_id) && wallet.foreign_api_port().is_some() && - wallet.secret_key().is_some() { - if !Tor::is_service_running(service_id) { - let r = CornerRadius::default(); - View::item_button(ui, r, POWER, Some(Colors::green()), || { - let api_port = wallet.foreign_api_port().unwrap(); - let key = wallet.secret_key().unwrap(); - Tor::start_service(api_port, key, service_id); - }); - } else { - let r = CornerRadius::default(); - View::item_button(ui, r, POWER, Some(Colors::red()), || { - Tor::stop_service(service_id); - }); + // Draw button to enable/disable Tor listener for current wallet. + if wallet.foreign_api_port().is_some() && wallet.secret_key().is_some() { + let port = wallet.foreign_api_port().unwrap(); + let key = wallet.secret_key().unwrap(); + if !Tor::is_service_starting(service_id) { + if !Tor::is_service_running(service_id) { + let r = CornerRadius::default(); + View::item_button(ui, r, POWER, Some(Colors::green()), || { + Tor::start_service(port, key.clone(), service_id); + }); + } else { + let r = CornerRadius::default(); + View::item_button(ui, r, POWER, Some(Colors::red()), || { + Tor::stop_service(service_id); + }); + } } } - // Draw button to show Tor transport settings. let button_rounding = View::item_rounding(1, 3, true); View::item_button(ui, button_rounding, WRENCH, None, || { diff --git a/src/gui/views/wallets/wallet/transport/settings.rs b/src/gui/views/wallets/wallet/transport/settings.rs index d74d0e92..9e2e1952 100644 --- a/src/gui/views/wallets/wallet/transport/settings.rs +++ b/src/gui/views/wallets/wallet/transport/settings.rs @@ -25,13 +25,13 @@ use crate::wallet::Wallet; /// Wallet transport settings content. pub struct WalletTransportSettingsContent { /// Tor transport content settings. - tor_settings_content: TorSettingsContent, + pub tor_settings_content: TorSettingsContent, } impl Default for WalletTransportSettingsContent { fn default() -> Self { Self { - tor_settings_content: TorSettingsContent::default(), + tor_settings_content: TorSettingsContent::default() } } } @@ -62,19 +62,7 @@ impl WalletTransportSettingsContent { ui.add_space(8.0); ui.vertical_centered_justified(|ui| { View::button(ui, t!("close"), Colors::white_or_black(false), || { - if self.tor_settings_content.settings_changed { - // Restart running service or rebuild client. - let service_id = &wallet.identifier(); - if Tor::is_service_running(service_id) { - if let Some(key) = wallet.secret_key() { - if let Some(api_port) = wallet.foreign_api_port() { - Tor::restart_service(api_port, key, service_id); - } - } - } else { - Tor::rebuild_client(); - } - } + Tor::restart_services(); on_close(); }); }); diff --git a/src/gui/views/wallets/wallet/txs/content.rs b/src/gui/views/wallets/wallet/txs/content.rs index 9a693a2f..11806736 100644 --- a/src/gui/views/wallets/wallet/txs/content.rs +++ b/src/gui/views/wallets/wallet/txs/content.rs @@ -130,11 +130,17 @@ impl WalletTransactionsContent { .can_refresh(!refresh && !wallet.syncing() && !txs.is_empty()) .min_refresh_distance(70.0) .scroll_area_ui(ui, |ui| { + let rows_size = if txs.is_empty() { + 0 + } else { + // Last index is for list pagination. + txs.len() + 1 + }; ScrollArea::vertical() .id_salt(Id::from("wallet_tx_list_scroll").with(config.id)) .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) .auto_shrink([false; 2]) - .show_rows(ui, Self::TX_ITEM_HEIGHT, txs.len(), |ui, row_range| { + .show_rows(ui, Self::TX_ITEM_HEIGHT, rows_size, |ui, row_range| { ui.add_space(1.0); View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { self.tx_list_ui(ui, row_range, &wallet, txs); @@ -159,6 +165,24 @@ impl WalletTransactionsContent { txs: &Vec) { let data = wallet.get_data().unwrap(); for index in row_range { + if index == txs.len() && ui.is_visible() { + // Load more txs when needed. + if !wallet.more_txs_loading() { + if let Some(data) = wallet.get_data() { + if txs.len() as u32 >= data.txs_limit { + wallet.load_more_txs(); + } + } + } + // Show loader when more txs are loading. + if wallet.more_txs_loading() { + ui.vertical_centered(|ui| { + ui.add_space(24.0); + View::small_loading_spinner(ui); + }); + } + return; + } let mut rect = ui.available_rect_before_wrap(); rect.min += egui::emath::vec2(6.0, 0.0); rect.max -= egui::emath::vec2(6.0, 0.0); diff --git a/src/gui/views/wallets/wallet/txs/tx.rs b/src/gui/views/wallets/wallet/txs/tx.rs index db076f66..f2988b1c 100644 --- a/src/gui/views/wallets/wallet/txs/tx.rs +++ b/src/gui/views/wallets/wallet/txs/tx.rs @@ -102,14 +102,8 @@ impl WalletTransactionContent { self.share_ui(ui, wallet, tx, cb); } else { if let Some(proof_content) = self.proof_content.as_mut() { - // Payment proof file name setup. - let file_name = if let Some(slate_id) = tx.data.tx_slate_id { - slate_id.to_string() - } else { - tx.data.id.to_string() - }; // Draw payment proof sharing content. - proof_content.share_ui(ui, file_name, cb); + proof_content.share_ui(ui, tx, cb); } else if tx.proof.is_some() && !tx.sending_tor() && tx.action_error.is_none() { ui.vertical_centered(|ui| { diff --git a/src/tor/tor.rs b/src/tor/tor.rs index c0303352..9fd94170 100644 --- a/src/tor/tor.rs +++ b/src/tor/tor.rs @@ -30,6 +30,7 @@ use std::str::FromStr; use std::sync::Arc; use std::time::Duration; use std::{fs, thread}; +use log::error; use safelog::DisplayRedacted; use tls_api::{TlsConnector as TlsConnectorTrait, TlsConnectorBuilder}; use tls_api_native_tls::TlsConnector; @@ -45,7 +46,6 @@ use tor_hsservice::{ use tor_keymgr::{ArtiNativeKeystore, KeyMgrBuilder, KeystoreSelector}; use tor_llcrypto::pk::ed25519::ExpandedKeypair; use tor_rtcompat::tokio::TokioNativeTlsRuntime; -use tor_rtcompat::Runtime; use crate::http::HttpClient; use crate::tor::http::ArtiHttpConnector; @@ -60,8 +60,14 @@ lazy_static! { pub struct Tor { /// Tor client and config. client_config: Arc, TorClientConfig)>>, + /// Client to check services availability. + check_client: Arc>> + >>, /// Mapping of running Onion services identifiers to proxy. - run: Arc, Arc)>>>, + run: Arc, Arc)> + >>, /// Starting Onion services identifiers. start: Arc>>, /// Failed Onion services identifiers. @@ -79,7 +85,6 @@ impl Default for Tor { fs::write(TorConfig::webtunnel_path(), webtunnel).unwrap_or_default(); } } - // Create Tor client. let runtime = TokioNativeTlsRuntime::create().unwrap(); let config = Self::build_config(true); @@ -88,11 +93,12 @@ impl Default for Tor { .create_unbootstrapped() .unwrap(); Self { + client_config: Arc::new(RwLock::new((client, config))), + check_client: Arc::new(RwLock::new(None)), run: Arc::new(RwLock::new(BTreeMap::new())), start: Arc::new(RwLock::new(BTreeSet::new())), fail: Arc::new(RwLock::new(BTreeSet::new())), check: Arc::new(RwLock::new(BTreeSet::new())), - client_config: Arc::new(RwLock::new((client, config))), } } } @@ -122,13 +128,6 @@ impl Tor { config } - /// Recreate Tor client with configuration. - pub fn rebuild_client() { - let config = Self::build_config(false); - let r_client = TOR_SERVER_STATE.client_config.read(); - r_client.0.reconfigure(&config, tor_config::Reconfigure::AllOrNothing).unwrap(); - } - /// Send post request using Tor. pub async fn post(body: String, url: String) -> Option { if let Some(proxy) = TorConfig::get_proxy() { @@ -147,10 +146,25 @@ impl Tor { }; match res { Ok(res) => { - let body = res.into_body().collect().await.unwrap().to_bytes().into(); - Some(String::from_utf8(body).unwrap()) + match res.into_body().collect().await { + Ok(r) => { + let body = r.to_bytes().into(); + match String::from_utf8(body) { + Ok(r) => Some(r), + Err(e) => { + error!("Tor: POST with proxy, response to string error: {}", e); + None + } + } + } + Err(e) => { + error!("Tor: POST with proxy, response parse error: {}", e); + None + } + } } - Err(_) => { + Err(e) => { + error!("Tor: POST failed with proxy: {}", e); None } } @@ -162,12 +176,11 @@ impl Tor { } // Bootstrap client. let (client, _) = Self::client_config(); - let client = client.isolated_client(); - client.bootstrap().await.unwrap(); + client.bootstrap().await.unwrap_or_default(); // Create http tor-powered client to post data. - let tls_connector = TlsConnector::builder().unwrap().build().unwrap(); - let tor_connector = ArtiHttpConnector::new(client, tls_connector); - let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(tor_connector); + let tls_conn = TlsConnector::builder().unwrap().build().unwrap(); + let conn = ArtiHttpConnector::new(client, tls_conn); + let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(conn); // Create request. let req = hyper_tor::Request::builder() .method(hyper_tor::Method::POST) @@ -179,9 +192,13 @@ impl Tor { match http.request(req).await { Ok(r) => match hyper_tor::body::to_bytes(r).await { Ok(raw) => resp = Some(String::from_utf8_lossy(&raw).to_string()), - Err(_) => {} + Err(e) => { + error!("Tor: POST response parse error: {}", e); + } }, - Err(_) => {} + Err(e) => { + error!("Tor: POST failed: {}", e); + } } resp } @@ -216,21 +233,45 @@ impl Tor { r_services.contains(id) } - // Restart Onion service. - pub fn restart_service(port: u16, key: SecretKey, id: &String) { - Self::stop_service(id); - Self::rebuild_client(); - Self::start_service(port, key, id) + /// Restart running Onion services. + pub fn restart_services() { + // Stop all services saving port key for relaunch. + let service_ids = { + let r_services = TOR_SERVER_STATE.run.read().clone(); + r_services.keys().map(|s| s.to_string()).collect::>() + }; + let mut services: BTreeMap = BTreeMap::new(); + for id in service_ids.clone() { + if let Some(res) = Self::stop_service(&id) { + services.insert(id, res); + } + } + // Reconfigure client. + let config = Self::build_config(false); + let r_client = TOR_SERVER_STATE.client_config.read(); + r_client.0.reconfigure(&config, tor_config::Reconfigure::WarnOnFailures).unwrap(); + // Start services. + for id in services.keys() { + let (port, key) = services.get(id).unwrap(); + Self::start_service(port.clone(), key.clone(), &id); + } } - /// Stop running Onion service. - pub fn stop_service(id: &String) { - let mut w_services = TOR_SERVER_STATE.run.write(); - if let Some((svc, proxy)) = w_services.remove(id) { - proxy.shutdown(); - drop(svc); - drop(proxy); + /// Stop running Onion service returning port and key. + pub fn stop_service(id: &String) -> Option<(u16, SecretKey)> { + { + // Remove service from starting. + let mut w_services = TOR_SERVER_STATE.start.write(); + w_services.remove(id); } + let mut w_services = TOR_SERVER_STATE.run.write(); + if let Some((port, key, svc, proxy)) = w_services.remove(id) { + proxy.shutdown(); + drop(proxy); + drop(svc); + return Some((port, key)); + } + None } /// Start Onion service from listening local port and [`SecretKey`]. @@ -278,49 +319,49 @@ impl Tor { } let (client, config) = Self::client_config(); - let c = client.clone(); client .runtime() .spawn(async move { // Add service key to keystore. - let hs_nickname = HsNickname::new(service_id.clone()).unwrap(); - if let Err(_) = Self::add_service_key(config.fs_mistrust(), &key, &hs_nickname) { + let hs = HsNickname::new(service_id.clone()).unwrap(); + if let Err(_) = Self::add_service_key(config.fs_mistrust(), &key, &hs) { on_error(service_id); return; } + let (c, _) = Self::client_config(); // Bootstrap client. - if let Err(_) = c.bootstrap().await { - on_error(service_id); - return; + if c.bootstrap_status().as_frac() == 0.0 { + if let Err(_) = c.bootstrap().await { + on_error(service_id); + return; + } + // Launch first time after delay. + thread::sleep(Duration::from_millis(10000)); } // Launch Onion service. let service_config = OnionServiceConfigBuilder::default() - .nickname(hs_nickname.clone()) + .nickname(hs.clone()) .build() .unwrap(); if let Ok(res) = c.launch_onion_service(service_config) { if let Some((service, request)) = res { + let onion_addr = service.onion_address(); // Launch service proxy. let addr = SocketAddr::new(IpAddr::from(Ipv4Addr::LOCALHOST), port); - tokio::spawn(Self::run_service_proxy( + let proxy = tokio::spawn(Self::run_service_proxy( addr, - c.clone(), - service.clone(), request, - hs_nickname.clone(), + hs.clone(), )).await.unwrap(); + // Save running service. + let mut w_services = TOR_SERVER_STATE.run.write(); + let id = service_id.clone(); + w_services.insert(id, (port, key.clone(), service, proxy)); // Check service availability. - let addr = service.onion_address() - .unwrap() - .display_unredacted() - .to_string(); + let addr = onion_addr.unwrap().display_unredacted().to_string(); let url = format!("http://{}/", addr); if !Self::is_service_checking(&service_id) { - Self::check_service(service_id, c, url, port, key) - } else { - // Remove service from starting. - let mut w_services = TOR_SERVER_STATE.start.write(); - w_services.remove(&service_id); + Self::check_service(service_id, url, port, key) } return; } @@ -333,7 +374,6 @@ impl Tor { /// Check service availability. fn check_service(service_id: String, - client: TorClient, url: String, port: u16, key: SecretKey) { @@ -341,14 +381,25 @@ impl Tor { let mut w_services = TOR_SERVER_STATE.check.write(); w_services.insert(service_id.clone()); } - let client_check = client.clone(); + let (client, _) = Self::client_config(); thread::spawn(move || { // Wait 5 sec before check. thread::sleep(Duration::from_millis(5000)); + // Request client setup. + if TOR_SERVER_STATE.check_client.read().is_none() { + let (client_check, _) = Self::client_config(); + let tls_conn = TlsConnector::builder().unwrap().build().unwrap(); + let conn = ArtiHttpConnector::new(client_check, tls_conn); + let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(conn); + let mut w_client = TOR_SERVER_STATE.check_client.write(); + *w_client = Some(http); + } + let r_client = TOR_SERVER_STATE.check_client.read(); + let http = r_client.clone().unwrap(); let runtime = client.runtime(); runtime .spawn(async move { - const MAX_ERRORS: i32 = 12; + const MAX_ERRORS: i32 = 16; let mut errors_count = 0; loop { // Check if service is running. @@ -365,18 +416,12 @@ impl Tor { if !is_running(&service_id) { break; } - // Send request. - let uri = hyper_tor::Uri::from_str(url.clone().as_str()).unwrap(); - let tls_conn = TlsConnector::builder().unwrap().build().unwrap(); - let conn = ArtiHttpConnector::new(client_check.clone(), tls_conn); - let http = hyper_tor::Client::builder().build::<_, hyper_tor::Body>(conn); let duration = { - let check = http.get(uri); let mut on_error = |service_id: &String| -> bool { if !is_running(service_id) { return true; } - // Restart service on 3rd error. + // Restart service after maximum amount of errors. errors_count += 1; if errors_count == MAX_ERRORS { // Remove service from checking. @@ -390,13 +435,16 @@ impl Tor { let key = key.clone(); let id = service_id.clone(); thread::spawn(move || { - thread::sleep(Duration::from_secs(1)); - Self::restart_service(port, key, &id); + Self::stop_service(&id); + Self::start_service(port, key, &id); }); return true; } false }; + // Send request. + let uri = hyper_tor::Uri::from_str(url.clone().as_str()).unwrap(); + let check = http.get(uri); // Check with timeout of 20s. match tokio::time::timeout(Duration::from_millis(20000), check).await { Ok(resp) => { @@ -409,13 +457,15 @@ impl Tor { let mut w_services = TOR_SERVER_STATE.start.write(); w_services.remove(&service_id); errors_count = 0; - // Check again after 60s. - Duration::from_millis(60000) + // Check again after 20s. + Duration::from_millis(20000) } - Err(_) => { + Err(e) => { if on_error(&service_id) { break; } + error!("Tor check failed: {} for {}, errors: {}/{}", + e, service_id, errors_count, MAX_ERRORS); // Check again after 5s. Duration::from_millis(5000) } @@ -425,13 +475,15 @@ impl Tor { if on_error(&service_id) { break; } + error!("Tor check for {} timeout, errors: {}/{}", + &service_id, errors_count, MAX_ERRORS); // Check again after 5s. Duration::from_millis(5000) } } }; // Wait to check service again. - thread::park_timeout(duration); + thread::sleep(duration); } }) .unwrap(); @@ -439,17 +491,15 @@ impl Tor { } /// Launch Onion service proxy. - async fn run_service_proxy( + async fn run_service_proxy( addr: SocketAddr, - client: TorClient, - service: Arc, request: S, nickname: HsNickname, - ) where - R: Runtime, + ) -> Arc where S: futures::Stream + Unpin + Send + 'static, { let id = nickname.to_string(); + let (client, _) = Self::client_config(); let runtime = client.runtime().clone(); // Setup proxy to forward request from Tor address to local address. @@ -464,15 +514,13 @@ impl Tor { // Remove service from failed. let mut w_services = TOR_SERVER_STATE.fail.write(); w_services.remove(&id); - // Save running service. - let mut w_services = TOR_SERVER_STATE.run.write(); - w_services.insert(id.clone(), (service.clone(), proxy.clone())); // Start proxy for launched service. + let p = proxy.clone(); client .runtime() .spawn(async move { - match proxy.handle_requests(runtime, nickname.clone(), request).await { + match p.handle_requests(runtime, nickname.clone(), request).await { Ok(()) => { // Remove service from running. let mut w_services = TOR_SERVER_STATE.run.write(); @@ -491,6 +539,7 @@ impl Tor { } }) .unwrap(); + proxy } /// Save Onion service key to keystore. diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 62a57185..14b6760b 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -148,11 +148,17 @@ pub struct WalletAccount { pub struct WalletData { /// Balance data for current account. pub info: WalletInfo, + /// Transactions data. - pub txs: Option> + pub txs: Option>, + /// Number of txs to show on select from database. + pub txs_limit: u32, } impl WalletData { + /// Number of transactions per select to show at list. + pub const TXS_LIMIT: u32 = 30; + /// Update transaction action status. pub fn on_tx_action(&mut self, id: String, action: Option) { if self.txs.is_none() { diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 551301c3..c754f4a0 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -23,7 +23,7 @@ use crate::AppConfig; use futures::channel::oneshot; use grin_api::{ApiServer, Router}; use grin_chain::SyncStatus; -use grin_keychain::{ExtKeychain, Identifier, Keychain}; +use grin_keychain::{ExtKeychain, Keychain}; use grin_util::secp::SecretKey; use grin_util::types::ZeroingString; use grin_util::{Mutex, ToHex}; @@ -33,7 +33,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, init_send_tx, retrieve_summary_info, retrieve_txs, verify_payment_proof}; -use grin_wallet_libwallet::{address, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, RetrieveTxQueryArgs, RetrieveTxQuerySortField, RetrieveTxQuerySortOrder, Slate, SlateState, SlateVersion, SlatepackAddress, StatusMessage, StoredProofInfo, TxLogEntry, TxLogEntryType, VersionedSlate, WalletBackend, WalletInfo, WalletInitStatus, WalletInst, WalletLCProvider}; +use grin_wallet_libwallet::{address, Error, InitTxArgs, IssueInvoiceTxArgs, NodeClient, PaymentProof, Slate, SlateState, SlateVersion, SlatepackAddress, StatusMessage, StoredProofInfo, TxLogEntry, TxLogEntryType, VersionedSlate, WalletBackend, WalletInitStatus, WalletInst, WalletLCProvider}; use grin_wallet_util::OnionV3Address; use parking_lot::RwLock; use rand::Rng; @@ -43,13 +43,15 @@ use std::io::Write; use std::net::{SocketAddr, TcpListener, ToSocketAddrs}; use std::path::PathBuf; use std::str::FromStr; -use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU8, Ordering}; use std::sync::mpsc::Sender; use std::sync::{mpsc, Arc}; use std::thread::Thread; use std::time::Duration; use std::{fs, thread}; +use chrono::Utc; use log::error; +use num_bigint::BigInt; use uuid::Uuid; /// Contains wallet instance, configuration and state, handles wallet commands. @@ -69,6 +71,8 @@ pub struct Wallet { /// Wallet accounts. accounts: Arc>>, + /// Timestamp when wallet account was selected to form unique identifier for transport. + account_time: Arc, /// Wallet sync thread. sync_thread: Arc>>, @@ -85,6 +89,8 @@ pub struct Wallet { data: Arc>>, /// Flag to check if wallet data was synced from node. from_node: Arc, + /// Flag to check if more transactions need to be loaded. + more_txs_loading: Arc, /// Flag to check if wallet reopening is needed. reopen: Arc, @@ -138,20 +144,22 @@ impl Wallet { connection: Arc::new(RwLock::new(connection)), keychain_mask: Arc::new(RwLock::new(None)), slatepack_address: Arc::new(RwLock::new(None)), + accounts: Arc::new(RwLock::new(vec![])), + account_time: Arc::new(Default::default()), sync_thread: Arc::from(RwLock::new(None)), - foreign_api_server: Arc::new(RwLock::new(None)), - secret_key: Arc::new(RwLock::new(None)), + syncing: Arc::new(AtomicBool::new(false)), + info_sync_progress: Arc::from(AtomicU8::new(0)), + sync_error: Arc::from(AtomicBool::new(false)), + sync_attempts: Arc::new(AtomicU8::new(0)), + data: Arc::new(RwLock::new(None)), + from_node: Arc::new(AtomicBool::new(false)), + more_txs_loading: Arc::new(AtomicBool::new(false)), reopen: Arc::new(AtomicBool::new(false)), is_open: Arc::from(AtomicBool::new(false)), closing: Arc::new(AtomicBool::new(false)), deleted: Arc::new(AtomicBool::new(false)), - sync_error: Arc::from(AtomicBool::new(false)), - info_sync_progress: Arc::from(AtomicU8::new(0)), - accounts: Arc::new(RwLock::new(vec![])), - data: Arc::new(RwLock::new(None)), - from_node: Arc::new(AtomicBool::new(false)), - sync_attempts: Arc::new(AtomicU8::new(0)), - syncing: Arc::new(AtomicBool::new(false)), + foreign_api_server: Arc::new(RwLock::new(None)), + secret_key: Arc::new(RwLock::new(None)), repair_needed: Arc::new(AtomicBool::new(false)), repair_progress: Arc::new(AtomicU8::new(0)), files_moving: Arc::new(AtomicBool::new(false)), @@ -349,6 +357,7 @@ impl Wallet { 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())?; + self.account_time.store(Utc::now().timestamp(), Ordering::Relaxed); // Start new synchronization thread or wake up existing one. let mut thread_w = self.sync_thread.write(); @@ -370,29 +379,17 @@ impl Wallet { } } - // 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, self.keychain_mask().as_ref(), Some(&mut api), |api, m| { + // Set Slatepack address and secret key. + if let Ok((key, addr)) = self.get_secret_key_addr() { + let mut w_key = self.secret_key.write(); + *w_key = Some(key); let mut w_address = self.slatepack_address.write(); - *w_address = Some(api.get_slatepack_address(m, 0)?.to_string()); - Ok(()) - })?; + *w_address = Some(addr.to_string()); + } Ok(()) } - /// Get parent key identifier for current account. - 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(); - let lc = w_lock.lc_provider()?; - let w_inst = lc.wallet_inst()?; - Ok(w_inst.parent_key_id()) - } - /// Get keychain mask [`SecretKey`]. pub fn keychain_mask(&self) -> Option { let r_key = self.keychain_mask.read(); @@ -405,8 +402,8 @@ impl Wallet { r_key.clone() } - /// Retrieve wallet [`SecretKey`] for transport. - fn get_secret_key(&self) -> Result { + /// Retrieve wallet [`SecretKey`] and Slatepack address for transport. + fn get_secret_key_addr(&self) -> Result<(SecretKey, SlatepackAddress), Error> { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let mut w_lock = instance.lock(); @@ -416,13 +413,15 @@ impl Wallet { let parent_key_id = w_inst.parent_key_id(); let sec_key = address::address_from_derivation_path(&k, &parent_key_id, 0) .map_err(|e| Error::TorConfig(format!("{:?}", e)))?; - Ok(sec_key) + let addr = SlatepackAddress::try_from(&sec_key)?; + Ok((sec_key, addr)) } /// Get unique opened wallet identifier, including current account. pub fn identifier(&self) -> String { let config = self.get_config(); - format!("wallet_{}_{}", config.id, config.account.to_hex()) + let account_ts = self.account_time.load(Ordering::Relaxed); + format!("{}_{}_{}", config.id, config.account.to_hex(), account_ts) } /// Get Slatepack address to receive txs at transport. @@ -629,7 +628,7 @@ impl Wallet { } /// Select transaction by slate id. - fn tx_by_id(&self, id: Uuid) -> Option { + fn retrieve_tx_by_id(&self, id: Uuid) -> Option { let r_inst = self.instance.as_ref().read(); let inst = r_inst.clone().unwrap(); let mask = self.keychain_mask(); @@ -642,6 +641,44 @@ impl Wallet { None } + /// Select transactions with provided limit. + fn retrieve_txs(&self, limit: u32) -> Result, Error> { + let r_inst = self.instance.as_ref().read(); + let inst = r_inst.clone().unwrap(); + let mut wallet_lock = inst.lock(); + let lc = wallet_lock.lc_provider()?; + let w = lc.wallet_inst()?; + let parent_key_id = w.parent_key_id(); + // Retrieve txs from database. + let txs_iter = w.tx_log_iter() + .filter(|tx_entry| tx_entry.parent_key_id == parent_key_id) + .filter(|tx_entry| { + if tx_entry.tx_type == TxLogEntryType::TxSent + || tx_entry.tx_type == TxLogEntryType::TxSentCancelled { + BigInt::from(tx_entry.amount_debited) + - BigInt::from(tx_entry.amount_credited) + >= BigInt::from(1) + } else { + BigInt::from(tx_entry.amount_credited) + - BigInt::from(tx_entry.amount_debited) + >= BigInt::from(1) + } + }); + let mut return_txs: Vec = txs_iter.collect(); + // Sort txs by creation date and confirmation status reversing an order. + return_txs.sort_by_key(|tx| if !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent || + tx.tx_type == TxLogEntryType::TxReceived) { + i64::MAX + } else { + tx.creation_ts.timestamp() + }); + // return_txs.sort_by_key(|tx| tx.confirmed); + return_txs.reverse(); + // Apply limit. + return_txs = return_txs.into_iter().take(limit as usize).collect(); + Ok(return_txs) + } + /// Send a task to the wallet. pub fn task(&self, task: WalletTask) { let r_tasks = self.tasks_sender.read(); @@ -683,21 +720,34 @@ impl Wallet { /// Set active account from provided label. pub fn set_active_account(&self, label: &String) -> Result<(), Error> { - 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, self.keychain_mask().as_ref(), Some(&mut api), |api, m| { - api.set_active_account(m, label)?; - // Set Slatepack address. - let mut w_address = self.slatepack_address.write(); - *w_address = Some(api.get_slatepack_address(m, 0)?.to_string()); - Ok(()) - })?; - // Stop service from previous account. let cur_service_id = self.identifier(); Tor::stop_service(&cur_service_id); + // Clear secret key for previous account. + { + let mut w_key = self.secret_key.write(); + *w_key = None; + } + + // Set new active account. + let r_inst = self.instance.as_ref().read(); + let instance = r_inst.clone().unwrap(); + let mut api = Owner::new(instance.clone(), None); + controller::owner_single_use(None, self.keychain_mask().as_ref(), Some(&mut api), |api, m| { + api.set_active_account(m, label)?; + self.account_time.store(Utc::now().timestamp(), Ordering::Relaxed); + Ok(()) + })?; + + // Setup secret key and Slatepack address. + if let Ok((key, addr)) = self.get_secret_key_addr() { + let mut w_key = self.secret_key.write(); + *w_key = Some(key); + let mut w_address = self.slatepack_address.write(); + *w_address = Some(addr.to_string()); + } + // Save account label into config. let mut w_config = self.config.write(); w_config.account = label.to_owned(); @@ -707,10 +757,6 @@ impl Wallet { let mut w_data = self.data.write(); *w_data = None; - // Clear secret key for previous account. - let mut w_key = self.secret_key.write(); - *w_key = None; - // Reset progress values. self.info_sync_progress.store(0, Ordering::Relaxed); @@ -754,6 +800,32 @@ impl Wallet { r_data.clone() } + /// Load more transactions at list by increasing limit. + pub fn load_more_txs(&self) { + self.more_txs_loading.store(true, Ordering::Relaxed); + let wallet = self.clone(); + thread::spawn(move || { + // Wait when current sync will be finished. + if wallet.syncing() { + thread::sleep(Duration::from_secs(1)); + } + // Sync wallet data with new limit. + { + let mut w_data = wallet.data.write(); + if w_data.is_some() { + w_data.as_mut().unwrap().txs_limit += WalletData::TXS_LIMIT; + } + } + sync_wallet_data(&wallet, false); + wallet.more_txs_loading.store(false, Ordering::Relaxed); + }); + } + + /// Check if more transaction are loading. + pub fn more_txs_loading(&self) -> bool { + self.more_txs_loading.load(Ordering::Relaxed) + } + /// Sync wallet data from node at sync thread or locally synchronously. pub fn sync(&self) { let thread_r = self.sync_thread.read(); @@ -1463,14 +1535,6 @@ fn start_sync(wallet: Wallet) -> Thread { } } - // Setup secret key if not set. - if wallet.secret_key().is_none() { - let mut w_key = wallet.secret_key.write(); - if let Ok(key) = wallet.get_secret_key() { - *w_key = Some(key); - } - } - // Start unfailed Tor service if API server is running. let service_id = wallet.identifier(); if wallet.auto_start_tor_listener() && api_server_running && @@ -1531,10 +1595,11 @@ async fn handle_task(w: &Wallet, t: WalletTask) { } } Err(e) => { - if let Some(tx) = w.tx_by_id(s.id) { + error!("send tor finalize error: {:?}", e); + sync_wallet_data(&w, false); + if let Some(tx) = w.retrieve_tx_by_id(s.id) { let _ = w.cancel(&tx); } - error!("send tor finalize error: {:?}", e); w.on_tx_error(id, Some(e)); } } @@ -1607,7 +1672,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) { } } Err(e) => { - if let Some(tx) = w.tx_by_id(s.id) { + if let Some(tx) = w.retrieve_tx_by_id(s.id) { let _ = w.cancel(&tx); } error!("message tx finalize error: {:?}", e); @@ -1686,7 +1751,7 @@ async fn handle_task(w: &Wallet, t: WalletTask) { } } Err(e) => { - if let Some(tx) = w.tx_by_id(slate.id) { + if let Some(tx) = w.retrieve_tx_by_id(slate.id) { let _ = w.cancel(&tx); } error!("tx finalize error: {:?}", e); @@ -1799,19 +1864,27 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { update_accounts(wallet, last_height, spendable); if wallet.info_sync_progress() == 100 || !from_node { + // Transactions limit setup. + let txs_limit = { + let r_data = wallet.data.read(); + if r_data.is_some() { + let data = r_data.as_ref().unwrap(); + data.txs_limit + } else { + WalletData::TXS_LIMIT + } + }; // Update wallet info. { let mut w_data = wallet.data.write(); - let txs = if w_data.is_some() { - w_data.clone().unwrap().txs + if w_data.is_some() { + w_data.as_mut().unwrap().info = info; } else { - None - }; - *w_data = Some(WalletData { info: info.clone(), txs }); + *w_data = Some(WalletData { info, txs: None, txs_limit }); + } } - // Update wallet transactions. - if update_txs(wallet, instance.clone(), info).is_ok() { + if update_txs(wallet, txs_limit).is_ok() { if !wallet.from_node.load(Ordering::Relaxed) { wallet.from_node.store(from_node, Ordering::Relaxed); } @@ -1845,42 +1918,39 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { } /// Update wallet transactions. -fn update_txs(wallet: &Wallet, inst: 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), - min_amount: Some(1), - ..Default::default() - }; - let mask = wallet.keychain_mask(); - let txs = retrieve_txs(inst, mask.as_ref(), &None, false, None, None, Some(txs_args.clone()))?; +fn update_txs(wallet: &Wallet, mut txs_limit: u32) -> Result<(), Error> { + let txs = wallet.retrieve_txs(txs_limit)?; // 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() || - (tx.tx_slate_id.is_none() && tx.payment_proof.is_some())) - } - Err(_) => { - true - } - } + // Filter transactions to not show txs without slate (usually unspent outputs). + let mut filter_txs = txs.iter().map(|v| v.clone()).filter(|tx| { + tx.tx_slate_id.is_some() || (tx.tx_slate_id.is_none() && tx.payment_proof.is_some()) }).collect::>(); + // Sort to show unconfirmed at top. + filter_txs.sort_by_key(|tx| { + tx.confirmed || tx.tx_type == TxLogEntryType::TxReceivedCancelled || + tx.tx_type == TxLogEntryType::TxSentCancelled || + tx.tx_type == TxLogEntryType::TxReverted + }); + + // Update limit with actual length. + let txs_size = txs.len() as u32; + let filter_size = filter_txs.len() as u32; + if txs_size > filter_size && txs_limit >= filter_size { + txs_limit = txs_limit - (txs_size - filter_size); + } + + // Update existing tx list. 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 { + for tx in &filter_txs { let mut height: Option = None; let mut broadcasting_height: Option = None; let mut action: Option = None; @@ -1892,6 +1962,7 @@ fn update_txs(wallet: &Wallet, inst: WalletInstance, info: WalletInfo) action_error = t.action_error.clone(); height = t.height; broadcasting_height = t.broadcasting_height; + proof = t.proof.clone(); break; } } @@ -1908,7 +1979,6 @@ fn update_txs(wallet: &Wallet, inst: WalletInstance, info: WalletInfo) if unconfirmed { new.update_slate_state(wallet); } - // Payment proof setup. if proof.is_none() && tx.payment_proof.is_some() && tx.payment_proof.as_ref().unwrap().receiver_signature.is_some() && @@ -1919,7 +1989,6 @@ fn update_txs(wallet: &Wallet, inst: WalletInstance, info: WalletInfo) new.proof = proof; } } - // Initial tx heights setup. if let Some(slate_id) = tx.tx_slate_id { let id = slate_id.to_string(); @@ -1952,7 +2021,10 @@ fn update_txs(wallet: &Wallet, inst: WalletInstance, info: WalletInfo) } // Update wallet txs. let mut w_data = wallet.data.write(); - *w_data = Some(WalletData { info, txs: Some(new_txs) }); + if w_data.is_some() { + w_data.as_mut().unwrap().txs_limit = txs_limit; + w_data.as_mut().unwrap().txs = Some(new_txs); + } Ok(()) } diff --git a/wallet b/wallet index c51433d0..a6e0cdae 160000 --- a/wallet +++ b/wallet @@ -1 +1 @@ -Subproject commit c51433d02b4d7df246f82a427fd19cdb0b38e608 +Subproject commit a6e0cdae0c071feec23544036a5eadfadbcdb93f