From c5364b486c30526ce69ed8487b8a317e19ba8297 Mon Sep 17 00:00:00 2001 From: Martin Stefcek Date: Wed, 1 Sep 2021 19:56:02 +0200 Subject: [PATCH] Add general notifications support for console wallet. Add update notification to the wallet. --- Cargo.lock | 3 +- .../tari_app_grpc/proto/base_node.proto | 9 +-- applications/tari_app_grpc/proto/types.proto | 6 ++ applications/tari_app_grpc/proto/wallet.proto | 2 + applications/tari_console_wallet/Cargo.toml | 2 +- .../src/grpc/wallet_grpc_server.rs | 16 ++++ .../tari_console_wallet/src/init/mod.rs | 12 +++ .../tari_console_wallet/src/ui/app.rs | 8 +- .../src/ui/components/component.rs | 13 +++- .../src/ui/components/mod.rs | 1 + .../src/ui/components/notification_tab.rs | 73 +++++++++++++++++++ .../src/ui/components/tabs_container.rs | 7 +- .../src/ui/state/app_state.rs | 50 ++++++++++++- .../src/ui/state/wallet_event_monitor.rs | 35 ++++++++- base_layer/wallet/Cargo.toml | 3 +- base_layer/wallet/src/config.rs | 8 +- base_layer/wallet/src/wallet.rs | 61 +++++++++++++++- base_layer/wallet/tests/wallet/mod.rs | 4 + base_layer/wallet_ffi/src/lib.rs | 2 + clients/wallet_grpc_client/.prettierrc | 3 + clients/wallet_grpc_client/index.js | 1 + .../features/BaseNodeAutoUpdate.feature | 5 +- .../features/WalletAutoUpdate.feature | 14 ++++ integration_tests/features/support/steps.js | 46 ++++++++++-- integration_tests/features/support/world.js | 2 +- integration_tests/helpers/walletClient.js | 4 + 26 files changed, 356 insertions(+), 34 deletions(-) create mode 100644 applications/tari_console_wallet/src/ui/components/notification_tab.rs create mode 100644 clients/wallet_grpc_client/.prettierrc create mode 100644 integration_tests/features/WalletAutoUpdate.feature diff --git a/Cargo.lock b/Cargo.lock index abf0623097..0e0bc56b6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5029,6 +5029,7 @@ dependencies = [ "rand 0.8.4", "serde 1.0.130", "serde_json", + "tari_common", "tari_common_types", "tari_comms", "tari_comms_dht", @@ -5049,7 +5050,7 @@ dependencies = [ [[package]] name = "tari_wallet_ffi" -version = "0.17.4" +version = "0.17.5" dependencies = [ "chrono", "env_logger 0.7.1", diff --git a/applications/tari_app_grpc/proto/base_node.proto b/applications/tari_app_grpc/proto/base_node.proto index 8d6f282ced..1396613acd 100644 --- a/applications/tari_app_grpc/proto/base_node.proto +++ b/applications/tari_app_grpc/proto/base_node.proto @@ -48,7 +48,7 @@ service BaseNode { rpc GetBlockFees (BlockGroupRequest) returns (BlockGroupResponse); // Get Version rpc GetVersion(Empty) returns (StringValue); - // Get Version + // Check for new updates rpc CheckForUpdates(Empty) returns (SoftwareUpdate); // Get coins in circulation rpc GetTokensInCirculation(GetBlocksRequest) returns (stream ValueAtHeightResponse); @@ -333,13 +333,6 @@ enum TransactionLocation { NOT_STORED = 3; } -message SoftwareUpdate { - bool has_update = 1; - string version = 2; - string sha = 3; - string download_url = 4; -} - message MempoolStatsResponse { uint64 total_txs = 1; uint64 unconfirmed_txs = 2; diff --git a/applications/tari_app_grpc/proto/types.proto b/applications/tari_app_grpc/proto/types.proto index fb016d33a3..49306660b7 100644 --- a/applications/tari_app_grpc/proto/types.proto +++ b/applications/tari_app_grpc/proto/types.proto @@ -352,3 +352,9 @@ message ListConnectedPeersResponse { repeated Peer connected_peers = 1; } +message SoftwareUpdate { + bool has_update = 1; + string version = 2; + string sha = 3; + string download_url = 4; +} diff --git a/applications/tari_app_grpc/proto/wallet.proto b/applications/tari_app_grpc/proto/wallet.proto index bc561f0e02..aad57e28c4 100644 --- a/applications/tari_app_grpc/proto/wallet.proto +++ b/applications/tari_app_grpc/proto/wallet.proto @@ -30,6 +30,8 @@ import "types.proto"; service Wallet { // This returns the current version rpc GetVersion (GetVersionRequest) returns (GetVersionResponse); + // Check for new updates + rpc CheckForUpdates (Empty) returns (SoftwareUpdate); // This returns the identity information rpc Identify (GetIdentityRequest) returns (GetIdentityResponse); // This returns a coinbase transaction diff --git a/applications/tari_console_wallet/Cargo.toml b/applications/tari_console_wallet/Cargo.toml index 378a480d95..6e4d7d4c99 100644 --- a/applications/tari_console_wallet/Cargo.toml +++ b/applications/tari_console_wallet/Cargo.toml @@ -12,7 +12,7 @@ tari_app_utilities = { path = "../tari_app_utilities", features = ["wallet"]} tari_comms = { path = "../../comms"} tari_comms_dht = { path = "../../comms/dht"} tari_common_types = {path = "../../base_layer/common_types"} -tari_p2p = { path = "../../base_layer/p2p" } +tari_p2p = { path = "../../base_layer/p2p", features = ["auto-update"] } tari_app_grpc = { path = "../tari_app_grpc", features = ["wallet"] } tari_shutdown = { path = "../../infrastructure/shutdown" } tari_key_manager = { path = "../../base_layer/key_manager" } diff --git a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs index b0c7779431..c18784d82a 100644 --- a/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs +++ b/applications/tari_console_wallet/src/grpc/wallet_grpc_server.rs @@ -79,6 +79,22 @@ impl wallet_server::Wallet for WalletGrpcServer { })) } + async fn check_for_updates( + &self, + _: Request, + ) -> Result, Status> { + let mut resp = tari_rpc::SoftwareUpdate::default(); + + if let Some(ref update) = *self.wallet.get_software_updater().new_update_notifier().borrow() { + resp.has_update = true; + resp.version = update.version().to_string(); + resp.sha = update.to_hash_hex(); + resp.download_url = update.download_url().to_string(); + } + + Ok(Response::new(resp)) + } + async fn identify(&self, _: Request) -> Result, Status> { let identity = self.wallet.comms.node_identity(); Ok(Response::new(GetIdentityResponse { diff --git a/applications/tari_console_wallet/src/init/mod.rs b/applications/tari_console_wallet/src/init/mod.rs index b877c6b729..df26c2e4d0 100644 --- a/applications/tari_console_wallet/src/init/mod.rs +++ b/applications/tari_console_wallet/src/init/mod.rs @@ -37,6 +37,7 @@ use tari_comms::{ use tari_comms_dht::{DbConnectionUrl, DhtConfig}; use tari_core::transactions::CryptoFactories; use tari_p2p::{ + auto_update::AutoUpdateConfig, initialization::CommsConfig, peer_seeds::SeedPeer, transport::TransportType::Tor, @@ -360,6 +361,15 @@ pub async fn init_wallet( config.base_node_event_channel_size, ); + let updater_config = AutoUpdateConfig { + name_server: config.dns_seeds_name_server, + update_uris: config.autoupdate_dns_hosts.clone(), + use_dnssec: config.dns_seeds_use_dnssec, + download_base_url: "https://tari-binaries.s3.amazonaws.com/latest".to_string(), + hashes_url: config.autoupdate_hashes_url.clone(), + hashes_sig_url: config.autoupdate_hashes_sig_url.clone(), + }; + let factories = CryptoFactories::default(); let wallet_config = WalletConfig::new( comms_config.clone(), @@ -391,6 +401,8 @@ pub async fn init_wallet( )), Some(config.buffer_rate_limit_console_wallet), Some(config.scan_for_utxo_interval), + Some(updater_config), + config.autoupdate_check_interval, ); let mut wallet = Wallet::start( diff --git a/applications/tari_console_wallet/src/ui/app.rs b/applications/tari_console_wallet/src/ui/app.rs index 028a37de81..3cc3a80075 100644 --- a/applications/tari_console_wallet/src/ui/app.rs +++ b/applications/tari_console_wallet/src/ui/app.rs @@ -28,6 +28,7 @@ use crate::{ log_tab::LogTab, menu::Menu, network_tab::NetworkTab, + notification_tab::NotificationTab, receive_tab::ReceiveTab, send_tab::SendTab, tabs_container::TabsContainer, @@ -87,7 +88,8 @@ impl App { .add("Send".into(), Box::new(SendTab::new())) .add("Receive".into(), Box::new(ReceiveTab::new())) .add("Network".into(), Box::new(NetworkTab::new(base_node_selected))) - .add("Log".into(), Box::new(LogTab::new())); + .add("Log".into(), Box::new(LogTab::new())) + .add("Notifications".into(), Box::new(NotificationTab::new())); let base_node_status = BaseNode::new(); let menu = Menu::new(); @@ -164,10 +166,10 @@ impl App { .split(max_width_layout[0]); let title_halves = Layout::default() .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .constraints([Constraint::Percentage(55), Constraint::Percentage(45)].as_ref()) .split(title_chunks[0]); - self.tabs.draw_titles(f, title_halves[0]); + self.tabs.draw_titles(f, title_halves[0], &self.app_state); self.base_node_status.draw(f, title_halves[1], &self.app_state); self.tabs.draw_content(f, title_chunks[1], &mut self.app_state); diff --git a/applications/tari_console_wallet/src/ui/components/component.rs b/applications/tari_console_wallet/src/ui/components/component.rs index d979e9f4b3..d421fc04d7 100644 --- a/applications/tari_console_wallet/src/ui/components/component.rs +++ b/applications/tari_console_wallet/src/ui/components/component.rs @@ -1,5 +1,11 @@ use crate::ui::state::AppState; -use tui::{backend::Backend, layout::Rect, Frame}; +use tui::{ + backend::Backend, + layout::Rect, + style::{Color, Style}, + text::{Span, Spans}, + Frame, +}; pub trait Component { fn draw(&mut self, f: &mut Frame, area: Rect, app_state: &AppState); @@ -13,4 +19,9 @@ pub trait Component { fn on_esc(&mut self, _app_state: &mut AppState) {} fn on_backspace(&mut self, _app_state: &mut AppState) {} fn on_tick(&mut self, _app_state: &mut AppState) {} + + // Create custom title based on data in AppState. + fn format_title(&self, title: &String, _app_state: &AppState) -> Spans { + Spans::from(Span::styled(title.clone(), Style::default().fg(Color::White))) + } } diff --git a/applications/tari_console_wallet/src/ui/components/mod.rs b/applications/tari_console_wallet/src/ui/components/mod.rs index acfd103c32..d4b18bc637 100644 --- a/applications/tari_console_wallet/src/ui/components/mod.rs +++ b/applications/tari_console_wallet/src/ui/components/mod.rs @@ -26,6 +26,7 @@ mod component; pub mod log_tab; pub(crate) mod menu; pub mod network_tab; +pub mod notification_tab; pub mod receive_tab; pub mod send_tab; pub mod tabs_container; diff --git a/applications/tari_console_wallet/src/ui/components/notification_tab.rs b/applications/tari_console_wallet/src/ui/components/notification_tab.rs new file mode 100644 index 0000000000..a632b21b9a --- /dev/null +++ b/applications/tari_console_wallet/src/ui/components/notification_tab.rs @@ -0,0 +1,73 @@ +use crate::ui::{components::Component, state::AppState}; +use tari_comms::runtime::Handle; +// use tari_p2p::auto_update::SoftwareUpdaterService; +use tui::{ + backend::Backend, + layout::{Constraint, Layout, Rect}, + style::{Color, Modifier, Style}, + text::{Span, Spans}, + widgets::{Block, Borders, Paragraph, Wrap}, + Frame, +}; + +pub struct NotificationTab {} + +impl NotificationTab { + pub fn new() -> Self { + Self {} + } + + fn draw_notifications(&mut self, f: &mut Frame, area: Rect, app_state: &AppState) + where B: Backend { + let block = Block::default().borders(Borders::ALL).title(Span::styled( + "Update", + Style::default().fg(Color::White).add_modifier(Modifier::BOLD), + )); + f.render_widget(block, area); + let notifications_area = Layout::default() + .constraints([Constraint::Min(42)].as_ref()) + .margin(1) + .split(area); + let mut text: Vec = app_state + .get_notifications() + .iter() + .map(|(time, line)| { + Spans::from(vec![ + Span::styled( + time.format("%Y-%m-%d %H:%M:%S ").to_string(), + Style::default().fg(Color::LightGreen), + ), + Span::raw(line), + ]) + }) + .collect(); + text.reverse(); + let paragraph = Paragraph::new(text.clone()).wrap(Wrap { trim: true }); + f.render_widget(paragraph, notifications_area[0]); + } +} + +impl Component for NotificationTab { + fn draw(&mut self, f: &mut Frame, area: Rect, app_state: &AppState) { + let areas = Layout::default() + .constraints([Constraint::Min(42)].as_ref()) + .split(area); + self.draw_notifications(f, areas[0], app_state); + } + + fn on_tick(&mut self, app_state: &mut AppState) { + // Constantly read the messages when in this tab. + Handle::current().block_on(app_state.mark_notifications_as_read()); + } + + fn format_title(&self, title: &String, app_state: &AppState) -> Spans { + // Create custom title based on notifications count. + match app_state.unread_notifications_count() > 0 { + true => Spans::from(Span::styled( + format!("{}({})", title, app_state.unread_notifications_count()), + Style::default().fg(Color::LightGreen), + )), + false => Spans::from(Span::styled(title.clone(), Style::default().fg(Color::White))), + } + } +} diff --git a/applications/tari_console_wallet/src/ui/components/tabs_container.rs b/applications/tari_console_wallet/src/ui/components/tabs_container.rs index 1dd88b1ff6..a4535412fc 100644 --- a/applications/tari_console_wallet/src/ui/components/tabs_container.rs +++ b/applications/tari_console_wallet/src/ui/components/tabs_container.rs @@ -25,7 +25,7 @@ use tui::{ backend::Backend, layout::Rect, style::{Color, Modifier, Style}, - text::{Span, Spans}, + text::Span, widgets::{Block, Borders, Tabs}, Frame, }; @@ -65,11 +65,12 @@ impl TabsContainer { } } - pub fn draw_titles(&self, f: &mut Frame, area: Rect) { + pub fn draw_titles(&self, f: &mut Frame, area: Rect, app_state: &AppState) { let titles = self .titles .iter() - .map(|t| Spans::from(Span::styled(t, Style::default().fg(Color::White)))) + .enumerate() + .map(|(i, title)| self.tabs[i].format_title(title, app_state)) .collect(); let tabs = Tabs::new(titles) .block(Block::default().borders(Borders::ALL).title(Span::styled( diff --git a/applications/tari_console_wallet/src/ui/state/app_state.rs b/applications/tari_console_wallet/src/ui/state/app_state.rs index b4a45df7b4..319358ba12 100644 --- a/applications/tari_console_wallet/src/ui/state/app_state.rs +++ b/applications/tari_console_wallet/src/ui/state/app_state.rs @@ -27,9 +27,11 @@ use std::{ }; use bitflags::bitflags; +use chrono::{DateTime, Local}; use log::*; use qrcode::{render::unicode, QrCode}; use tari_crypto::{ristretto::RistrettoPublicKey, tari_utilities::hex::Hex}; +use tari_p2p::auto_update::SoftwareUpdaterHandle; use tokio::{ sync::{watch, RwLock}, task, @@ -415,8 +417,35 @@ impl AppState { pub fn toggle_abandoned_coinbase_filter(&mut self) { self.completed_tx_filter.toggle(TransactionFilter::ABANDONED_COINBASES); } -} + pub fn get_notifications(&self) -> &Vec<(DateTime, String)> { + &self.cached_data.notifications + } + + pub fn unread_notifications_count(&self) -> u32 { + self.cached_data.new_notification_count + } + + pub async fn mark_notifications_as_read(&mut self) { + // Do not update if not necessary + if self.unread_notifications_count() > 0 { + { + let mut inner = self.inner.write().await; + inner.mark_notifications_as_read(); + } + self.update_cache().await; + } + } + + // Not used currently. + // pub async fn _add_notification(&mut self, notification: String) { + // { + // let mut inner = self.inner.write().await; + // inner.add_notification(notification); + // } + // self.update_cache().await; + // } +} pub struct AppStateInner { updated: bool, data: AppStateData, @@ -802,6 +831,21 @@ impl AppStateInner { } }); } + + pub fn add_notification(&mut self, notification: String) { + self.data.notifications.push((Local::now(), notification)); + self.data.new_notification_count += 1; + self.updated = true; + } + + pub fn mark_notifications_as_read(&mut self) { + self.data.new_notification_count = 0; + self.updated = true; + } + + pub fn get_software_updater(&self) -> SoftwareUpdaterHandle { + self.wallet.get_software_updater() + } } #[derive(Clone)] @@ -818,6 +862,8 @@ struct AppStateData { base_node_previous: Peer, base_node_list: Vec<(String, Peer)>, base_node_peer_custom: Option, + notifications: Vec<(DateTime, String)>, + new_notification_count: u32, } impl AppStateData { @@ -882,6 +928,8 @@ impl AppStateData { base_node_previous, base_node_list, base_node_peer_custom: base_node_config.base_node_custom, + notifications: Vec::new(), + new_notification_count: 0, } } } diff --git a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs index e7df30b653..158aed730e 100644 --- a/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs +++ b/applications/tari_console_wallet/src/ui/state/wallet_event_monitor.rs @@ -24,6 +24,7 @@ use crate::{notifier::Notifier, ui::state::AppStateInner}; use log::*; use std::sync::Arc; use tari_comms::{connectivity::ConnectivityEvent, peer_manager::Peer}; +use tari_p2p::auto_update::SoftwareUpdate; use tari_wallet::{ base_node_service::{handle::BaseNodeEvent, service::BaseNodeState}, output_manager_service::{handle::OutputManagerEvent, TxId}, @@ -57,6 +58,13 @@ impl WalletEventMonitor { let mut connectivity_status = wallet_connectivity.get_connectivity_status_watch(); let mut base_node_events = self.app_state_inner.read().await.get_base_node_event_stream(); + let mut software_update_notif = self + .app_state_inner + .read() + .await + .get_software_updater() + .new_update_notifier() + .clone(); info!(target: LOG_TARGET, "Wallet Event Monitor starting"); loop { @@ -103,7 +111,7 @@ impl WalletEventMonitor { _ => (), } }, - Err(broadcast::error::RecvError::Lagged(n)) => { + Err(broadcast::error::RecvError::Lagged(n)) => { warn!(target: LOG_TARGET, "Missed {} from Transaction events", n); } Err(broadcast::error::RecvError::Closed) => {} @@ -113,6 +121,22 @@ impl WalletEventMonitor { trace!(target: LOG_TARGET, "Wallet Event Monitor received wallet connectivity status changed"); self.trigger_peer_state_refresh().await; }, + Ok(_) = software_update_notif.changed() => { + trace!(target: LOG_TARGET, "Wallet Event Monitor received wallet auto update status changed"); + let update:Option = match *software_update_notif.borrow() { + Some(ref update) => Some(update.clone()), + None => None + }; + if let Some(update) = update { + self.add_notification(format!( + "Version {} of the {} is available: {} (sha: {})", + update.version(), + update.app(), + update.download_url(), + update.to_hash_hex() + )).await; + } + }, result = connectivity_events.recv() => { match result { Ok(msg) => { @@ -146,7 +170,7 @@ impl WalletEventMonitor { } } }, - Err(broadcast::error::RecvError::Lagged(n)) => { + Err(broadcast::error::RecvError::Lagged(n)) => { warn!(target: LOG_TARGET, "Missed {} from Base node Service events", n); } Err(broadcast::error::RecvError::Closed) => {} @@ -166,7 +190,7 @@ impl WalletEventMonitor { Err(broadcast::error::RecvError::Closed) => {} } }, - _ = shutdown_signal.wait() => { + _ = shutdown_signal.wait() => { info!(target: LOG_TARGET, "Wallet Event Monitor shutting down because the shutdown signal was received"); break; }, @@ -237,4 +261,9 @@ impl WalletEventMonitor { warn!(target: LOG_TARGET, "Error refresh app_state: {}", e); } } + + async fn add_notification(&mut self, notification: String) { + let mut inner = self.app_state_inner.write().await; + inner.add_notification(notification); + } } diff --git a/base_layer/wallet/Cargo.toml b/base_layer/wallet/Cargo.toml index 9644b20611..df89ef759c 100644 --- a/base_layer/wallet/Cargo.toml +++ b/base_layer/wallet/Cargo.toml @@ -7,12 +7,13 @@ version = "0.9.6" edition = "2018" [dependencies] +tari_common = { path = "../../common" } tari_common_types = { version = "^0.9", path = "../../base_layer/common_types" } tari_comms = { version = "^0.9", path = "../../comms" } tari_comms_dht = { version = "^0.9", path = "../../comms/dht" } tari_crypto = "0.11.1" tari_key_manager = { version = "^0.9", path = "../key_manager" } -tari_p2p = { version = "^0.9", path = "../p2p" } +tari_p2p = { version = "^0.9", path = "../p2p", features = ["auto-update"] } tari_service_framework = { version = "^0.9", path = "../service_framework" } tari_shutdown = { version = "^0.9", path = "../../infrastructure/shutdown" } tari_storage = { version = "^0.9", path = "../../infrastructure/storage" } diff --git a/base_layer/wallet/src/config.rs b/base_layer/wallet/src/config.rs index 3844fc13e7..908e7bbc56 100644 --- a/base_layer/wallet/src/config.rs +++ b/base_layer/wallet/src/config.rs @@ -23,7 +23,7 @@ use std::time::Duration; use tari_core::{consensus::NetworkConsensus, transactions::CryptoFactories}; -use tari_p2p::initialization::CommsConfig; +use tari_p2p::{auto_update::AutoUpdateConfig, initialization::CommsConfig}; use crate::{ base_node_service::config::BaseNodeServiceConfig, @@ -44,6 +44,8 @@ pub struct WalletConfig { pub network: NetworkConsensus, pub base_node_service_config: BaseNodeServiceConfig, pub scan_for_utxo_interval: Duration, + pub updater_config: Option, + pub autoupdate_check_interval: Option, } impl WalletConfig { @@ -58,6 +60,8 @@ impl WalletConfig { buffer_size: Option, rate_limit: Option, scan_for_utxo_interval: Option, + updater_config: Option, + autoupdate_check_interval: Option, ) -> Self { Self { comms_config, @@ -69,6 +73,8 @@ impl WalletConfig { network, base_node_service_config: base_node_service_config.unwrap_or_default(), scan_for_utxo_interval: scan_for_utxo_interval.unwrap_or_else(|| Duration::from_secs(43200)), + updater_config, + autoupdate_check_interval, } } } diff --git a/base_layer/wallet/src/wallet.rs b/base_layer/wallet/src/wallet.rs index 24e4573181..e5e6264192 100644 --- a/base_layer/wallet/src/wallet.rs +++ b/base_layer/wallet/src/wallet.rs @@ -29,6 +29,7 @@ use aes_gcm::{ use digest::Digest; use log::*; use rand::rngs::OsRng; +use tari_common::configuration::bootstrap::ApplicationType; use tari_crypto::{ common::Blake256, keys::SecretKey, @@ -55,7 +56,12 @@ use tari_core::transactions::{ CryptoFactories, }; use tari_key_manager::key_manager::KeyManager; -use tari_p2p::{comms_connector::pubsub_connector, initialization, initialization::P2pInitializer}; +use tari_p2p::{ + auto_update::{SoftwareUpdaterHandle, SoftwareUpdaterService}, + comms_connector::pubsub_connector, + initialization, + initialization::P2pInitializer, +}; use tari_service_framework::StackBuilder; use tari_shutdown::ShutdownSignal; @@ -103,6 +109,7 @@ where pub contacts_service: ContactsServiceHandle, pub base_node_service: BaseNodeServiceHandle, pub utxo_scanner_service: UtxoScannerHandle, + pub updater_service: SoftwareUpdaterHandle, pub db: WalletDatabase, pub factories: CryptoFactories, _u: PhantomData, @@ -194,6 +201,20 @@ where node_identity.clone(), )); + // Check if we have update config. FFI wallets don't do this, the update on mobile is done differently. + let stack = match config.updater_config { + Some(updater_config) => stack.add_initializer(SoftwareUpdaterService::new( + ApplicationType::ConsoleWallet, + env!("CARGO_PKG_VERSION") + .to_string() + .parse() + .expect("Unable to parse console wallet version."), + updater_config, + config.autoupdate_check_interval, + )), + _ => stack, + }; + let mut handles = stack.build().await?; let comms = handles @@ -210,6 +231,7 @@ where let base_node_service_handle = handles.expect_handle::(); let utxo_scanner_service_handle = handles.expect_handle::(); let wallet_connectivity = handles.expect_handle::(); + let updater_handle = handles.expect_handle::(); persist_one_sided_payment_script_for_node_identity(&mut output_manager_handle, comms.node_identity()) .await @@ -236,6 +258,7 @@ where contacts_service: contacts_handle, base_node_service: base_node_service_handle, utxo_scanner_service: utxo_scanner_service_handle, + updater_service: updater_handle, wallet_connectivity, db: wallet_database, factories, @@ -300,6 +323,42 @@ where .map_err(WalletError::BaseNodeServiceError) } + pub async fn check_for_update(&self) -> Option { + let mut updater = self.updater_service.clone(); + debug!( + target: LOG_TARGET, + "Checking for updates (current version: {})...", + env!("CARGO_PKG_VERSION").to_string() + ); + match updater.check_for_updates().await { + Some(update) => { + debug!( + target: LOG_TARGET, + "Version {} of the {} is available: {} (sha: {})", + update.version(), + update.app(), + update.download_url(), + update.to_hash_hex() + ); + Some(format!( + "Version {} of the {} is available: {} (sha: {})", + update.version(), + update.app(), + update.download_url(), + update.to_hash_hex() + )) + }, + None => { + debug!(target: LOG_TARGET, "No updates found.",); + None + }, + } + } + + pub fn get_software_updater(&self) -> SoftwareUpdaterHandle { + self.updater_service.clone() + } + /// Import an external spendable UTXO into the wallet. The output will be added to the Output Manager and made /// spendable. A faux incoming transaction will be created to provide a record of the event. The TxId of the /// generated transaction is returned. diff --git a/base_layer/wallet/tests/wallet/mod.rs b/base_layer/wallet/tests/wallet/mod.rs index c1320e5409..671d3db23c 100644 --- a/base_layer/wallet/tests/wallet/mod.rs +++ b/base_layer/wallet/tests/wallet/mod.rs @@ -151,6 +151,8 @@ async fn create_wallet( None, None, None, + None, + None, ); let metadata = ChainMetadata::new(std::u64::MAX, Vec::new(), 0, 0, 0); @@ -710,6 +712,8 @@ async fn test_import_utxo() { None, None, None, + None, + None, ); let mut alice_wallet = Wallet::start( config, diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index 1f85e33d06..21306353f6 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -2925,6 +2925,8 @@ pub unsafe extern "C" fn wallet_create( None, None, None, + None, + None, ); let mut recovery_lookup = match runtime.block_on(wallet_database.get_client_key_value(RECOVERY_KEY.to_owned())) { diff --git a/clients/wallet_grpc_client/.prettierrc b/clients/wallet_grpc_client/.prettierrc new file mode 100644 index 0000000000..168d9d2a0c --- /dev/null +++ b/clients/wallet_grpc_client/.prettierrc @@ -0,0 +1,3 @@ +{ + "endOfLine": "auto" +} diff --git a/clients/wallet_grpc_client/index.js b/clients/wallet_grpc_client/index.js index d638ba1f5c..1770f1d06b 100644 --- a/clients/wallet_grpc_client/index.js +++ b/clients/wallet_grpc_client/index.js @@ -40,6 +40,7 @@ function Client(address) { "listConnectedPeers", "getNetworkStatus", "cancelTransaction", + "checkForUpdates", ]; this.waitForReady = (...args) => { diff --git a/integration_tests/features/BaseNodeAutoUpdate.feature b/integration_tests/features/BaseNodeAutoUpdate.feature index 4f7041ed60..d9d8331e6d 100644 --- a/integration_tests/features/BaseNodeAutoUpdate.feature +++ b/integration_tests/features/BaseNodeAutoUpdate.feature @@ -1,14 +1,13 @@ @auto_update Feature: AutoUpdate - # Not sure why this takes so long on CI @long-running - Scenario: Auto update finds a new update + Scenario: Auto update finds a new update on base node Given I have a node NODE_A with auto update enabled Then NODE_A has a new software update - Scenario: Auto update ignores update with invalid signature + Scenario: Auto update ignores update with invalid signature on base node Given I have a node NODE_A with auto update configured with a bad signature And I wait 10 seconds Then NODE_A does not have a new software update diff --git a/integration_tests/features/WalletAutoUpdate.feature b/integration_tests/features/WalletAutoUpdate.feature new file mode 100644 index 0000000000..4967d2ffbc --- /dev/null +++ b/integration_tests/features/WalletAutoUpdate.feature @@ -0,0 +1,14 @@ +@auto_update +Feature: AutoUpdate + + # Not sure why this takes so long on CI + @long-running @broken + Scenario: Auto update finds a new update on wallet + Given I have a wallet WALLET with auto update enabled + Then WALLET has a new software update + + @broken + Scenario: Auto update ignores update with invalid signature on wallet + Given I have a wallet WALLET with auto update configured with a bad signature + And I wait 10 seconds + Then WALLET does not have a new software update diff --git a/integration_tests/features/support/steps.js b/integration_tests/features/support/steps.js index ef93992b5e..cb554e0d6f 100644 --- a/integration_tests/features/support/steps.js +++ b/integration_tests/features/support/steps.js @@ -21,9 +21,9 @@ let lastResult; const AUTOUPDATE_HASHES_TXT_URL = "https://raw.githubusercontent.com/sdbondi/tari/autoupdate-test-branch/meta/hashes.txt"; const AUTOUPDATE_HASHES_TXT_SIG_URL = - "https://github.com/sdbondi/tari/raw/base-node-auto-update/meta/good.sig"; + "https://github.com/sdbondi/tari/raw/autoupdate-test-branch/meta/good.sig"; const AUTOUPDATE_HASHES_TXT_BAD_SIG_URL = - "https://github.com/sdbondi/tari/raw/base-node-auto-update/meta/bad.sig"; + "https://github.com/sdbondi/tari/raw/autoupdate-test-branch/meta/bad.sig"; Given(/I have a seed node (.*)/, { timeout: 20 * 1000 }, async function (name) { return await this.createSeedNode(name); @@ -97,7 +97,7 @@ Given( }, }); await node.startNew(); - this.addNode(name, node); + await this.addNode(name, node); } ); @@ -116,7 +116,41 @@ Given( }, }); await node.startNew(); - this.addNode(name, node); + await this.addNode(name, node); + } +); + +Given( + /I have a wallet (.*) with auto update enabled/, + { timeout: 20 * 1000 }, + async function (name) { + await this.createAndAddWallet(name, "", { + common: { + auto_update: { + enabled: true, + dns_hosts: ["_test_autoupdate.tari.io"], + hashes_url: AUTOUPDATE_HASHES_TXT_URL, + hashes_sig_url: AUTOUPDATE_HASHES_TXT_SIG_URL, + }, + }, + }); + } +); + +Given( + /I have a wallet (.*) with auto update configured with a bad signature/, + { timeout: 20 * 1000 }, + async function (name) { + await this.createAndAddWallet(name, "", { + common: { + auto_update: { + enabled: true, + dns_hosts: ["_test_autoupdate.tari.io"], + hashes_url: AUTOUPDATE_HASHES_TXT_URL, + hashes_sig_url: AUTOUPDATE_HASHES_TXT_BAD_SIG_URL, + }, + }, + }); } ); @@ -933,7 +967,7 @@ Then( /(.*) does not have a new software update/, { timeout: 1200 * 1000 }, async function (name) { - let client = this.getClient(name); + let client = await this.getNodeOrWalletClient(name); await sleep(5000); await waitFor( async () => client.checkForUpdates().has_update, @@ -947,7 +981,7 @@ Then( /(.+) has a new software update/, { timeout: 1200 * 1000 }, async function (name) { - let client = this.getClient(name); + let client = await this.getNodeOrWalletClient(name); await waitFor( async () => { return client.checkForUpdates().has_update; diff --git a/integration_tests/features/support/world.js b/integration_tests/features/support/world.js index 91e8b45f16..8a57262bb6 100644 --- a/integration_tests/features/support/world.js +++ b/integration_tests/features/support/world.js @@ -267,7 +267,7 @@ class CustomWorld { } let wallet = this.wallets[name.trim()]; if (wallet) { - let client = await wallet.connectClient(); + client = await wallet.connectClient(); client.isNode = false; client.isWallet = true; return client; diff --git a/integration_tests/helpers/walletClient.js b/integration_tests/helpers/walletClient.js index f3a38ff35a..5fb69c8080 100644 --- a/integration_tests/helpers/walletClient.js +++ b/integration_tests/helpers/walletClient.js @@ -27,6 +27,10 @@ class WalletClient { return await this.client.getVersion(); } + async checkForUpdates() { + return await this.client.checkForUpdates({}); + } + async getBalance() { return await this.client.getBalance().then((balance) => ({ available_balance: parseInt(balance.available_balance),